PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.8.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.8.7
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / assets / js / desktop.js

desktop.js in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.8.7, at assets/js/desktop.js

28,058 lines 911.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 var desktopMode = function(exports) {
2 "use strict";
3 var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
4 function installMyWordpressEarlyStub() {
5 const w = window;
6 w.wp = w.wp ?? {};
7 const wp = w.wp;
8 if (!wp.desktop) {
9 wp.desktop = {};
10 }
11 const desktop = wp.desktop;
12 if (desktop.myWordpress) {
13 return;
14 }
15 const queue = [];
16 const stub = {
17 registerEntityKind: (kind, renderer) => {
18 const slot = { unregister: null };
19 const entry = { kind, renderer, slot };
20 queue.push(entry);
21 return () => {
22 if (slot.unregister) {
23 slot.unregister();
24 slot.unregister = null;
25 return;
26 }
27 const i = queue.indexOf(entry);
28 if (i !== -1) {
29 queue.splice(i, 1);
30 }
31 };
32 },
33 __pendingKinds: queue
34 };
35 desktop.myWordpress = stub;
36 }
37 installMyWordpressEarlyStub();
38 function getWpHooks$1() {
39 const hooks = window.wp?.hooks;
40 if (!hooks) {
41 throw new Error(
42 "[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."
43 );
44 }
45 return hooks;
46 }
47 function addFilter(hookName2, namespace, callback, priority) {
48 getWpHooks$1().addFilter(
49 hookName2,
50 namespace,
51 callback,
52 priority
53 );
54 }
55 function addAction(hookName2, namespace, callback, priority) {
56 getWpHooks$1().addAction(
57 hookName2,
58 namespace,
59 callback,
60 priority
61 );
62 }
63 function removeAction(hookName2, namespace) {
64 return getWpHooks$1().removeAction(hookName2, namespace);
65 }
66 function applyFilters(hookName2, value, ...args) {
67 return getWpHooks$1().applyFilters(hookName2, value, ...args);
68 }
69 function doAction(hookName2, ...args) {
70 getWpHooks$1().doAction(hookName2, ...args);
71 }
72 function didAction(hookName2) {
73 return getWpHooks$1().didAction(hookName2);
74 }
75 function rawHooks() {
76 return getWpHooks$1();
77 }
78 const HOOKS = {
79 /** Action, fires once after shell boot; plugins register here. */
80 INIT: "desktop-mode.init",
81 /** Filter, receives the wallpaper registry array. */
82 WALLPAPERS: "desktop-mode.wallpapers",
83 /** Action before a canvas wallpaper mounts. */
84 WALLPAPER_MOUNTING: "desktop-mode.wallpaper.mounting",
85 /** Action after a canvas wallpaper mounts successfully. */
86 WALLPAPER_MOUNTED: "desktop-mode.wallpaper.mounted",
87 /** Action before a canvas wallpaper tears down. */
88 WALLPAPER_UNMOUNTING: "desktop-mode.wallpaper.unmounting",
89 /** Action when a canvas wallpaper's mount throws / rejects. */
90 WALLPAPER_MOUNT_FAILED: "desktop-mode.wallpaper.mount-failed",
91 /** Action mirroring document.visibilitychange for active canvas wallpapers. */
92 WALLPAPER_VISIBILITY: "desktop-mode.wallpaper.visibility",
93 // ------------------------------------------------------------------
94 // Observability — iframe errors, iframe network, shell-side errors,
95 // monitor entry aggregation. Designed for dashboard / debug widget
96 // plugins that want genuine admin observability (Gutenberg save
97 // failures, admin-ajax 500s, plugin exceptions) rather than just the
98 // shell's own console-error surface.
99 // ------------------------------------------------------------------
100 /**
101 * Action, fires when a chromeless iframe's `error` or
102 * `unhandledrejection` handler catches an exception. Payload: `{
103 * windowId: string, kind: 'error' | 'unhandledrejection', message:
104 * string, filename: string | null, lineno: number | null, colno:
105 * number | null, stack: string | null }`. Origin-filtered at the
106 * parent shell; cross-origin iframe errors never reach here.
107 */
108 /**
109 * Action, fires once per iframe when the chromeless bridge
110 * script has finished wiring its message listeners. Payload:
111 * `{ windowId: string }`. Subscribers get a reliable "safe to
112 * talk to this iframe" signal — the browser's native `load`
113 * event fires before our bridge attaches, so messages sent on
114 * `load` can be dropped on the floor. Use this instead when
115 * timing matters (first-focus dispatch, auto-fill handshakes).
116 *
117 * @since 0.11.0
118 */
119 IFRAME_READY: "desktop-mode.iframe.ready",
120 IFRAME_ERROR: "desktop-mode.iframe.error",
121 /**
122 * Action, fires when a `fetch` or `XMLHttpRequest` inside a
123 * chromeless iframe completes (success OR failure). Payload: `{
124 * windowId: string, method: string, url: string, status: number,
125 * duration: number, failed: boolean }`. Subscribers get a faithful
126 * view of admin-ajax + REST calls that previously never left the
127 * iframe boundary. `status === 0` indicates a network failure with
128 * no response received.
129 */
130 IFRAME_NETWORK_COMPLETED: "desktop-mode.iframe.network-completed",
131 /**
132 * Action, fires when one of the shell's own try/catch barriers
133 * catches an exception. Payload: `{ scope:
134 * 'widget-mount' | 'widget-teardown' | 'window-open' | 'wallpaper-mount' |
135 * 'wallpaper-teardown' | 'session-save' | 'menu-refresh' | string,
136 * id?: string, error: unknown }`. Paired with the existing
137 * `console.error` calls — a monitor widget can surface these as
138 * first-class entries.
139 */
140 SHELL_ERROR: "desktop-mode.shell.error",
141 /**
142 * Action, fires once per `wp.desktop.broadcast()` call with the
143 * fully-resolved `{ topic, payload }` detail. Lets plugins log,
144 * mirror, or augment broadcast traffic without subscribing for
145 * every individual topic.
146 */
147 BROADCAST: "desktop-mode.broadcast",
148 /**
149 * Filter, applies to a `MonitorEntry` before a monitor widget
150 * renders it. Plugins can mutate the entry (rewrite the message,
151 * add `extra` fields) or return `null` to suppress it. Used by
152 * monitor widgets to converge every plugin on the same shape —
153 * see `MonitorEntry` in `src/types.ts`.
154 */
155 MONITOR_ENTRY: "desktop-mode.monitor.entry",
156 /**
157 * Filter, applies to the list of "solid" surfaces wallpapers
158 * should consider for collision / accumulation effects (snow
159 * piling, leaves settling, rain splash). Seeded by the shell
160 * with: every visible (non-minimized) window's top edge; the
161 * desktop-area floor; the dock's outward-facing edge; and every
162 * mounted widget card's top edge.
163 *
164 * Plugins that own their own DOM (e.g. floating pickers,
165 * custom overlays) can push additional surfaces so snow
166 * accumulates on them too.
167 *
168 * Each entry is a `WallpaperSurface` — see
169 * `src/wallpapers/surfaces.ts` for the shape. Rects are in
170 * viewport coordinates (clientX / clientY), matching what a
171 * canvas mounted inside `#desktop-mode-wallpaper` reads.
172 */
173 WALLPAPER_SURFACES: "desktop-mode.wallpaper.surfaces",
174 // ------------------------------------------------------------------
175 // Window lifecycle actions. All payloads share a `windowId: string`
176 // field; additional fields are documented per-hook in the JS
177 // reference. These mirror the existing `desktop-mode-window-*`
178 // CustomEvents but ship under the hook bus so plugins can use one
179 // idiomatic API for everything the shell emits.
180 // ------------------------------------------------------------------
181 /**
182 * Filter, last call before a window's resolved geometry (x, y,
183 * width, height, initialState) is baked into the `WindowConfig`
184 * passed to the `Window` constructor. Lets a plugin override
185 * default placement for windows it owns, snap restored bounds to
186 * a different region, or force a particular initial state.
187 *
188 * Signature:
189 *
190 * ( geometry: ResolvedWindowGeometry, ctx: WindowGeometryContext )
191 * => ResolvedWindowGeometry
192 *
193 * Where `ResolvedWindowGeometry = { x, y, width, height, state? }`
194 * and `ctx = { windowId, baseId, hasSavedGeometry, callerPinned,
195 * desktopRect }`.
196 *
197 * - `hasSavedGeometry` is `true` when the user previously
198 * dragged or resized this window and the resolved geometry
199 * includes those restored values. Plugins that want to
200 * "leave the user's saved layout alone" should bail when
201 * this is true.
202 * - `callerPinned` is `true` when the caller of `manager.open()`
203 * passed at least one of `{ x, y, width, height, initialState }`
204 * explicitly. For NATIVE windows this is usually true (the
205 * framework's native-window opener passes the registry's
206 * declared dimensions); for admin-page iframe windows opened
207 * from the dock this is usually false. The filter is free to
208 * override registry defaults — `callerPinned: true` does NOT
209 * mean "leave it alone."
210 *
211 * The shell re-clamps `width`/`height` to the registered
212 * `minWidth`/`minHeight` after the filter returns — a buggy
213 * filter cannot ship a sub-minimum window. `x` and `y` are
214 * NOT re-clamped to the desktop rect after the filter (plugins
215 * sometimes want to place windows partially off-screen for
216 * deliberate stylistic reasons); the filter is responsible for
217 * its own viewport math when it cares.
218 *
219 * Companion of `desktop_mode_register_window` server-side
220 * defaults — runs every time a window opens, not just at
221 * registration.
222 *
223 * @since 0.25.0
224 */
225 WINDOW_GEOMETRY: "desktop-mode.window.geometry",
226 /** Action, fires when a window is added to the stack. */
227 WINDOW_OPENED: "desktop-mode.window.opened",
228 /**
229 * Action, fires when a window's body enters the loading state — at
230 * construction (every window starts loading) and whenever a plugin
231 * calls {@link NativeRenderContext.window.markLoading} or
232 * `Window.markContentLoading()` mid-life. Payload: `{ windowId }`.
233 *
234 * The shell shows a `<wpd-spinner>` overlay while the window is in
235 * the loading state and fades content in on the loaded transition.
236 * Subscribe to this hook (or to {@link WINDOW_CONTENT_LOADED}) when
237 * you need to react to either edge — analytics, instrumentation,
238 * decorating the spinner with a per-window message.
239 *
240 * Edge-triggered: idempotent calls don't re-fire. The matching
241 * `desktop-mode-window-content-loading` CustomEvent dispatches on
242 * `document` with the same payload.
243 *
244 * @since 0.6.0
245 */
246 WINDOW_CONTENT_LOADING: "desktop-mode.window.content-loading",
247 /**
248 * Action, fires when a window's body content becomes ready — for
249 * iframe windows the moment the chromeless bridge announces
250 * `desktop-mode-ready`, for native windows after the user's
251 * `render( body )` callback (or its returned promise) resolves, and
252 * whenever a plugin calls {@link NativeRenderContext.window.markReady}
253 * or `Window.markContentLoaded()` mid-life. Payload: `{ windowId }`.
254 *
255 * The unified "window content is ready" signal across both render
256 * strategies — use this instead of branching on iframe vs. native.
257 * Iframe-only consumers can still subscribe to {@link IFRAME_READY},
258 * which fires alongside this hook for iframe windows. The shell
259 * removes the loading overlay and fades the content in on this
260 * transition.
261 *
262 * Edge-triggered: only fires on a loading → ready transition.
263 * The matching `desktop-mode-window-content-loaded` CustomEvent
264 * dispatches on `document` with the same payload.
265 *
266 * @since 0.6.0
267 */
268 WINDOW_CONTENT_LOADED: "desktop-mode.window.content-loaded",
269 /**
270 * Filter, applied to the loading-overlay HTMLElement just after
271 * the shell paints its default `<wpd-spinner>` and after any
272 * per-window inline customization (`config.loading.render`)
273 * runs. Receives the overlay element; context: `{ windowId,
274 * config }`. Plugins may mutate the element (e.g.
275 * `host.replaceChildren( myBrandedLoader )` to swap out the
276 * default entirely, or `host.querySelector('wpd-spinner')!.
277 * setAttribute('preset', 'comet')` to retune the spinner) or
278 * return a different element to replace the overlay wholesale.
279 *
280 * Use cases: a brand-skin plugin that overrides every window's
281 * spinner with its own logo; a status-bar plugin that adds
282 * "Loading… 47% — fetching posts" text; an A/B-test framework
283 * that swaps the loader during an experiment.
284 *
285 * Resolution order for the loading overlay:
286 * 1. Default content (`<wpd-spinner>`) is painted.
287 * 2. Per-window `config.loading.render( host, ctx )` runs.
288 * 3. This filter runs.
289 * 4. The result is appended to the window body.
290 *
291 * @since 0.6.0
292 */
293 WINDOW_LOADING_OVERLAY: "desktop-mode.window.loading-overlay",
294 /**
295 * Action, fires when `manager.open(...)` is called for a baseId
296 * whose window already exists on the active desktop. This is the
297 * unambiguous "user requested to open this window again" signal
298 * — distinct from focus changes (which double-fire on alt-tab and
299 * skip when already focused) and from `WINDOW_OPENED` (which only
300 * fires on first creation). Payload:
301 * `{ windowId: string, baseId: string, wasMinimized: boolean }`.
302 *
303 * Plugins that hold per-window state (e.g. the code-editor's
304 * active file) should listen here to re-orient the existing
305 * window's content to whatever the caller wants to show — the
306 * open-window call is synchronous, so any state the caller sets
307 * BEFORE invoking `openWindow` is already in place when this
308 * fires.
309 */
310 WINDOW_REOPENED: "desktop-mode.window.reopened",
311 /**
312 * Action, fires BEFORE the window's element is detached from the
313 * DOM but AFTER the manager has already removed it from the stack.
314 * Payload: `{ windowId: string, element: HTMLElement }`.
315 *
316 * Use this for cleanup that needs a reference to the live
317 * element (removing anchored snow, wallpaper particles pinned to
318 * window tops, measurement caches keyed by element). `WINDOW_CLOSED`
319 * fires immediately after and only carries the id, which means
320 * subscribers would otherwise have to re-query the DOM — by then
321 * the element is gone, so they can't match at all.
322 */
323 WINDOW_CLOSING: "desktop-mode.window.closing",
324 /** Action, fires when a window is removed from the stack. */
325 WINDOW_CLOSED: "desktop-mode.window.closed",
326 /** Action, fires when focus changes to a different window. */
327 WINDOW_FOCUSED: "desktop-mode.window.focused",
328 /**
329 * Action, fires for the window that LOST focus when another
330 * window takes over. Symmetric counterpart to
331 * `WINDOW_FOCUSED`. Payload: `{ windowId: string, focusedTo:
332 * string | null }` — `focusedTo` identifies the new top of
333 * the stack so blur subscribers can ignore alt-tabs to a
334 * sibling they own.
335 *
336 * No-op when there's no previously-focused window (initial
337 * boot, all-windows-closed). Manager fires this BEFORE
338 * `WINDOW_FOCUSED` so subscribers see "blur old, focus new"
339 * in deterministic order.
340 *
341 * @since 0.5.5
342 */
343 WINDOW_BLURRED: "desktop-mode.window.blurred",
344 /** Action, fires when a window is minimized. */
345 WINDOW_MINIMIZED: "desktop-mode.window.minimized",
346 /** Action, fires when a window is restored from minimized. */
347 WINDOW_RESTORED: "desktop-mode.window.restored",
348 /** Action, fires when a window is maximized (fills desktop area). */
349 WINDOW_MAXIMIZED: "desktop-mode.window.maximized",
350 /** Action, fires when a window exits maximized state. */
351 WINDOW_UNMAXIMIZED: "desktop-mode.window.unmaximized",
352 /** Action, fires when a window enters fullscreen / focus mode. */
353 WINDOW_FULLSCREEN_ENTERED: "desktop-mode.window.fullscreen-entered",
354 /** Action, fires when a window exits fullscreen / focus mode. */
355 WINDOW_FULLSCREEN_EXITED: "desktop-mode.window.fullscreen-exited",
356 /**
357 * Filter, decides whether a fullscreen ("focus mode") window
358 * should auto-exit when focus moves to a different window.
359 *
360 * Default is `true` so a newly-focused window is never silently
361 * occluded by a fullscreen one (its `z-index` sits above all
362 * other windows). Plugins whose fullscreen surface is meant to
363 * persist across focus changes — slideshows, video players,
364 * immersive games — can return `false` to keep their window
365 * fullscreen.
366 *
367 * Signature:
368 *
369 * ( shouldExit: boolean, ctx: {
370 * windowId: string, // the fullscreen window
371 * focusedTo: string, // the window gaining focus
372 * } ) => boolean
373 *
374 * @since 0.8.6
375 */
376 WINDOW_AUTO_EXIT_FULLSCREEN: "desktop-mode.window.auto-exit-fullscreen",
377 /**
378 * Action, fires at most once per animation frame during an
379 * active drag or resize with the live geometry. Payload: `{
380 * windowId: string, x: number, y: number, width: number,
381 * height: number, state: WindowState, phase: 'drag' | 'resize' }`.
382 *
383 * Intended for per-frame collision-aware wallpapers (snow piling
384 * on window tops, rain splash on edges) that would otherwise
385 * poll `getBoundingClientRect` every rAF. Coalesced via
386 * `requestAnimationFrame` so a pointermove storm collapses to
387 * one fire per paint — matches the cadence a wallpaper's own
388 * ticker runs at.
389 *
390 * NOT fired at drag/resize end — `WINDOW_DRAG_END` /
391 * `WINDOW_RESIZE_END` handle the settled geometry. Subscribers
392 * that only want the final position should listen to those
393 * instead.
394 */
395 WINDOW_BOUNDS_CHANGED: "desktop-mode.window.bounds-changed",
396 /** Action, fires at drag-end with the final `{ x, y }` position. */
397 WINDOW_MOVED: "desktop-mode.window.moved",
398 /** Action, fires at resize-end with the final `{ width, height }`. */
399 WINDOW_RESIZED: "desktop-mode.window.resized",
400 /** Action, fires when title-bar drag begins. */
401 WINDOW_DRAG_START: "desktop-mode.window.drag-start",
402 /** Action, fires when title-bar drag ends. Payload mirrors WINDOW_MOVED. */
403 WINDOW_DRAG_END: "desktop-mode.window.drag-end",
404 /** Action, fires when the resize handle is first pressed. */
405 WINDOW_RESIZE_START: "desktop-mode.window.resize-start",
406 /** Action, fires when resize completes. Payload mirrors WINDOW_RESIZED. */
407 WINDOW_RESIZE_END: "desktop-mode.window.resize-end",
408 /** Action, fires when the user "detaches" a window to a classic tab. */
409 WINDOW_DETACHED: "desktop-mode.window.detached",
410 /**
411 * Action, fires when the user clicks the title-bar reload button
412 * on an iframe-backed window. Payload: `{ windowId: string, url:
413 * string }` where `url` is the URL being reloaded (the active
414 * primary or external sub-tab). Subscribers can use this to
415 * invalidate their own cache, force a save before navigation,
416 * track usage as a UX signal, or sync state across companion
417 * surfaces. Native windows do not fire this — they own their
418 * DOM directly and the reload button doesn't apply.
419 */
420 WINDOW_RELOADED: "desktop-mode.window.reloaded",
421 /** Action, fires when iframe title updates change the window title. */
422 WINDOW_TITLE_CHANGED: "desktop-mode.window.title-changed",
423 /**
424 * Action, fires when a window's `setHighlight()` mode changes.
425 * Payload: `{ windowId: string, mode: 'preview' | 'persistent' | null,
426 * color?: string }`. Lets onboarding / guidance / drag-bridge
427 * plugins react when another module flagged one of their
428 * windows as the focus of a multi-step interaction without
429 * having to observe DOM mutations.
430 *
431 * @since 0.24.0
432 */
433 WINDOW_HIGHLIGHT_CHANGED: "desktop-mode.window.highlight-changed",
434 /**
435 * Action, fires when a window's body element's dimensions
436 * change — mount, user resize, viewport reflow. Payload: `{
437 * windowId: string, width: number, height: number }`. Body
438 * dimensions exclude the title bar + tab strip, matching what a
439 * canvas or layout engine inside the body would measure.
440 */
441 WINDOW_BODY_RESIZED: "desktop-mode.window.body-resized",
442 // ------------------------------------------------------------------
443 // Native-window lifecycle. These fire ONLY for windows constructed
444 // with `native: true` — iframe windows have no render phase to
445 // intercept. Use them to wrap / instrument / cancel the paint of
446 // plugin-contributed native windows (the Calculator, Jorvy, custom
447 // native launchers).
448 // ------------------------------------------------------------------
449 /**
450 * Filter, applied to the body element a native window will render
451 * into, just BEFORE the user's `render( body )` callback runs.
452 * Payload: the `HTMLElement`; context: `{ windowId, config }`.
453 *
454 * Return the same element (or a wrapper) to intercept. Subscribers
455 * commonly use this to inject a consistent shell (padding,
456 * background, decorative chrome) around every native window
457 * without every plugin re-implementing the pattern.
458 */
459 NATIVE_WINDOW_BEFORE_RENDER: "desktop-mode.native-window.before-render",
460 /**
461 * Action, fires AFTER a native window's `render( body )` callback
462 * returns. Payload: `{ windowId, body, config }`. Observability
463 * hook — analytics / auto-focus / post-render measurement.
464 */
465 NATIVE_WINDOW_AFTER_RENDER: "desktop-mode.native-window.after-render",
466 /**
467 * Filter, applied when a native window is about to start its
468 * close animation. Return `false` to CANCEL the close — the
469 * window stays open. Payload: `true`; context: `{ windowId,
470 * config }`. Any non-`false` return (including `undefined`) lets
471 * the close proceed.
472 *
473 * Intended for "unsaved changes" guards: a calculator with a
474 * pending operation can prompt the user and abort the close
475 * mid-flight. Does NOT apply to iframe windows — their close is
476 * driven by browser navigation patterns the shell doesn't own.
477 */
478 NATIVE_WINDOW_BEFORE_CLOSE: "desktop-mode.native-window.before-close",
479 // ------------------------------------------------------------------
480 // Window-chrome customization framework. Plugins drive per-window
481 // appearance (theme, controls, slots, full chrome render) through
482 // the `wp.desktop.registerWindow*` registries; these hooks expose
483 // every resolution step so plugins can mutate or observe the
484 // chrome pipeline without owning a registration.
485 //
486 // Layers 1-3 (theme, controls, slots) are Stable. Layer 4 (chrome
487 // render) is Experimental — `WINDOW_CHROME_RENDER` may change.
488 // ------------------------------------------------------------------
489 /**
490 * Filter, applied to the resolved CSS-variable map for a window.
491 * Receives `Record< string, string >`; context: `{ windowId,
492 * config }`. Plugins return a mutated map to override or augment
493 * the per-window theme tokens — e.g. tint every Gutenberg
494 * window's title bar to brand colour.
495 *
496 * Stable since 0.6.0.
497 */
498 WINDOW_CHROME_THEME: "desktop-mode.window.chrome.theme",
499 /**
500 * Filter, applied to the resolved control list for a window.
501 * Receives `WindowControlDef[]`; context: `{ windowId, config,
502 * placement: 'left' | 'right' | 'controls' }`. Plugins return a
503 * mutated array to reorder, hide, or inject controls per-window.
504 *
505 * Stable since 0.6.0.
506 */
507 WINDOW_CHROME_CONTROLS: "desktop-mode.window.chrome.controls",
508 /**
509 * Filter, applied per slot when the chrome paints. Receives the
510 * slot host element; context: `{ windowId, slot, config }`.
511 * Plugins can mutate `host` (append decorative children, set
512 * inline styles) without owning a `WindowSlotDef` registration.
513 * The shell never reads the return value — this is an action-
514 * shaped filter so existing `addFilter` plumbing applies.
515 *
516 * Stable since 0.6.0.
517 */
518 WINDOW_CHROME_SLOT: "desktop-mode.window.chrome.slot",
519 /**
520 * Filter, applied to the chrome id selected for a window.
521 * Receives the resolved id (defaults to `'core/standard'`);
522 * context: `{ windowId, config }`. Returning a different id
523 * swaps the chrome registration. **Experimental** — chrome
524 * render contract may change.
525 *
526 * @since 0.6.0
527 */
528 WINDOW_CHROME_RENDER: "desktop-mode.window.chrome.render",
529 /**
530 * Action, fires after a window's chrome has been mounted /
531 * remounted. Payload: `{ windowId, chromeId }`. Subscribers can
532 * post-decorate the chrome (attach observers, anchor pickers).
533 *
534 * @since 0.6.0
535 */
536 WINDOW_CHROME_APPLIED: "desktop-mode.window.chrome.applied",
537 /**
538 * Action, fires after a window's theme tokens are applied to its
539 * outer element. Payload: `{ windowId, themeId, tokens }`. Lets
540 * plugins react to theme changes without diffing CSS variables.
541 *
542 * @since 0.6.0
543 */
544 WINDOW_CHROME_THEME_CHANGED: "desktop-mode.window.chrome.theme-changed",
545 /**
546 * Action, fires when a user clicks a desktop icon (a shortcut
547 * tile registered server-side via `desktop_mode_register_icon()`
548 * and rendered on the wallpaper). Payload: `{ id: string,
549 * target: 'window' | 'url' }`. Fires BEFORE the default open
550 * action — plugins cannot cancel the open from this hook, but
551 * can use it to track click-throughs or augment behaviour (e.g.
552 * play a sound, surface a confirmation toast).
553 *
554 * @since 0.11.0
555 */
556 DESKTOP_ICON_CLICKED: "desktop-mode.desktop-icon.clicked",
557 /**
558 * Action, fires after the wallpaper icon grid is rendered or
559 * re-rendered. Payload:
560 *
561 * {
562 * ids: string[]; // paint order
563 * container: HTMLElement; // <div class="desktop-mode-icons">
564 * tiles: ReadonlyMap<string, HTMLElement>; // id → tile <button>
565 * }
566 *
567 * Plugins that decorate icons with surfaces the framework doesn't
568 * natively expose (drag handles, status dots, cursor adornments)
569 * subscribe here so their decorations survive a live menu refresh
570 * that legitimately rebuilds the grid. The `container` and
571 * `tiles` map mirror the {@link DOCK_AFTER_RENDER}
572 * `tileElements` contract — reach into them directly instead of
573 * re-`querySelector`ing the rendered DOM.
574 *
575 * Notification badges have a first-class API since 0.24.0 —
576 * use `wp.desktop.icons.setBadge( id, count )` (and subscribe
577 * to {@link ICON_BADGE_CHANGED}) instead of decorating from
578 * here. The framework persists badge state across rebuilds, so
579 * a plugin that uses the API doesn't need to re-decorate on
580 * every render.
581 *
582 * Suppressed entirely when the rendered DOM is unchanged from
583 * the previous call (the fingerprint short-circuit upstream
584 * skips both the rebuild and this signal). When the icon list
585 * is empty the hook does not fire at all — the previous
586 * container is removed and no new one is appended.
587 *
588 * @since 0.21.0
589 * @since 0.25.0 — `container` + `tiles` added to the payload
590 * (`ids` retained for back-compat).
591 */
592 DESKTOP_ICONS_RENDERED: "desktop-mode.desktop-icons.rendered",
593 /**
594 * Action, fires whenever the badge count on a desktop icon
595 * changes. Payload: `{ iconId: string, count: number,
596 * previousCount: number }`. Symmetric to {@link DOCK_ITEM_APPENDED}
597 * and the dock/taskbar `wpd-dock-item-badge-changed` CustomEvent
598 * — the icon rail's lifecycle hook for badge transitions.
599 *
600 * Mirrors `desktop-mode/badge-changed` on the activity bus with
601 * `rail: 'icon'`. Subscribe to whichever surface fits — the
602 * activity channel composes across rails for global widgets,
603 * this hook fires only for icon-rail badges with the previous
604 * count carried alongside for delta-aware consumers.
605 *
606 * @since 0.24.0
607 */
608 ICON_BADGE_CHANGED: "desktop-mode.icon.badge-changed",
609 // ------------------------------------------------------------------
610 // Cross-plugin composition.
611 // ------------------------------------------------------------------
612 /**
613 * Action, fires ONCE after every shell-shipped `<wpd-*>` custom
614 * element has registered with `customElements`. Payload: `{
615 * tags: string[] }` — the list of registered tag names. Plugins
616 * that need to defer work until the component registry is
617 * complete (e.g. hydrate user content that uses these tags)
618 * subscribe here instead of polling `customElements.get()`.
619 */
620 COMPONENTS_REGISTERED: "desktop-mode.components.registered",
621 /**
622 * Action, fires after `wp.desktop.registerSystemTile()` inserts
623 * a tile into the unified dock. Payload: `{ id: string }`. Useful
624 * for plugins that want to decorate tiles they didn't register
625 * themselves — analytics, theming, per-tile badges.
626 */
627 DOCK_ITEM_APPENDED: "desktop-mode.dock.item-appended",
628 /**
629 * Action, fires after a system tile is removed from a rail
630 * via `Dock.removeSystemItem()` (typically the server-driven
631 * native-window-sync path on plugin deactivation). Payload:
632 * `{ id: string, placement: 'dock' | 'taskbar' }`. Symmetric
633 * to {@link DOCK_ITEM_APPENDED}; lets analytics / decorators /
634 * cleanup hooks see the full lifecycle without polling the DOM.
635 *
636 * @since 0.24.0
637 */
638 DOCK_ITEM_REMOVED: "desktop-mode.dock.item-removed",
639 // ------------------------------------------------------------------
640 // Dock decoration hooks — render-pipeline filters and actions the
641 // default `Dock` renderer fires while painting tiles. Plugins
642 // compose decoration (animations, classNames, wrappers, tooltips)
643 // without forking the renderer. Custom rail renderers SHOULD fire
644 // the same hooks for ecosystem compatibility — see
645 // `docs/examples/dock-decoration-hooks.md` for the contract.
646 //
647 // Every detail object carries `{ rail, orientation, dockId,
648 // container }` so a single subscriber can disambiguate when two
649 // rails coexist (Classic layout's left side bar + bottom dock).
650 // `dockId` matches the host element's `id` (e.g. `'desktop-mode-dock'`
651 // or `'desktop-mode-side-dock'`) and is the stable
652 // disambiguator — `rail` and `orientation` are convenience
653 // projections of where the renderer is painting.
654 // ------------------------------------------------------------------
655 /**
656 * Action, fires at the start of every dock paint pass — both the
657 * initial mount and every `replaceItems()` that follows on the
658 * live menu-refresh path. Payload `DockRenderContext`. Use this
659 * to invalidate cached per-render decoration state before the
660 * tiles repopulate.
661 *
662 * @since 0.18.0
663 */
664 DOCK_BEFORE_RENDER: "desktop-mode.dock.before-render",
665 /**
666 * Action, fires once every menu and system tile has landed in
667 * the DOM for a paint pass. Payload `DockRenderContext` plus a
668 * frozen `tileElements: ReadonlyMap<string, HTMLElement>` so a
669 * plugin can decorate every tile in one sweep. Symmetric to
670 * {@link DOCK_BEFORE_RENDER}.
671 *
672 * @since 0.18.0
673 */
674 DOCK_AFTER_RENDER: "desktop-mode.dock.after-render",
675 /**
676 * Filter, runs once per tile while the renderer is composing the
677 * className list. Plugins may add, remove, or reorder classes.
678 * Signature: `( classes: string[], detail: DockTileContext ) =>
679 * string[]`. Order is preserved.
680 *
681 * @since 0.18.0
682 */
683 DOCK_TILE_CLASS: "desktop-mode.dock.tile-class",
684 /**
685 * Filter, runs once per tile after the renderer finishes building
686 * the element but before it lands in the DOM. Return the same
687 * element with mutations, or replace with a wrapper — the shell
688 * inserts whatever you return. Signature:
689 * `( el: HTMLElement, detail: DockTileContext ) => HTMLElement`.
690 *
691 * Returning a different node still has to expose a stable
692 * `[data-menu-slug="<id>"]` (or `[data-system-id="<id>"]`)
693 * descendant for active-state / badge updates to find the tile;
694 * wrap, don't replace.
695 *
696 * @since 0.18.0
697 */
698 DOCK_TILE_ELEMENT: "desktop-mode.dock.tile-element",
699 /**
700 * Action, fires once per tile after it has been inserted into
701 * the DOM. Payload `DockTileContext` plus the resolved `el`. Use
702 * for post-insertion decoration where computed layout matters
703 * (measurements, IntersectionObserver bindings, etc.).
704 *
705 * @since 0.18.0
706 */
707 DOCK_TILE_RENDERED: "desktop-mode.dock.tile-rendered",
708 /**
709 * Filter, resolves the tooltip text for a tile. Runs once at
710 * bind time so the dock doesn't re-filter on every pointerenter.
711 * Signature: `( label: string, detail: DockTileContext ) =>
712 * string`. Return an empty string to suppress the tooltip.
713 *
714 * @since 0.18.0
715 */
716 DOCK_TILE_TOOLTIP: "desktop-mode.dock.tile-tooltip",
717 /**
718 * Filter, resolves the body content of a single hover-peek card.
719 * Runs once per card build (i.e., on every show of the peek for
720 * a multi-instance dock tile that has ≥1 open window). Lets a
721 * plugin render a custom thumbnail, status block, or any other
722 * markup inside the card in place of (or alongside) the default
723 * mini-window styling.
724 *
725 * Signature:
726 * ( body: HTMLElement, detail: DockPeekCardContext ) => HTMLElement
727 *
728 * Where `body` is the `<span class="desktop-mode-dock-peek__card-body">`
729 * element that the peek would otherwise populate with ghosted
730 * content lines. The filter may:
731 * - Mutate `body` in place (e.g., append a custom child) and
732 * return it.
733 * - Empty `body` and append plugin-owned children.
734 * - Return an entirely different element to replace `body`.
735 *
736 * `detail.window` is the live `Window` instance the card represents
737 * — plugins can read `window.config`, call `window.getCurrentUrl()`,
738 * subscribe to lifecycle events, etc. `detail.item` is the dock
739 * item descriptor (id / title / icon / url).
740 *
741 * The filter is invoked under the `applyFilters` namespace
742 * `desktop-mode.dock.peek-card-content`.
743 *
744 * @since 0.6.2
745 */
746 DOCK_PEEK_CARD_CONTENT: "desktop-mode.dock.peek-card-content",
747 /**
748 * Filter, runs once per peek card right before it's appended to
749 * the popover. Receives the fully-built default card (with its
750 * mini-window chrome already populated) and can return either
751 * the same node, a mutated version, or an entirely different
752 * element to replace the card outright. Use this when the
753 * `peek-card-content` body filter isn't enough — e.g., when a
754 * plugin wants to swap the whole card chrome (custom titlebar,
755 * different shape) or wrap the card in a third-party component.
756 *
757 * Signature:
758 * ( card: HTMLElement, detail: DockPeekCardContext ) => HTMLElement
759 *
760 * If a plugin returns a brand-new node, it is responsible for
761 * preserving anything the peek relies on:
762 * - The `desktop-mode-dock-peek__card` class (used by the
763 * fan-out animation timing + hover styles).
764 * - A `click` handler if the card should still focus the
765 * window. The default click handler lives on the original
766 * node — replacing the node loses it.
767 *
768 * @since 0.6.2
769 */
770 DOCK_PEEK_CARD_ELEMENT: "desktop-mode.dock.peek-card-element",
771 // ------------------------------------------------------------------
772 // Overview / Arrange lifecycle actions.
773 //
774 // The "Arrange" admin-bar menu drives two layout algorithms —
775 // Cascade (instantly reposition every window in a staggered
776 // stack) and Overview (zoom-out grid view with click-to-focus).
777 // These hooks surface the state transitions so plugins can
778 // instrument analytics, apply custom transitions, override
779 // thumbnail decorations, etc. All actions; a filter for
780 // mutating the overview layout may be added later if plugins
781 // want to reorder or group thumbnails.
782 // ------------------------------------------------------------------
783 /** Action, fires before the overview enter animation starts. */
784 OVERVIEW_ENTERING: "desktop-mode.overview.entering",
785 /** Action, fires once the overview enter animation has completed. */
786 OVERVIEW_ENTERED: "desktop-mode.overview.entered",
787 /**
788 * Action, fires at the start of the overview-exit animation.
789 * Payload: `{ windowId?: string, reason: 'select' | 'cancel' }` —
790 * `windowId` set when the user clicked a thumbnail (reason
791 * 'select'); omitted when the user pressed Escape or clicked
792 * the backdrop (reason 'cancel').
793 */
794 OVERVIEW_EXITING: "desktop-mode.overview.exiting",
795 /** Action, fires once the overview-exit animation has settled. */
796 OVERVIEW_EXITED: "desktop-mode.overview.exited",
797 /** Action, fires when the cursor enters a thumbnail. Payload `{ windowId }`. */
798 OVERVIEW_WINDOW_HOVER: "desktop-mode.overview.window-hover",
799 /** Action, fires when the cursor leaves a thumbnail. Payload `{ windowId }`. */
800 OVERVIEW_WINDOW_UNHOVER: "desktop-mode.overview.window-unhover",
801 /** Action, fires the instant a thumbnail click is registered (before exit + maximize kick in). Payload `{ windowId }`. */
802 OVERVIEW_WINDOW_CLICK: "desktop-mode.overview.window-click",
803 /** Action, fires before cascade computes + applies new positions. Payload `{ windowCount }`. */
804 ARRANGE_CASCADE_STARTING: "desktop-mode.arrange.cascade.starting",
805 /** Action, fires after cascade has positioned every window. Payload `{ windowCount }`. */
806 ARRANGE_CASCADE_APPLIED: "desktop-mode.arrange.cascade.applied",
807 /** Action, fires before tile computes + applies new positions. Payload `{ windowCount, cols, rows }`. */
808 ARRANGE_TILE_STARTING: "desktop-mode.arrange.tile.starting",
809 /** Action, fires after tile has positioned every window. Payload `{ windowCount, cols, rows }`. */
810 ARRANGE_TILE_APPLIED: "desktop-mode.arrange.tile.applied",
811 /**
812 * Filter on the tile-grid dimensions chosen by the built-in
813 * algorithm. Receives `{ cols, rows }` plus a context arg
814 * `{ windowCount, areaWidth, areaHeight }`. Plugins can return
815 * a different `{ cols, rows }` to enforce a custom layout
816 * (fixed-column newsroom, golden-ratio cells, etc.). Returned
817 * values are validated — non-positive integers, or a product
818 * smaller than `windowCount`, fall back to the original.
819 */
820 ARRANGE_TILE_DIMENSIONS: "desktop-mode.arrange.tile.dimensions",
821 /** Action, fires when snap-to-grid is toggled. Payload `{ enabled }`. */
822 ARRANGE_SNAP_CHANGED: "desktop-mode.arrange.snap.changed",
823 /**
824 * Filter on the snap-grid cell size. Receives
825 * `{ cellWidth, cellHeight }` plus a context arg
826 * `{ areaWidth, areaHeight }`. Plugins can return different
827 * dimensions to enforce a Tetris-style fixed grid, a musical
828 * staff aspect, etc. Non-positive returns fall back to the
829 * original.
830 */
831 ARRANGE_SNAP_CELL_SIZE: "desktop-mode.arrange.snap.cell-size",
832 /**
833 * Action, fires when the user clicks a plugin-registered entry in
834 * the Arrange admin-bar submenu (items added via the
835 * `desktop_mode_arrange_menu_items` PHP filter). Payload `{ id }`
836 * where `id` is the item's `id` field as registered. Plugins
837 * subscribe here to run their custom arrangement logic.
838 */
839 ARRANGE_CUSTOM_ACTION: "desktop-mode.arrange.custom-action",
840 // ------------------------------------------------------------------
841 // Snap-zones — Windows-style edge snapping with a split-overview
842 // picker to fill the opposite half after commit.
843 // ------------------------------------------------------------------
844 /**
845 * Action, fires when the drag cursor enters a snap zone and the
846 * shell shows the target-position preview. Payload
847 * `{ windowId, zone: 'left' | 'right' }`.
848 */
849 SNAP_ZONE_PENDING: "desktop-mode.snap.zone-pending",
850 /**
851 * Action, fires when the drag cursor leaves the snap zone without
852 * releasing — the preview disappears. Payload `{ windowId }`.
853 */
854 SNAP_ZONE_CANCELED: "desktop-mode.snap.zone-canceled",
855 /**
856 * Action, fires once the window has animated into its snapped
857 * bounds. Payload `{ windowId, zone: 'left' | 'right' }`.
858 */
859 SNAP_ZONE_COMMITTED: "desktop-mode.snap.zone-committed",
860 /**
861 * Action, fires when a user picks a thumbnail from the split
862 * overview to fill the opposite half. Payload
863 * `{ windowId, zone: 'left' | 'right' }`.
864 */
865 SNAP_SPLIT_FILLED: "desktop-mode.snap.split-filled",
866 // ------------------------------------------------------------------
867 // Widgets — the right-side column. Widgets paint above the
868 // wallpaper but beneath windows. Lifecycle mirrors canvas
869 // wallpapers: register via filter, mount/unmount actions bracket
870 // each paint, mount-failed fires on sync throws / async rejects.
871 // ------------------------------------------------------------------
872 /** Filter, receives the widget registry array. */
873 WIDGETS: "desktop-mode.widgets",
874 /** Action before a widget mounts. Payload `{ id, container, ctx }`. */
875 WIDGET_MOUNTING: "desktop-mode.widget.mounting",
876 /** Action after a widget mounts successfully. Payload `{ id, container, ctx }`. */
877 WIDGET_MOUNTED: "desktop-mode.widget.mounted",
878 /** Action before a widget tears down. Payload `{ id }`. */
879 WIDGET_UNMOUNTING: "desktop-mode.widget.unmounting",
880 /** Action when a widget's mount throws / rejects. Payload `{ id, error }`. */
881 WIDGET_MOUNT_FAILED: "desktop-mode.widget.mount-failed",
882 /** Action when the user adds a widget via the picker. Payload `{ id }`. */
883 WIDGET_ADDED: "desktop-mode.widget.added",
884 /** Action when the user removes a widget via the card's × button. Payload `{ id }`. */
885 WIDGET_REMOVED: "desktop-mode.widget.removed",
886 // ------------------------------------------------------------------
887 // Virtual-desktop ("Spaces") lifecycle actions.
888 //
889 // Spaces let users group windows into separate workspaces and flip
890 // between them from the overview top bar. These hooks expose every
891 // state change so plugins can persist per-space state, sync custom
892 // indicators, or react to the user's workspace context.
893 // ------------------------------------------------------------------
894 /** Action, fires when a new desktop is created. Payload `{ desktopId }`. */
895 DESKTOP_CREATED: "desktop-mode.desktop.created",
896 /** Action, fires when a desktop is closed. Payload `{ desktopId, migratedTo }`. */
897 DESKTOP_CLOSED: "desktop-mode.desktop.closed",
898 /** Action, fires when the active desktop changes. Payload `{ from, to }`. */
899 DESKTOP_SWITCHED: "desktop-mode.desktop.switched",
900 /**
901 * Filter. Returns the id of the "primary" desktop — the one the
902 * shell treats as canonical for batch operations. Receives the
903 * default (first desktop's id) and the full `Desktop[]` list.
904 * @since 0.14.0
905 */
906 PRIMARY_DESKTOP_ID: "desktop-mode.primary-desktop-id",
907 // ------------------------------------------------------------------
908 // Batch window operations.
909 // ------------------------------------------------------------------
910 /**
911 * Action, fires before {@link WindowManager.closeAll} starts
912 * iterating. Payload `{ candidates: Window[] }` — every window the
913 * shell is about to close (after `exceptIds` was applied).
914 * @since 0.14.0
915 */
916 WINDOWS_BEFORE_CLOSE_ALL: "desktop-mode.windows.before-close-all",
917 /**
918 * Filter, runs inside {@link WindowManager.closeAll}. Receives the
919 * candidate `Window[]` list and returns the (possibly trimmed) list
920 * that will actually be closed. Plugins use this to PROTECT specific
921 * windows from a bulk close — e.g. keep the active draft open.
922 * Returning an empty array cancels the close entirely.
923 * @since 0.14.0
924 */
925 WINDOWS_CLOSE_ALL: "desktop-mode.windows.close-all",
926 /**
927 * Action, fires after {@link WindowManager.closeAll} has finished.
928 * Payload `{ closed: number, skipped: Window[] }`.
929 * @since 0.14.0
930 */
931 WINDOWS_AFTER_CLOSE_ALL: "desktop-mode.windows.after-close-all",
932 // ------------------------------------------------------------------
933 // Slash-command lifecycle.
934 // ------------------------------------------------------------------
935 /**
936 * Filter. Runs immediately before a command's `run()` is invoked.
937 * Receives `{ proceed: true, slug, args, command }` and may return
938 * the same shape with `proceed: false` to cancel the run.
939 * @since 0.14.0
940 */
941 COMMAND_BEFORE_RUN: "desktop-mode.command.before-run",
942 /**
943 * Action, fires after a command's `run()` resolves successfully.
944 * Payload `{ slug, args, command, result }`.
945 * @since 0.14.0
946 */
947 COMMAND_AFTER_RUN: "desktop-mode.command.after-run",
948 /**
949 * Action, fires when a command's `run()` throws. Payload
950 * `{ slug, args, command, error }`.
951 * @since 0.14.0
952 */
953 COMMAND_ERROR: "desktop-mode.command.error",
954 // ------------------------------------------------------------------
955 // Shell-level lifecycle actions.
956 // ------------------------------------------------------------------
957 /**
958 * Action, fires (debounced) after the browser viewport stops
959 * resizing. Payload `{ width, height }` describes the shell's
960 * bounding rect — plugins that render canvas-driven UIs hook here
961 * to adjust their render surface.
962 */
963 SHELL_RESIZED: "desktop-mode.shell.resized",
964 /**
965 * Action mirroring `document.visibilitychange` for the shell as a
966 * whole. Payload `{ state: 'visible' | 'hidden' }`. Different from
967 * the wallpaper-specific visibility action in that it fires
968 * regardless of which wallpaper (if any) is active.
969 */
970 SHELL_VISIBILITY: "desktop-mode.shell.visibility",
971 /**
972 * Action — fires when a `wp.desktop.connect()` connection
973 * completes its iframe handshake. Payload:
974 * `{ connectionId, targetWindowId, topics }`.
975 *
976 * @since 0.17.0
977 */
978 CONNECTION_OPENED: "desktop-mode.connection.opened",
979 /**
980 * Action — fires when a connection tears down. Payload:
981 * `{ connectionId, reason: 'disconnect' | 'window-closed' | 'navigated' }`.
982 *
983 * @since 0.17.0
984 */
985 CONNECTION_CLOSED: "desktop-mode.connection.closed",
986 /**
987 * Action — fires for every message routed through a connection.
988 * Payload: `{ connectionId, topic, direction: 'in' | 'out' }`.
989 * Used for debug consoles + traffic auditing; high-volume topics
990 * fire this many times per second, so subscribers should be
991 * cheap.
992 *
993 * @since 0.17.0
994 */
995 CONNECTION_MESSAGE: "desktop-mode.connection.message",
996 /**
997 * Filter — fires when an iframe calls
998 * `wp.desktop.iframe.requestConnection()`. Default value is
999 * `true` (accept). Return `false` to reject, or an object
1000 * `{ topics: string[] }` to accept while narrowing the topic
1001 * list. `$context` carries `{ windowId, requestId, topics }`.
1002 *
1003 * @since 0.18.0
1004 */
1005 IFRAME_CONNECTION_REQUEST: "desktop-mode.iframe.connection-request",
1006 // ------------------------------------------------------------------
1007 // OS-file drop manager (since 0.30.0). Catches files dragged from
1008 // the user's host OS (Finder / Explorer / Nautilus) onto any
1009 // desktop-mode surface and routes them through a confirmation
1010 // dialog before uploading to the Media Library. Authoritative
1011 // constants live in `src/os-file-drop/hooks.ts`; mirrored here so
1012 // every hook the shell fires is reachable from a single `HOOKS`
1013 // import. See `docs/examples/os-file-drop.md`.
1014 // ------------------------------------------------------------------
1015 /** Filter — `(files: File[], ctx) => File[]`, before mime/size check. */
1016 FILE_DROP_FILES_DETECTED: "desktop-mode.drop.files-detected",
1017 /** Action — `{ rejections, context }` for files that failed policy. */
1018 FILE_DROP_FILES_REJECTED: "desktop-mode.drop.files-rejected",
1019 /** Filter — `(entry, ctx) => entry`, per-file dialog defaults. */
1020 FILE_DROP_DIALOG_FIELDS: "desktop-mode.drop.dialog-fields",
1021 /** Filter — `(payload, ctx) => payload | null`, last call before POST. */
1022 FILE_DROP_BEFORE_UPLOAD: "desktop-mode.drop.before-upload",
1023 /** Action — `{ file, fields, context, abort }` once XHR is open and about to send. @since 0.31.0 */
1024 FILE_DROP_UPLOAD_STARTED: "desktop-mode.drop.upload-started",
1025 /** Action — `{ file, fields, context, loaded, total, indeterminate }` per progress tick. @since 0.31.0 */
1026 FILE_DROP_UPLOAD_PROGRESS: "desktop-mode.drop.upload-progress",
1027 /** Action — `{ file, result, fields, context }` after successful upload. `file` since 0.31.0. */
1028 FILE_DROP_AFTER_UPLOAD: "desktop-mode.drop.after-upload",
1029 /** Action — `{ file, error, context }` on upload failure. */
1030 FILE_DROP_UPLOAD_FAILED: "desktop-mode.drop.upload-failed"
1031 };
1032 let _whenReadySeq = 0;
1033 function whenReady(cb) {
1034 if (didAction(HOOKS.INIT) > 0) {
1035 Promise.resolve().then(cb);
1036 return;
1037 }
1038 const ns = `desktop-mode/when-ready-${++_whenReadySeq}`;
1039 addAction(HOOKS.INIT, ns, cb);
1040 }
1041 function isReady() {
1042 return didAction(HOOKS.INIT) > 0;
1043 }
1044 let inflight$1 = null;
1045 function isLoaded$1() {
1046 return !!window.desktopModeWindowSystem;
1047 }
1048 function injectScript$1(scriptUrl) {
1049 return new Promise((resolve2, reject) => {
1050 const existing = document.querySelector(
1051 'script[data-desktop-mode-window-system="1"]'
1052 );
1053 const finish = () => {
1054 if (isLoaded$1()) {
1055 resolve2();
1056 return;
1057 }
1058 reject(
1059 new Error(
1060 "[desktop-mode] window-system bundle loaded but did not register `window.desktopModeWindowSystem`."
1061 )
1062 );
1063 };
1064 if (existing) {
1065 if (isLoaded$1()) {
1066 finish();
1067 } else {
1068 existing.addEventListener("load", finish);
1069 existing.addEventListener(
1070 "error",
1071 () => reject(new Error("failed to load window-system bundle"))
1072 );
1073 }
1074 return;
1075 }
1076 const s = document.createElement("script");
1077 s.src = scriptUrl;
1078 s.async = true;
1079 s.dataset.desktopModeWindowSystem = "1";
1080 s.addEventListener("load", finish);
1081 s.addEventListener(
1082 "error",
1083 () => reject(new Error("failed to load window-system bundle"))
1084 );
1085 document.head.appendChild(s);
1086 });
1087 }
1088 function windowSystemBundleUrl() {
1089 const cfg = window.desktopModeConfig;
1090 return cfg?.windowSystemBundleUrl ?? "";
1091 }
1092 function preloadWindowSystem(scriptUrl) {
1093 if (!scriptUrl || isLoaded$1() || inflight$1) {
1094 return;
1095 }
1096 inflight$1 = injectScript$1(scriptUrl).catch((err) => {
1097 inflight$1 = null;
1098 if (typeof console !== "undefined") {
1099 console.warn(
1100 "[desktop-mode] window-system preload failed; will retry on first open():",
1101 err
1102 );
1103 }
1104 });
1105 }
1106 async function ensureWindowSystemLoaded(scriptUrl) {
1107 if (isLoaded$1()) {
1108 return window.desktopModeWindowSystem;
1109 }
1110 if (!scriptUrl) {
1111 const fn = window.desktopModeWindowSystem;
1112 if (fn) {
1113 return fn;
1114 }
1115 throw new Error(
1116 "[desktop-mode] ensureWindowSystemLoaded(): no bundle URL configured and `window.desktopModeWindowSystem` is not pre-registered."
1117 );
1118 }
1119 if (!inflight$1) {
1120 inflight$1 = injectScript$1(scriptUrl);
1121 }
1122 await inflight$1;
1123 return window.desktopModeWindowSystem;
1124 }
1125 const CANARY_TAG = "wpd-confirm-dialog";
1126 let inflight = null;
1127 function isLoaded() {
1128 return typeof window.customElements !== "undefined" && !!window.customElements.get(CANARY_TAG);
1129 }
1130 function injectScript(scriptUrl) {
1131 return new Promise((resolve2, reject) => {
1132 const existing = document.querySelector(
1133 'script[data-desktop-mode-shell-overlays="1"]'
1134 );
1135 const finish = () => {
1136 if (isLoaded()) {
1137 resolve2();
1138 return;
1139 }
1140 reject(
1141 new Error(
1142 "[desktop-mode] shell-overlays bundle loaded but did not register the overlay components."
1143 )
1144 );
1145 };
1146 if (existing) {
1147 if (isLoaded()) {
1148 finish();
1149 } else {
1150 existing.addEventListener("load", finish);
1151 existing.addEventListener(
1152 "error",
1153 () => reject(new Error("failed to load shell-overlays bundle"))
1154 );
1155 }
1156 return;
1157 }
1158 const s = document.createElement("script");
1159 s.src = scriptUrl;
1160 s.async = true;
1161 s.dataset.desktopModeShellOverlays = "1";
1162 s.addEventListener("load", finish);
1163 s.addEventListener(
1164 "error",
1165 () => reject(new Error("failed to load shell-overlays bundle"))
1166 );
1167 document.head.appendChild(s);
1168 });
1169 }
1170 function preloadShellOverlays(scriptUrl) {
1171 if (!scriptUrl || isLoaded() || inflight) {
1172 return;
1173 }
1174 inflight = injectScript(scriptUrl).catch((err) => {
1175 inflight = null;
1176 if (typeof console !== "undefined") {
1177 console.warn(
1178 "[desktop-mode] shell-overlays preload failed; will retry on first overlay use:",
1179 err
1180 );
1181 }
1182 });
1183 }
1184 function ensureShellOverlaysLoaded(scriptUrl) {
1185 if (isLoaded()) {
1186 return Promise.resolve();
1187 }
1188 if (!scriptUrl) {
1189 return Promise.resolve();
1190 }
1191 if (!inflight) {
1192 inflight = injectScript(scriptUrl);
1193 }
1194 return inflight;
1195 }
1196 function shellOverlaysBundleUrl() {
1197 const cfg = window.desktopModeConfig;
1198 return cfg?.shellOverlaysBundleUrl ?? "";
1199 }
1200 function openWithShellOverlays(isStillCurrent, fn) {
1201 const url = shellOverlaysBundleUrl();
1202 if (isLoaded() || !url) {
1203 fn();
1204 return;
1205 }
1206 void ensureShellOverlaysLoaded(url).then(() => {
1207 if (!isStillCurrent()) {
1208 return;
1209 }
1210 fn();
1211 }).catch((err) => {
1212 if (typeof console !== "undefined") {
1213 console.warn(
1214 "[desktop-mode] shell-overlays failed to load; menu/dialog suppressed:",
1215 err
1216 );
1217 }
1218 });
1219 }
1220 const TEXT_DOMAIN = "desktop-mode";
1221 function i18n() {
1222 return window.wp?.i18n;
1223 }
1224 function __(text, domain = TEXT_DOMAIN) {
1225 return i18n()?.__(text, domain) ?? text;
1226 }
1227 function _n(single, plural, number, domain = TEXT_DOMAIN) {
1228 return i18n()?._n(single, plural, number, domain) ?? (number === 1 ? single : plural);
1229 }
1230 function sprintf(format, ...args) {
1231 const impl = i18n()?.sprintf;
1232 if (impl) {
1233 return impl(format, ...args);
1234 }
1235 let i = 0;
1236 return format.replace(/%[sd]/g, () => String(args[i++] ?? ""));
1237 }
1238 function isValidGrid(candidate, windowCount) {
1239 if (!candidate || typeof candidate !== "object") {
1240 return false;
1241 }
1242 const c = candidate.cols;
1243 const r = candidate.rows;
1244 if (typeof c !== "number" || typeof r !== "number") {
1245 return false;
1246 }
1247 if (!Number.isFinite(c) || !Number.isFinite(r)) {
1248 return false;
1249 }
1250 if (c < 1 || r < 1) {
1251 return false;
1252 }
1253 return Math.floor(c) * Math.floor(r) >= windowCount;
1254 }
1255 function isValidCellSize(candidate) {
1256 if (!candidate || typeof candidate !== "object") {
1257 return false;
1258 }
1259 const w = candidate.cellWidth;
1260 const h = candidate.cellHeight;
1261 if (typeof w !== "number" || typeof h !== "number") {
1262 return false;
1263 }
1264 if (!Number.isFinite(w) || !Number.isFinite(h)) {
1265 return false;
1266 }
1267 return w > 0 && h > 0;
1268 }
1269 function pickGridDimensions(n, width, height) {
1270 if (n <= 1) {
1271 return { cols: 1, rows: 1 };
1272 }
1273 const areaAspect = width / Math.max(1, height);
1274 const max = 6;
1275 let best = { cols: n, rows: 1, score: Infinity };
1276 for (let cols = 1; cols <= Math.min(max, n); cols++) {
1277 const rows = Math.min(max, Math.ceil(n / cols));
1278 if (cols * rows < n) {
1279 continue;
1280 }
1281 const cellAspect = width / cols / Math.max(1, height / rows);
1282 const aspectDelta = Math.abs(cellAspect - areaAspect);
1283 const emptyCells = cols * rows - n;
1284 const score = aspectDelta + emptyCells * 0.05;
1285 if (score < best.score) {
1286 best = { cols, rows, score };
1287 }
1288 }
1289 return { cols: best.cols, rows: best.rows };
1290 }
1291 function computeOverviewLayout(windows, rect, topInset = 0) {
1292 const n = windows.length;
1293 if (n === 0) {
1294 return [];
1295 }
1296 const cols = Math.ceil(Math.sqrt(n));
1297 const rows = Math.ceil(n / cols);
1298 const padding = 40;
1299 const gap = 24;
1300 const labelReserve = 34;
1301 const cellWidth = (rect.width - padding * 2 - gap * (cols - 1)) / cols;
1302 const cellHeight = (rect.height - padding * 2 - topInset - gap * (rows - 1)) / rows;
1303 const thumbCellHeight = Math.max(40, cellHeight - labelReserve);
1304 return windows.map((win, i) => {
1305 const col = i % cols;
1306 const row = Math.floor(i / cols);
1307 const cellX = rect.left + padding + col * (cellWidth + gap);
1308 const cellY = rect.top + topInset + padding + row * (cellHeight + gap) + labelReserve;
1309 const sourceW = win.element.offsetWidth;
1310 const sourceH = win.element.offsetHeight;
1311 const scale = Math.min(
1312 cellWidth / sourceW,
1313 thumbCellHeight / sourceH
1314 );
1315 const scaledW = sourceW * scale;
1316 const scaledH = sourceH * scale;
1317 return {
1318 win,
1319 x: cellX + (cellWidth - scaledW) / 2,
1320 y: cellY + (thumbCellHeight - scaledH) / 2,
1321 scale
1322 };
1323 });
1324 }
1325 const OVERVIEW_TOP_BAR_RESERVE = 120;
1326 function enterOverview(mgr) {
1327 if (mgr._overviewActive) {
1328 return;
1329 }
1330 const onActive = mgr._stack.filter(
1331 (w) => w.config.desktopId === mgr._activeDesktopId
1332 );
1333 if (onActive.length > 0 && onActive.every((w) => w.state === "minimized")) {
1334 for (const w of onActive) {
1335 try {
1336 w.restore();
1337 } catch (err) {
1338 if (typeof console !== "undefined") {
1339 console.error(
1340 "[desktop-mode] enterOverview: window.restore() threw for",
1341 w.id,
1342 err
1343 );
1344 }
1345 }
1346 }
1347 }
1348 const eligible = mgr._stack.filter(
1349 (w) => w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
1350 );
1351 mgr._overviewActive = true;
1352 doAction(HOOKS.OVERVIEW_ENTERING, {});
1353 mgr._overviewSnapshot.clear();
1354 for (const w of eligible) {
1355 mgr._overviewSnapshot.set(w.id, {
1356 transform: w.element.style.transform || "",
1357 transition: w.element.style.transition || ""
1358 });
1359 }
1360 for (const w of eligible) {
1361 if (w.state === "fullscreen") {
1362 w.toggleFullscreen();
1363 }
1364 }
1365 const currentRect = mgr._desktop.getBoundingClientRect();
1366 const docks = Array.from(
1367 document.querySelectorAll(".desktop-mode-dock")
1368 );
1369 let reclaimedWidth = 0;
1370 for (const d of docks) {
1371 const r = d.getBoundingClientRect();
1372 const verticallyOverlaps = r.bottom > currentRect.top && r.top < currentRect.bottom;
1373 const isHorizontalRail = r.height > r.width;
1374 if (verticallyOverlaps && isHorizontalRail) {
1375 reclaimedWidth += r.width;
1376 }
1377 }
1378 const targetRect = new DOMRect(
1379 0,
1380 0,
1381 currentRect.width + reclaimedWidth,
1382 currentRect.height
1383 );
1384 mgr._desktop.classList.add("desktop-mode-area--overview");
1385 const shell = document.getElementById("desktop-mode-shell");
1386 shell?.classList.add("desktop-mode-shell--overview");
1387 mgr._overviewTopBar = buildOverviewTopBar(mgr);
1388 mgr._desktop.appendChild(mgr._overviewTopBar);
1389 const layout = computeOverviewLayout(
1390 eligible,
1391 targetRect,
1392 OVERVIEW_TOP_BAR_RESERVE
1393 );
1394 mgr._overviewLabels.clear();
1395 for (const item of layout) {
1396 const el = item.win.element;
1397 el.classList.add("desktop-mode-window--overview");
1398 const dx = item.x - el.offsetLeft;
1399 const dy = item.y - el.offsetTop;
1400 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
1401 const label = createOverviewLabel(item);
1402 el.insertAdjacentElement("afterend", label);
1403 mgr._overviewLabels.set(item.win.id, label);
1404 }
1405 const pressTargetForEvent = (e) => {
1406 const target = e.target;
1407 const winEl = target?.closest(
1408 ".desktop-mode-window--overview"
1409 );
1410 if (winEl) {
1411 return {
1412 id: winEl.id.replace(/^wp-window-/, ""),
1413 element: winEl
1414 };
1415 }
1416 if (target === mgr._desktop) {
1417 return { id: "backdrop", element: mgr._desktop };
1418 }
1419 return null;
1420 };
1421 mgr._overviewPointerDownHandler = (e) => {
1422 if (e.button !== 0) {
1423 mgr._overviewPressTarget = null;
1424 return;
1425 }
1426 mgr._overviewPressTarget = pressTargetForEvent(e);
1427 if (mgr._overviewPressTarget) {
1428 e.preventDefault();
1429 e.stopPropagation();
1430 }
1431 };
1432 mgr._overviewPointerUpHandler = (e) => {
1433 if (e.button !== 0) {
1434 return;
1435 }
1436 const pressed = mgr._overviewPressTarget;
1437 mgr._overviewPressTarget = null;
1438 if (!pressed) {
1439 return;
1440 }
1441 const rect = pressed.element.getBoundingClientRect();
1442 const inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
1443 if (!inside) {
1444 return;
1445 }
1446 e.preventDefault();
1447 e.stopPropagation();
1448 if (pressed.id === "backdrop") {
1449 exitOverview(mgr);
1450 return;
1451 }
1452 const selected = mgr.getById(pressed.id);
1453 doAction(HOOKS.OVERVIEW_WINDOW_CLICK, { windowId: pressed.id });
1454 exitOverview(mgr, selected, true);
1455 };
1456 mgr._overviewKeyHandler = (e) => {
1457 if (e.key === "Escape") {
1458 exitOverview(mgr);
1459 return;
1460 }
1461 if (e.key === "Enter") {
1462 e.preventDefault();
1463 if (mgr._overviewAddTileFocused) {
1464 commitAddTile(mgr);
1465 return;
1466 }
1467 exitOverview(mgr);
1468 }
1469 };
1470 mgr._desktop.addEventListener(
1471 "pointerdown",
1472 mgr._overviewPointerDownHandler,
1473 true
1474 );
1475 mgr._desktop.addEventListener(
1476 "pointerup",
1477 mgr._overviewPointerUpHandler,
1478 true
1479 );
1480 mgr._overviewClickBlocker = (e) => {
1481 const target = e.target;
1482 if (target?.closest(".desktop-mode-overview-top-bar")) {
1483 return;
1484 }
1485 e.stopPropagation();
1486 e.preventDefault();
1487 };
1488 mgr._desktop.addEventListener(
1489 "click",
1490 mgr._overviewClickBlocker,
1491 true
1492 );
1493 document.addEventListener("keydown", mgr._overviewKeyHandler);
1494 mgr._lastOverviewHoverId = null;
1495 mgr._overviewMouseHandler = (e) => {
1496 const target = e.target;
1497 const winEl = target?.closest(
1498 ".desktop-mode-window--overview"
1499 );
1500 const newId = winEl ? winEl.id.replace(/^wp-window-/, "") : null;
1501 if (newId === mgr._lastOverviewHoverId) {
1502 return;
1503 }
1504 if (mgr._lastOverviewHoverId) {
1505 doAction(HOOKS.OVERVIEW_WINDOW_UNHOVER, {
1506 windowId: mgr._lastOverviewHoverId
1507 });
1508 }
1509 if (newId) {
1510 doAction(HOOKS.OVERVIEW_WINDOW_HOVER, { windowId: newId });
1511 }
1512 mgr._lastOverviewHoverId = newId;
1513 };
1514 mgr._desktop.addEventListener("mouseover", mgr._overviewMouseHandler);
1515 window.setTimeout(() => {
1516 if (mgr._overviewActive) {
1517 doAction(HOOKS.OVERVIEW_ENTERED, {});
1518 }
1519 }, 300);
1520 }
1521 function buildOverviewTopBar(mgr) {
1522 const bar = document.createElement("div");
1523 bar.className = "desktop-mode-overview-top-bar";
1524 const list2 = document.createElement("div");
1525 list2.className = "desktop-mode-overview-top-bar__list";
1526 bar.appendChild(list2);
1527 for (const d of mgr._desktops) {
1528 list2.appendChild(buildDesktopTile(mgr, d));
1529 }
1530 const addTile = document.createElement("button");
1531 addTile.type = "button";
1532 addTile.className = "desktop-mode-overview-top-bar__tile desktop-mode-overview-top-bar__tile--add";
1533 if (mgr._overviewAddTileFocused) {
1534 addTile.classList.add(
1535 "desktop-mode-overview-top-bar__tile--cursor"
1536 );
1537 }
1538 addTile.setAttribute("aria-label", __("Add new desktop"));
1539 addTile.innerHTML = '<span class="desktop-mode-overview-top-bar__tile-plus" aria-hidden="true">+</span>';
1540 addTile.addEventListener("click", (e) => {
1541 e.preventDefault();
1542 e.stopPropagation();
1543 commitAddTile(mgr);
1544 });
1545 list2.appendChild(addTile);
1546 return bar;
1547 }
1548 function commitAddTile(mgr) {
1549 const created = createDesktop(mgr);
1550 mgr._overviewAddTileFocused = false;
1551 exitOverviewToDesktop(mgr, created.id);
1552 }
1553 function buildDesktopTile(mgr, d) {
1554 const tile2 = document.createElement("button");
1555 tile2.type = "button";
1556 tile2.className = "desktop-mode-overview-top-bar__tile";
1557 tile2.dataset.desktopId = d.id;
1558 if (d.id === mgr._activeDesktopId && !mgr._overviewAddTileFocused) {
1559 tile2.classList.add("desktop-mode-overview-top-bar__tile--active");
1560 }
1561 tile2.setAttribute("aria-label", sprintf(__("Switch to %s"), d.label));
1562 const preview = document.createElement("span");
1563 preview.className = "desktop-mode-overview-top-bar__tile-preview";
1564 const count = mgr._stack.filter(
1565 (w) => w.config.desktopId === d.id
1566 ).length;
1567 if (count > 0) {
1568 const badge = document.createElement("span");
1569 badge.className = "desktop-mode-overview-top-bar__tile-count";
1570 badge.textContent = String(count);
1571 preview.appendChild(badge);
1572 }
1573 tile2.appendChild(preview);
1574 const label = document.createElement("span");
1575 label.className = "desktop-mode-overview-top-bar__tile-label";
1576 label.textContent = d.label;
1577 tile2.appendChild(label);
1578 const closeBtn = document.createElement("span");
1579 closeBtn.className = "desktop-mode-overview-top-bar__tile-close";
1580 closeBtn.setAttribute("role", "button");
1581 closeBtn.setAttribute("tabindex", "0");
1582 closeBtn.setAttribute("aria-label", sprintf(__("Close %s"), d.label));
1583 closeBtn.innerHTML = '<svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true"><path d="M2.5 2.5l7 7M9.5 2.5l-7 7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
1584 closeBtn.addEventListener("click", (e) => {
1585 e.preventDefault();
1586 e.stopPropagation();
1587 closeDesktop(mgr, d.id);
1588 refreshOverviewTopBar(mgr);
1589 });
1590 tile2.appendChild(closeBtn);
1591 tile2.addEventListener("click", (e) => {
1592 e.preventDefault();
1593 e.stopPropagation();
1594 exitOverviewToDesktop(mgr, d.id);
1595 });
1596 return tile2;
1597 }
1598 function refreshOverviewTopBar(mgr) {
1599 if (!mgr._overviewTopBar) {
1600 return;
1601 }
1602 const fresh = buildOverviewTopBar(mgr);
1603 mgr._overviewTopBar.replaceWith(fresh);
1604 mgr._overviewTopBar = fresh;
1605 }
1606 function exitOverviewToDesktop(mgr, desktopId) {
1607 switchDesktop(mgr, desktopId);
1608 exitOverview(mgr);
1609 }
1610 function createOverviewLabel(item) {
1611 const label = document.createElement("div");
1612 label.className = "desktop-mode-overview-label";
1613 label.dataset.windowId = item.win.id;
1614 const thumbW = item.win.element.offsetWidth * item.scale;
1615 label.style.left = `${item.x}px`;
1616 label.style.top = `${item.y - 34}px`;
1617 label.style.width = `${thumbW}px`;
1618 const iconClass = item.win.config.icon || "dashicons-admin-generic";
1619 const icon = document.createElement("span");
1620 icon.className = `desktop-mode-overview-label__icon dashicons ${iconClass}`;
1621 icon.setAttribute("aria-hidden", "true");
1622 label.appendChild(icon);
1623 const title = document.createElement("span");
1624 title.className = "desktop-mode-overview-label__title";
1625 title.textContent = item.win.config.title;
1626 label.appendChild(title);
1627 const tabCount = item.win.getExternalTabCount();
1628 if (tabCount > 0) {
1629 const meta = document.createElement("span");
1630 meta.className = "desktop-mode-overview-label__meta";
1631 meta.textContent = sprintf(
1632 // translators: %d is the number of external sub-tabs open on this window.
1633 _n("· %d open tab", "· %d open tabs", tabCount),
1634 tabCount
1635 );
1636 label.appendChild(meta);
1637 }
1638 return label;
1639 }
1640 function exitOverview(mgr, selected, maximize = false) {
1641 if (!mgr._overviewActive) {
1642 return;
1643 }
1644 mgr._overviewActive = false;
1645 mgr._overviewAddTileFocused = false;
1646 doAction(HOOKS.OVERVIEW_EXITING, {
1647 windowId: selected && maximize ? selected.id : void 0,
1648 reason: selected && maximize ? "select" : "cancel"
1649 });
1650 mgr._desktop.classList.remove("desktop-mode-area--overview");
1651 const shell = document.getElementById("desktop-mode-shell");
1652 shell?.classList.remove("desktop-mode-shell--overview");
1653 for (const [id, snap] of mgr._overviewSnapshot) {
1654 const w = mgr.getById(id);
1655 if (!w) {
1656 continue;
1657 }
1658 w.element.style.transform = snap.transform;
1659 }
1660 if (selected && maximize) {
1661 mgr.focus(selected);
1662 selected.maximize();
1663 }
1664 for (const label of mgr._overviewLabels.values()) {
1665 label.classList.add("desktop-mode-overview-label--out");
1666 }
1667 if (mgr._overviewTopBar) {
1668 mgr._overviewTopBar.classList.add(
1669 "desktop-mode-overview-top-bar--out"
1670 );
1671 }
1672 const ANIMATION_MS = 280;
1673 window.setTimeout(() => {
1674 for (const w of mgr._stack) {
1675 w.element.classList.remove("desktop-mode-window--overview");
1676 }
1677 for (const label of mgr._overviewLabels.values()) {
1678 label.remove();
1679 }
1680 mgr._overviewLabels.clear();
1681 mgr._overviewSnapshot.clear();
1682 if (mgr._overviewTopBar) {
1683 mgr._overviewTopBar.remove();
1684 mgr._overviewTopBar = null;
1685 }
1686 if (mgr._overviewClickBlocker) {
1687 mgr._desktop.removeEventListener(
1688 "click",
1689 mgr._overviewClickBlocker,
1690 true
1691 );
1692 mgr._overviewClickBlocker = null;
1693 }
1694 doAction(HOOKS.OVERVIEW_EXITED, {
1695 windowId: selected && maximize ? selected.id : void 0,
1696 reason: selected && maximize ? "select" : "cancel"
1697 });
1698 }, ANIMATION_MS);
1699 if (mgr._overviewPointerDownHandler) {
1700 mgr._desktop.removeEventListener(
1701 "pointerdown",
1702 mgr._overviewPointerDownHandler,
1703 true
1704 );
1705 mgr._overviewPointerDownHandler = null;
1706 }
1707 if (mgr._overviewPointerUpHandler) {
1708 mgr._desktop.removeEventListener(
1709 "pointerup",
1710 mgr._overviewPointerUpHandler,
1711 true
1712 );
1713 mgr._overviewPointerUpHandler = null;
1714 }
1715 mgr._overviewPressTarget = null;
1716 if (mgr._overviewKeyHandler) {
1717 document.removeEventListener("keydown", mgr._overviewKeyHandler);
1718 mgr._overviewKeyHandler = null;
1719 }
1720 if (mgr._overviewMouseHandler) {
1721 mgr._desktop.removeEventListener(
1722 "mouseover",
1723 mgr._overviewMouseHandler
1724 );
1725 mgr._overviewMouseHandler = null;
1726 }
1727 if (mgr._lastOverviewHoverId) {
1728 doAction(HOOKS.OVERVIEW_WINDOW_UNHOVER, {
1729 windowId: mgr._lastOverviewHoverId
1730 });
1731 mgr._lastOverviewHoverId = null;
1732 }
1733 }
1734 function getDesktops(mgr) {
1735 return [...mgr._desktops];
1736 }
1737 function getActiveDesktop(mgr) {
1738 const found = mgr._desktops.find((d) => d.id === mgr._activeDesktopId);
1739 return found ?? mgr._desktops[0];
1740 }
1741 function getActiveDesktopId(mgr) {
1742 return getActiveDesktop(mgr).id;
1743 }
1744 function applyDesktopVisibility(mgr, win) {
1745 const visible = win.config.desktopId === mgr._activeDesktopId;
1746 win.element.style.display = visible ? "" : "none";
1747 }
1748 function refreshDesktopVisibility(mgr) {
1749 for (const w of mgr._stack) {
1750 applyDesktopVisibility(mgr, w);
1751 }
1752 }
1753 function createDesktop(mgr) {
1754 mgr._desktopSeq++;
1755 const desktop = {
1756 id: `desktop-${mgr._desktopSeq}`,
1757 // translators: %d is the desktop number (e.g., "Desktop 2")
1758 label: sprintf(__("Desktop %d"), mgr._desktopSeq)
1759 };
1760 mgr._desktops.push(desktop);
1761 doAction(HOOKS.DESKTOP_CREATED, { desktopId: desktop.id });
1762 return desktop;
1763 }
1764 function switchDesktop(mgr, id, opts) {
1765 if (id === mgr._activeDesktopId) {
1766 return;
1767 }
1768 if (!mgr._desktops.some((d) => d.id === id)) {
1769 return;
1770 }
1771 const previousId = mgr._activeDesktopId;
1772 mgr._activeDesktopId = id;
1773 if (mgr._overviewActive) {
1774 relayoutOverviewForActiveDesktop(mgr);
1775 refreshOverviewTopBar(mgr);
1776 } else {
1777 refreshDesktopVisibility(mgr);
1778 if (opts?.direction) {
1779 animateDesktopSwitch(mgr, opts.direction);
1780 }
1781 const topOnNew = [...mgr._stack].reverse().find(
1782 (w) => w.config.desktopId === id && w.state !== "minimized"
1783 );
1784 if (topOnNew) {
1785 mgr.focus(topOnNew);
1786 }
1787 }
1788 doAction(HOOKS.DESKTOP_SWITCHED, {
1789 from: previousId,
1790 to: id
1791 });
1792 }
1793 function animateDesktopSwitch(mgr, direction) {
1794 const el = mgr._desktop;
1795 const cls = direction === "next" ? "desktop-mode-area--sliding-from-right" : "desktop-mode-area--sliding-from-left";
1796 el.classList.remove(
1797 "desktop-mode-area--sliding-from-right",
1798 "desktop-mode-area--sliding-from-left"
1799 );
1800 void el.offsetWidth;
1801 el.classList.add(cls);
1802 const onEnd = (e) => {
1803 if (!e.animationName.startsWith("desktop-mode-area-slide-from-")) {
1804 return;
1805 }
1806 el.classList.remove(cls);
1807 el.removeEventListener("animationend", onEnd);
1808 };
1809 el.addEventListener("animationend", onEnd);
1810 }
1811 function closeDesktop(mgr, id) {
1812 if (mgr._desktops.length <= 1) {
1813 return;
1814 }
1815 const idx = mgr._desktops.findIndex((d) => d.id === id);
1816 if (idx === -1) {
1817 return;
1818 }
1819 const survivorIdx = idx > 0 ? idx - 1 : 1;
1820 const survivor = mgr._desktops[survivorIdx];
1821 for (const w of mgr._stack) {
1822 if (w.config.desktopId === id) {
1823 w.config.desktopId = survivor.id;
1824 }
1825 }
1826 mgr._desktops.splice(idx, 1);
1827 const wasActive = mgr._activeDesktopId === id;
1828 if (wasActive) {
1829 mgr._activeDesktopId = survivor.id;
1830 }
1831 if (mgr._overviewActive) {
1832 relayoutOverviewForActiveDesktop(mgr);
1833 } else {
1834 refreshDesktopVisibility(mgr);
1835 }
1836 doAction(HOOKS.DESKTOP_CLOSED, {
1837 desktopId: id,
1838 migratedTo: survivor.id
1839 });
1840 }
1841 function relayoutOverviewForActiveDesktop(mgr) {
1842 for (const [winId, snap] of mgr._overviewSnapshot) {
1843 const w = mgr.getById(winId);
1844 if (w) {
1845 w.element.style.transform = snap.transform;
1846 w.element.style.transition = snap.transition;
1847 w.element.classList.remove("desktop-mode-window--overview");
1848 }
1849 }
1850 for (const label of mgr._overviewLabels.values()) {
1851 label.remove();
1852 }
1853 mgr._overviewLabels.clear();
1854 mgr._overviewSnapshot.clear();
1855 refreshDesktopVisibility(mgr);
1856 const eligible = mgr._stack.filter(
1857 (w) => w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
1858 );
1859 if (eligible.length === 0) {
1860 return;
1861 }
1862 for (const w of eligible) {
1863 mgr._overviewSnapshot.set(w.id, {
1864 transform: w.element.style.transform || "",
1865 transition: w.element.style.transition || ""
1866 });
1867 }
1868 const live = mgr._desktop.getBoundingClientRect();
1869 const targetRect = new DOMRect(0, 0, live.width, live.height);
1870 const layout = computeOverviewLayout(
1871 eligible,
1872 targetRect,
1873 OVERVIEW_TOP_BAR_RESERVE
1874 );
1875 for (const item of layout) {
1876 const el = item.win.element;
1877 el.classList.add("desktop-mode-window--overview");
1878 const dx = item.x - el.offsetLeft;
1879 const dy = item.y - el.offsetTop;
1880 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
1881 const label = createOverviewLabel(item);
1882 el.insertAdjacentElement("afterend", label);
1883 mgr._overviewLabels.set(item.win.id, label);
1884 }
1885 }
1886 function seedDesktops(mgr, desktops, activeDesktopId) {
1887 if (desktops.length === 0) {
1888 return;
1889 }
1890 mgr._desktops = desktops.map((d) => ({ ...d }));
1891 mgr._activeDesktopId = desktops.some((d) => d.id === activeDesktopId) ? activeDesktopId : desktops[0].id;
1892 let highest = 0;
1893 for (const d of desktops) {
1894 const match = d.id.match(/^desktop-(\d+)$/);
1895 if (match) {
1896 const n = parseInt(match[1], 10);
1897 if (Number.isFinite(n) && n > highest) {
1898 highest = n;
1899 }
1900 }
1901 }
1902 mgr._desktopSeq = Math.max(mgr._desktopSeq, highest);
1903 }
1904 function cascade(mgr) {
1905 const eligible = mgr._stack.filter(
1906 (w) => w.config.desktopId === mgr._activeDesktopId
1907 );
1908 if (eligible.length === 0) {
1909 return;
1910 }
1911 doAction(HOOKS.ARRANGE_CASCADE_STARTING, {
1912 windowCount: eligible.length
1913 });
1914 for (const w of eligible) {
1915 if (w.state === "minimized") {
1916 w.restore();
1917 }
1918 if (w.state === "fullscreen") {
1919 w.toggleFullscreen();
1920 }
1921 if (w.state === "maximized") {
1922 w.toggleMaximize();
1923 }
1924 }
1925 const rect = mgr._desktop.getBoundingClientRect();
1926 const padding = 30;
1927 const offset = 30;
1928 const targetWidth = Math.min(Math.round(rect.width * 0.7), 1100);
1929 const targetHeight = Math.min(Math.round(rect.height * 0.75), 750);
1930 const maxStepsX = Math.max(
1931 1,
1932 Math.floor((rect.width - targetWidth - padding) / offset)
1933 );
1934 const maxStepsY = Math.max(
1935 1,
1936 Math.floor((rect.height - targetHeight - padding) / offset)
1937 );
1938 const maxSteps = Math.min(maxStepsX, maxStepsY);
1939 eligible.forEach((w, i) => {
1940 const step = i % Math.max(1, maxSteps);
1941 w.element.style.left = `${padding + step * offset}px`;
1942 w.element.style.top = `${padding + step * offset}px`;
1943 w.element.style.width = `${targetWidth}px`;
1944 w.element.style.height = `${targetHeight}px`;
1945 });
1946 const focused = mgr.getFocused();
1947 if (focused) {
1948 mgr.focus(focused);
1949 }
1950 document.dispatchEvent(
1951 new CustomEvent("desktop-mode-window-changed", {
1952 detail: { reason: "cascade" }
1953 })
1954 );
1955 doAction(HOOKS.ARRANGE_CASCADE_APPLIED, {
1956 windowCount: eligible.length
1957 });
1958 }
1959 function tile(mgr) {
1960 const eligible = mgr._stack.filter(
1961 (w) => w.config.desktopId === mgr._activeDesktopId
1962 );
1963 if (eligible.length === 0) {
1964 return;
1965 }
1966 for (const w of eligible) {
1967 if (w.state === "minimized") {
1968 w.restore();
1969 }
1970 if (w.state === "fullscreen") {
1971 w.toggleFullscreen();
1972 }
1973 if (w.state === "maximized") {
1974 w.toggleMaximize();
1975 }
1976 }
1977 const rect = mgr._desktop.getBoundingClientRect();
1978 const auto = pickGridDimensions(
1979 eligible.length,
1980 rect.width,
1981 rect.height
1982 );
1983 const filtered = applyFilters(
1984 HOOKS.ARRANGE_TILE_DIMENSIONS,
1985 auto,
1986 {
1987 windowCount: eligible.length,
1988 areaWidth: rect.width,
1989 areaHeight: rect.height
1990 }
1991 );
1992 const { cols, rows } = isValidGrid(filtered, eligible.length) ? { cols: Math.floor(filtered.cols), rows: Math.floor(filtered.rows) } : auto;
1993 doAction(HOOKS.ARRANGE_TILE_STARTING, {
1994 windowCount: eligible.length,
1995 cols,
1996 rows
1997 });
1998 const padding = 16;
1999 const gap = 12;
2000 const cellWidth = Math.floor(
2001 (rect.width - padding * 2 - gap * (cols - 1)) / cols
2002 );
2003 const cellHeight = Math.floor(
2004 (rect.height - padding * 2 - gap * (rows - 1)) / rows
2005 );
2006 eligible.forEach((w, i) => {
2007 const col = i % cols;
2008 const row = Math.floor(i / cols);
2009 w.element.style.left = `${padding + col * (cellWidth + gap)}px`;
2010 w.element.style.top = `${padding + row * (cellHeight + gap)}px`;
2011 w.element.style.width = `${cellWidth}px`;
2012 w.element.style.height = `${cellHeight}px`;
2013 });
2014 const focused = mgr.getFocused();
2015 if (focused) {
2016 mgr.focus(focused);
2017 }
2018 document.dispatchEvent(
2019 new CustomEvent("desktop-mode-window-changed", {
2020 detail: { reason: "tile" }
2021 })
2022 );
2023 doAction(HOOKS.ARRANGE_TILE_APPLIED, {
2024 windowCount: eligible.length,
2025 cols,
2026 rows
2027 });
2028 }
2029 const SNAP_STORAGE_KEY = "desktop-mode-snap-to-grid";
2030 function loadSnapEnabled() {
2031 try {
2032 return window.localStorage.getItem(SNAP_STORAGE_KEY) === "1";
2033 } catch {
2034 return false;
2035 }
2036 }
2037 function setSnapEnabled(mgr, enabled) {
2038 if (mgr._snapEnabled === enabled) {
2039 return;
2040 }
2041 mgr._snapEnabled = enabled;
2042 try {
2043 window.localStorage.setItem(SNAP_STORAGE_KEY, enabled ? "1" : "0");
2044 } catch {
2045 }
2046 doAction(HOOKS.ARRANGE_SNAP_CHANGED, { enabled });
2047 }
2048 function getSnapConfig(mgr) {
2049 if (!mgr._snapEnabled) {
2050 return { enabled: false, cellWidth: 0, cellHeight: 0 };
2051 }
2052 const rect = mgr._desktop.getBoundingClientRect();
2053 const targetCols = rect.width >= rect.height ? 12 : 8;
2054 const auto = {
2055 cellWidth: Math.max(40, Math.round(rect.width / targetCols)),
2056 cellHeight: Math.max(
2057 40,
2058 Math.round(rect.height / Math.round(targetCols * 0.66))
2059 )
2060 };
2061 const filtered = applyFilters(
2062 HOOKS.ARRANGE_SNAP_CELL_SIZE,
2063 auto,
2064 { areaWidth: rect.width, areaHeight: rect.height }
2065 );
2066 const { cellWidth, cellHeight } = isValidCellSize(filtered) ? filtered : auto;
2067 return { enabled: true, cellWidth, cellHeight };
2068 }
2069 function enterSplitOverview(mgr, anchor, zone) {
2070 if (mgr._splitOverviewActive) {
2071 return;
2072 }
2073 mgr._splitOverviewActive = true;
2074 mgr._splitOverviewAnchor = anchor;
2075 mgr._splitOverviewZone = zone;
2076 const eligible = mgr._stack.filter(
2077 (w) => w !== anchor && w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
2078 );
2079 if (eligible.length === 0) {
2080 cleanupSplitOverviewState(mgr);
2081 return;
2082 }
2083 mgr._splitOverviewSnapshot.clear();
2084 for (const w of eligible) {
2085 mgr._splitOverviewSnapshot.set(w.id, {
2086 transform: w.element.style.transform || "",
2087 transition: w.element.style.transition || ""
2088 });
2089 }
2090 mgr._desktop.classList.add("desktop-mode-area--split-overview");
2091 const rect = oppositeHalfRect(mgr, zone);
2092 const layout = computeOverviewLayout(eligible, rect, 0);
2093 mgr._splitOverviewLabels.clear();
2094 for (const item of layout) {
2095 const el = item.win.element;
2096 el.classList.add("desktop-mode-window--overview");
2097 const dx = item.x - el.offsetLeft;
2098 const dy = item.y - el.offsetTop;
2099 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
2100 const label = createOverviewLabel(item);
2101 el.insertAdjacentElement("afterend", label);
2102 mgr._splitOverviewLabels.set(item.win.id, label);
2103 }
2104 const pressTargetForEvent = (e) => {
2105 const target = e.target;
2106 const winEl = target?.closest(
2107 ".desktop-mode-window--overview"
2108 );
2109 if (winEl) {
2110 return {
2111 id: winEl.id.replace(/^wp-window-/, ""),
2112 element: winEl
2113 };
2114 }
2115 if (target) {
2116 return { id: "dismiss", element: mgr._desktop };
2117 }
2118 return null;
2119 };
2120 mgr._splitOverviewPointerDown = (e) => {
2121 if (e.button !== 0) {
2122 mgr._splitOverviewPressTarget = null;
2123 return;
2124 }
2125 mgr._splitOverviewPressTarget = pressTargetForEvent(e);
2126 if (mgr._splitOverviewPressTarget) {
2127 e.preventDefault();
2128 e.stopPropagation();
2129 }
2130 };
2131 mgr._splitOverviewPointerUp = (e) => {
2132 if (e.button !== 0) {
2133 return;
2134 }
2135 const pressed = mgr._splitOverviewPressTarget;
2136 mgr._splitOverviewPressTarget = null;
2137 if (!pressed) {
2138 return;
2139 }
2140 const r = pressed.element.getBoundingClientRect();
2141 const inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
2142 if (!inside) {
2143 return;
2144 }
2145 e.preventDefault();
2146 e.stopPropagation();
2147 if (pressed.id === "dismiss") {
2148 exitSplitOverview(mgr);
2149 return;
2150 }
2151 const selected = mgr.getById(pressed.id);
2152 if (!selected) {
2153 exitSplitOverview(mgr);
2154 return;
2155 }
2156 fillOppositeHalfAndExit(mgr, selected);
2157 };
2158 mgr._splitOverviewKey = (e) => {
2159 if (e.key === "Escape") {
2160 exitSplitOverview(mgr);
2161 }
2162 };
2163 mgr._splitOverviewClickBlocker = (e) => {
2164 e.stopPropagation();
2165 e.preventDefault();
2166 };
2167 mgr._desktop.addEventListener(
2168 "pointerdown",
2169 mgr._splitOverviewPointerDown,
2170 true
2171 );
2172 mgr._desktop.addEventListener(
2173 "pointerup",
2174 mgr._splitOverviewPointerUp,
2175 true
2176 );
2177 mgr._desktop.addEventListener(
2178 "click",
2179 mgr._splitOverviewClickBlocker,
2180 true
2181 );
2182 document.addEventListener("keydown", mgr._splitOverviewKey);
2183 }
2184 function fillOppositeHalfAndExit(mgr, selected) {
2185 const anchorZone = mgr._splitOverviewZone;
2186 if (!anchorZone) {
2187 exitSplitOverview(mgr);
2188 return;
2189 }
2190 const partnerZone = anchorZone === "left" ? "right" : "left";
2191 selected.element.style.transform = "";
2192 selected.element.classList.remove("desktop-mode-window--overview");
2193 selected.applySnap(partnerZone);
2194 mgr._splitOverviewSnapshot.delete(selected.id);
2195 mgr.focus(selected);
2196 doAction(HOOKS.SNAP_SPLIT_FILLED, {
2197 windowId: selected.id,
2198 zone: partnerZone
2199 });
2200 exitSplitOverview(mgr);
2201 }
2202 function exitSplitOverview(mgr) {
2203 if (!mgr._splitOverviewActive) {
2204 return;
2205 }
2206 mgr._splitOverviewActive = false;
2207 for (const [id, snap] of mgr._splitOverviewSnapshot) {
2208 const w = mgr.getById(id);
2209 if (!w) {
2210 continue;
2211 }
2212 w.element.style.transform = snap.transform;
2213 }
2214 for (const label of mgr._splitOverviewLabels.values()) {
2215 label.classList.add("desktop-mode-overview-label--out");
2216 }
2217 mgr._desktop.classList.remove("desktop-mode-area--split-overview");
2218 const ANIMATION_MS = 260;
2219 window.setTimeout(() => {
2220 for (const w of mgr._stack) {
2221 if (mgr._splitOverviewSnapshot.has(w.id)) {
2222 w.element.classList.remove("desktop-mode-window--overview");
2223 }
2224 }
2225 for (const label of mgr._splitOverviewLabels.values()) {
2226 label.remove();
2227 }
2228 cleanupSplitOverviewState(mgr);
2229 }, ANIMATION_MS);
2230 if (mgr._splitOverviewPointerDown) {
2231 mgr._desktop.removeEventListener(
2232 "pointerdown",
2233 mgr._splitOverviewPointerDown,
2234 true
2235 );
2236 mgr._splitOverviewPointerDown = null;
2237 }
2238 if (mgr._splitOverviewPointerUp) {
2239 mgr._desktop.removeEventListener(
2240 "pointerup",
2241 mgr._splitOverviewPointerUp,
2242 true
2243 );
2244 mgr._splitOverviewPointerUp = null;
2245 }
2246 if (mgr._splitOverviewClickBlocker) {
2247 mgr._desktop.removeEventListener(
2248 "click",
2249 mgr._splitOverviewClickBlocker,
2250 true
2251 );
2252 mgr._splitOverviewClickBlocker = null;
2253 }
2254 if (mgr._splitOverviewKey) {
2255 document.removeEventListener("keydown", mgr._splitOverviewKey);
2256 mgr._splitOverviewKey = null;
2257 }
2258 mgr._splitOverviewPressTarget = null;
2259 }
2260 function cleanupSplitOverviewState(mgr) {
2261 mgr._splitOverviewSnapshot.clear();
2262 mgr._splitOverviewLabels.clear();
2263 mgr._splitOverviewAnchor = null;
2264 mgr._splitOverviewZone = null;
2265 mgr._splitOverviewActive = false;
2266 }
2267 const SNAP_EDGE_THRESHOLD = 30;
2268 const SNAP_COMMIT_MS = 260;
2269 function detectSnapZone(clientX, desktopRect) {
2270 if (clientX <= desktopRect.left + SNAP_EDGE_THRESHOLD) {
2271 return "left";
2272 }
2273 if (clientX >= desktopRect.right - SNAP_EDGE_THRESHOLD) {
2274 return "right";
2275 }
2276 return null;
2277 }
2278 function snapZoneBounds(mgr, zone) {
2279 const rect = mgr._desktop.getBoundingClientRect();
2280 const halfW = Math.floor(rect.width / 2);
2281 const height = Math.floor(rect.height);
2282 return {
2283 x: zone === "left" ? 0 : rect.width - halfW,
2284 y: 0,
2285 width: halfW,
2286 height
2287 };
2288 }
2289 function oppositeHalfRect(mgr, zone) {
2290 const rect = mgr._desktop.getBoundingClientRect();
2291 const halfW = Math.floor(rect.width / 2);
2292 const height = Math.floor(rect.height);
2293 if (zone === "left") {
2294 return new DOMRect(halfW, 0, halfW, height);
2295 }
2296 return new DOMRect(0, 0, halfW, height);
2297 }
2298 function showSnapPreview(mgr, zone) {
2299 if (mgr._snapPendingZone === zone && mgr._snapPreviewEl) {
2300 return;
2301 }
2302 mgr._snapPendingZone = zone;
2303 if (!mgr._snapPreviewEl) {
2304 const el = document.createElement("div");
2305 el.className = "desktop-mode-snap-preview";
2306 el.setAttribute("aria-hidden", "true");
2307 mgr._desktop.appendChild(el);
2308 mgr._snapPreviewEl = el;
2309 Promise.resolve().then(() => {
2310 el.classList.add("desktop-mode-snap-preview--visible");
2311 });
2312 }
2313 const b = snapZoneBounds(mgr, zone);
2314 mgr._snapPreviewEl.style.left = `${b.x}px`;
2315 mgr._snapPreviewEl.style.top = `${b.y}px`;
2316 mgr._snapPreviewEl.style.width = `${b.width}px`;
2317 mgr._snapPreviewEl.style.height = `${b.height}px`;
2318 mgr._snapPreviewEl.dataset.zone = zone;
2319 }
2320 function hideSnapPreview(mgr) {
2321 if (!mgr._snapPreviewEl) {
2322 mgr._snapPendingZone = null;
2323 return;
2324 }
2325 const el = mgr._snapPreviewEl;
2326 mgr._snapPreviewEl = null;
2327 mgr._snapPendingZone = null;
2328 el.classList.remove("desktop-mode-snap-preview--visible");
2329 window.setTimeout(() => {
2330 el.remove();
2331 }, SNAP_COMMIT_MS);
2332 }
2333 function updateSnapZoneForDrag(mgr, win, clientX) {
2334 if (mgr._splitOverviewActive) {
2335 return;
2336 }
2337 const rect = mgr._desktop.getBoundingClientRect();
2338 const zone = detectSnapZone(clientX, rect);
2339 const previous = mgr._snapPendingZone;
2340 if (zone) {
2341 showSnapPreview(mgr, zone);
2342 if (previous !== zone) {
2343 doAction(HOOKS.SNAP_ZONE_PENDING, {
2344 windowId: win.id,
2345 zone
2346 });
2347 }
2348 } else if (previous) {
2349 hideSnapPreview(mgr);
2350 doAction(HOOKS.SNAP_ZONE_CANCELED, { windowId: win.id });
2351 }
2352 }
2353 function commitSnapIfPending(mgr, win) {
2354 const zone = mgr._snapPendingZone;
2355 if (!zone) {
2356 return false;
2357 }
2358 hideSnapPreview(mgr);
2359 if (win.state === "normal") {
2360 win._savedGeometry = {
2361 x: win.element.offsetLeft,
2362 y: win.element.offsetTop,
2363 width: win.element.offsetWidth,
2364 height: win.element.offsetHeight
2365 };
2366 }
2367 win.applySnap(zone);
2368 doAction(HOOKS.SNAP_ZONE_COMMITTED, {
2369 windowId: win.id,
2370 zone
2371 });
2372 window.requestAnimationFrame(() => {
2373 enterSplitOverview(mgr, win, zone);
2374 });
2375 return true;
2376 }
2377 function abortSnapIfPending(mgr) {
2378 if (mgr._snapPendingZone) {
2379 hideSnapPreview(mgr);
2380 }
2381 }
2382 const NATIVE_GEOMETRY_STORAGE_KEY = "desktop-mode-native-window-geometry";
2383 const MAX_ENTRIES = 64;
2384 const MAX_DIMENSION = 8192;
2385 function readMap$1() {
2386 try {
2387 const raw = window.localStorage.getItem(NATIVE_GEOMETRY_STORAGE_KEY);
2388 if (!raw) {
2389 return {};
2390 }
2391 const parsed = JSON.parse(raw);
2392 if (!parsed || typeof parsed !== "object") {
2393 return {};
2394 }
2395 return parsed;
2396 } catch {
2397 return {};
2398 }
2399 }
2400 function writeMap$1(map) {
2401 try {
2402 window.localStorage.setItem(
2403 NATIVE_GEOMETRY_STORAGE_KEY,
2404 JSON.stringify(map)
2405 );
2406 } catch {
2407 }
2408 }
2409 function loadNativeWindowGeometry(baseId) {
2410 if (!baseId) {
2411 return null;
2412 }
2413 const map = readMap$1();
2414 const entry = map[baseId];
2415 if (!entry) {
2416 return null;
2417 }
2418 const width = Number(entry.width);
2419 const height = Number(entry.height);
2420 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2421 return null;
2422 }
2423 const state2 = entry.state === "maximized" ? "maximized" : void 0;
2424 const x = Number(entry.x);
2425 const y = Number(entry.y);
2426 const hasPosition = Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0 && x <= MAX_DIMENSION && y <= MAX_DIMENSION;
2427 return {
2428 width: Math.round(width),
2429 height: Math.round(height),
2430 ...hasPosition ? { x: Math.round(x), y: Math.round(y) } : {},
2431 ...state2 ? { state: state2 } : {}
2432 };
2433 }
2434 function saveNativeWindowGeometry(baseId, geometry) {
2435 if (!baseId) {
2436 return;
2437 }
2438 const width = Math.round(Number(geometry.width));
2439 const height = Math.round(Number(geometry.height));
2440 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2441 return;
2442 }
2443 const map = readMap$1();
2444 const prev = map[baseId];
2445 const state2 = prev && prev.state === "maximized" ? "maximized" : void 0;
2446 const carriedX = typeof prev?.x === "number" ? prev.x : void 0;
2447 const carriedY = typeof prev?.y === "number" ? prev.y : void 0;
2448 if (prev && prev.width === width && prev.height === height && prev.state === state2 && prev.x === carriedX && prev.y === carriedY) {
2449 return;
2450 }
2451 upsertEntry(map, baseId, {
2452 width,
2453 height,
2454 ...typeof carriedX === "number" && typeof carriedY === "number" ? { x: carriedX, y: carriedY } : {},
2455 ...state2 ? { state: state2 } : {}
2456 });
2457 writeMapTrimmed(map);
2458 }
2459 function saveNativeWindowPosition(baseId, position) {
2460 if (!baseId) {
2461 return;
2462 }
2463 const x = Math.round(Number(position.x));
2464 const y = Math.round(Number(position.y));
2465 if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || y < 0 || x > MAX_DIMENSION || y > MAX_DIMENSION) {
2466 return;
2467 }
2468 const map = readMap$1();
2469 const prev = map[baseId];
2470 if (!prev) {
2471 return;
2472 }
2473 if (prev.x === x && prev.y === y) {
2474 return;
2475 }
2476 upsertEntry(map, baseId, {
2477 ...prev,
2478 x,
2479 y
2480 });
2481 writeMapTrimmed(map);
2482 }
2483 function setNativeWindowSavedState(baseId, state2, defaults) {
2484 if (!baseId) {
2485 return;
2486 }
2487 const map = readMap$1();
2488 const prev = map[baseId];
2489 if (!prev) {
2490 if (state2 === null || !defaults) {
2491 return;
2492 }
2493 const width = Math.round(Number(defaults.width));
2494 const height = Math.round(Number(defaults.height));
2495 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2496 return;
2497 }
2498 upsertEntry(map, baseId, { width, height, state: state2 });
2499 writeMapTrimmed(map);
2500 return;
2501 }
2502 if (state2 === null) {
2503 if (!prev.state) {
2504 return;
2505 }
2506 const { state: _state2, ...rest } = prev;
2507 upsertEntry(map, baseId, rest);
2508 writeMapTrimmed(map);
2509 return;
2510 }
2511 if (prev.state === state2) {
2512 return;
2513 }
2514 upsertEntry(map, baseId, {
2515 ...prev,
2516 state: state2
2517 });
2518 writeMapTrimmed(map);
2519 }
2520 function upsertEntry(map, baseId, entry) {
2521 delete map[baseId];
2522 map[baseId] = entry;
2523 }
2524 function writeMapTrimmed(map) {
2525 const keys = Object.keys(map);
2526 if (keys.length > MAX_ENTRIES) {
2527 const trimmed = {};
2528 for (const key of keys.slice(-MAX_ENTRIES)) {
2529 trimmed[key] = map[key];
2530 }
2531 writeMap$1(trimmed);
2532 return;
2533 }
2534 writeMap$1(map);
2535 }
2536 const BASE_Z_INDEX = 100;
2537 const CASCADE_OFFSET = 30;
2538 class WindowManager {
2539 constructor(desktop) {
2540 this._stack = [];
2541 this.cascadeIndex = 0;
2542 this._desktops = [
2543 // translators: default desktop name — "Desktop 1"
2544 { id: "desktop-1", label: "Desktop 1" }
2545 ];
2546 this._activeDesktopId = "desktop-1";
2547 this._desktopSeq = 1;
2548 this.onToggleStartupRequested = null;
2549 this.desktopResizeObserver = null;
2550 this._reflowRestoreTimer = null;
2551 this._snapEnabled = loadSnapEnabled();
2552 this._overviewActive = false;
2553 this._overviewSnapshot = /* @__PURE__ */ new Map();
2554 this._overviewLabels = /* @__PURE__ */ new Map();
2555 this._overviewPointerDownHandler = null;
2556 this._overviewPointerUpHandler = null;
2557 this._overviewKeyHandler = null;
2558 this._overviewPressTarget = null;
2559 this._overviewClickBlocker = null;
2560 this._overviewTopBar = null;
2561 this._overviewMouseHandler = null;
2562 this._lastOverviewHoverId = null;
2563 this._overviewAddTileFocused = false;
2564 this._snapPendingZone = null;
2565 this._snapPreviewEl = null;
2566 this._splitOverviewActive = false;
2567 this._splitOverviewAnchor = null;
2568 this._splitOverviewZone = null;
2569 this._splitOverviewSnapshot = /* @__PURE__ */ new Map();
2570 this._splitOverviewLabels = /* @__PURE__ */ new Map();
2571 this._splitOverviewPointerDown = null;
2572 this._splitOverviewPointerUp = null;
2573 this._splitOverviewPressTarget = null;
2574 this._splitOverviewClickBlocker = null;
2575 this._splitOverviewKey = null;
2576 this._desktop = desktop;
2577 if (typeof ResizeObserver !== "undefined") {
2578 this.desktopResizeObserver = new ResizeObserver(
2579 () => this.reflowStatefulWindows()
2580 );
2581 this.desktopResizeObserver.observe(desktop);
2582 }
2583 this.installIframeFocusBridge();
2584 }
2585 /**
2586 * Clicks inside an iframe don't cross the browsing-context
2587 * boundary — pointerdown / focusin in the iframe's document never
2588 * reach the parent. BUT the parent `window` does lose focus,
2589 * because focus moves to the iframe's content window.
2590 *
2591 * We use that signal: listen for `window.blur` on the parent,
2592 * check `document.activeElement` — if it's an iframe, walk up to
2593 * its owning `.desktop-mode-window`, find the matching Window in
2594 * our stack, and focus it. Covers clicks on the primary iframe
2595 * AND any external-tab sub-iframes mounted as descendants of the
2596 * window element.
2597 */
2598 installIframeFocusBridge() {
2599 window.addEventListener("blur", () => {
2600 window.setTimeout(() => {
2601 const active2 = this._desktop.ownerDocument?.activeElement ?? null;
2602 if (!active2 || active2.tagName !== "IFRAME") {
2603 return;
2604 }
2605 const winEl = active2.closest(
2606 ".desktop-mode-window"
2607 );
2608 if (!winEl) {
2609 return;
2610 }
2611 const id = winEl.id.replace(/^wp-window-/, "");
2612 const win = this.getById(id);
2613 if (!win) {
2614 return;
2615 }
2616 if (this._overviewActive) {
2617 return;
2618 }
2619 if (this.getFocused() === win) {
2620 return;
2621 }
2622 this.focus(win);
2623 }, 0);
2624 });
2625 }
2626 /**
2627 * Re-apply state-driven bounds to any window whose geometry is
2628 * derived from the desktop area's dimensions: maximized (full
2629 * area) and snapped-left / snapped-right (half area). Called from
2630 * the desktop-area ResizeObserver so shrinking the browser window
2631 * drags the stateful windows along with it.
2632 *
2633 * Inlines the geometry writes instead of calling `applySnap` —
2634 * that method emits `_emitChange('state')` which would spam the
2635 * session saver on every resize tick. Viewport resize is an
2636 * INCOMING shape change (the shell reshaped us), not an outgoing
2637 * user action worth persisting.
2638 *
2639 * Also toggles `desktop-mode-window--reflowing` so the base
2640 * left/top/width/height transition doesn't interpolate between
2641 * every ResizeObserver tick — without that, the windows would
2642 * always lag ~250 ms behind a browser edge-drag.
2643 *
2644 * Skipped while overview is active — windows are mid-transform
2645 * and touching their inline geometry would desync the live
2646 * transform math; overview exit re-applies state correctly via
2647 * its own path.
2648 */
2649 reflowStatefulWindows() {
2650 if (this._overviewActive) {
2651 return;
2652 }
2653 for (const w of this._stack) {
2654 const parent = w.element.parentElement;
2655 if (!parent) {
2656 continue;
2657 }
2658 if (w.state === "maximized") {
2659 w.element.classList.add("desktop-mode-window--reflowing");
2660 w.element.style.width = `${parent.clientWidth}px`;
2661 w.element.style.height = `${parent.clientHeight}px`;
2662 } else if (w.state === "snapped-left" || w.state === "snapped-right") {
2663 w.element.classList.add("desktop-mode-window--reflowing");
2664 const halfW = Math.floor(parent.clientWidth / 2);
2665 const height = parent.clientHeight;
2666 const left = w.state === "snapped-left" ? 0 : halfW;
2667 w.element.style.left = `${left}px`;
2668 w.element.style.top = "0px";
2669 w.element.style.width = `${halfW}px`;
2670 w.element.style.height = `${height}px`;
2671 }
2672 }
2673 if (this._reflowRestoreTimer !== null) {
2674 window.clearTimeout(this._reflowRestoreTimer);
2675 }
2676 this._reflowRestoreTimer = window.setTimeout(() => {
2677 this._reflowRestoreTimer = null;
2678 for (const w of this._stack) {
2679 w.element.classList.remove("desktop-mode-window--reflowing");
2680 }
2681 }, 140);
2682 }
2683 /**
2684 * Open a new window — or focus an existing one — for the given
2685 * page.
2686 *
2687 * Matches any existing window sharing the same `baseId`
2688 * (defaulting to the config's `id`). For singleton pages
2689 * (Settings, Dashboard, …) `baseId === id`, so this behaves
2690 * exactly like strict id matching. For multi pages, clicking the
2691 * dock icon while a window is already open focuses the
2692 * most-recent instance rather than creating a twin.
2693 *
2694 * To force a brand-new instance alongside an existing one, use
2695 * {@link openNew}.
2696 */
2697 async open(config) {
2698 if (!config || typeof config !== "object") {
2699 throw new TypeError(
2700 "windowManager.open() requires a config object with at least { id, url, title }; received " + (config === null ? "null" : typeof config)
2701 );
2702 }
2703 if (typeof config.id !== "string" || config.id === "") {
2704 throw new TypeError(
2705 "windowManager.open(): config.id must be a non-empty string."
2706 );
2707 }
2708 if (typeof config.url !== "string" || config.url === "") {
2709 throw new TypeError(
2710 'windowManager.open(): config.url must be a non-empty string. Pass an admin URL (e.g. "/wp-admin/edit.php") or a hash fragment (e.g. "#my-window") for native windows.'
2711 );
2712 }
2713 if (typeof config.title !== "string") {
2714 throw new TypeError(
2715 "windowManager.open(): config.title must be a string."
2716 );
2717 }
2718 const baseId = config.baseId || config.id;
2719 const existing = this.getByBaseIdOnActiveDesktop(baseId);
2720 if (existing) {
2721 const wasMinimized = existing.state === "minimized";
2722 this.focus(existing);
2723 if (wasMinimized) {
2724 existing.restore();
2725 }
2726 const reopenedDetail = {
2727 windowId: existing.id,
2728 baseId,
2729 wasMinimized
2730 };
2731 document.dispatchEvent(
2732 new CustomEvent("desktop-mode-window-reopened", { detail: reopenedDetail })
2733 );
2734 doAction(HOOKS.WINDOW_REOPENED, reopenedDetail);
2735 return existing;
2736 }
2737 const id = this.getByBaseId(baseId) ? this.nextInstanceId(baseId) : config.id;
2738 return this.createWindow({ ...config, id, baseId });
2739 }
2740 /**
2741 * Open a brand-new window even if one is already open for this
2742 * page. Only makes sense for pages flagged `multi`.
2743 *
2744 * Duplicates always open in the floating ('normal') state and at
2745 * a fresh cascade slot — the per-baseId saved size / state /
2746 * position preferences apply to the primary instance only.
2747 * Spawning a maximized twin alongside the maximized primary
2748 * would hide the primary; landing a twin on top of the primary's
2749 * remembered position would hide it too. Callers can override
2750 * either default by passing `initialState` / `x` / `y` explicitly.
2751 */
2752 async openNew(config) {
2753 const baseId = config.baseId || config.id;
2754 const nextId2 = this.nextInstanceId(baseId);
2755 const cascadeX = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2756 const cascadeY = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2757 return this.createWindow({
2758 initialState: "normal",
2759 x: cascadeX,
2760 y: cascadeY,
2761 ...config,
2762 id: nextId2,
2763 baseId
2764 });
2765 }
2766 /**
2767 * Build and mount a window element. Common tail shared by
2768 * `open()` and `openNew()`.
2769 */
2770 async createWindow(config) {
2771 const desktopRect = this._desktop.getBoundingClientRect();
2772 const defaultWidth = Math.min(Math.round(desktopRect.width * 0.8), 1200);
2773 const defaultHeight = Math.min(Math.round(desktopRect.height * 0.8), 800);
2774 const cascadeX = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2775 const cascadeY = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2776 const resolvedBaseId = config.baseId || config.id;
2777 const minWidth = config.minWidth ?? 320;
2778 const minHeight = config.minHeight ?? 200;
2779 const hasExplicitWidth = typeof config.width === "number";
2780 const hasExplicitHeight = typeof config.height === "number";
2781 const hasExplicitX = typeof config.x === "number";
2782 const hasExplicitY = typeof config.y === "number";
2783 const hasExplicitState = typeof config.initialState === "string";
2784 const saved = !hasExplicitWidth || !hasExplicitHeight || !hasExplicitState || !hasExplicitX || !hasExplicitY ? loadNativeWindowGeometry(resolvedBaseId) : null;
2785 const resolvedWidth = config.width ?? (saved ? Math.max(saved.width, minWidth) : defaultWidth);
2786 const resolvedHeight = config.height ?? (saved ? Math.max(saved.height, minHeight) : defaultHeight);
2787 const resolvedState = config.initialState ?? (saved?.state === "maximized" ? "maximized" : void 0);
2788 let clampedSavedX;
2789 let clampedSavedY;
2790 if (saved && typeof saved.x === "number" && typeof saved.y === "number") {
2791 const margin = 12;
2792 const maxX = Math.max(
2793 0,
2794 desktopRect.width - resolvedWidth - margin
2795 );
2796 const maxY = Math.max(
2797 0,
2798 desktopRect.height - resolvedHeight - margin
2799 );
2800 clampedSavedX = Math.max(margin, Math.min(saved.x, maxX));
2801 clampedSavedY = Math.max(margin, Math.min(saved.y, maxY));
2802 }
2803 const resolvedX = config.x ?? clampedSavedX ?? cascadeX;
2804 const resolvedY = config.y ?? clampedSavedY ?? cascadeY;
2805 const callerPinned = hasExplicitWidth || hasExplicitHeight || hasExplicitX || hasExplicitY || hasExplicitState;
2806 const hasSavedGeometry = !!saved;
2807 const preFilterGeometry = {
2808 x: resolvedX,
2809 y: resolvedY,
2810 width: resolvedWidth,
2811 height: resolvedHeight,
2812 state: resolvedState
2813 };
2814 let filtered;
2815 try {
2816 filtered = applyFilters(
2817 HOOKS.WINDOW_GEOMETRY,
2818 preFilterGeometry,
2819 {
2820 windowId: config.id,
2821 baseId: resolvedBaseId,
2822 hasSavedGeometry,
2823 callerPinned,
2824 desktopRect: {
2825 width: desktopRect.width,
2826 height: desktopRect.height
2827 }
2828 }
2829 );
2830 } catch (err) {
2831 doAction(HOOKS.SHELL_ERROR, {
2832 scope: "window-geometry-filter",
2833 windowId: config.id,
2834 error: err
2835 });
2836 if (typeof console !== "undefined") {
2837 console.error(
2838 `[desktop-mode] WINDOW_GEOMETRY filter threw for "${config.id}":`,
2839 err
2840 );
2841 }
2842 filtered = preFilterGeometry;
2843 }
2844 const coalesce = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback;
2845 const safeFiltered = filtered && typeof filtered === "object" ? filtered : preFilterGeometry;
2846 const finalWidth = Math.max(
2847 coalesce(safeFiltered.width, resolvedWidth),
2848 minWidth
2849 );
2850 const finalHeight = Math.max(
2851 coalesce(safeFiltered.height, resolvedHeight),
2852 minHeight
2853 );
2854 const finalX = coalesce(safeFiltered.x, resolvedX);
2855 const finalY = coalesce(safeFiltered.y, resolvedY);
2856 const finalState = safeFiltered.state ?? resolvedState;
2857 const fullConfig = {
2858 icon: config.icon || "dashicons-admin-generic",
2859 ...config,
2860 // Spread `config` first so callers can pass through any
2861 // extras (render, ownerHandle, parentUrl, …), then pin the
2862 // dimensions + state we resolved above. The pin has to
2863 // follow the spread because an explicit `width: undefined`
2864 // from the caller would otherwise blow away the default.
2865 x: finalX,
2866 y: finalY,
2867 width: finalWidth,
2868 height: finalHeight,
2869 minWidth,
2870 minHeight,
2871 ...finalState ? { initialState: finalState } : {},
2872 baseId: resolvedBaseId,
2873 // New windows always join the active desktop. A caller can
2874 // pre-seed `desktopId` (e.g. session restore) by passing it
2875 // in `config`, which the spread above preserves.
2876 desktopId: config.desktopId || this._activeDesktopId
2877 };
2878 this.cascadeIndex++;
2879 const [system] = await Promise.all([
2880 ensureWindowSystemLoaded(windowSystemBundleUrl()),
2881 ensureShellOverlaysLoaded(shellOverlaysBundleUrl())
2882 ]);
2883 const win = system.createWindow(fullConfig);
2884 win.onFocusRequest = (w) => this.focus(w);
2885 win.onClose = (w) => this.remove(w);
2886 win.onMinimize = () => {
2887 const visible = this._stack.filter((w) => w.state !== "minimized");
2888 if (visible.length > 0) {
2889 this.focus(visible[visible.length - 1]);
2890 }
2891 };
2892 win.onOpenAnother = (w) => {
2893 const baseId = w.config.baseId || w.id;
2894 if (w.config.native) {
2895 const api = window.wp?.desktop;
2896 if (api?.openNewWindow?.(baseId, { source: "open-another" })) {
2897 return;
2898 }
2899 }
2900 void this.openNew({
2901 id: baseId,
2902 baseId,
2903 url: w.config.url || "",
2904 title: w.config.title,
2905 icon: w.config.icon,
2906 submenu: w.config.submenu,
2907 multi: true
2908 });
2909 };
2910 win.onOpenInNewWindow = (w) => {
2911 const baseId = w.config.baseId || w.id;
2912 if (w.config.native) {
2913 const api = window.wp?.desktop;
2914 if (api?.openNewWindow?.(baseId, { source: "open-in-new-window" })) {
2915 return;
2916 }
2917 }
2918 const currentUrl = w.getCurrentUrl();
2919 void this.openNew({
2920 id: baseId,
2921 baseId,
2922 url: currentUrl || w.config.url || "",
2923 title: w.config.title,
2924 icon: w.config.icon,
2925 submenu: w.config.submenu,
2926 multi: true
2927 });
2928 };
2929 win.onToggleStartup = (w) => {
2930 this.onToggleStartupRequested?.(w);
2931 };
2932 win.snapConfigProvider = () => this.getSnapConfig();
2933 win.onDragMove = (w, clientX) => {
2934 updateSnapZoneForDrag(this, w, clientX);
2935 };
2936 win.onDragEnd = (w) => {
2937 if (this._snapPendingZone) {
2938 return commitSnapIfPending(this, w);
2939 }
2940 abortSnapIfPending(this);
2941 return false;
2942 };
2943 this._stack.push(win);
2944 this._desktop.appendChild(win.element);
2945 applyDesktopVisibility(this, win);
2946 win.hydrateNative();
2947 this.focus(win);
2948 const openedDetail = {
2949 windowId: win.id,
2950 page: config.url,
2951 title: config.title,
2952 url: config.url
2953 };
2954 document.dispatchEvent(
2955 new CustomEvent("desktop-mode-window-opened", { detail: openedDetail })
2956 );
2957 doAction(HOOKS.WINDOW_OPENED, openedDetail);
2958 return win;
2959 }
2960 /**
2961 * Find the next unused suffixed id for a given baseId. Prefers
2962 * the bare baseId itself if free (user closed the original), then
2963 * walks `-2`, `-3`, … until it lands on one not currently in the
2964 * stack.
2965 */
2966 nextInstanceId(baseId) {
2967 const taken = new Set(this._stack.map((w) => w.id));
2968 if (!taken.has(baseId)) {
2969 return baseId;
2970 }
2971 let n = 2;
2972 while (taken.has(`${baseId}-${n}`)) {
2973 n++;
2974 }
2975 return `${baseId}-${n}`;
2976 }
2977 /** Focus a window: bring it to top of z-stack. */
2978 focus(win) {
2979 const previouslyFocused = this._stack.length > 0 ? this._stack[this._stack.length - 1] : null;
2980 const priorFullscreen = this._stack.find(
2981 (w) => w !== win && w.isFocused() && w.isFullscreen()
2982 );
2983 if (priorFullscreen) {
2984 const shouldExit = applyFilters(
2985 HOOKS.WINDOW_AUTO_EXIT_FULLSCREEN,
2986 true,
2987 { windowId: priorFullscreen.id, focusedTo: win.id }
2988 );
2989 if (shouldExit) {
2990 priorFullscreen.toggleFullscreen();
2991 }
2992 }
2993 const idx = this._stack.indexOf(win);
2994 if (idx > -1) {
2995 this._stack.splice(idx, 1);
2996 }
2997 this._stack.push(win);
2998 this._stack.forEach((w, i) => {
2999 w.setZIndex(BASE_Z_INDEX + i);
3000 w.setFocused(i === this._stack.length - 1);
3001 });
3002 if (previouslyFocused && previouslyFocused !== win && previouslyFocused.id !== win.id) {
3003 const blurredDetail = {
3004 windowId: previouslyFocused.id,
3005 focusedTo: win.id
3006 };
3007 document.dispatchEvent(
3008 new CustomEvent("desktop-mode-window-blurred", { detail: blurredDetail })
3009 );
3010 doAction(HOOKS.WINDOW_BLURRED, blurredDetail);
3011 }
3012 const focusedDetail = { windowId: win.id };
3013 document.dispatchEvent(
3014 new CustomEvent("desktop-mode-window-focused", { detail: focusedDetail })
3015 );
3016 doAction(HOOKS.WINDOW_FOCUSED, focusedDetail);
3017 }
3018 /** Remove a window from the stack and DOM. */
3019 remove(win) {
3020 const idx = this._stack.indexOf(win);
3021 if (idx > -1) {
3022 this._stack.splice(idx, 1);
3023 }
3024 if (this._stack.length > 0) {
3025 this.focus(this._stack[this._stack.length - 1]);
3026 }
3027 const closingDetail = { windowId: win.id, element: win.element };
3028 document.dispatchEvent(
3029 new CustomEvent("desktop-mode-window-closing", { detail: closingDetail })
3030 );
3031 doAction(HOOKS.WINDOW_CLOSING, closingDetail);
3032 const closedDetail = { windowId: win.id };
3033 document.dispatchEvent(
3034 new CustomEvent("desktop-mode-window-closed", { detail: closedDetail })
3035 );
3036 doAction(HOOKS.WINDOW_CLOSED, closedDetail);
3037 }
3038 /** Get a window by its ID. */
3039 getById(id) {
3040 return this._stack.find((w) => w.id === id);
3041 }
3042 /**
3043 * Get the most-recently-focused window for a given baseId.
3044 *
3045 * Multi-instance windows share a baseId; the stack is ordered
3046 * bottom to top by focus, so iterating from the end finds the
3047 * best candidate to bring forward when the user re-clicks the
3048 * dock icon.
3049 */
3050 getByBaseId(baseId) {
3051 for (let i = this._stack.length - 1; i >= 0; i--) {
3052 const w = this._stack[i];
3053 if ((w.config.baseId || w.id) === baseId) {
3054 return w;
3055 }
3056 }
3057 return void 0;
3058 }
3059 /**
3060 * Like {@link getByBaseId} but only considers windows on the
3061 * currently-active virtual desktop. The dock's "open or focus"
3062 * path uses this — a Plugins instance that lives on Desktop 2 is
3063 * invisible from Desktop 1's dock click, so clicking Plugins on
3064 * Desktop 1 should open a fresh instance there instead of trying
3065 * to focus the far-off sibling (which would silently do nothing
3066 * because the other desktop's windows are display: none here).
3067 */
3068 getByBaseIdOnActiveDesktop(baseId) {
3069 for (let i = this._stack.length - 1; i >= 0; i--) {
3070 const w = this._stack[i];
3071 if ((w.config.baseId || w.id) !== baseId) {
3072 continue;
3073 }
3074 const winDesktop = w.config.desktopId || this._activeDesktopId;
3075 if (winDesktop === this._activeDesktopId) {
3076 return w;
3077 }
3078 }
3079 return void 0;
3080 }
3081 /**
3082 * Get every open window sharing the given baseId, ordered by
3083 * instance slot (bare baseId first, then `-2`, `-3`, …) rather
3084 * than z-order — so the dock's instance rail keeps a stable
3085 * left-to-right order even as the user focuses between windows.
3086 */
3087 getAllByBaseId(baseId) {
3088 const instanceSlot = (id) => {
3089 if (id === baseId) {
3090 return 1;
3091 }
3092 const prefix = `${baseId}-`;
3093 if (id.startsWith(prefix)) {
3094 const n = parseInt(id.slice(prefix.length), 10);
3095 return Number.isFinite(n) ? n : 999;
3096 }
3097 return 999;
3098 };
3099 return this._stack.filter((w) => (w.config.baseId || w.id) === baseId).sort((a, b) => instanceSlot(a.id) - instanceSlot(b.id));
3100 }
3101 /** Get all open windows. */
3102 getAll() {
3103 return [...this._stack];
3104 }
3105 /**
3106 * Find the window whose iframe's contentWindow matches the given
3107 * message source. Used by cross-frame bridges to attribute inbound
3108 * `postMessage` events to the originating window without reaching
3109 * into `_stack`.
3110 */
3111 findByIframeSource(source) {
3112 if (!source) {
3113 return void 0;
3114 }
3115 return this._stack.find(
3116 (w) => w.iframe !== null && w.iframe.contentWindow === source
3117 );
3118 }
3119 /** Get the currently focused (topmost) window. */
3120 getFocused() {
3121 return this._stack.length > 0 ? this._stack[this._stack.length - 1] : void 0;
3122 }
3123 /**
3124 * "Is the window with this id currently in front of the user?"
3125 *
3126 * Returns true when the window exists in the manager AND it
3127 * isn't minimized AND it's the currently focused (topmost)
3128 * window. False otherwise — including for unknown ids, closed
3129 * windows, minimized windows, or windows that exist but aren't
3130 * on top.
3131 *
3132 * The canonical query for plugins implementing the "show
3133 * something *only when the user can't already see my
3134 * window*" pattern (badge counts, attention pulses, sounds,
3135 * toasts). Plugins that previously hand-rolled
3136 * `getById(id) && state !== 'minimized' && focused` can
3137 * collapse to this.
3138 *
3139 * @since 0.5.5
3140 *
3141 * @param id Window id to query.
3142 * @return True when the user is actively looking at this window.
3143 */
3144 isActive(id) {
3145 const win = this.getById(id);
3146 if (!win) {
3147 return false;
3148 }
3149 if (win.state === "minimized") {
3150 return false;
3151 }
3152 const focused = this.getFocused();
3153 return !!focused && focused.id === id;
3154 }
3155 // ---- Virtual desktop delegations ----
3156 getDesktops() {
3157 return getDesktops(this);
3158 }
3159 getActiveDesktop() {
3160 return getActiveDesktop(this);
3161 }
3162 getActiveDesktopId() {
3163 return getActiveDesktopId(this);
3164 }
3165 createDesktop() {
3166 return createDesktop(this);
3167 }
3168 switchDesktop(id, opts) {
3169 switchDesktop(this, id, opts);
3170 }
3171 closeDesktop(id) {
3172 closeDesktop(this, id);
3173 }
3174 /**
3175 * Returns the "primary" desktop id — the one new sessions land on
3176 * and that batch operations like {@link closeAll} treat as the
3177 * survivor when an `onlyOnPrimary` mode is requested.
3178 *
3179 * Default: the first desktop in `getDesktops()`. Filterable via
3180 * `desktop-mode.primary-desktop-id` so downstream code that wants a
3181 * different convention (e.g. a pinned "Inbox" desktop) can override
3182 * without having to fork the manager.
3183 *
3184 * @since 0.14.0
3185 */
3186 getPrimaryDesktopId() {
3187 const all2 = this.getDesktops();
3188 const fallback = all2.length > 0 ? all2[0].id : "desktop-1";
3189 const filtered = applyFilters(
3190 HOOKS.PRIMARY_DESKTOP_ID,
3191 fallback,
3192 all2
3193 );
3194 if (typeof filtered !== "string" || filtered === "") {
3195 return fallback;
3196 }
3197 const exists = all2.some((d) => d.id === filtered);
3198 return exists ? filtered : fallback;
3199 }
3200 /**
3201 * Close every open window in batch.
3202 *
3203 * Hook chain:
3204 *
3205 * 1. `desktop-mode.windows.before-close-all` — action. Subscribers
3206 * can prepare for the wipe (cancel pending saves, dismiss
3207 * menus, etc.). Detail: `{ candidates: Window[] }`.
3208 *
3209 * 2. `desktop-mode.windows.close-all` — filter. Receives the
3210 * candidate Window list and returns the (possibly smaller) list
3211 * that will actually be closed. Plugins use this to PROTECT
3212 * specific windows — e.g. keep a draft post window open during
3213 * a "Close all" operation. Returning an empty array cancels
3214 * the close entirely.
3215 *
3216 * 3. Each surviving window's `close()` is called.
3217 *
3218 * 4. `desktop-mode.windows.after-close-all` — action. Detail:
3219 * `{ closed: number, skipped: Window[] }`.
3220 *
3221 * @since 0.14.0
3222 *
3223 * @param options Close options.
3224 * @param options.exceptIds Window ids to skip even before the filter runs.
3225 * @return Number of windows actually closed.
3226 */
3227 closeAll(options) {
3228 const exceptSet = new Set(options?.exceptIds ?? []);
3229 const initialCandidates = this._stack.filter(
3230 (w) => !exceptSet.has(w.id)
3231 );
3232 doAction(HOOKS.WINDOWS_BEFORE_CLOSE_ALL, { candidates: initialCandidates });
3233 const filtered = applyFilters(
3234 HOOKS.WINDOWS_CLOSE_ALL,
3235 initialCandidates
3236 );
3237 const finalList = Array.isArray(filtered) ? filtered : initialCandidates;
3238 const skipped = initialCandidates.filter((w) => !finalList.includes(w));
3239 let closed = 0;
3240 for (const win of finalList.slice()) {
3241 try {
3242 win.close();
3243 closed++;
3244 } catch (err) {
3245 if (typeof console !== "undefined") {
3246 console.error(
3247 "[desktop-mode] closeAll: window.close() threw for",
3248 win.id,
3249 err
3250 );
3251 }
3252 }
3253 }
3254 doAction(HOOKS.WINDOWS_AFTER_CLOSE_ALL, { closed, skipped });
3255 return closed;
3256 }
3257 /**
3258 * Minimize every currently-non-minimized window. Returns the
3259 * exact set that was minimized — i.e., excludes windows already
3260 * in the `'minimized'` state — so callers can pair the call with
3261 * a later {@link restoreFrom} that touches only the windows
3262 * they minimized.
3263 *
3264 * The "Show Desktop" gesture (clicking the wallpaper) routes
3265 * through this method (and {@link restoreFrom} on the second
3266 * click); plugin authors building expand/collapse UIs that
3267 * mimic the gesture should use these primitives instead of
3268 * rolling the loop themselves.
3269 *
3270 * @public
3271 * @since 0.18.0
3272 */
3273 minimizeAll() {
3274 const minimized = [];
3275 for (const win of this._stack.slice()) {
3276 if (win.state === "minimized") {
3277 continue;
3278 }
3279 try {
3280 win.minimize();
3281 minimized.push(win);
3282 } catch (err) {
3283 if (typeof console !== "undefined") {
3284 console.error(
3285 "[desktop-mode] minimizeAll: window.minimize() threw for",
3286 win.id,
3287 err
3288 );
3289 }
3290 }
3291 }
3292 return minimized;
3293 }
3294 /**
3295 * Restore the given window list — the symmetric counterpart to
3296 * {@link minimizeAll}. Skips windows that have since been
3297 * closed and windows the user manually un-minimized between
3298 * the minimize and the restore.
3299 *
3300 * Pass the array {@link minimizeAll} returned to restore
3301 * exactly what you minimized; pass any subset to restore
3302 * selectively.
3303 *
3304 * @public
3305 * @since 0.18.0
3306 */
3307 restoreFrom(windows) {
3308 if (!Array.isArray(windows)) {
3309 return;
3310 }
3311 const live = new Set(this._stack);
3312 for (const win of windows) {
3313 if (!live.has(win)) {
3314 continue;
3315 }
3316 if (win.state !== "minimized") {
3317 continue;
3318 }
3319 try {
3320 win.restore();
3321 } catch (err) {
3322 if (typeof console !== "undefined") {
3323 console.error(
3324 "[desktop-mode] restoreFrom: window.restore() threw for",
3325 win.id,
3326 err
3327 );
3328 }
3329 }
3330 }
3331 }
3332 /**
3333 * Toggle the "Show Desktop" state — if every live window is
3334 * already minimized, restore them all; otherwise minimize the
3335 * non-minimized cohort. Returns `true` when the new state is
3336 * "showing the desktop" (everything minimized after the call),
3337 * `false` when windows have just been restored.
3338 *
3339 * Mirrors the wallpaper-click gesture exactly, in one call.
3340 *
3341 * @public
3342 * @since 0.18.0
3343 */
3344 toggleShowDesktop() {
3345 const all2 = this._stack.slice();
3346 if (all2.length === 0) {
3347 return false;
3348 }
3349 const allMinimized = all2.every((w) => w.state === "minimized");
3350 if (allMinimized) {
3351 for (const win of all2) {
3352 try {
3353 win.restore();
3354 } catch {
3355 }
3356 }
3357 return false;
3358 }
3359 this.minimizeAll();
3360 return true;
3361 }
3362 // ---- Arrange + snap delegations ----
3363 cascade() {
3364 cascade(this);
3365 }
3366 tile() {
3367 tile(this);
3368 }
3369 isSnapEnabled() {
3370 return this._snapEnabled;
3371 }
3372 setSnapEnabled(enabled) {
3373 setSnapEnabled(this, enabled);
3374 }
3375 getSnapConfig() {
3376 return getSnapConfig(this);
3377 }
3378 // ---- Overview delegations ----
3379 enterOverview() {
3380 enterOverview(this);
3381 }
3382 exitOverview(selected, maximize = false) {
3383 exitOverview(this, selected, maximize);
3384 }
3385 /**
3386 * Snapshot every open window's current geometry + state.
3387 *
3388 * Returns a plain array of `{ windowId, rect, state, element }`
3389 * entries — one per window in the stack, regardless of which
3390 * virtual desktop owns it. Rect coordinates are in desktop-area
3391 * space (the same coordinate space the windows themselves use
3392 * inline-style left/top); `state` is the live `WindowState`, and
3393 * `element` is the window's outer DOM node.
3394 *
3395 * Intended for wallpaper / overlay plugins that used to scrape
3396 * `document.querySelectorAll('.desktop-mode-window')` + read the
3397 * `--minimized` / `--maximized` modifier classes by name. The
3398 * accessor decouples plugin code from the shell's CSS class
3399 * naming, so a future refactor of modifier prefixes is not an
3400 * ecosystem break.
3401 *
3402 * The array contains every window in the stack — callers filter
3403 * on `state` if they want only "actually visible" (typically
3404 * `state !== 'minimized'`). Minimized windows are included so
3405 * plugins that care about the "will be restored to X geometry"
3406 * case still have the data; filtering them out would be a
3407 * subtraction the caller can do but the provider can't reverse.
3408 *
3409 * Order matches the internal z-stack: earliest-opened first,
3410 * focused window last.
3411 */
3412 getVisibleRects() {
3413 return this._stack.map((w) => {
3414 const snap = w.getSnapshot();
3415 return {
3416 windowId: w.id,
3417 rect: {
3418 x: snap.x,
3419 y: snap.y,
3420 width: snap.width,
3421 height: snap.height
3422 },
3423 state: snap.state,
3424 element: w.element
3425 };
3426 });
3427 }
3428 /**
3429 * Serialize the current window stack for session persistence.
3430 *
3431 * Order in the returned `windows` array mirrors z-order (earliest
3432 * opened / lowest-z first, focused last) so restoring preserves
3433 * the stacking the user left behind.
3434 */
3435 snapshot() {
3436 const focused = this.getFocused();
3437 const persistable = this._stack.filter((w) => !w.config.native);
3438 const windows = persistable.map((w) => {
3439 const snap = w.getSnapshot();
3440 const externalTabs = w.getExternalTabsSnapshot();
3441 return {
3442 id: w.id,
3443 baseId: w.config.baseId || w.id,
3444 desktopId: w.config.desktopId || this._activeDesktopId,
3445 url: w.getCurrentUrl(),
3446 title: w.config.title,
3447 icon: w.config.icon,
3448 state: snap.state,
3449 x: snap.x,
3450 y: snap.y,
3451 width: snap.width,
3452 height: snap.height,
3453 ...externalTabs.length > 0 ? { externalTabs } : {}
3454 };
3455 });
3456 const focusedId = focused && !focused.config.native ? focused.id : "";
3457 return {
3458 windows,
3459 desktops: this.getDesktops(),
3460 activeDesktop: this._activeDesktopId,
3461 focused: focusedId,
3462 updated: Math.floor(Date.now() / 1e3)
3463 };
3464 }
3465 seedDesktops(desktops, activeDesktopId) {
3466 seedDesktops(this, desktops, activeDesktopId);
3467 }
3468 }
3469 function cycleableWindows(mgr) {
3470 const activeDesktopId = mgr.getActiveDesktopId();
3471 const domOrder = Array.from(mgr._desktop.children);
3472 return mgr.getAll().filter((w) => {
3473 const winDesktop = w.config.desktopId || activeDesktopId;
3474 return winDesktop === activeDesktopId;
3475 }).sort(
3476 (a, b) => domOrder.indexOf(a.element) - domOrder.indexOf(b.element)
3477 );
3478 }
3479 function cycleFocus(mgr, direction) {
3480 if (mgr._overviewActive) {
3481 return;
3482 }
3483 const list2 = cycleableWindows(mgr);
3484 if (list2.length < 2) {
3485 return;
3486 }
3487 const focused = mgr.getFocused();
3488 const currentIdx = focused ? list2.indexOf(focused) : -1;
3489 const step = direction === "next" ? 1 : -1;
3490 const nextIdx = (currentIdx + step + list2.length) % list2.length;
3491 const target = list2[nextIdx];
3492 if (target.state === "minimized") {
3493 target.restore();
3494 } else {
3495 mgr.focus(target);
3496 }
3497 }
3498 let installed$3 = false;
3499 function isTextEntryFocus(doc) {
3500 let el = doc.activeElement;
3501 while (el && el.shadowRoot && el.shadowRoot.activeElement) {
3502 el = el.shadowRoot.activeElement;
3503 }
3504 if (!el) {
3505 return false;
3506 }
3507 if (el instanceof HTMLIFrameElement) {
3508 return true;
3509 }
3510 if (el instanceof HTMLTextAreaElement) {
3511 return true;
3512 }
3513 if (el instanceof HTMLInputElement) {
3514 const textTypes = /* @__PURE__ */ new Set([
3515 "text",
3516 "search",
3517 "url",
3518 "email",
3519 "password",
3520 "tel",
3521 "number",
3522 "date",
3523 "datetime-local",
3524 "month",
3525 "week",
3526 "time"
3527 ]);
3528 return textTypes.has(el.type);
3529 }
3530 if (el instanceof HTMLElement && el.isContentEditable === true) {
3531 return true;
3532 }
3533 const ce = el.getAttribute("contenteditable");
3534 return ce !== null && ce !== "false";
3535 }
3536 function installWindowSwitcherShortcut(mgr) {
3537 if (installed$3) {
3538 return;
3539 }
3540 installed$3 = true;
3541 document.addEventListener(
3542 "keydown",
3543 (e) => {
3544 if (e.ctrlKey || e.metaKey || e.altKey) {
3545 return;
3546 }
3547 if (e.code !== "Backquote") {
3548 return;
3549 }
3550 if (isTextEntryFocus(document)) {
3551 return;
3552 }
3553 e.preventDefault();
3554 cycleFocus(mgr, e.shiftKey ? "prev" : "next");
3555 },
3556 true
3557 );
3558 const origin = window.location.origin;
3559 window.addEventListener("message", (e) => {
3560 if (e.origin !== origin) {
3561 return;
3562 }
3563 const data = e.data;
3564 if (!data || data.type !== "desktop-mode-window-switch") {
3565 return;
3566 }
3567 cycleFocus(mgr, data.direction === "prev" ? "prev" : "next");
3568 });
3569 }
3570 function switchToAdjacentDesktop(mgr, direction) {
3571 const desktops = mgr.getDesktops();
3572 if (desktops.length < 2) {
3573 return false;
3574 }
3575 const activeId = mgr.getActiveDesktopId();
3576 const idx = desktops.findIndex((d) => d.id === activeId);
3577 if (idx === -1) {
3578 return false;
3579 }
3580 const step = direction === "next" ? 1 : -1;
3581 const targetIdx = (idx + step + desktops.length) % desktops.length;
3582 if (targetIdx === idx) {
3583 return false;
3584 }
3585 mgr.switchDesktop(desktops[targetIdx].id, { direction });
3586 return true;
3587 }
3588 function cycleOverviewCursor(mgr, direction) {
3589 if (!mgr._overviewActive) {
3590 return false;
3591 }
3592 const desktops = mgr.getDesktops();
3593 const cycleLength = desktops.length + 1;
3594 const ADD_INDEX = desktops.length;
3595 const currentIdx = mgr._overviewAddTileFocused ? ADD_INDEX : desktops.findIndex((d) => d.id === mgr.getActiveDesktopId());
3596 if (currentIdx === -1) {
3597 return false;
3598 }
3599 const step = direction === "next" ? 1 : -1;
3600 const targetIdx = (currentIdx + step + cycleLength) % cycleLength;
3601 if (targetIdx === currentIdx) {
3602 return false;
3603 }
3604 if (targetIdx === ADD_INDEX) {
3605 mgr._overviewAddTileFocused = true;
3606 refreshOverviewTopBar(mgr);
3607 return true;
3608 }
3609 mgr._overviewAddTileFocused = false;
3610 mgr.switchDesktop(desktops[targetIdx].id, { direction });
3611 return true;
3612 }
3613 function toggleOverview(mgr) {
3614 if (mgr._overviewActive) {
3615 mgr.exitOverview();
3616 } else {
3617 mgr.enterOverview();
3618 }
3619 return true;
3620 }
3621 function toggleShowDesktop(mgr) {
3622 if (mgr._overviewActive) {
3623 return false;
3624 }
3625 if (mgr.getAll().length === 0) {
3626 return false;
3627 }
3628 mgr.toggleShowDesktop();
3629 return true;
3630 }
3631 function exitOverviewIfActive(mgr) {
3632 if (!mgr._overviewActive) {
3633 return false;
3634 }
3635 mgr.exitOverview();
3636 return true;
3637 }
3638 function isShowDesktopActive(mgr) {
3639 const all2 = mgr.getAll();
3640 if (all2.length === 0) {
3641 return false;
3642 }
3643 return all2.every((w) => w.state === "minimized");
3644 }
3645 function exitShowDesktopIfActive(mgr) {
3646 if (!isShowDesktopActive(mgr)) {
3647 return false;
3648 }
3649 mgr.toggleShowDesktop();
3650 return true;
3651 }
3652 let installed$2 = false;
3653 function installDesktopArrowShortcuts(mgr) {
3654 if (installed$2) {
3655 return;
3656 }
3657 installed$2 = true;
3658 document.addEventListener(
3659 "keydown",
3660 (e) => {
3661 if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) {
3662 return;
3663 }
3664 if (e.code !== "ArrowLeft" && e.code !== "ArrowRight" && e.code !== "ArrowUp" && e.code !== "ArrowDown") {
3665 return;
3666 }
3667 if (isTextEntryFocus(document)) {
3668 return;
3669 }
3670 let handled = false;
3671 switch (e.code) {
3672 case "ArrowLeft":
3673 handled = mgr._overviewActive ? cycleOverviewCursor(mgr, "prev") : switchToAdjacentDesktop(mgr, "prev");
3674 break;
3675 case "ArrowRight":
3676 handled = mgr._overviewActive ? cycleOverviewCursor(mgr, "next") : switchToAdjacentDesktop(mgr, "next");
3677 break;
3678 case "ArrowUp":
3679 handled = exitOverviewIfActive(mgr) || exitShowDesktopIfActive(mgr) || toggleOverview(mgr);
3680 break;
3681 case "ArrowDown":
3682 handled = exitOverviewIfActive(mgr) || toggleShowDesktop(mgr);
3683 break;
3684 }
3685 if (handled) {
3686 e.preventDefault();
3687 }
3688 },
3689 true
3690 );
3691 }
3692 const IDENTITY_PARAMS = [
3693 "post_type",
3694 "page",
3695 "taxonomy",
3696 // WooCommerce (and other React-app-style plugins) register
3697 // SEPARATE top-level admin menus that all share `?page=wc-admin`
3698 // and only differ by `path` (e.g. `path=/analytics/overview`,
3699 // `path=/marketing`). Without `path` in the identity set, every
3700 // such menu collapses to the same window id — opening any one of
3701 // them lights up the dock indicator for ALL of them. WC's
3702 // /admin/path query is the most prominent example today; future
3703 // plugins that route inside `admin.php?page=` via a custom param
3704 // can either piggyback on `path` or grow this list.
3705 "path",
3706 // The post ID on `post.php?post=X&action=edit`. Without this, every
3707 // individual post edit URL collapses to `post-php`, so clicking a
3708 // second row in the Posts window just refocuses the first post's
3709 // window instead of opening the new one.
3710 "post",
3711 // Site-editor entity path: `site-editor.php?p=/wp_template_part/
3712 // twentytwentyfive//footer-columns`. Each template / template
3713 // part / pattern / navigation entity is a distinct "page" from
3714 // the user's perspective — picking "Header" after "Footer column"
3715 // should open a new window, not refocus the existing footer one.
3716 // Without `p` in identity, every site-editor URL collapses to
3717 // `site-editor-php` and the second pick is a no-op.
3718 "p"
3719 ];
3720 function slugify$1(path) {
3721 return path.replace(/\.php/g, "-php").replace(/[?&=]/g, "-").replace(/[^a-zA-Z0-9_-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "") || "index";
3722 }
3723 function deriveWindowId(url, adminUrl) {
3724 let parsed = null;
3725 try {
3726 parsed = new URL(url, adminUrl);
3727 } catch (err) {
3728 parsed = null;
3729 }
3730 if (parsed) {
3731 const basePath = new URL(adminUrl).pathname;
3732 const filename = parsed.pathname.replace(basePath, "").replace(/^\/+/, "");
3733 const significant = new URLSearchParams();
3734 for (const key of IDENTITY_PARAMS) {
3735 const value = parsed.searchParams.get(key);
3736 if (value) {
3737 significant.set(key, value);
3738 }
3739 }
3740 const query = significant.toString();
3741 return slugify$1(query ? `${filename}?${query}` : filename);
3742 }
3743 let path = url.replace(adminUrl, "");
3744 if (path.startsWith("/")) {
3745 path = path.substring(1);
3746 }
3747 return slugify$1(path);
3748 }
3749 function sanitizeClassName(value) {
3750 return value.replace(/[^a-zA-Z0-9_-]/g, "");
3751 }
3752 function applyTileEntryStagger(tile2) {
3753 tile2.style.setProperty(
3754 "--desktop-mode-file-tile-enter-delay",
3755 `${(Math.random() * 0.25).toFixed(3)}s`
3756 );
3757 tile2.style.setProperty(
3758 "--desktop-mode-file-tile-enter-duration",
3759 `${(0.3 + Math.random() * 0.25).toFixed(3)}s`
3760 );
3761 }
3762 function urlMatchKey(url) {
3763 try {
3764 const parsed = new URL(url, window.location.origin);
3765 parsed.searchParams.delete("desktop_mode_chromeless");
3766 parsed.searchParams.delete("desktop_mode_portal");
3767 return parsed.pathname.replace(/\/+$/, "") + "?" + parsed.searchParams.toString();
3768 } catch {
3769 return url;
3770 }
3771 }
3772 function sanitizeIconSvg(svg) {
3773 if (typeof svg !== "string" || svg === "") {
3774 return "";
3775 }
3776 if (typeof DOMParser === "undefined") {
3777 return "";
3778 }
3779 let doc;
3780 try {
3781 doc = new DOMParser().parseFromString(svg, "image/svg+xml");
3782 } catch {
3783 return "";
3784 }
3785 const root = doc.documentElement;
3786 if (!root || root.nodeName.toLowerCase() !== "svg") {
3787 return "";
3788 }
3789 if (doc.getElementsByTagName("parsererror").length > 0) {
3790 return "";
3791 }
3792 const BANNED_TAGS = /* @__PURE__ */ new Set(["script", "style", "foreignobject", "iframe", "object", "embed"]);
3793 const walk2 = (el) => {
3794 const children = Array.from(el.children);
3795 for (const child of children) {
3796 if (BANNED_TAGS.has(child.nodeName.toLowerCase())) {
3797 child.remove();
3798 continue;
3799 }
3800 for (const attr of Array.from(child.attributes)) {
3801 const name = attr.name.toLowerCase();
3802 const value = attr.value.trim().toLowerCase();
3803 if (name.startsWith("on")) {
3804 child.removeAttribute(attr.name);
3805 continue;
3806 }
3807 if (value.startsWith("javascript:")) {
3808 child.removeAttribute(attr.name);
3809 }
3810 }
3811 walk2(child);
3812 }
3813 };
3814 walk2(root);
3815 for (const attr of Array.from(root.attributes)) {
3816 const name = attr.name.toLowerCase();
3817 const value = attr.value.trim().toLowerCase();
3818 if (name.startsWith("on") || value.startsWith("javascript:")) {
3819 root.removeAttribute(attr.name);
3820 }
3821 }
3822 return root.outerHTML;
3823 }
3824 const _parentSubs = /* @__PURE__ */ new Map();
3825 const _nativeSubs = /* @__PURE__ */ new Map();
3826 function bucket(root, windowId, channel, create) {
3827 let perWindow = root.get(windowId);
3828 if (!perWindow) {
3829 if (!create) {
3830 return void 0;
3831 }
3832 perWindow = /* @__PURE__ */ new Map();
3833 root.set(windowId, perWindow);
3834 }
3835 let bucketSet = perWindow.get(channel);
3836 if (!bucketSet) {
3837 if (!create) {
3838 return void 0;
3839 }
3840 bucketSet = /* @__PURE__ */ new Set();
3841 perWindow.set(channel, bucketSet);
3842 }
3843 return bucketSet;
3844 }
3845 function dispatch(root, windowId, channel, payload) {
3846 const meta = { channel, windowId };
3847 const exact = bucket(root, windowId, channel, false);
3848 if (exact) {
3849 for (const cb of Array.from(exact)) {
3850 try {
3851 cb(payload, meta);
3852 } catch (err) {
3853 if (typeof console !== "undefined") {
3854 console.error(
3855 `[desktop-mode] window-channel subscriber for "${channel}" threw:`,
3856 err
3857 );
3858 }
3859 }
3860 }
3861 }
3862 const wildcard = bucket(root, windowId, "*", false);
3863 if (wildcard) {
3864 for (const cb of Array.from(wildcard)) {
3865 try {
3866 cb(payload, meta);
3867 } catch (err) {
3868 if (typeof console !== "undefined") {
3869 console.error(
3870 `[desktop-mode] window-channel wildcard subscriber for "${windowId}" threw:`,
3871 err
3872 );
3873 }
3874 }
3875 }
3876 }
3877 }
3878 function addParentSubscriber(windowId, channel, cb) {
3879 const set = bucket(_parentSubs, windowId, channel, true);
3880 set.add(cb);
3881 let removed = false;
3882 return () => {
3883 if (removed) {
3884 return;
3885 }
3886 removed = true;
3887 set.delete(cb);
3888 };
3889 }
3890 function dispatchFromWindow(windowId, channel, payload) {
3891 dispatch(_parentSubs, windowId, channel, payload);
3892 }
3893 function dispatchToNative(windowId, channel, payload) {
3894 dispatch(_nativeSubs, windowId, channel, payload);
3895 }
3896 const _readyWindows = /* @__PURE__ */ new Set();
3897 const _loadingWindows = /* @__PURE__ */ new Set();
3898 const _pendingSends = /* @__PURE__ */ new Map();
3899 function markWindowContentReady(windowId) {
3900 if (!_readyWindows.has(windowId)) {
3901 _readyWindows.add(windowId);
3902 const queued = _pendingSends.get(windowId);
3903 if (queued) {
3904 _pendingSends.delete(windowId);
3905 for (const m of queued) {
3906 try {
3907 m.flush();
3908 } catch (err) {
3909 if (typeof console !== "undefined") {
3910 console.error(
3911 `[desktop-mode] flushing queued window-send for "${m.channel}" threw:`,
3912 err
3913 );
3914 }
3915 }
3916 }
3917 }
3918 }
3919 if (_loadingWindows.delete(windowId)) {
3920 doAction(HOOKS.WINDOW_CONTENT_LOADED, { windowId });
3921 if (typeof document !== "undefined") {
3922 document.dispatchEvent(
3923 new CustomEvent("desktop-mode-window-content-loaded", {
3924 detail: { windowId }
3925 })
3926 );
3927 }
3928 }
3929 }
3930 const WINDOW_CONFIG_KEY = Symbol.for("desktop-mode/window-config");
3931 function getWindowConfigFromElement(el) {
3932 return el[WINDOW_CONFIG_KEY];
3933 }
3934 function buildDefaultLoadingOverlay() {
3935 const overlay = document.createElement("div");
3936 overlay.className = "desktop-mode-window__loading";
3937 overlay.setAttribute("aria-hidden", "true");
3938 const spinner = document.createElement("wpd-spinner");
3939 spinner.setAttribute("preset", "classic");
3940 spinner.setAttribute("size", "clamp(96px, 14vw, 192px)");
3941 spinner.setAttribute("label", __("Loading window content"));
3942 overlay.appendChild(spinner);
3943 return overlay;
3944 }
3945 function createLoadingOverlay(config) {
3946 let overlay = buildDefaultLoadingOverlay();
3947 const ctx = { windowId: config.id, config };
3948 if (typeof config.loading?.render === "function") {
3949 try {
3950 config.loading.render(overlay, ctx);
3951 } catch (err) {
3952 if (typeof console !== "undefined") {
3953 console.error(
3954 `[desktop-mode] loading.render threw for "${config.id}":`,
3955 err
3956 );
3957 }
3958 }
3959 }
3960 try {
3961 const filtered = applyFilters(
3962 HOOKS.WINDOW_LOADING_OVERLAY,
3963 overlay,
3964 ctx
3965 );
3966 if (filtered instanceof HTMLElement) {
3967 overlay = filtered;
3968 }
3969 } catch (err) {
3970 if (typeof console !== "undefined") {
3971 console.error(
3972 `[desktop-mode] WINDOW_LOADING_OVERLAY filter threw for "${config.id}":`,
3973 err
3974 );
3975 }
3976 }
3977 if (overlay && !overlay.classList.contains("desktop-mode-window__loading")) {
3978 overlay.classList.add("desktop-mode-window__loading");
3979 }
3980 return overlay;
3981 }
3982 function removeLoadingOverlay(windowEl) {
3983 const overlay = windowEl.querySelector(":scope .desktop-mode-window__loading");
3984 overlay?.remove();
3985 }
3986 function ensureLoadingOverlay(windowEl) {
3987 const body = windowEl.querySelector(
3988 ":scope .desktop-mode-window__body"
3989 );
3990 if (!body) {
3991 return;
3992 }
3993 const existing = body.querySelector(":scope .desktop-mode-window__loading");
3994 if (existing) {
3995 return;
3996 }
3997 const config = getWindowConfigFromElement(windowEl);
3998 body.appendChild(config ? createLoadingOverlay(config) : buildDefaultLoadingOverlay());
3999 }
4000 const FADE_OUT_MS$1 = 250;
4001 let _installed$3 = false;
4002 function findWindowElement(windowId) {
4003 if (!windowId) {
4004 return null;
4005 }
4006 return document.getElementById(`wp-window-${windowId}`);
4007 }
4008 function installWindowLoadingTransitions() {
4009 if (_installed$3) {
4010 return;
4011 }
4012 _installed$3 = true;
4013 _installSubscriptions();
4014 }
4015 function _installSubscriptions() {
4016 addAction(
4017 HOOKS.WINDOW_CONTENT_LOADING,
4018 "desktop-mode/window-loading-enter",
4019 (e) => {
4020 const el = findWindowElement(e?.windowId ?? "");
4021 if (!el) {
4022 return;
4023 }
4024 const body = el.querySelector(
4025 ":scope .desktop-mode-window__body"
4026 );
4027 if (!body) {
4028 return;
4029 }
4030 body.classList.add("desktop-mode-window__body--loading");
4031 ensureLoadingOverlay(el);
4032 }
4033 );
4034 addAction(
4035 HOOKS.WINDOW_CONTENT_LOADED,
4036 "desktop-mode/window-loading-exit",
4037 (e) => {
4038 const el = findWindowElement(e?.windowId ?? "");
4039 if (!el) {
4040 return;
4041 }
4042 const body = el.querySelector(
4043 ":scope .desktop-mode-window__body"
4044 );
4045 if (!body) {
4046 return;
4047 }
4048 body.classList.remove("desktop-mode-window__body--loading");
4049 window.setTimeout(() => {
4050 if (!body.classList.contains("desktop-mode-window__body--loading")) {
4051 removeLoadingOverlay(el);
4052 }
4053 }, FADE_OUT_MS$1);
4054 }
4055 );
4056 addAction(
4057 HOOKS.INIT,
4058 "desktop-mode/loading-overlay-init-sweep",
4059 () => {
4060 queueMicrotask(() => repaintLoadingOverlays());
4061 }
4062 );
4063 }
4064 function repaintLoadingOverlays() {
4065 const bodies = document.querySelectorAll(
4066 ".desktop-mode-window__body--loading"
4067 );
4068 bodies.forEach((body) => {
4069 const windowEl = body.closest(".desktop-mode-window");
4070 if (!windowEl) {
4071 return;
4072 }
4073 body.querySelector(":scope .desktop-mode-window__loading")?.remove();
4074 ensureLoadingOverlay(windowEl);
4075 });
4076 }
4077 const SHARED_STORES_SLOT = "__desktopModeSharedStores";
4078 function resolveSlot() {
4079 const w = window;
4080 let slot = w[SHARED_STORES_SLOT];
4081 if (!slot) {
4082 slot = /* @__PURE__ */ new Map();
4083 w[SHARED_STORES_SLOT] = slot;
4084 }
4085 return slot;
4086 }
4087 function createSharedStore(key, initialState) {
4088 const slot = resolveSlot();
4089 let record = slot.get(key);
4090 if (!record) {
4091 record = {
4092 state: initialState(),
4093 listeners: /* @__PURE__ */ new Set(),
4094 rebuild: initialState
4095 };
4096 slot.set(key, record);
4097 }
4098 const handle = {
4099 // `record.state` is the live reference. The getter on the
4100 // `state` field reads the latest value even if `reset()`
4101 // reassigned it to a fresh object.
4102 get state() {
4103 return record.state;
4104 },
4105 set state(next) {
4106 record.state = next;
4107 },
4108 getState() {
4109 return record.state;
4110 },
4111 notify() {
4112 for (const cb of Array.from(record.listeners)) {
4113 try {
4114 cb(record.state);
4115 } catch (err) {
4116 console.error(
4117 `[desktop-mode/shared-store:${key}] subscriber threw:`,
4118 err
4119 );
4120 }
4121 }
4122 },
4123 subscribe(cb) {
4124 record.listeners.add(cb);
4125 return () => {
4126 record.listeners.delete(cb);
4127 };
4128 },
4129 setState(patch) {
4130 const cur = record.state;
4131 if (typeof cur !== "object" || cur === null) {
4132 console.warn(
4133 `[desktop-mode/shared-store:${key}] setState called on a primitive store; use the state setter instead.`
4134 );
4135 return;
4136 }
4137 Object.assign(cur, patch);
4138 handle.notify();
4139 },
4140 reset() {
4141 const fresh = record.rebuild();
4142 const cur = record.state;
4143 if (typeof cur === "object" && cur !== null && typeof fresh === "object" && fresh !== null) {
4144 const target = cur;
4145 for (const k of Object.keys(target)) {
4146 delete target[k];
4147 }
4148 Object.assign(target, fresh);
4149 } else {
4150 record.state = fresh;
4151 }
4152 record.listeners.clear();
4153 }
4154 };
4155 return handle;
4156 }
4157 const remapStore = createSharedStore(
4158 "desktop-mode/native-url-remap",
4159 () => ({ remaps: [], deps: null })
4160 );
4161 function bindNativeUrlRemap(bound) {
4162 remapStore.state.deps = bound;
4163 }
4164 function registerNativeUrlRemap(entry) {
4165 if (!entry || typeof entry.id !== "string" || entry.id.trim() === "") {
4166 return () => {
4167 };
4168 }
4169 if (typeof entry.nativeWindowId !== "string" || entry.nativeWindowId === "") {
4170 return () => {
4171 };
4172 }
4173 if (typeof entry.matches !== "function") {
4174 return () => {
4175 };
4176 }
4177 const remaps = remapStore.state.remaps;
4178 const existingIdx = remaps.findIndex((r) => r.id === entry.id);
4179 if (existingIdx >= 0) {
4180 remaps.splice(existingIdx, 1);
4181 }
4182 remaps.push(entry);
4183 return () => unregisterNativeUrlRemap(entry.id);
4184 }
4185 function unregisterNativeUrlRemap(id) {
4186 const remaps = remapStore.state.remaps;
4187 const i = remaps.findIndex((r) => r.id === id);
4188 if (i >= 0) {
4189 remaps.splice(i, 1);
4190 }
4191 }
4192 function resolveNativeUrlRemap(url) {
4193 const { deps: deps2, remaps } = remapStore.state;
4194 if (!deps2 || !url) {
4195 return null;
4196 }
4197 let parsed;
4198 try {
4199 parsed = new URL(url, deps2.adminUrl);
4200 } catch {
4201 return null;
4202 }
4203 const snapshot = deps2.getSnapshot();
4204 for (const entry of remaps) {
4205 if (!entry.matches(url, parsed)) {
4206 continue;
4207 }
4208 if (entry.enabled && !entry.enabled(snapshot)) {
4209 continue;
4210 }
4211 return entry.nativeWindowId;
4212 }
4213 return null;
4214 }
4215 function tryNativeUrlRemap(url) {
4216 const { deps: deps2, remaps } = remapStore.state;
4217 if (!deps2 || !url) {
4218 return false;
4219 }
4220 let parsed;
4221 try {
4222 parsed = new URL(url, deps2.adminUrl);
4223 } catch {
4224 return false;
4225 }
4226 const snapshot = deps2.getSnapshot();
4227 for (const entry of remaps) {
4228 if (!entry.matches(url, parsed)) {
4229 continue;
4230 }
4231 if (entry.enabled && !entry.enabled(snapshot)) {
4232 continue;
4233 }
4234 if (entry.onMatch) {
4235 try {
4236 entry.onMatch(url, parsed);
4237 } catch (err) {
4238 console.warn(
4239 `[desktop-mode] URL remap onMatch hook threw for "${entry.id}":`,
4240 err
4241 );
4242 }
4243 }
4244 if (deps2.openById(entry.nativeWindowId)) {
4245 return true;
4246 }
4247 }
4248 return false;
4249 }
4250 const HOOK_PREFIX = "desktop-mode.activity.";
4251 function hookName(channel) {
4252 return `${HOOK_PREFIX}${String(channel)}`;
4253 }
4254 let subscribeSeq = 0;
4255 const activity = {
4256 publish(channel, payload) {
4257 doAction(hookName(channel), payload);
4258 },
4259 subscribe(channel, cb) {
4260 const ns = `desktop-mode/activity-sub/${++subscribeSeq}`;
4261 const hook = hookName(channel);
4262 addAction(
4263 hook,
4264 ns,
4265 (payload) => cb(payload)
4266 );
4267 let removed = false;
4268 return () => {
4269 if (removed) {
4270 return;
4271 }
4272 removed = true;
4273 removeAction(hook, ns);
4274 };
4275 },
4276 filter(channel, value, ...args) {
4277 return applyFilters(hookName(channel), value, ...args);
4278 }
4279 };
4280 const DEFAULT_DURATION_MS = 4e3;
4281 const FADE_OUT_MS = 200;
4282 function showToast(options) {
4283 const intent = activity.filter(
4284 "desktop-mode/toast-requested",
4285 { ...options }
4286 );
4287 if (!intent || intent.cancel === true) {
4288 return () => void 0;
4289 }
4290 let dismissRequested = false;
4291 let realDismiss = null;
4292 openWithShellOverlays(
4293 () => !dismissRequested,
4294 () => {
4295 realDismiss = renderToast(intent);
4296 }
4297 );
4298 return () => {
4299 dismissRequested = true;
4300 if (realDismiss) {
4301 realDismiss();
4302 }
4303 };
4304 }
4305 function renderToast(intent) {
4306 const container = ensureContainer();
4307 const toast = document.createElement("wpd-toast");
4308 toast.textContent = intent.message;
4309 if (intent.action) {
4310 toast.setAttribute("action", intent.action.label);
4311 toast.addEventListener("wpd-toast-action", () => {
4312 intent.action?.onClick();
4313 dismiss();
4314 });
4315 }
4316 container.appendChild(toast);
4317 let dismissed = false;
4318 let dismissTimer = null;
4319 const dismiss = () => {
4320 if (dismissed) {
4321 return;
4322 }
4323 dismissed = true;
4324 if (dismissTimer !== null) {
4325 window.clearTimeout(dismissTimer);
4326 dismissTimer = null;
4327 }
4328 toast.setAttribute("state", "out");
4329 window.setTimeout(() => {
4330 toast.remove();
4331 }, FADE_OUT_MS);
4332 };
4333 requestAnimationFrame(() => {
4334 toast.setAttribute("state", "in");
4335 });
4336 dismissTimer = window.setTimeout(
4337 dismiss,
4338 intent.duration ?? DEFAULT_DURATION_MS
4339 );
4340 activity.publish("desktop-mode/toast-shown", { ...intent });
4341 return dismiss;
4342 }
4343 function ensureContainer() {
4344 const existing = document.querySelector(
4345 "wpd-toast-container"
4346 );
4347 if (existing) {
4348 return existing;
4349 }
4350 const el = document.createElement("wpd-toast-container");
4351 document.body.appendChild(el);
4352 return el;
4353 }
4354 const store$d = createSharedStore(
4355 "desktop-mode/destructive-admin-actions",
4356 () => ({ entries: [] })
4357 );
4358 function registerDestructiveAdminAction(entry) {
4359 if (!entry || typeof entry.id !== "string" || entry.id.trim() === "") {
4360 return () => {
4361 };
4362 }
4363 if (typeof entry.matches !== "function") {
4364 return () => {
4365 };
4366 }
4367 const entries = store$d.state.entries;
4368 const idx = entries.findIndex((e) => e.id === entry.id);
4369 if (idx >= 0) {
4370 entries.splice(idx, 1);
4371 }
4372 entries.push(entry);
4373 return () => unregisterDestructiveAdminAction(entry.id);
4374 }
4375 function unregisterDestructiveAdminAction(id) {
4376 const entries = store$d.state.entries;
4377 const idx = entries.findIndex((e) => e.id === id);
4378 if (idx >= 0) {
4379 entries.splice(idx, 1);
4380 }
4381 }
4382 function listDestructiveAdminActions() {
4383 return store$d.state.entries.slice();
4384 }
4385 const adminLinkDepsStore = createSharedStore(
4386 "desktop-mode/admin-link-deps",
4387 () => ({ deps: null })
4388 );
4389 function bindAdminLinkDispatch(deps2) {
4390 adminLinkDepsStore.state.deps = deps2;
4391 }
4392 function collectRegistrationErrors(def, checks) {
4393 if (!def || typeof def !== "object") {
4394 return ["def (not an object)"];
4395 }
4396 const d = def;
4397 const errors = [];
4398 for (const check of checks) {
4399 if (!check.valid(d)) {
4400 errors.push(`${check.field} (${check.message})`);
4401 }
4402 }
4403 return errors;
4404 }
4405 class RegistrationError extends Error {
4406 constructor(kind, errors, def) {
4407 super(
4408 `[desktop-mode] ${kind} registration rejected — fields: ` + errors.join(", ") + "."
4409 );
4410 this.name = "RegistrationError";
4411 this.kind = kind;
4412 this.errors = errors;
4413 this.def = def;
4414 }
4415 }
4416 function throwOnRegistrationErrors(kind, errors, def) {
4417 if (errors.length === 0) {
4418 return;
4419 }
4420 throw new RegistrationError(kind, errors, def);
4421 }
4422 const store$c = createSharedStore(
4423 "desktop-mode/wallpaper-registry",
4424 () => ({
4425 seed: [],
4426 listeners: /* @__PURE__ */ new Set()
4427 })
4428 );
4429 const seed$3 = store$c.state.seed;
4430 const listeners$b = store$c.state.listeners;
4431 function register$2(def) {
4432 throwOnRegistrationErrors(
4433 "Wallpaper",
4434 collectRegistrationErrors(def, WALLPAPER_CHECKS),
4435 def
4436 );
4437 const idx = seed$3.findIndex((w) => w.id === def.id);
4438 if (idx >= 0) {
4439 seed$3[idx] = def;
4440 } else {
4441 seed$3.push(def);
4442 }
4443 notify$d();
4444 }
4445 function unregister$2(id) {
4446 const idx = seed$3.findIndex((w) => w.id === id);
4447 if (idx >= 0) {
4448 seed$3.splice(idx, 1);
4449 notify$d();
4450 }
4451 }
4452 function notify$d() {
4453 const snapshot = Array.from(listeners$b);
4454 for (const cb of snapshot) {
4455 try {
4456 cb();
4457 } catch (err) {
4458 if (typeof console !== "undefined") {
4459 console.error(
4460 "[desktop-mode] wallpaper registry listener threw:",
4461 err
4462 );
4463 }
4464 }
4465 }
4466 }
4467 function all$1() {
4468 const copy = seed$3.slice();
4469 const filtered = applyFilters(HOOKS.WALLPAPERS, copy);
4470 if (!Array.isArray(filtered)) {
4471 if (typeof console !== "undefined") {
4472 console.warn(
4473 "[desktop-mode] `desktop-mode.wallpapers` filter returned a non-array; falling back to seed list."
4474 );
4475 }
4476 return copy;
4477 }
4478 return filtered.filter(isValidDef$1);
4479 }
4480 function get$1(id) {
4481 return all$1().find((w) => w.id === id);
4482 }
4483 const WALLPAPER_CHECKS = [
4484 {
4485 field: "id",
4486 message: "missing or not a non-empty string",
4487 valid: (d) => typeof d.id === "string" && d.id !== ""
4488 },
4489 {
4490 field: "label",
4491 message: "missing or not a non-empty string",
4492 valid: (d) => typeof d.label === "string" && d.label !== ""
4493 },
4494 {
4495 field: "preview",
4496 message: "missing or not a non-empty string",
4497 valid: (d) => typeof d.preview === "string" && d.preview !== ""
4498 },
4499 {
4500 field: "type",
4501 message: 'must be "css" or "canvas"',
4502 valid: (d) => d.type === "css" || d.type === "canvas"
4503 },
4504 {
4505 field: "value/resolveValue/mount",
4506 message: "css types need `value` or `resolveValue`; canvas types need `mount`",
4507 valid: (d) => {
4508 if (d.type === "css") {
4509 return typeof d.value === "string" || typeof d.resolveValue === "function";
4510 }
4511 if (d.type === "canvas") {
4512 return typeof d.mount === "function";
4513 }
4514 return true;
4515 }
4516 }
4517 ];
4518 function isValidDef$1(def) {
4519 return collectRegistrationErrors(def, WALLPAPER_CHECKS).length === 0;
4520 }
4521 const STORAGE_KEY = "desktop-mode-os-settings";
4522 const CUSTOM_GRADIENT_ID = "custom-gradient";
4523 const CUSTOM_IMAGE_ID = "custom-image";
4524 const DEFAULT_WALLPAPER_ID = "dark";
4525 const DEFAULT_ACCENTS = [
4526 { id: "wp-blue", label: "WordPress Blue", value: "#2271b1" },
4527 { id: "indigo", label: "Indigo", value: "#3858e9" },
4528 { id: "teal", label: "Teal", value: "#04a4cc" },
4529 { id: "emerald", label: "Emerald", value: "#059669" },
4530 { id: "amber", label: "Amber", value: "#d97706" },
4531 { id: "rose", label: "Rose", value: "#e11d48" }
4532 ];
4533 function getAccents() {
4534 const config = window.wp?.desktop?.config;
4535 const raw = config?.accentColors;
4536 if (!Array.isArray(raw) || raw.length === 0) {
4537 return DEFAULT_ACCENTS;
4538 }
4539 const clean = [];
4540 for (const entry of raw) {
4541 if (entry && typeof entry === "object" && typeof entry.id === "string" && typeof entry.label === "string" && typeof entry.value === "string" && entry.id !== "" && entry.label !== "" && /^#[0-9a-f]{3,8}$/i.test(entry.value)) {
4542 clean.push({ id: entry.id, label: entry.label, value: entry.value });
4543 }
4544 }
4545 return clean.length > 0 ? clean : DEFAULT_ACCENTS;
4546 }
4547 function getDefaultWallpaperId() {
4548 const config = window.wp?.desktop?.config;
4549 const raw = config?.defaultWallpaper;
4550 if (typeof raw === "string" && raw !== "") {
4551 return raw;
4552 }
4553 return DEFAULT_WALLPAPER_ID;
4554 }
4555 const DOCK_SIZES = [
4556 { id: "compact", label: "Compact", width: 48, icon: 18 },
4557 { id: "default", label: "Default", width: 56, icon: 20 },
4558 { id: "large", label: "Large", width: 72, icon: 26 }
4559 ];
4560 const DESKTOP_LAYOUTS = [
4561 { id: "classic", label: "Classic" },
4562 { id: "unified", label: "Unified" },
4563 { id: "spatial", label: "Spatial" }
4564 ];
4565 const DEFAULTS = {
4566 wallpaper: DEFAULT_WALLPAPER_ID,
4567 accent: "wp-blue",
4568 dockSize: "default",
4569 desktopLayout: "classic",
4570 dockRailRenderer: "default",
4571 customGradient: {
4572 from: "#2271b1",
4573 to: "#7c3aed",
4574 angle: 135
4575 },
4576 customImage: null,
4577 libraryHdOnly: true,
4578 ai: {
4579 enabled: false,
4580 provider: "openai",
4581 apiKey: "",
4582 apiKeys: {},
4583 transport: "off"
4584 },
4585 // Opt-out as of 0.8.0. Fresh installs land on the native Posts
4586 // window — same screen the rest of desktop mode is built for. A
4587 // user can still flip this off to fall back to the chromeless
4588 // `edit.php` iframe, but the new default is "use the native UI."
4589 heartbeatRate: 60,
4590 nativePostsEnabled: true,
4591 nativePostsHiddenColumns: [],
4592 // Same opt-out posture as Posts — fresh installs land on the
4593 // native Pages window, users can flip back to the iframe.
4594 nativePagesEnabled: true,
4595 // Native Users window — same opt-out posture. Capability-gated
4596 // server-side (the window is only registered for users with
4597 // `list_users`), so flipping this off only affects the small set
4598 // of users who can see the Users tile in the first place.
4599 nativeUsersEnabled: true,
4600 // Native Plugins window — replaces `plugins.php` and
4601 // `plugin-install.php`. Same opt-out posture; cap-gated on
4602 // `activate_plugins` server-side, so flipping this off only
4603 // affects users who could see the Plugins tile anyway.
4604 nativePluginsEnabled: true,
4605 // Native Comments window — replaces `edit-comments.php`. Same
4606 // opt-out posture; cap-gated on `edit_posts` server-side.
4607 nativeCommentsEnabled: true,
4608 showDesktopOnWallpaperClick: false,
4609 showPostStatusRibbons: true,
4610 foldersSharingEnabled: true,
4611 itemVisibility: {},
4612 dockOrder: [],
4613 dockPromotedPositions: {}
4614 };
4615 const AI_TRANSPORTS = [
4616 { id: "off", label: "Off" },
4617 { id: "sse", label: "Streaming (SSE)" }
4618 ];
4619 const AI_PROVIDERS = [
4620 {
4621 id: "openai",
4622 label: "OpenAI",
4623 apiKeyLabel: "OpenAI API key",
4624 apiKeyLink: "https://platform.openai.com/api-keys"
4625 }
4626 ];
4627 function getAiProviders() {
4628 const cfg = window.desktopModeConfig;
4629 const list2 = cfg?.aiProviders;
4630 if (!Array.isArray(list2) || list2.length === 0) {
4631 return AI_PROVIDERS;
4632 }
4633 return list2.map((p) => ({
4634 id: p.id,
4635 label: p.label,
4636 description: p.description,
4637 apiKeyLabel: p.api_key_label,
4638 apiKeyLink: p.api_key_link
4639 }));
4640 }
4641 function isHexColor(value) {
4642 return typeof value === "string" && /^#[0-9a-f]{3,8}$/i.test(value);
4643 }
4644 const NONCE_HEADER = "X-WP-Nonce";
4645 function injectRestNonce(input, init2) {
4646 const nonce = readRestNonce$3();
4647 if (!nonce) {
4648 return init2;
4649 }
4650 const url = resolveUrl(input);
4651 if (!url || !isSameOriginRestUrl(url)) {
4652 return init2;
4653 }
4654 const baseHeaders = init2?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
4655 const headers = new Headers(baseHeaders ?? {});
4656 if (headers.has(NONCE_HEADER)) {
4657 return init2;
4658 }
4659 headers.set(NONCE_HEADER, nonce);
4660 return { ...init2 ?? {}, headers };
4661 }
4662 function readRestNonce$3() {
4663 if (typeof window === "undefined") {
4664 return void 0;
4665 }
4666 const cfg = window.desktopModeConfig;
4667 const value = cfg?.restNonce;
4668 return typeof value === "string" && value.length > 0 ? value : void 0;
4669 }
4670 function resolveUrl(input) {
4671 try {
4672 const base = typeof window !== "undefined" && window.location ? window.location.href : void 0;
4673 if (typeof input === "string") {
4674 return new URL(input, base);
4675 }
4676 if (input instanceof URL) {
4677 return input;
4678 }
4679 if (typeof Request !== "undefined" && input instanceof Request) {
4680 return new URL(input.url, base);
4681 }
4682 return null;
4683 } catch {
4684 return null;
4685 }
4686 }
4687 function isSameOriginRestUrl(url) {
4688 if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) {
4689 return false;
4690 }
4691 if (url.pathname.includes("/wp-json/")) {
4692 return true;
4693 }
4694 if (url.searchParams.has("rest_route")) {
4695 return true;
4696 }
4697 return false;
4698 }
4699 function trackedFetch$1(input, init2, opts = {}) {
4700 const fn = window.wp?.desktop?.fetch;
4701 if (typeof fn === "function") {
4702 return fn(input, init2, opts);
4703 }
4704 const finalInit = injectRestNonce(input, init2);
4705 return fetch(input, finalInit);
4706 }
4707 function loadState() {
4708 const serverRaw = _readServerSettings();
4709 if (serverRaw) {
4710 const state2 = _parseRaw(serverRaw);
4711 _writeLocalStorage(state2);
4712 return state2;
4713 }
4714 try {
4715 const cached = window.localStorage.getItem(STORAGE_KEY);
4716 if (cached) {
4717 return _parseRaw(JSON.parse(cached));
4718 }
4719 } catch {
4720 }
4721 return structuredDefaults();
4722 }
4723 function _readServerSettings() {
4724 const config = window.desktopModeConfig;
4725 const raw = config?.osSettings;
4726 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
4727 return null;
4728 }
4729 return raw;
4730 }
4731 function _parseRaw(parsed) {
4732 const accents = getAccents();
4733 return {
4734 wallpaper: typeof parsed.wallpaper === "string" && parsed.wallpaper !== "" ? parsed.wallpaper : getDefaultWallpaperId(),
4735 accent: accents.some((a) => a.id === parsed.accent) ? parsed.accent : DEFAULTS.accent,
4736 dockSize: DOCK_SIZES.some((d) => d.id === parsed.dockSize) ? parsed.dockSize : DEFAULTS.dockSize,
4737 desktopLayout: DESKTOP_LAYOUTS.some(
4738 (l) => l.id === parsed.desktopLayout
4739 ) ? parsed.desktopLayout : DEFAULTS.desktopLayout,
4740 // Dock rail renderer — any sanitize_key()-clean string
4741 // survives; the registry resolves at use time and falls back
4742 // to `'default'` when the picked renderer isn't registered.
4743 dockRailRenderer: typeof parsed.dockRailRenderer === "string" && /^[a-z0-9_-]+$/.test(parsed.dockRailRenderer) ? parsed.dockRailRenderer : DEFAULTS.dockRailRenderer,
4744 customGradient: sanitizeCustomGradient(parsed.customGradient),
4745 customImage: sanitizeCustomImage(parsed.customImage),
4746 libraryHdOnly: typeof parsed.libraryHdOnly === "boolean" ? parsed.libraryHdOnly : DEFAULTS.libraryHdOnly,
4747 ai: sanitizeAi(parsed.ai),
4748 heartbeatRate: parsed.heartbeatRate === 15 || parsed.heartbeatRate === 30 || parsed.heartbeatRate === 45 || parsed.heartbeatRate === 60 ? parsed.heartbeatRate : DEFAULTS.heartbeatRate,
4749 nativePostsEnabled: typeof parsed.nativePostsEnabled === "boolean" ? parsed.nativePostsEnabled : DEFAULTS.nativePostsEnabled,
4750 nativePostsHiddenColumns: Array.isArray(parsed.nativePostsHiddenColumns) ? parsed.nativePostsHiddenColumns.filter((v) => typeof v === "string" && v !== "").slice(0, 32) : DEFAULTS.nativePostsHiddenColumns.slice(),
4751 nativePagesEnabled: typeof parsed.nativePagesEnabled === "boolean" ? parsed.nativePagesEnabled : DEFAULTS.nativePagesEnabled,
4752 nativeUsersEnabled: typeof parsed.nativeUsersEnabled === "boolean" ? parsed.nativeUsersEnabled : DEFAULTS.nativeUsersEnabled,
4753 nativePluginsEnabled: typeof parsed.nativePluginsEnabled === "boolean" ? parsed.nativePluginsEnabled : DEFAULTS.nativePluginsEnabled,
4754 nativeCommentsEnabled: typeof parsed.nativeCommentsEnabled === "boolean" ? parsed.nativeCommentsEnabled : DEFAULTS.nativeCommentsEnabled,
4755 showDesktopOnWallpaperClick: typeof parsed.showDesktopOnWallpaperClick === "boolean" ? parsed.showDesktopOnWallpaperClick : DEFAULTS.showDesktopOnWallpaperClick,
4756 showPostStatusRibbons: typeof parsed.showPostStatusRibbons === "boolean" ? parsed.showPostStatusRibbons : DEFAULTS.showPostStatusRibbons,
4757 foldersSharingEnabled: typeof parsed.foldersSharingEnabled === "boolean" ? parsed.foldersSharingEnabled : DEFAULTS.foldersSharingEnabled,
4758 itemVisibility: sanitizeItemVisibility(parsed.itemVisibility),
4759 dockOrder: sanitizeDockOrder(parsed.dockOrder),
4760 dockPromotedPositions: sanitizeDockPromotedPositions(
4761 parsed.dockPromotedPositions
4762 )
4763 };
4764 }
4765 function sanitizeItemVisibility(raw) {
4766 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
4767 return {};
4768 }
4769 const allowed = [
4770 "both",
4771 "dock",
4772 "desktop",
4773 "hidden"
4774 ];
4775 const out = {};
4776 let count = 0;
4777 for (const [k, v] of Object.entries(raw)) {
4778 if (count >= 256) {
4779 break;
4780 }
4781 if (typeof k !== "string" || k === "") {
4782 continue;
4783 }
4784 if (typeof v !== "string") {
4785 continue;
4786 }
4787 const placement = v;
4788 if (!allowed.includes(placement)) {
4789 continue;
4790 }
4791 out[k] = placement;
4792 count++;
4793 }
4794 return out;
4795 }
4796 function sanitizeDockOrder(raw) {
4797 if (!Array.isArray(raw)) {
4798 return [];
4799 }
4800 const out = [];
4801 const seen = /* @__PURE__ */ new Set();
4802 for (const id of raw) {
4803 if (typeof id !== "string" || id === "" || seen.has(id)) {
4804 continue;
4805 }
4806 seen.add(id);
4807 out.push(id);
4808 if (out.length >= 256) {
4809 break;
4810 }
4811 }
4812 return out;
4813 }
4814 function sanitizeDockPromotedPositions(raw) {
4815 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
4816 return {};
4817 }
4818 const out = {};
4819 let count = 0;
4820 const MAX_COORD = 1e5;
4821 for (const [k, v] of Object.entries(raw)) {
4822 if (count >= 256) {
4823 break;
4824 }
4825 if (typeof k !== "string" || k === "") {
4826 continue;
4827 }
4828 if (!v || typeof v !== "object" || Array.isArray(v)) {
4829 continue;
4830 }
4831 const pos = v;
4832 if (typeof pos.x !== "number" || typeof pos.y !== "number" || !Number.isFinite(pos.x) || !Number.isFinite(pos.y) || Math.abs(pos.x) > MAX_COORD || Math.abs(pos.y) > MAX_COORD) {
4833 continue;
4834 }
4835 out[k] = { x: pos.x, y: pos.y };
4836 count++;
4837 }
4838 return out;
4839 }
4840 let _syncTimer = null;
4841 const SYNC_DEBOUNCE_MS = 250;
4842 let _lastConfirmedState = null;
4843 function setLastConfirmedState(state2) {
4844 _lastConfirmedState = _cloneState(state2);
4845 }
4846 function _cloneState(state2) {
4847 return {
4848 ...state2,
4849 customGradient: { ...state2.customGradient },
4850 customImage: state2.customImage ? { ...state2.customImage } : null,
4851 ai: { ...state2.ai, apiKeys: { ...state2.ai.apiKeys } },
4852 nativePostsHiddenColumns: state2.nativePostsHiddenColumns.slice(),
4853 itemVisibility: { ...state2.itemVisibility },
4854 dockOrder: state2.dockOrder.slice(),
4855 dockPromotedPositions: Object.fromEntries(
4856 Object.entries(state2.dockPromotedPositions).map(([k, v]) => [
4857 k,
4858 { ...v }
4859 ])
4860 )
4861 };
4862 }
4863 function saveState(state2, opts = {}) {
4864 _writeLocalStorage(state2);
4865 _scheduleSyncToServer(state2, opts.windowId);
4866 }
4867 function _writeLocalStorage(state2) {
4868 try {
4869 window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state2));
4870 } catch {
4871 }
4872 }
4873 function _scheduleSyncToServer(state2, windowId) {
4874 if (_syncTimer !== null) {
4875 clearTimeout(_syncTimer);
4876 }
4877 if (windowId) {
4878 _pendingActivityWindowId = windowId;
4879 }
4880 _emitSaveLifecycle("pending");
4881 _syncTimer = setTimeout(() => {
4882 _syncTimer = null;
4883 const id = _pendingActivityWindowId;
4884 _pendingActivityWindowId = null;
4885 _postToServer(state2, id);
4886 }, SYNC_DEBOUNCE_MS);
4887 }
4888 let _pendingActivityWindowId = null;
4889 function _postToServer(state2, windowId) {
4890 const config = window.desktopModeConfig;
4891 const url = config?.osSettingsUrl;
4892 const nonce = config?.restNonce;
4893 if (!url || !nonce) {
4894 _emitSaveLifecycle("saved");
4895 return;
4896 }
4897 _emitSaveLifecycle("saving");
4898 const attributedWindowId = windowId || "desktop-mode-os-settings";
4899 trackedFetch$1(
4900 url,
4901 {
4902 method: "POST",
4903 headers: {
4904 "Content-Type": "application/json",
4905 "X-WP-Nonce": nonce
4906 },
4907 body: JSON.stringify({ settings: state2 })
4908 },
4909 { windowId: attributedWindowId }
4910 ).then((res) => {
4911 if (!res.ok) {
4912 throw new Error(`${res.status} ${res.statusText}`);
4913 }
4914 _lastConfirmedState = _cloneState(state2);
4915 _emitSaveLifecycle("saved");
4916 }).catch((err) => {
4917 if (_lastConfirmedState) {
4918 _writeLocalStorage(_lastConfirmedState);
4919 _emitSaveLifecycle(
4920 "failed",
4921 err instanceof Error ? err.message : String(err),
4922 _cloneState(_lastConfirmedState)
4923 );
4924 } else {
4925 _emitSaveLifecycle(
4926 "failed",
4927 err instanceof Error ? err.message : String(err)
4928 );
4929 }
4930 });
4931 }
4932 function _emitSaveLifecycle(phase, error, rolledBackTo) {
4933 const detail = { phase };
4934 if (error) {
4935 detail.error = error;
4936 }
4937 if (rolledBackTo) {
4938 detail.rolledBackTo = rolledBackTo;
4939 }
4940 document.dispatchEvent(
4941 new CustomEvent("desktop-mode-os-settings-save-lifecycle", { detail })
4942 );
4943 }
4944 function structuredDefaults() {
4945 return {
4946 ...DEFAULTS,
4947 customGradient: { ...DEFAULTS.customGradient },
4948 customImage: null,
4949 ai: { ...DEFAULTS.ai }
4950 };
4951 }
4952 function sanitizeAi(raw) {
4953 if (!raw || typeof raw !== "object") {
4954 return { ...DEFAULTS.ai, apiKeys: {} };
4955 }
4956 const { enabled, provider, apiKey, apiKeys, transport } = raw;
4957 const known = getAiProviders();
4958 const validProvider = typeof provider === "string" && known.some((p) => p.id === provider) ? provider : DEFAULTS.ai.provider;
4959 const cleanKeys = {};
4960 if (apiKeys && typeof apiKeys === "object") {
4961 for (const [pid, val] of Object.entries(apiKeys)) {
4962 if (typeof val === "string") {
4963 cleanKeys[pid] = val.slice(0, 512);
4964 }
4965 }
4966 }
4967 const validTransport = typeof transport === "string" && AI_TRANSPORTS.some((t) => t.id === transport) ? transport : DEFAULTS.ai.transport;
4968 return {
4969 enabled: typeof enabled === "boolean" ? enabled : DEFAULTS.ai.enabled,
4970 provider: validProvider,
4971 apiKey: typeof apiKey === "string" ? apiKey : DEFAULTS.ai.apiKey,
4972 apiKeys: cleanKeys,
4973 transport: validTransport
4974 };
4975 }
4976 function sanitizeCustomGradient(raw) {
4977 if (!raw || typeof raw !== "object") {
4978 return { ...DEFAULTS.customGradient };
4979 }
4980 const { from, to, angle } = raw;
4981 return {
4982 from: isHexColor(from) ? from : DEFAULTS.customGradient.from,
4983 to: isHexColor(to) ? to : DEFAULTS.customGradient.to,
4984 angle: typeof angle === "number" && Number.isFinite(angle) && angle >= 0 && angle <= 360 ? angle : DEFAULTS.customGradient.angle
4985 };
4986 }
4987 function sanitizeCustomImage(raw) {
4988 if (!raw || typeof raw !== "object") {
4989 return null;
4990 }
4991 const { id, url } = raw;
4992 if (typeof id !== "number" || !Number.isFinite(id) || id <= 0) {
4993 return null;
4994 }
4995 if (typeof url !== "string" || !/^https?:\/\//i.test(url)) {
4996 return null;
4997 }
4998 return { id, url };
4999 }
5000 const store$b = createSharedStore(
5001 "desktop-mode/dock-rail-registry",
5002 () => ({
5003 registry: /* @__PURE__ */ new Map(),
5004 listeners: /* @__PURE__ */ new Set(),
5005 activeId: "default"
5006 })
5007 );
5008 const registry$8 = store$b.state.registry;
5009 const listeners$a = store$b.state.listeners;
5010 const ID_RE = /^[a-z0-9_-]+$/;
5011 function register$1(renderer) {
5012 if (!renderer || typeof renderer !== "object") {
5013 throw new TypeError(
5014 "[desktop-mode] registerDockRailRenderer: renderer must be an object."
5015 );
5016 }
5017 if (typeof renderer.id !== "string" || !ID_RE.test(renderer.id)) {
5018 throw new TypeError(
5019 `[desktop-mode] registerDockRailRenderer: id must match /^[a-z0-9_-]+$/, got: ${String(renderer.id)}`
5020 );
5021 }
5022 if (typeof renderer.label !== "string" || renderer.label === "") {
5023 throw new TypeError(
5024 "[desktop-mode] registerDockRailRenderer: label must be a non-empty string."
5025 );
5026 }
5027 if (typeof renderer.mount !== "function") {
5028 throw new TypeError(
5029 "[desktop-mode] registerDockRailRenderer: mount must be a function."
5030 );
5031 }
5032 if (renderer.apiVersion !== void 0 && renderer.apiVersion !== 1) {
5033 throw new TypeError(
5034 `[desktop-mode] registerDockRailRenderer: unsupported apiVersion ${renderer.apiVersion} (this shell speaks v1).`
5035 );
5036 }
5037 registry$8.set(renderer.id, renderer);
5038 notify$c();
5039 }
5040 function unregister$1(id) {
5041 if (registry$8.delete(id)) {
5042 notify$c();
5043 }
5044 }
5045 function unregisterByOwner$1(owner) {
5046 if (!owner) {
5047 return 0;
5048 }
5049 let removed = 0;
5050 for (const [id, renderer] of Array.from(registry$8.entries())) {
5051 if (renderer.owner === owner) {
5052 registry$8.delete(id);
5053 removed++;
5054 }
5055 }
5056 if (removed > 0) {
5057 notify$c();
5058 }
5059 return removed;
5060 }
5061 function list() {
5062 return Array.from(registry$8.values());
5063 }
5064 function subscribe$3(cb) {
5065 listeners$a.add(cb);
5066 return () => {
5067 listeners$a.delete(cb);
5068 };
5069 }
5070 function setActiveRenderer(id) {
5071 if (store$b.state.activeId === id) {
5072 return;
5073 }
5074 store$b.state.activeId = id;
5075 notify$c();
5076 }
5077 function resolveActive() {
5078 return registry$8.get(store$b.state.activeId) ?? registry$8.get("default") ?? registry$8.values().next().value;
5079 }
5080 function notify$c() {
5081 const snapshot = Array.from(listeners$a);
5082 for (const cb of snapshot) {
5083 try {
5084 cb();
5085 } catch (err) {
5086 if (typeof console !== "undefined") {
5087 console.error(
5088 "[desktop-mode] dock-rail-renderer listener threw:",
5089 err
5090 );
5091 }
5092 }
5093 }
5094 }
5095 function hashTitleToHue(input) {
5096 if (!input) {
5097 return 214;
5098 }
5099 let hash2 = 5381;
5100 for (let i = 0; i < input.length; i++) {
5101 hash2 = Math.imul(hash2, 33) + input.charCodeAt(i);
5102 }
5103 return (hash2 % 360 + 360) % 360;
5104 }
5105 const SHOW_DELAY_MS = 180;
5106 const HIDE_DELAY_MS = 220;
5107 const STAGGER_MS = 32;
5108 function attachDockPeek(deps2) {
5109 const { tile: tile2 } = deps2;
5110 let popover = null;
5111 let showTimer = null;
5112 let hideTimer = null;
5113 let inside = false;
5114 const cancelShow = () => {
5115 if (showTimer !== null) {
5116 window.clearTimeout(showTimer);
5117 showTimer = null;
5118 }
5119 };
5120 const cancelHide = () => {
5121 if (hideTimer !== null) {
5122 window.clearTimeout(hideTimer);
5123 hideTimer = null;
5124 }
5125 };
5126 const tearDown = () => {
5127 cancelShow();
5128 cancelHide();
5129 if (popover) {
5130 popover.remove();
5131 popover = null;
5132 }
5133 deps2.suppressTooltip(false);
5134 };
5135 const onPointerEnterTile = (e) => {
5136 if (e.pointerType !== "mouse") {
5137 return;
5138 }
5139 if (!shouldShowPeek(deps2)) {
5140 return;
5141 }
5142 inside = true;
5143 cancelHide();
5144 if (popover) {
5145 return;
5146 }
5147 showTimer = window.setTimeout(() => {
5148 showTimer = null;
5149 if (!inside) {
5150 return;
5151 }
5152 showPeek();
5153 }, SHOW_DELAY_MS);
5154 };
5155 const onPointerLeaveTile = (e) => {
5156 if (popover && e.relatedTarget instanceof Node && popover.contains(e.relatedTarget)) {
5157 return;
5158 }
5159 inside = false;
5160 cancelShow();
5161 scheduleHide();
5162 };
5163 const scheduleHide = () => {
5164 cancelHide();
5165 hideTimer = window.setTimeout(() => {
5166 hideTimer = null;
5167 if (inside) {
5168 return;
5169 }
5170 tearDown();
5171 }, HIDE_DELAY_MS);
5172 };
5173 const showPeek = () => {
5174 deps2.suppressTooltip(true);
5175 popover = buildPopover(deps2, () => tearDown());
5176 document.body.appendChild(popover);
5177 inheritShellSchemeVars(popover);
5178 positionPopover(popover, tile2, deps2.getOrientation());
5179 requestAnimationFrame(() => {
5180 popover?.classList.add("desktop-mode-dock-peek--open");
5181 });
5182 popover.addEventListener("pointerenter", () => {
5183 inside = true;
5184 cancelHide();
5185 });
5186 popover.addEventListener("pointerleave", (e) => {
5187 if (e.relatedTarget instanceof Node && tile2.contains(e.relatedTarget)) {
5188 return;
5189 }
5190 inside = false;
5191 scheduleHide();
5192 });
5193 };
5194 tile2.addEventListener("pointerenter", onPointerEnterTile);
5195 tile2.addEventListener("pointerleave", onPointerLeaveTile);
5196 return () => {
5197 tile2.removeEventListener("pointerenter", onPointerEnterTile);
5198 tile2.removeEventListener("pointerleave", onPointerLeaveTile);
5199 tearDown();
5200 };
5201 }
5202 function shouldShowPeek(deps2) {
5203 return deps2.getInstances().length >= 1;
5204 }
5205 function buildPopover(deps2, dismiss) {
5206 const root = document.createElement("div");
5207 root.className = "desktop-mode-dock-peek";
5208 root.setAttribute("role", "menu");
5209 root.setAttribute("aria-label", sprintf(
5210 // translators: %s is the dock item's admin-page title (e.g., "Posts")
5211 __("%s — open windows"),
5212 deps2.item.title
5213 ));
5214 const cards = document.createElement("div");
5215 cards.className = "desktop-mode-dock-peek__cards";
5216 root.appendChild(cards);
5217 const instances = deps2.getInstances();
5218 let cardIndex = 0;
5219 for (const win of instances) {
5220 const card = buildInstanceCard(win, deps2, cardIndex++, dismiss);
5221 cards.appendChild(card);
5222 }
5223 if (deps2.enableGhost !== false) {
5224 const ghost = buildGhostCard(deps2, cardIndex, dismiss);
5225 cards.appendChild(ghost);
5226 }
5227 return root;
5228 }
5229 function buildInstanceCard(win, deps2, index2, dismiss) {
5230 const card = document.createElement("button");
5231 card.type = "button";
5232 card.setAttribute("role", "menuitem");
5233 card.className = "desktop-mode-dock-peek__card desktop-mode-dock-peek__card--instance";
5234 card.style.setProperty("--peek-card-index", String(index2));
5235 card.style.setProperty(
5236 "--peek-card-delay",
5237 `${index2 * STAGGER_MS}ms`
5238 );
5239 const title = win.config.title || deps2.item.title;
5240 card.style.setProperty(
5241 "--peek-card-hue",
5242 `${hashTitleToHue(win.id || title)}`
5243 );
5244 card.style.setProperty(
5245 "--peek-card-vt-name",
5246 `desktop-mode-peek-card-${win.id}`
5247 );
5248 const titlebar = document.createElement("span");
5249 titlebar.className = "desktop-mode-dock-peek__card-titlebar";
5250 const dots = document.createElement("span");
5251 dots.className = "desktop-mode-dock-peek__card-dots";
5252 dots.setAttribute("aria-hidden", "true");
5253 for (let i = 0; i < 3; i++) {
5254 dots.appendChild(document.createElement("i"));
5255 }
5256 titlebar.appendChild(dots);
5257 const iconHost = document.createElement("span");
5258 iconHost.className = "desktop-mode-dock-peek__card-icon";
5259 iconHost.setAttribute("aria-hidden", "true");
5260 const iconCls = win.config.icon || deps2.item.icon;
5261 if (iconCls.startsWith("dashicons-")) {
5262 iconHost.classList.add("dashicons", sanitizeClassName(iconCls));
5263 } else {
5264 iconHost.classList.add("dashicons", "dashicons-admin-generic");
5265 }
5266 titlebar.appendChild(iconHost);
5267 const label = document.createElement("span");
5268 label.className = "desktop-mode-dock-peek__card-label";
5269 label.textContent = title;
5270 titlebar.appendChild(label);
5271 card.appendChild(titlebar);
5272 const defaultBody = document.createElement("span");
5273 defaultBody.className = "desktop-mode-dock-peek__card-body";
5274 defaultBody.setAttribute("aria-hidden", "true");
5275 for (let i = 0; i < 3; i++) {
5276 const line = document.createElement("span");
5277 line.className = "desktop-mode-dock-peek__card-line";
5278 defaultBody.appendChild(line);
5279 }
5280 const ctx = { window: win, item: deps2.item };
5281 const body = applyFilters(
5282 HOOKS.DOCK_PEEK_CARD_CONTENT,
5283 defaultBody,
5284 ctx
5285 );
5286 if (body !== defaultBody) {
5287 body.classList.add("desktop-mode-dock-peek__card-body--custom");
5288 }
5289 card.appendChild(body);
5290 card.addEventListener("click", () => {
5291 spawnFocusViewTransition(deps2, win, card, dismiss);
5292 });
5293 card.addEventListener("pointerenter", () => {
5294 if (deps2.windowManager.getFocused() === win) {
5295 return;
5296 }
5297 deps2.windowManager.focus(win);
5298 });
5299 const finalCard = applyFilters(
5300 HOOKS.DOCK_PEEK_CARD_ELEMENT,
5301 card,
5302 ctx
5303 );
5304 return finalCard;
5305 }
5306 function spawnFocusViewTransition(deps2, win, card, dismiss) {
5307 const doc = document;
5308 const vtName = `desktop-mode-peek-card-${win.id}`;
5309 const focus = () => {
5310 dismiss();
5311 deps2.windowManager.focus(win);
5312 };
5313 if (typeof doc.startViewTransition !== "function") {
5314 focus();
5315 return;
5316 }
5317 const targetEl = win.element;
5318 card.style.setProperty("view-transition-name", vtName);
5319 targetEl.style.setProperty("view-transition-name", vtName);
5320 const transition = doc.startViewTransition(focus);
5321 const cleanup = () => {
5322 card.style.removeProperty("view-transition-name");
5323 targetEl.style.removeProperty("view-transition-name");
5324 };
5325 const t = transition;
5326 if (t.finished && typeof t.finished.then === "function") {
5327 t.finished.then(cleanup, cleanup);
5328 } else {
5329 Promise.resolve().then(cleanup);
5330 }
5331 }
5332 function buildGhostCard(deps2, index2, dismiss) {
5333 const card = document.createElement("button");
5334 card.type = "button";
5335 card.setAttribute("role", "menuitem");
5336 card.className = "desktop-mode-dock-peek__card desktop-mode-dock-peek__card--ghost";
5337 card.style.setProperty("--peek-card-index", String(index2));
5338 card.style.setProperty(
5339 "--peek-card-delay",
5340 `${index2 * STAGGER_MS}ms`
5341 );
5342 const plus = document.createElement("span");
5343 plus.className = "desktop-mode-dock-peek__card-plus";
5344 plus.setAttribute("aria-hidden", "true");
5345 plus.textContent = "+";
5346 card.appendChild(plus);
5347 const label = document.createElement("span");
5348 label.className = "desktop-mode-dock-peek__card-label";
5349 label.textContent = sprintf(
5350 // translators: %s is the admin-page title (e.g., "Posts")
5351 __("New %s"),
5352 deps2.item.title
5353 );
5354 card.appendChild(label);
5355 card.addEventListener("click", () => {
5356 spawnWithViewTransition(deps2, dismiss);
5357 });
5358 return card;
5359 }
5360 function spawnWithViewTransition(deps2, dismiss) {
5361 const doc = document;
5362 const spawn = () => {
5363 dismiss();
5364 deps2.openNew();
5365 };
5366 if (typeof doc.startViewTransition === "function") {
5367 doc.startViewTransition(spawn);
5368 return;
5369 }
5370 spawn();
5371 }
5372 const VIEWPORT_MARGIN_PX = 12;
5373 const SHELL_SCHEME_VARS = [
5374 "--wp-admin-theme-color",
5375 "--desktop-mode-titlebar-bg",
5376 "--desktop-mode-titlebar-bg-focused",
5377 "--desktop-mode-titlebar-color",
5378 "--desktop-mode-titlebar-color-focused"
5379 ];
5380 function inheritShellSchemeVars(popover) {
5381 const shell = document.querySelector(".desktop-mode-shell");
5382 if (!shell) {
5383 return;
5384 }
5385 const computed = window.getComputedStyle(shell);
5386 for (const name of SHELL_SCHEME_VARS) {
5387 const value = computed.getPropertyValue(name).trim();
5388 if (value) {
5389 popover.style.setProperty(name, value);
5390 }
5391 }
5392 }
5393 function positionPopover(popover, tile2, orientation) {
5394 const rect = tile2.getBoundingClientRect();
5395 popover.dataset.orientation = orientation;
5396 if (orientation === "bottom") {
5397 popover.style.left = `${rect.left + rect.width / 2}px`;
5398 popover.style.top = `${rect.top - 12}px`;
5399 } else if (orientation === "right") {
5400 popover.style.top = `${rect.top + rect.height / 2}px`;
5401 popover.style.left = `${rect.left - 12}px`;
5402 } else {
5403 popover.style.top = `${rect.top + rect.height / 2}px`;
5404 popover.style.left = `${rect.right + 12}px`;
5405 }
5406 requestAnimationFrame(() => clampToViewport$1(popover));
5407 }
5408 function clampToViewport$1(popover, orientation) {
5409 const rect = popover.getBoundingClientRect();
5410 const vh = window.innerHeight;
5411 const vw = window.innerWidth;
5412 const min = VIEWPORT_MARGIN_PX;
5413 let dy = 0;
5414 let dx = 0;
5415 if (rect.top < min) {
5416 dy = min - rect.top;
5417 } else if (rect.bottom > vh - min) {
5418 dy = vh - min - rect.bottom;
5419 }
5420 if (rect.left < min) {
5421 dx = min - rect.left;
5422 } else if (rect.right > vw - min) {
5423 dx = vw - min - rect.right;
5424 }
5425 if (dx === 0 && dy === 0) {
5426 return;
5427 }
5428 popover.style.setProperty("--peek-clamp-x", `${dx}px`);
5429 popover.style.setProperty("--peek-clamp-y", `${dy}px`);
5430 popover.classList.add("desktop-mode-dock-peek--clamped");
5431 }
5432 function tryOpenExternalUrl(url) {
5433 try {
5434 const parsed = new URL(url, window.location.origin);
5435 if (parsed.origin === window.location.origin) {
5436 return false;
5437 }
5438 window.open(parsed.toString(), "_blank", "noopener,noreferrer");
5439 return true;
5440 } catch {
5441 return false;
5442 }
5443 }
5444 function synthDockId(desktopIconId) {
5445 return `desktop:${desktopIconId}`;
5446 }
5447 function synthIconId(dockItemId) {
5448 return `dock:${dockItemId}`;
5449 }
5450 function canonicalItemId(id) {
5451 if (id.startsWith("dock:")) {
5452 return id.slice(5);
5453 }
5454 if (id.startsWith("desktop:")) {
5455 return id.slice(8);
5456 }
5457 return id;
5458 }
5459 function resolvePlacement(id, nativeRail, visibility) {
5460 const override = visibility[id];
5461 if (override) {
5462 return override;
5463 }
5464 return nativeRail;
5465 }
5466 function shouldShowOnDock(placement) {
5467 return placement === "dock" || placement === "both";
5468 }
5469 function shouldShowOnDesktop(placement) {
5470 return placement === "desktop" || placement === "both";
5471 }
5472 function applyDockPlacement(dockItems, desktopIcons, settings, dockedNativeWindows) {
5473 const visibility = settings.itemVisibility;
5474 const order = settings.dockOrder;
5475 const kept = [];
5476 for (const item of dockItems) {
5477 const placement = resolvePlacement(item.id, "dock", visibility);
5478 if (shouldShowOnDock(placement)) {
5479 kept.push(item);
5480 }
5481 }
5482 for (const icon of desktopIcons) {
5483 const placement = resolvePlacement(icon.id, "desktop", visibility);
5484 if (!shouldShowOnDock(placement)) {
5485 continue;
5486 }
5487 if (icon.window && dockedNativeWindows && dockedNativeWindows.has(icon.window)) {
5488 continue;
5489 }
5490 kept.push({
5491 id: synthIconId(icon.id),
5492 title: icon.title,
5493 icon: icon.icon,
5494 url: icon.url || "",
5495 // Carry the native-window id forward so the dock can light
5496 // the active-dot indicator + show the hover-peek card when
5497 // the target window is open. Without this, window-target
5498 // icons (no `url`) synthesize a tile whose only id-bearing
5499 // field is an empty string — deriveWindowId('') matches
5500 // nothing the window manager has stored.
5501 windowId: icon.window || void 0,
5502 badge: 0,
5503 submenu: [],
5504 isCore: false
5505 });
5506 }
5507 return applyOrder(kept, order);
5508 }
5509 function applyDesktopPlacement(desktopIcons, dockItems, visibility) {
5510 const out = [];
5511 for (const icon of desktopIcons) {
5512 const placement = resolvePlacement(icon.id, "desktop", visibility);
5513 if (shouldShowOnDesktop(placement)) {
5514 out.push(icon);
5515 }
5516 }
5517 let synthIndex = 0;
5518 for (const item of dockItems) {
5519 const placement = resolvePlacement(item.id, "dock", visibility);
5520 if (!shouldShowOnDesktop(placement)) {
5521 continue;
5522 }
5523 out.push({
5524 id: synthDockId(item.id),
5525 title: item.title,
5526 icon: item.icon,
5527 window: "",
5528 url: item.url || "",
5529 // Place synthesized dock-promoted icons after server-registered
5530 // ones. Stable ordering by source-list index inside the bucket.
5531 position: 2e3 + synthIndex++
5532 });
5533 }
5534 return out;
5535 }
5536 function applyOrder(items, order) {
5537 if (order.length === 0 || items.length <= 1) {
5538 return items;
5539 }
5540 const byId = /* @__PURE__ */ new Map();
5541 for (const item of items) {
5542 byId.set(item.id, item);
5543 }
5544 const out = [];
5545 const placed = /* @__PURE__ */ new Set();
5546 for (const id of order) {
5547 const item = byId.get(id);
5548 if (item) {
5549 out.push(item);
5550 placed.add(id);
5551 }
5552 }
5553 for (const item of items) {
5554 if (!placed.has(item.id)) {
5555 out.push(item);
5556 }
5557 }
5558 return out;
5559 }
5560 function html(strings, ...values) {
5561 return { __wpdHtml: true, strings, values };
5562 }
5563 function isTemplateResult(v) {
5564 return !!v && v.__wpdHtml === true;
5565 }
5566 const MARKER_PREFIX = "$$wpd$$";
5567 const MARKER_RE = /\$\$wpd\$\$(\d+)\$\$/g;
5568 function joinWithMarkers(strings) {
5569 let out = strings[0];
5570 for (let i = 1; i < strings.length; i++) {
5571 out += `${MARKER_PREFIX}${i - 1}$$` + strings[i];
5572 }
5573 return out;
5574 }
5575 const compiledCache = /* @__PURE__ */ new WeakMap();
5576 function compile(strings) {
5577 const cached = compiledCache.get(strings);
5578 if (cached) {
5579 return cached;
5580 }
5581 const template = document.createElement("template");
5582 template.innerHTML = joinWithMarkers(strings);
5583 const recipes = [];
5584 const walk2 = (node, path) => {
5585 if (node.nodeType === Node.ELEMENT_NODE) {
5586 const el = node;
5587 for (const attr of Array.from(el.attributes)) {
5588 const rawName = attr.name;
5589 const rawValue = attr.value;
5590 const prefix = rawName[0];
5591 if (MARKER_RE.test(rawValue)) {
5592 MARKER_RE.lastIndex = 0;
5593 if (prefix === "@") {
5594 const match = MARKER_RE.exec(rawValue);
5595 MARKER_RE.lastIndex = 0;
5596 recipes.push({
5597 path,
5598 kind: "event",
5599 name: rawName.slice(1),
5600 valueIndex: match ? Number(match[1]) : 0
5601 });
5602 el.removeAttribute(rawName);
5603 } else if (prefix === ".") {
5604 const match = MARKER_RE.exec(rawValue);
5605 MARKER_RE.lastIndex = 0;
5606 recipes.push({
5607 path,
5608 kind: "prop",
5609 name: rawName.slice(1),
5610 valueIndex: match ? Number(match[1]) : 0
5611 });
5612 el.removeAttribute(rawName);
5613 } else if (prefix === "?") {
5614 const match = MARKER_RE.exec(rawValue);
5615 MARKER_RE.lastIndex = 0;
5616 recipes.push({
5617 path,
5618 kind: "bool",
5619 name: rawName.slice(1),
5620 valueIndex: match ? Number(match[1]) : 0
5621 });
5622 el.removeAttribute(rawName);
5623 } else {
5624 const fragments = [];
5625 const indices = [];
5626 let lastEnd = 0;
5627 let m;
5628 MARKER_RE.lastIndex = 0;
5629 while ((m = MARKER_RE.exec(rawValue)) !== null) {
5630 fragments.push(rawValue.slice(lastEnd, m.index));
5631 indices.push(Number(m[1]));
5632 lastEnd = m.index + m[0].length;
5633 }
5634 fragments.push(rawValue.slice(lastEnd));
5635 recipes.push({
5636 path,
5637 kind: "attr",
5638 name: rawName,
5639 template: fragments,
5640 valueIndices: indices
5641 });
5642 el.setAttribute(rawName, "");
5643 }
5644 }
5645 }
5646 }
5647 const children = Array.from(node.childNodes);
5648 let shift = 0;
5649 for (let i = 0; i < children.length; i++) {
5650 const child = children[i];
5651 const liveIndex = i + shift;
5652 if (child.nodeType === Node.TEXT_NODE) {
5653 const text = child.textContent || "";
5654 if (!MARKER_RE.test(text)) {
5655 MARKER_RE.lastIndex = 0;
5656 continue;
5657 }
5658 MARKER_RE.lastIndex = 0;
5659 const parent = child.parentNode;
5660 let lastEnd = 0;
5661 let m;
5662 const newNodes = [];
5663 const newRecipes = [];
5664 MARKER_RE.lastIndex = 0;
5665 while ((m = MARKER_RE.exec(text)) !== null) {
5666 if (m.index > lastEnd) {
5667 newNodes.push(document.createTextNode(text.slice(lastEnd, m.index)));
5668 }
5669 const placeholder = document.createTextNode("");
5670 newNodes.push(placeholder);
5671 newRecipes.push({
5672 path: [...path, liveIndex + newNodes.length - 1],
5673 kind: "node",
5674 valueIndex: Number(m[1])
5675 });
5676 lastEnd = m.index + m[0].length;
5677 }
5678 if (lastEnd < text.length) {
5679 newNodes.push(document.createTextNode(text.slice(lastEnd)));
5680 }
5681 for (const nn of newNodes) {
5682 parent.insertBefore(nn, child);
5683 }
5684 parent.removeChild(child);
5685 shift += newNodes.length - 1;
5686 recipes.push(...newRecipes);
5687 } else {
5688 walk2(child, [...path, liveIndex]);
5689 }
5690 }
5691 };
5692 walk2(template.content, []);
5693 const buildParts = (fragment) => {
5694 const out = [];
5695 for (const r of recipes) {
5696 let node = fragment;
5697 for (const idx of r.path) {
5698 node = node.childNodes[idx];
5699 }
5700 if (r.kind === "node") {
5701 out.push({
5702 kind: "node",
5703 valueIndex: r.valueIndex,
5704 child: {
5705 anchor: node,
5706 state: null
5707 }
5708 });
5709 } else if (r.kind === "attr") {
5710 out.push({
5711 kind: "attr",
5712 element: node,
5713 name: r.name,
5714 template: r.template,
5715 valueIndices: r.valueIndices
5716 });
5717 } else if (r.kind === "event") {
5718 out.push({
5719 kind: "event",
5720 valueIndex: r.valueIndex,
5721 element: node,
5722 name: r.name
5723 });
5724 } else if (r.kind === "prop") {
5725 out.push({
5726 kind: "prop",
5727 valueIndex: r.valueIndex,
5728 element: node,
5729 name: r.name
5730 });
5731 } else if (r.kind === "bool") {
5732 out.push({
5733 kind: "bool",
5734 valueIndex: r.valueIndex,
5735 element: node,
5736 name: r.name
5737 });
5738 }
5739 }
5740 return out;
5741 };
5742 const entry = { template, buildParts };
5743 compiledCache.set(strings, entry);
5744 return entry;
5745 }
5746 const mountState = /* @__PURE__ */ new WeakMap();
5747 function render$1(result, container) {
5748 const existing = mountState.get(container);
5749 if (existing && existing.strings === result.strings) {
5750 applyValues(existing.parts, result.values);
5751 return;
5752 }
5753 const compiled = compile(result.strings);
5754 const fragment = compiled.template.content.cloneNode(true);
5755 const parts = compiled.buildParts(fragment);
5756 while (container.firstChild) {
5757 container.removeChild(container.firstChild);
5758 }
5759 container.appendChild(fragment);
5760 applyValues(parts, result.values);
5761 mountState.set(container, { strings: result.strings, parts });
5762 }
5763 function applyValues(parts, values) {
5764 for (const part of parts) {
5765 if (part.kind === "node") {
5766 updateChildPart(part.child, values[part.valueIndex]);
5767 } else if (part.kind === "attr") {
5768 let composed = part.template[0];
5769 for (let i = 0; i < part.valueIndices.length; i++) {
5770 composed += formatText(values[part.valueIndices[i]]);
5771 composed += part.template[i + 1];
5772 }
5773 if (composed !== part.last) {
5774 part.last = composed;
5775 if (composed === "") {
5776 part.element.removeAttribute(part.name);
5777 } else {
5778 part.element.setAttribute(part.name, composed);
5779 }
5780 }
5781 } else if (part.kind === "event") {
5782 const next = values[part.valueIndex];
5783 if (next !== part.current) {
5784 if (part.current) {
5785 part.element.removeEventListener(part.name, part.current);
5786 }
5787 if (next) {
5788 part.element.addEventListener(part.name, next);
5789 }
5790 part.current = next;
5791 }
5792 } else if (part.kind === "prop") {
5793 const next = values[part.valueIndex];
5794 if (next !== part.last) {
5795 part.last = next;
5796 part.element[part.name] = next;
5797 }
5798 } else if (part.kind === "bool") {
5799 const next = !!values[part.valueIndex];
5800 if (next !== part.last) {
5801 part.last = next;
5802 if (next) {
5803 part.element.setAttribute(part.name, "");
5804 } else {
5805 part.element.removeAttribute(part.name);
5806 }
5807 }
5808 }
5809 }
5810 }
5811 function updateChildPart(child, value) {
5812 if (value === null || value === void 0 || value === false) {
5813 if (child.state) {
5814 disposeChildState(child.state);
5815 child.state = null;
5816 }
5817 return;
5818 }
5819 if (Array.isArray(value)) {
5820 updateArrayChild(child, value);
5821 return;
5822 }
5823 if (isTemplateResult(value)) {
5824 updateTemplateChild(child, value);
5825 return;
5826 }
5827 if (value instanceof Node) {
5828 updateNodeChild(child, value);
5829 return;
5830 }
5831 updateTextChild(child, formatText(value));
5832 }
5833 function updateNodeChild(child, node) {
5834 const old = child.state;
5835 if (old?.shape === "node" && old.node === node) {
5836 return;
5837 }
5838 if (old) {
5839 disposeChildState(old);
5840 }
5841 insertBeforeAnchor(child, [node]);
5842 child.state = { shape: "node", node };
5843 }
5844 function updateTextChild(child, text) {
5845 const old = child.state;
5846 if (old?.shape === "text") {
5847 if (old.text !== text) {
5848 old.node.textContent = text;
5849 old.text = text;
5850 }
5851 return;
5852 }
5853 if (old) {
5854 disposeChildState(old);
5855 }
5856 const node = document.createTextNode(text);
5857 insertBeforeAnchor(child, [node]);
5858 child.state = { shape: "text", node, text };
5859 }
5860 function updateTemplateChild(child, result) {
5861 const old = child.state;
5862 if (old?.shape === "template" && old.strings === result.strings) {
5863 applyValues(old.parts, result.values);
5864 return;
5865 }
5866 if (old) {
5867 disposeChildState(old);
5868 }
5869 const compiled = compile(result.strings);
5870 const fragment = compiled.template.content.cloneNode(true);
5871 const parts = compiled.buildParts(fragment);
5872 const topNodes = Array.from(fragment.childNodes);
5873 insertBeforeAnchor(child, [fragment]);
5874 applyValues(parts, result.values);
5875 child.state = {
5876 shape: "template",
5877 strings: result.strings,
5878 parts,
5879 nodes: topNodes
5880 };
5881 }
5882 function updateArrayChild(child, arr) {
5883 const old = child.state;
5884 if (old?.shape === "array" && old.entries.length === arr.length) {
5885 for (let i = 0; i < arr.length; i++) {
5886 updateChildPart(old.entries[i], arr[i]);
5887 }
5888 return;
5889 }
5890 if (old) {
5891 disposeChildState(old);
5892 }
5893 const entries = [];
5894 for (const v of arr) {
5895 const entryAnchor = document.createTextNode("");
5896 insertBeforeAnchor(child, [entryAnchor]);
5897 const entry = { anchor: entryAnchor, state: null };
5898 updateChildPart(entry, v);
5899 entries.push(entry);
5900 }
5901 child.state = { shape: "array", entries };
5902 }
5903 function insertBeforeAnchor(child, nodes) {
5904 const parent = child.anchor.parentNode;
5905 if (!parent) {
5906 return;
5907 }
5908 for (const node of nodes) {
5909 parent.insertBefore(node, child.anchor);
5910 }
5911 }
5912 function disposeChildState(state2) {
5913 if (state2.shape === "text") {
5914 state2.node.remove();
5915 return;
5916 }
5917 if (state2.shape === "template") {
5918 for (const node of state2.nodes) {
5919 if (node.parentNode) {
5920 node.parentNode.removeChild(node);
5921 }
5922 }
5923 return;
5924 }
5925 if (state2.shape === "node") {
5926 if (state2.node.parentNode) {
5927 state2.node.parentNode.removeChild(state2.node);
5928 }
5929 return;
5930 }
5931 for (const entry of state2.entries) {
5932 if (entry.state) {
5933 disposeChildState(entry.state);
5934 }
5935 entry.anchor.remove();
5936 }
5937 }
5938 function formatText(v) {
5939 if (v === null || v === void 0 || v === false) {
5940 return "";
5941 }
5942 return String(v);
5943 }
5944 const _Component = class _Component extends HTMLElement {
5945 constructor() {
5946 super();
5947 this._renderScheduled = false;
5948 this._propValues = {};
5949 const ctor = this.constructor;
5950 if (ctor.shadow) {
5951 this.attachShadow({ mode: "open" });
5952 this._renderRoot = this.shadowRoot;
5953 } else {
5954 this._renderRoot = this;
5955 }
5956 this._installPropAccessors();
5957 }
5958 static get observedAttributes() {
5959 return this.props.map(kebab);
5960 }
5961 connectedCallback() {
5962 this._adoptStyles();
5963 this.requestUpdate();
5964 }
5965 attributeChangedCallback(name, oldValue, newValue) {
5966 if (oldValue === newValue) {
5967 return;
5968 }
5969 const prop = camel(name);
5970 this._propValues[prop] = newValue;
5971 this.requestUpdate();
5972 }
5973 /**
5974 * Declarative class-name setter. Assign an array (or a
5975 * space-separated string) and the host's `class` attribute is
5976 * rewritten to match. Intended for programmatic styling — when
5977 * a plugin has enqueued its own stylesheet and wants to apply
5978 * one of those classes to a shell component:
5979 *
5980 * ```js
5981 * element.classNames = [ 'my-plugin-brand', 'is-active' ];
5982 * // → <wpd-select class="my-plugin-brand is-active">
5983 * ```
5984 *
5985 * The plain HTML `class="…"` attribute works just the same and
5986 * is always preferred when writing markup by hand — this setter
5987 * exists for the JS-API case where the caller has an array of
5988 * conditional classes in hand.
5989 *
5990 * Getter returns the current `classList` as a plain array for
5991 * symmetric read/write.
5992 *
5993 * @since 0.13.0
5994 */
5995 get classNames() {
5996 return Array.from(this.classList);
5997 }
5998 set classNames(next) {
5999 if (next === null || next === void 0) {
6000 this.removeAttribute("class");
6001 return;
6002 }
6003 const list2 = Array.isArray(next) ? next : String(next).split(/\s+/);
6004 const cleaned = list2.map((s) => String(s).trim()).filter((s) => s !== "");
6005 this.className = cleaned.join(" ");
6006 }
6007 /**
6008 * Request a re-render explicitly. Components rarely need this —
6009 * declare state via props + attribute observers and the render
6010 * loop picks up changes automatically.
6011 */
6012 requestUpdate() {
6013 this._scheduleRender();
6014 }
6015 /**
6016 * Dispatch a `CustomEvent` with a `detail`. Bubbles + composed
6017 * by default (matches typical WC UX — events cross shadow
6018 * boundaries, parents can listen without knowing about internal
6019 * structure).
6020 */
6021 emit(name, detail) {
6022 return this.dispatchEvent(
6023 new CustomEvent(name, {
6024 detail,
6025 bubbles: true,
6026 composed: true
6027 })
6028 );
6029 }
6030 // ------------------------------------------------------------------
6031 // Internals
6032 // ------------------------------------------------------------------
6033 /**
6034 * Wire every `static props` entry to a matched property getter +
6035 * setter on the element. Setting the property reflects into the
6036 * attribute (so downstream observers + CSS selectors see it);
6037 * reading the property falls back to the attribute.
6038 */
6039 _installPropAccessors() {
6040 const ctor = this.constructor;
6041 for (const prop of ctor.props) {
6042 if (Object.getOwnPropertyDescriptor(this, prop)) {
6043 continue;
6044 }
6045 const attr = kebab(prop);
6046 Object.defineProperty(this, prop, {
6047 get: () => {
6048 if (prop in this._propValues) {
6049 return this._propValues[prop];
6050 }
6051 return this.getAttribute(attr);
6052 },
6053 set: (value) => {
6054 let str;
6055 if (value === null || value === void 0 || value === false) {
6056 str = null;
6057 } else if (value === true) {
6058 str = "";
6059 } else {
6060 str = String(value);
6061 }
6062 this._propValues[prop] = str;
6063 if (str === null) {
6064 this.removeAttribute(attr);
6065 } else {
6066 this.setAttribute(attr, str);
6067 }
6068 this.requestUpdate();
6069 },
6070 enumerable: true,
6071 configurable: true
6072 });
6073 }
6074 }
6075 /**
6076 * Schedule a render on the next microtask. Multiple property
6077 * assignments in the same tick collapse into a single render.
6078 */
6079 _scheduleRender() {
6080 if (this._renderScheduled || !this.isConnected) {
6081 return;
6082 }
6083 this._renderScheduled = true;
6084 queueMicrotask(() => {
6085 this._renderScheduled = false;
6086 if (!this.isConnected) {
6087 return;
6088 }
6089 render$1(this.render(), this._renderRoot);
6090 });
6091 }
6092 /**
6093 * Mount adoptable stylesheets onto the shadow root (via
6094 * `adoptedStyleSheets`) or the light DOM (via one `<style>`
6095 * tag per def). No-op if `static styles` is empty.
6096 */
6097 _adoptStyles() {
6098 const ctor = this.constructor;
6099 if (ctor.styles.length === 0) {
6100 return;
6101 }
6102 if (ctor.shadow && this.shadowRoot) {
6103 const sheets = ctor.styles.map((s) => s.sheet).filter((s) => s !== null);
6104 this.shadowRoot.adoptedStyleSheets = sheets;
6105 if (sheets.length !== ctor.styles.length) {
6106 for (const s of ctor.styles) {
6107 if (!s.sheet) {
6108 const tag = document.createElement("style");
6109 tag.textContent = s.cssText;
6110 this.shadowRoot.appendChild(tag);
6111 }
6112 }
6113 }
6114 } else {
6115 this._adoptLightStyles(ctor);
6116 }
6117 }
6118 _adoptLightStyles(ctor) {
6119 if (_Component._lightStylesAdopted.has(ctor)) {
6120 return;
6121 }
6122 _Component._lightStylesAdopted.add(ctor);
6123 for (const s of ctor.styles) {
6124 const tag = document.createElement("style");
6125 tag.dataset.wpdUi = this.tagName.toLowerCase();
6126 tag.textContent = s.cssText;
6127 document.head.appendChild(tag);
6128 }
6129 }
6130 };
6131 _Component.props = [];
6132 _Component.styles = [];
6133 _Component.shadow = true;
6134 _Component._lightStylesAdopted = /* @__PURE__ */ new WeakSet();
6135 let Component = _Component;
6136 function defineComponent(tag, ctor) {
6137 if (customElements.get(tag)) {
6138 return;
6139 }
6140 customElements.define(tag, ctor);
6141 }
6142 function kebab(s) {
6143 return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
6144 }
6145 function camel(s) {
6146 return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
6147 }
6148 const SUPPORTS_CONSTRUCTABLE_SHEETS = (() => {
6149 try {
6150 const s = new CSSStyleSheet();
6151 return typeof s.replaceSync === "function";
6152 } catch {
6153 return false;
6154 }
6155 })();
6156 function css(strings, ...values) {
6157 let text = strings[0];
6158 for (let i = 1; i < strings.length; i++) {
6159 const v = values[i - 1];
6160 if (typeof v === "string" || typeof v === "number") {
6161 text += String(v);
6162 } else if (v && v.__wpdCss) {
6163 text += v.cssText;
6164 } else {
6165 throw new TypeError(
6166 "[wpd-ui] css`` interpolations must be strings, numbers, or other css`` results. Got: " + typeof v
6167 );
6168 }
6169 text += strings[i];
6170 }
6171 if (SUPPORTS_CONSTRUCTABLE_SHEETS) {
6172 const sheet = new CSSStyleSheet();
6173 sheet.replaceSync(text);
6174 return { __wpdCss: true, sheet, cssText: text };
6175 }
6176 return { __wpdCss: true, sheet: null, cssText: text };
6177 }
6178 function computeAutoId(element) {
6179 const parts = [];
6180 const tabs = [];
6181 let windowId = null;
6182 let node = element.parentElement;
6183 while (node) {
6184 if (node === document.body || node === document.documentElement) {
6185 break;
6186 }
6187 const id = node.id || "";
6188 if (id.startsWith("wp-window-")) {
6189 windowId = id.slice("wp-window-".length);
6190 break;
6191 }
6192 if (node.tagName.toLowerCase() === "wpd-tabpanel") {
6193 const forValue = node.getAttribute("for");
6194 if (forValue) {
6195 tabs.unshift(forValue);
6196 }
6197 }
6198 node = node.parentElement;
6199 }
6200 if (windowId) {
6201 parts.push(slugify(windowId));
6202 }
6203 for (const tab of tabs) {
6204 parts.push("tab-" + slugify(tab));
6205 }
6206 const label = element.getAttribute("label");
6207 if (label) {
6208 parts.push(slugify(label));
6209 }
6210 if (parts.length === 0) {
6211 return "wpd-unnamed";
6212 }
6213 return "wpd-" + parts.filter((p) => p !== "").join("-");
6214 }
6215 function slugify(s) {
6216 return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
6217 }
6218 function ensureAutoId(element) {
6219 if (element.id) {
6220 return element.id;
6221 }
6222 const id = computeAutoId(element);
6223 element.id = id;
6224 return id;
6225 }
6226 const dialogStyles = css`:host{display:none;position:fixed;inset:0;align-items:center;justify-content:center;background:rgba( 0,0,0,0.45 );backdrop-filter:blur( 2px );z-index:10000}:host( [ open ] ){display:flex}.dialog{width:min( 420px,92vw );background:var( --wpd-confirm-dialog-bg,var( --desktop-mode-bg,#1d2327 ) );color:var( --wpd-confirm-dialog-fg,var( --desktop-mode-fg,#fff ) );border:1px solid rgba( 255,255,255,0.08 );border-radius:10px;box-shadow:0 20px 50px rgba( 0,0,0,0.6 );padding:20px 22px 18px;display:flex;flex-direction:column;gap:10px;position:relative}.close{position:absolute;top:8px;right:10px;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:0;border-radius:6px;color:var( --wpd-confirm-dialog-fg-muted,rgba( 255,255,255,0.7 ) );cursor:pointer;font-size:22px;line-height:1;padding:0}.close:hover{background:rgba( 255,255,255,0.08 );color:inherit}.title{margin:0 0 4px;font-size:16px;font-weight:600}.message{margin:0;color:var( --wpd-confirm-dialog-fg-muted,rgba( 255,255,255,0.7 ) );line-height:1.45;white-space:pre-line}.actions{display:flex;justify-content:flex-end;gap:8px;margin-top:6px}.btn{border:0;border-radius:6px;padding:8px 14px;font-size:13px;cursor:pointer;font-weight:500}.btn--secondary{background:rgba( 255,255,255,0.08 );color:inherit}.btn--secondary:hover{background:rgba( 255,255,255,0.14 )}.btn--primary{background:var( --wp-admin-theme-color,#2271b1 );color:#fff}.btn--primary:hover{filter:brightness( 1.08 )}.btn--danger{background:#d63638;color:#fff}.btn--danger:hover{filter:brightness( 1.08 )}`;
6227 const _WpdConfirmDialog = class _WpdConfirmDialog extends Component {
6228 constructor() {
6229 super(...arguments);
6230 this._onKey = (e) => {
6231 if (e.key === "Escape") {
6232 e.preventDefault();
6233 this._cancel();
6234 }
6235 if (e.key === "Enter" && !e.isComposing) {
6236 e.preventDefault();
6237 this._confirm();
6238 }
6239 };
6240 this._onBackdrop = (e) => {
6241 const path = e.composedPath();
6242 const original = path.length > 0 ? path[0] : e.target;
6243 if (original === this) {
6244 this._cancel();
6245 }
6246 };
6247 this._confirm = () => {
6248 this.emit("wpd-confirm", { confirmed: true });
6249 this.removeAttribute("open");
6250 };
6251 this._cancel = () => {
6252 this.emit("wpd-cancel", { confirmed: false });
6253 this.removeAttribute("open");
6254 };
6255 }
6256 connectedCallback() {
6257 super.connectedCallback();
6258 this.setAttribute("role", "dialog");
6259 this.setAttribute("aria-modal", "true");
6260 this.addEventListener("keydown", this._onKey);
6261 this.addEventListener("click", this._onBackdrop);
6262 }
6263 disconnectedCallback() {
6264 this.removeEventListener("keydown", this._onKey);
6265 this.removeEventListener("click", this._onBackdrop);
6266 }
6267 render() {
6268 const title = this.title ?? "";
6269 const message = this.message ?? "";
6270 const confirmLabel = this["confirm-label"] || "Confirm";
6271 const cancelLabel = this["cancel-label"] || "Cancel";
6272 const isDanger = this.hasAttribute("danger");
6273 const hideCancel = this.hasAttribute("hide-cancel");
6274 const isDismissable = this.hasAttribute("dismissable");
6275 return html`
6276 <div class="dialog" tabindex="-1">
6277 ${isDismissable ? html`<button
6278 type="button"
6279 class="close"
6280 aria-label="Close"
6281 @click=${() => this._cancel()}
6282 >&times;</button>` : html``}
6283 ${title ? html`<h2 class="title">${title}</h2>` : html``}
6284 ${message ? html`<p class="message">${message}</p>` : html``}
6285 <div class="actions">
6286 ${hideCancel ? html`` : html`<button
6287 type="button"
6288 class="btn btn--secondary"
6289 @click=${() => this._cancel()}
6290 >
6291 ${cancelLabel}
6292 </button>`}
6293 <button
6294 type="button"
6295 class="btn ${isDanger ? "btn--danger" : "btn--primary"}"
6296 @click=${() => this._confirm()}
6297 >
6298 ${confirmLabel}
6299 </button>
6300 </div>
6301 </div>
6302 `;
6303 }
6304 };
6305 _WpdConfirmDialog.props = [
6306 "open",
6307 "title",
6308 "message",
6309 "confirm-label",
6310 "cancel-label",
6311 "danger",
6312 "hide-cancel",
6313 "dismissable"
6314 ];
6315 _WpdConfirmDialog.styles = [dialogStyles];
6316 _WpdConfirmDialog.help = {
6317 title: "Confirm dialog",
6318 summary: "Modal Yes/No replacement for window.confirm(). Two consumption paths: declarative element with `open` + `wpd-confirm` event, or the imperative Promise-returning `wpdConfirm()` helper.",
6319 status: "experimental",
6320 since: "0.9.0",
6321 props: [
6322 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
6323 { name: "title", type: "string", description: "Heading shown at the top." },
6324 { name: "message", type: "string", description: "Body copy. Newlines preserved." },
6325 { name: "confirm-label", type: "string", default: "Confirm", description: "Confirm-button label." },
6326 { name: "cancel-label", type: "string", default: "Cancel", description: "Cancel-button label." },
6327 { name: "danger", type: "boolean attribute", description: "Renders the confirm button red." },
6328 { name: "hide-cancel", type: "boolean attribute", description: "Hides the cancel button entirely. Useful when there is no alternative action — pair with `dismissable` so the user still has an explicit way to close." },
6329 { name: "dismissable", type: "boolean attribute", description: "Renders an X close button in the top-right corner. Click emits `wpd-cancel`." }
6330 ],
6331 events: [
6332 {
6333 name: "wpd-confirm",
6334 description: "Fires on confirm. Detail: `{ confirmed: true }`."
6335 },
6336 {
6337 name: "wpd-cancel",
6338 description: "Fires on cancel (Cancel button, Escape, backdrop click). Detail: `{ confirmed: false }`."
6339 }
6340 ]
6341 };
6342 let WpdConfirmDialog = _WpdConfirmDialog;
6343 defineComponent("wpd-confirm-dialog", WpdConfirmDialog);
6344 function wpdConfirm$1(options) {
6345 return new Promise((resolve2) => {
6346 const dialog2 = document.createElement("wpd-confirm-dialog");
6347 dialog2.setAttribute("open", "");
6348 if (options.title) {
6349 dialog2.setAttribute("title", options.title);
6350 }
6351 dialog2.setAttribute("message", options.message);
6352 if (options.confirmLabel) {
6353 dialog2.setAttribute("confirm-label", options.confirmLabel);
6354 }
6355 if (options.cancelLabel) {
6356 dialog2.setAttribute("cancel-label", options.cancelLabel);
6357 }
6358 if (options.danger) {
6359 dialog2.setAttribute("danger", "");
6360 }
6361 if (options.hideCancel) {
6362 dialog2.setAttribute("hide-cancel", "");
6363 }
6364 if (options.dismissable) {
6365 dialog2.setAttribute("dismissable", "");
6366 }
6367 const cleanup = (ok) => {
6368 dialog2.remove();
6369 resolve2(ok);
6370 };
6371 dialog2.addEventListener("wpd-confirm", () => cleanup(true));
6372 dialog2.addEventListener("wpd-cancel", () => cleanup(false));
6373 document.body.appendChild(dialog2);
6374 const inner = dialog2.shadowRoot?.querySelector(".dialog");
6375 (inner ?? dialog2).focus?.();
6376 });
6377 }
6378 const FALLBACK_BASE = "http://localhost/";
6379 function joinRestUrl(restRoot, path) {
6380 const base = typeof window !== "undefined" && window.location ? window.location.href : FALLBACK_BASE;
6381 const url = new URL(restRoot, base);
6382 const trimmed = path.replace(/^\/+/, "");
6383 const queryAt = trimmed.indexOf("?");
6384 const route = queryAt === -1 ? trimmed : trimmed.slice(0, queryAt);
6385 const extraQuery = queryAt === -1 ? "" : trimmed.slice(queryAt + 1);
6386 if (url.searchParams.has("rest_route")) {
6387 const existing = url.searchParams.get("rest_route") ?? "/";
6388 const prefix = existing.endsWith("/") ? existing : existing + "/";
6389 url.searchParams.set("rest_route", prefix + route);
6390 } else {
6391 const pathname = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
6392 url.pathname = pathname + route;
6393 }
6394 if (extraQuery) {
6395 const extras = new URLSearchParams(extraQuery);
6396 extras.forEach((value, key) => {
6397 url.searchParams.append(key, value);
6398 });
6399 }
6400 return url.toString();
6401 }
6402 function getApi() {
6403 const w = window;
6404 return w.wp?.desktop ?? null;
6405 }
6406 let activeMenu$3 = null;
6407 function closeMenu$1() {
6408 if (activeMenu$3) {
6409 activeMenu$3.remove();
6410 activeMenu$3 = null;
6411 }
6412 }
6413 function writeVisibility(canonicalId, placement) {
6414 const api = getApi();
6415 if (!api?.getOsSettings || !api?.updateOsSettings) {
6416 return;
6417 }
6418 const snap = api.getOsSettings();
6419 const next = { ...snap.itemVisibility };
6420 next[canonicalId] = placement;
6421 api.updateOsSettings({ itemVisibility: next });
6422 }
6423 let openGeneration$2 = 0;
6424 function openItemVisibilityMenu(opts) {
6425 closeMenu$1();
6426 const myGen = ++openGeneration$2;
6427 openWithShellOverlays(
6428 () => myGen === openGeneration$2,
6429 () => openItemVisibilityMenuImmediate(opts)
6430 );
6431 }
6432 function openItemVisibilityMenuImmediate(opts) {
6433 closeMenu$1();
6434 const canonical = canonicalItemId(opts.id);
6435 const options = [];
6436 if (opts.surface === "dock") {
6437 options.push({
6438 id: "hide-from-dock",
6439 label: __("Hide from dock"),
6440 icon: "dashicons-hidden",
6441 onPick: () => writeVisibility(canonical, "desktop")
6442 });
6443 options.push({
6444 id: "show-on-desktop-too",
6445 label: __("Also show on desktop"),
6446 icon: "dashicons-desktop",
6447 onPick: () => writeVisibility(canonical, "both")
6448 });
6449 } else {
6450 options.push({
6451 id: "hide-from-desktop",
6452 label: __("Hide from desktop"),
6453 icon: "dashicons-hidden",
6454 onPick: () => writeVisibility(canonical, "dock")
6455 });
6456 options.push({
6457 id: "show-on-dock-too",
6458 label: __("Also show on dock"),
6459 icon: "dashicons-menu",
6460 onPick: () => writeVisibility(canonical, "both")
6461 });
6462 }
6463 options.push({
6464 id: "hide-everywhere",
6465 label: __("Hide everywhere"),
6466 icon: "dashicons-no",
6467 danger: true,
6468 onPick: () => writeVisibility(canonical, "hidden")
6469 });
6470 options.push({
6471 id: "open-settings",
6472 label: __("Apps & Icons settings…"),
6473 icon: "dashicons-admin-generic",
6474 onPick: () => {
6475 const api = getApi();
6476 api?.openOsSettings?.({ tabId: "apps-icons" });
6477 }
6478 });
6479 if (opts.pluginFile) {
6480 const pluginFile = opts.pluginFile;
6481 const pluginLabel = opts.pluginName || opts.title;
6482 options.push({ kind: "separator" });
6483 options.push({
6484 id: "deactivate-plugin",
6485 // translators: %s is the owning plugin's display name.
6486 label: sprintf(__("Deactivate %s…"), pluginLabel),
6487 icon: "dashicons-trash",
6488 danger: true,
6489 onPick: () => {
6490 void confirmAndDeactivatePlugin(pluginFile, pluginLabel);
6491 }
6492 });
6493 }
6494 const menu = document.createElement("wpd-context-menu");
6495 menu.setAttribute("open", "");
6496 menu.classList.add("desktop-mode-item-visibility-menu");
6497 menu.dataset.itemId = opts.id;
6498 menu.style.position = "fixed";
6499 menu.style.left = "-9999px";
6500 menu.style.top = "-9999px";
6501 menu.style.visibility = "hidden";
6502 menu.style.zIndex = "1000000";
6503 const byKey = /* @__PURE__ */ new Map();
6504 for (const opt of options) {
6505 if (opt.kind === "separator") {
6506 const hr = document.createElement("hr");
6507 hr.style.cssText = "border: 0; border-top: 1px solid var( --wpd-context-menu-separator-color, rgba(255,255,255,0.12) ); margin: 4px 6px;";
6508 menu.appendChild(hr);
6509 continue;
6510 }
6511 byKey.set(opt.id, opt);
6512 const node = document.createElement("wpd-context-menu-option");
6513 node.dataset.menuItemId = opt.id;
6514 node.setAttribute("value", opt.id);
6515 if (opt.icon) {
6516 node.setAttribute("icon", opt.icon);
6517 }
6518 if (opt.danger) {
6519 node.setAttribute("danger", "");
6520 }
6521 node.textContent = opt.label;
6522 menu.appendChild(node);
6523 }
6524 menu.addEventListener("wpd-context-menu-pick", (e) => {
6525 const detail = e.detail;
6526 const key = detail?.id || detail?.value || "";
6527 const opt = byKey.get(key);
6528 closeMenu$1();
6529 try {
6530 opt?.onPick();
6531 } catch {
6532 }
6533 });
6534 document.body.appendChild(menu);
6535 activeMenu$3 = menu;
6536 const positionMenu = () => {
6537 if (menu !== activeMenu$3) {
6538 return;
6539 }
6540 const rect = menu.getBoundingClientRect();
6541 const margin = 8;
6542 let left = opts.x;
6543 let top;
6544 if (opts.surface === "dock") {
6545 top = Math.max(margin, opts.y - rect.height - margin);
6546 } else {
6547 top = opts.y;
6548 if (top + rect.height + margin > window.innerHeight) {
6549 top = Math.max(margin, opts.y - rect.height);
6550 }
6551 }
6552 if (left + rect.width + margin > window.innerWidth) {
6553 left = Math.max(margin, opts.x - rect.width);
6554 }
6555 menu.style.left = `${left}px`;
6556 menu.style.top = `${top}px`;
6557 menu.style.visibility = "";
6558 };
6559 requestAnimationFrame(positionMenu);
6560 const onOutside = (ev) => {
6561 if (!activeMenu$3) {
6562 return;
6563 }
6564 if (!activeMenu$3.contains(ev.target)) {
6565 closeMenu$1();
6566 document.removeEventListener("mousedown", onOutside, true);
6567 document.removeEventListener("keydown", onKey, true);
6568 }
6569 };
6570 const onKey = (ev) => {
6571 if (ev.key === "Escape") {
6572 closeMenu$1();
6573 document.removeEventListener("mousedown", onOutside, true);
6574 document.removeEventListener("keydown", onKey, true);
6575 }
6576 };
6577 document.addEventListener("mousedown", onOutside, true);
6578 document.addEventListener("keydown", onKey, true);
6579 }
6580 async function confirmAndDeactivatePlugin(pluginFile, title) {
6581 const confirmed = await wpdConfirm$1({
6582 /* translators: %s: plugin title. */
6583 title: sprintf(__("Deactivate %s?"), title),
6584 message: __(
6585 "This plugin will stop running on the site. You can re-activate it later from the Plugins screen."
6586 ),
6587 confirmLabel: __("Deactivate"),
6588 cancelLabel: __("Cancel"),
6589 danger: true
6590 });
6591 if (!confirmed) {
6592 return;
6593 }
6594 const cfg = window.desktopModeConfig ?? {};
6595 const restRoot = typeof cfg.restRoot === "string" && cfg.restRoot ? cfg.restRoot : `${window.location.origin}/wp-json/`;
6596 const restNonce = typeof cfg.restNonce === "string" && cfg.restNonce ? cfg.restNonce : "";
6597 const stripped = pluginFile.endsWith(".php") ? pluginFile.slice(0, -4) : pluginFile;
6598 const encoded = stripped.split("/").map(encodeURIComponent).join("/");
6599 const url = joinRestUrl(restRoot, `wp/v2/plugins/${encoded}`);
6600 try {
6601 const res = await trackedFetch$1(
6602 url,
6603 {
6604 method: "PUT",
6605 headers: {
6606 "Content-Type": "application/json",
6607 "X-WP-Nonce": restNonce
6608 },
6609 body: JSON.stringify({ status: "inactive" }),
6610 credentials: "same-origin"
6611 },
6612 { source: "desktop-mode/dock-deactivate-plugin" }
6613 );
6614 if (!res.ok) {
6615 throw new Error(`HTTP ${res.status}`);
6616 }
6617 } catch (err) {
6618 showToast({
6619 message: sprintf(
6620 /* translators: %s: plugin title. */
6621 __("Could not deactivate %s."),
6622 title
6623 ),
6624 duration: 4e3
6625 });
6626 console.error("[desktop-mode] deactivate plugin failed", err);
6627 return;
6628 }
6629 const closedTitles = closeWindowsForPlugin(pluginFile);
6630 const deactivatedMsg = closedTitles.length > 0 ? sprintf(
6631 /* translators: 1: plugin title. 2: number of windows that were closed. */
6632 __("%1$s deactivated. Closed %2$d window(s)."),
6633 title,
6634 closedTitles.length
6635 ) : sprintf(
6636 /* translators: %s: plugin title. */
6637 __("%s deactivated."),
6638 title
6639 );
6640 showToast({ message: deactivatedMsg, duration: 3e3 });
6641 const w = window;
6642 w.wp?.desktop?.refreshMenu?.();
6643 }
6644 function closeWindowsForPlugin(pluginFile) {
6645 const api = window.wp?.desktop;
6646 if (!api?.windowManager?.getAll) {
6647 return [];
6648 }
6649 const items = api.getMenuItems?.() ?? [];
6650 const owned = items.filter((i) => i.pluginFile === pluginFile);
6651 if (owned.length === 0) {
6652 return [];
6653 }
6654 const ownedKeys = /* @__PURE__ */ new Set();
6655 for (const item of owned) {
6656 ownedKeys.add(item.id);
6657 if (api.deriveWindowId) {
6658 ownedKeys.add(api.deriveWindowId(item.url));
6659 }
6660 }
6661 const toClose = /* @__PURE__ */ new Map();
6662 const windows = api.windowManager.getAll() ?? [];
6663 const derive = api.deriveWindowId;
6664 for (const w of windows) {
6665 if (ownedKeys.has(w.id)) {
6666 toClose.set(w.id, w);
6667 continue;
6668 }
6669 if (w.config?.baseId && ownedKeys.has(w.config.baseId)) {
6670 toClose.set(w.id, w);
6671 continue;
6672 }
6673 if (derive && w.config?.url) {
6674 const derivedFromConfig = derive(w.config.url);
6675 if (ownedKeys.has(derivedFromConfig)) {
6676 toClose.set(w.id, w);
6677 continue;
6678 }
6679 }
6680 if (derive && w.iframe) {
6681 let liveUrl = "";
6682 try {
6683 liveUrl = w.iframe.src || "";
6684 } catch {
6685 }
6686 if (liveUrl) {
6687 const derivedFromLive = derive(liveUrl);
6688 if (ownedKeys.has(derivedFromLive)) {
6689 toClose.set(w.id, w);
6690 }
6691 }
6692 }
6693 }
6694 const titles = [];
6695 for (const w of toClose.values()) {
6696 titles.push(w.config?.title ?? w.id);
6697 try {
6698 w.close();
6699 } catch {
6700 }
6701 }
6702 return titles;
6703 }
6704 const _Dock = class _Dock {
6705 constructor(container, windowManager, items, adminUrl, orientation = "left") {
6706 this.itemElements = /* @__PURE__ */ new Map();
6707 this.systemItems = [];
6708 this.systemItemElements = /* @__PURE__ */ new Map();
6709 this.systemSeparator = null;
6710 this.badgeOverrides = /* @__PURE__ */ new Map();
6711 this.attentionTimers = /* @__PURE__ */ new Map();
6712 this.peekTeardowns = /* @__PURE__ */ new Map();
6713 this.boundRefresh = () => void 0;
6714 this.container = container;
6715 this.windowManager = windowManager;
6716 this.items = items;
6717 this.adminUrl = adminUrl;
6718 this.orientation = orientation;
6719 this.rail = orientation === "bottom" ? "taskbar" : "dock";
6720 this.hooksNamespace = `desktop-mode/dock/${++_Dock.instanceCounter}`;
6721 this.container.setAttribute(
6722 "data-desktop-mode-dock-placement",
6723 orientation
6724 );
6725 this.tooltip = document.createElement("div");
6726 this.tooltip.className = "desktop-mode-dock__tooltip";
6727 this.tooltip.setAttribute("role", "tooltip");
6728 if (orientation === "bottom") {
6729 this.tooltip.classList.add("desktop-mode-dock__tooltip--above");
6730 } else if (orientation === "right") {
6731 this.tooltip.classList.add("desktop-mode-dock__tooltip--before");
6732 } else {
6733 this.tooltip.classList.add("desktop-mode-dock__tooltip--after");
6734 }
6735 document.body.appendChild(this.tooltip);
6736 this.render();
6737 this.bindWindowEvents();
6738 }
6739 /**
6740 * Build the base context object every dock decoration hook
6741 * receives. Read from `this` so a single subscriber can
6742 * disambiguate two coexisting rails by `dockId`.
6743 */
6744 buildHookContextBase() {
6745 return {
6746 rail: this.rail,
6747 orientation: this.orientation,
6748 dockId: this.container.id,
6749 container: this.container
6750 };
6751 }
6752 /**
6753 * Replace the menu-derived tile list with a fresh one, preserving
6754 * any JS-registered system tiles. Used by the live menu-refresh
6755 * path: after a plugin is activated or deactivated, the chromeless
6756 * bridge postMessages a fresh payload built from real admin
6757 * context, and the shell calls this so the dock repaints without
6758 * a tab reload.
6759 *
6760 * Old menu tiles are removed from both the DOM and the lookup
6761 * map; new tiles are inserted before the system separator (or
6762 * appended at the end if none exists yet), so the menu-items →
6763 * hairline → system-items ordering stays intact. Active-state
6764 * classes are re-computed once the new tiles are in place so
6765 * window indicators survive the swap.
6766 *
6767 * @param items New DockItem list. Pass `[]` to clear everything
6768 * menu-derived.
6769 */
6770 /**
6771 * Update the dock's orientation. Writes the new value to the
6772 * dock element's `data-desktop-mode-dock-placement` attribute (CSS
6773 * keys off it for layout) and keeps the tooltip anchor in sync.
6774 *
6775 * In practice, the layout dispatcher in `desktop.ts` rebuilds the
6776 * dock(s) from scratch on a layout change rather than re-orienting
6777 * a live instance — but this stays correct in case any caller
6778 * wants to flip orientation without the rebuild.
6779 */
6780 setOrientation(orientation) {
6781 if (this.orientation === orientation) {
6782 return;
6783 }
6784 this.orientation = orientation;
6785 this.container.setAttribute(
6786 "data-desktop-mode-dock-placement",
6787 orientation
6788 );
6789 this.tooltip.classList.remove(
6790 "desktop-mode-dock__tooltip--above",
6791 "desktop-mode-dock__tooltip--before",
6792 "desktop-mode-dock__tooltip--after"
6793 );
6794 if (orientation === "bottom") {
6795 this.tooltip.classList.add("desktop-mode-dock__tooltip--above");
6796 } else if (orientation === "right") {
6797 this.tooltip.classList.add("desktop-mode-dock__tooltip--before");
6798 } else {
6799 this.tooltip.classList.add("desktop-mode-dock__tooltip--after");
6800 }
6801 }
6802 replaceItems(items) {
6803 for (const itemId of this.itemElements.keys()) {
6804 const teardown = this.peekTeardowns.get(itemId);
6805 if (teardown) {
6806 teardown();
6807 this.peekTeardowns.delete(itemId);
6808 }
6809 }
6810 for (const el of this.itemElements.values()) {
6811 el.remove();
6812 }
6813 this.container.querySelectorAll(
6814 ".desktop-mode-dock__separator--group"
6815 ).forEach((el) => el.remove());
6816 this.itemElements.clear();
6817 this.items = items;
6818 const base = this.buildHookContextBase();
6819 doAction(HOOKS.DOCK_BEFORE_RENDER, {
6820 ...base,
6821 items,
6822 tileElements: this.itemElements
6823 });
6824 let insertedGroupSeparator = false;
6825 let tilesInsertedThisPass = 0;
6826 for (const item of items) {
6827 if (!insertedGroupSeparator && item.isCore === false) {
6828 if (tilesInsertedThisPass > 0) {
6829 const sep = document.createElement("div");
6830 sep.className = "desktop-mode-dock__separator desktop-mode-dock__separator--group";
6831 sep.setAttribute("aria-hidden", "true");
6832 if (this.systemSeparator) {
6833 this.container.insertBefore(sep, this.systemSeparator);
6834 } else {
6835 this.container.appendChild(sep);
6836 }
6837 }
6838 insertedGroupSeparator = true;
6839 }
6840 const btn = this.createItemButton(item);
6841 this.itemElements.set(item.id, btn);
6842 if (this.systemSeparator) {
6843 this.container.insertBefore(btn, this.systemSeparator);
6844 } else {
6845 this.container.appendChild(btn);
6846 }
6847 tilesInsertedThisPass++;
6848 const override = this.badgeOverrides.get(item.id);
6849 if (override !== void 0) {
6850 const primary = btn.querySelector(
6851 ".desktop-mode-dock__item-primary"
6852 );
6853 _applyBadgeNode(primary ?? btn, override);
6854 }
6855 doAction(HOOKS.DOCK_TILE_RENDERED, {
6856 ...base,
6857 item,
6858 isSystem: false,
6859 el: btn
6860 });
6861 }
6862 this.updateActiveStates();
6863 doAction(HOOKS.DOCK_AFTER_RENDER, {
6864 ...base,
6865 items,
6866 tileElements: this.itemElements
6867 });
6868 }
6869 /**
6870 * True when the rail currently has ANY renderable tile —
6871 * either a menu-derived item or a JS-registered system item.
6872 * Lets callers (the shell's live-refresh path) decide whether
6873 * to hide the whole rail without having to peek into two
6874 * internal maps. "System tiles keep the rail alive even when
6875 * menu items are empty" is the user-visible contract we enforce.
6876 */
6877 hasItems() {
6878 return this.itemElements.size > 0 || this.systemItemElements.size > 0;
6879 }
6880 /**
6881 * Remove a previously-registered system item. Used by the
6882 * server-driven native-window sync path — when a plugin is
6883 * deactivated, its native-window entry disappears from the
6884 * server's payload and the shell calls this to pull the tile
6885 * back off the rail without a reload.
6886 *
6887 * Idempotent: an unknown id is a silent no-op. The system
6888 * separator is kept in place as long as at least one system
6889 * item remains; removing the last system item also strips the
6890 * separator so the rail doesn't dangle a divider under nothing.
6891 */
6892 removeSystemItem(id) {
6893 const tile2 = this.systemItemElements.get(id);
6894 if (!tile2) {
6895 return;
6896 }
6897 tile2.remove();
6898 this.systemItemElements.delete(id);
6899 this.systemItems = this.systemItems.filter((s) => s.id !== id);
6900 this.badgeOverrides.delete(id);
6901 if (this.systemItemElements.size === 0 && this.systemSeparator) {
6902 this.systemSeparator.remove();
6903 this.systemSeparator = null;
6904 }
6905 doAction(HOOKS.DOCK_ITEM_REMOVED, { id, placement: this.rail });
6906 }
6907 /**
6908 * Set the badge count on a tile. Live-updates without a full
6909 * dock re-render — the existing tile's badge node is mutated in
6910 * place (or created if missing). Pass `0` to remove the badge.
6911 *
6912 * Resolves the tile in id order: menu items (`data-menu-slug`)
6913 * first, then system items (`data-system-id`), so callers can
6914 * use the same id surface regardless of which rail the tile
6915 * happens to live on.
6916 *
6917 * Idempotent: applying the same count is a no-op (no DOM mutation).
6918 *
6919 * @since 0.22.0
6920 *
6921 * @param itemId Tile id (menu slug for admin pages, system id
6922 * for `appendSystemItem` / `registerSystemTile`).
6923 * @param count Non-negative integer. `>99` renders as `99+`.
6924 */
6925 setBadge(itemId, count) {
6926 const tile2 = this._resolveTileElement(itemId);
6927 if (!tile2) {
6928 return;
6929 }
6930 const safe = Math.max(0, Math.floor(Number(count) || 0));
6931 if (safe === 0) {
6932 this.badgeOverrides.delete(itemId);
6933 } else {
6934 this.badgeOverrides.set(itemId, safe);
6935 }
6936 const primary = tile2.querySelector(
6937 ".desktop-mode-dock__item-primary"
6938 );
6939 _applyBadgeNode(primary ?? tile2, safe);
6940 activity.publish("desktop-mode/badge-changed", {
6941 itemId,
6942 count: safe,
6943 rail: this.rail
6944 });
6945 }
6946 /**
6947 * Clear the badge on a tile. Equivalent to `setBadge( id, 0 )`.
6948 *
6949 * @since 0.22.0
6950 */
6951 clearBadge(itemId) {
6952 this.setBadge(itemId, 0);
6953 }
6954 /**
6955 * Apply or clear an attention animation on a tile.
6956 *
6957 * - `'pulse'` — soft halo + scale, ~1.4 s loop. Default.
6958 * - `'shake'` — short horizontal jiggle.
6959 * - `'bounce'` — vertical bob, attention-grabbing.
6960 * - `null` — clear any active attention.
6961 *
6962 * Animations are gated on `prefers-reduced-motion: no-preference`;
6963 * the reduced-motion fallback shows a static accent ring for the
6964 * same duration so the affordance still works. `durationMs` of
6965 * `0` keeps the attention until the next call clears it.
6966 *
6967 * @since 0.22.0
6968 *
6969 * @param itemId Tile id.
6970 * @param mode Animation mode or `null` to clear.
6971 * @param opts Optional duration / intensity overrides.
6972 */
6973 setAttention(itemId, mode, opts = {}) {
6974 const tile2 = this._resolveTileElement(itemId);
6975 if (!tile2) {
6976 return;
6977 }
6978 const pending2 = this.attentionTimers.get(itemId);
6979 if (pending2 !== void 0) {
6980 window.clearTimeout(pending2);
6981 this.attentionTimers.delete(itemId);
6982 }
6983 tile2.classList.remove(
6984 "desktop-mode-dock__item--attention-pulse",
6985 "desktop-mode-dock__item--attention-shake",
6986 "desktop-mode-dock__item--attention-bounce",
6987 "desktop-mode-dock__item--intensity-subtle",
6988 "desktop-mode-dock__item--intensity-normal",
6989 "desktop-mode-dock__item--intensity-strong"
6990 );
6991 if (mode === null) {
6992 return;
6993 }
6994 tile2.classList.add(`desktop-mode-dock__item--attention-${mode}`);
6995 const intensity = opts.intensity ?? "normal";
6996 tile2.classList.add(`desktop-mode-dock__item--intensity-${intensity}`);
6997 const duration = opts.durationMs ?? 4e3;
6998 if (duration > 0) {
6999 const handle = window.setTimeout(() => {
7000 this.attentionTimers.delete(itemId);
7001 this.setAttention(itemId, null);
7002 }, duration);
7003 this.attentionTimers.set(itemId, handle);
7004 }
7005 }
7006 /**
7007 * Resolve a tile element by id — checks menu items first
7008 * (`data-menu-slug`), then system items (`data-system-id`). Used
7009 * by `setBadge` / `setAttention` so callers can reach either rail
7010 * with one id surface.
7011 */
7012 _resolveTileElement(itemId) {
7013 return this.itemElements.get(itemId) ?? this.systemItemElements.get(itemId) ?? null;
7014 }
7015 /**
7016 * Append a JS-registered system item to the dock.
7017 *
7018 * System items render after the menu-derived items, separated by a
7019 * hairline divider. Use for shell affordances that don't live in
7020 * the admin menu: OS Settings today, Jorvy and desktop widgets
7021 * later. Callers supply their own `onOpen` — the dock doesn't
7022 * assume the item opens a window at all.
7023 */
7024 appendSystemItem(item) {
7025 this.systemItems.push(item);
7026 if (!this.systemSeparator) {
7027 this.systemSeparator = document.createElement("div");
7028 this.systemSeparator.className = "desktop-mode-dock__separator";
7029 this.systemSeparator.setAttribute("aria-hidden", "true");
7030 this.container.appendChild(this.systemSeparator);
7031 }
7032 const tile2 = this.createSystemItemButton(item);
7033 this.systemItemElements.set(item.id, tile2);
7034 this.container.appendChild(tile2);
7035 this.updateActiveStates();
7036 doAction(HOOKS.DOCK_TILE_RENDERED, {
7037 ...this.buildHookContextBase(),
7038 item,
7039 isSystem: true,
7040 el: tile2
7041 });
7042 }
7043 /**
7044 * Render the dock contents.
7045 *
7046 * Items are ordered server-side with core WordPress menus first and
7047 * plugin-contributed menus after. We insert a `--group` separator
7048 * at the first core→plugin transition so the two clusters read as
7049 * distinct groups of tiles — "default apps" and "installed apps"
7050 * in macOS-dock parlance. The separator is skipped when the menu
7051 * contains only one kind (no plugin menus, or a theme's filter
7052 * reordered everything into one class).
7053 */
7054 render() {
7055 if (_Dock.activeDragReset) {
7056 const prev = _Dock.activeDragReset;
7057 _Dock.activeDragReset = null;
7058 prev();
7059 }
7060 for (const teardown of this.peekTeardowns.values()) {
7061 teardown();
7062 }
7063 this.peekTeardowns.clear();
7064 this.container.innerHTML = "";
7065 const base = this.buildHookContextBase();
7066 doAction(HOOKS.DOCK_BEFORE_RENDER, {
7067 ...base,
7068 items: this.items,
7069 tileElements: this.itemElements
7070 });
7071 let insertedGroupSeparator = false;
7072 for (const item of this.items) {
7073 if (!insertedGroupSeparator && item.isCore === false) {
7074 if (this.container.childElementCount > 0) {
7075 const sep = document.createElement("div");
7076 sep.className = "desktop-mode-dock__separator desktop-mode-dock__separator--group";
7077 sep.setAttribute("aria-hidden", "true");
7078 this.container.appendChild(sep);
7079 }
7080 insertedGroupSeparator = true;
7081 }
7082 const btn = this.createItemButton(item);
7083 this.itemElements.set(item.id, btn);
7084 this.container.appendChild(btn);
7085 doAction(HOOKS.DOCK_TILE_RENDERED, {
7086 ...base,
7087 item,
7088 isSystem: false,
7089 el: btn
7090 });
7091 }
7092 doAction(HOOKS.DOCK_AFTER_RENDER, {
7093 ...base,
7094 items: this.items,
7095 tileElements: this.itemElements
7096 });
7097 }
7098 /**
7099 * Create a tile for a JS-registered system item. Structurally simpler
7100 * than a menu tile — no submenu, no multi-instance rail, no badge —
7101 * but uses the same base classes so the hover / focus / active
7102 * styling is shared.
7103 */
7104 createSystemItemButton(item) {
7105 const ctx = {
7106 ...this.buildHookContextBase(),
7107 item,
7108 isSystem: true
7109 };
7110 const tile2 = document.createElement("div");
7111 const baseClasses = [
7112 "desktop-mode-dock__item",
7113 "desktop-mode-dock__item--system"
7114 ];
7115 const filteredClasses = applyFilters(
7116 HOOKS.DOCK_TILE_CLASS,
7117 baseClasses,
7118 ctx
7119 );
7120 tile2.className = filteredClasses.join(" ");
7121 tile2.dataset.systemId = item.id;
7122 const primary = document.createElement("button");
7123 primary.className = "desktop-mode-dock__item-primary";
7124 primary.setAttribute("type", "button");
7125 primary.setAttribute("aria-label", item.title);
7126 primary.appendChild(this.resolveIcon(item.icon, item.title));
7127 primary.addEventListener("click", () => item.onOpen());
7128 tile2.appendChild(primary);
7129 this.bindTooltipFiltered(tile2, item.title, ctx);
7130 const teardown = attachDockPeek({
7131 tile: tile2,
7132 item: {
7133 id: item.id,
7134 title: item.title,
7135 icon: item.icon,
7136 url: ""
7137 },
7138 getInstances: () => {
7139 const win = this.windowManager.getById(item.id);
7140 return win ? [win] : [];
7141 },
7142 enableGhost: !!item.multi,
7143 windowManager: this.windowManager,
7144 getOrientation: () => this.orientation,
7145 openNew: () => {
7146 const fn = item.onOpenNew ?? item.onOpen;
7147 fn();
7148 },
7149 suppressTooltip: (on) => {
7150 if (on) {
7151 this.tooltip.classList.remove(
7152 "desktop-mode-dock__tooltip--visible"
7153 );
7154 }
7155 }
7156 });
7157 this.peekTeardowns.set(`system:${item.id}`, teardown);
7158 return applyFilters(
7159 HOOKS.DOCK_TILE_ELEMENT,
7160 tile2,
7161 ctx
7162 );
7163 }
7164 /**
7165 * Create a single dock icon tile.
7166 *
7167 * A tile is a vertical stack: the primary icon button, plus — for
7168 * multi-capable pages — an instance rail rendered below it showing one
7169 * dot per open window and a trailing "+" to open another. The rail is
7170 * hydrated by {@link updateActiveStates}; here we only place the empty
7171 * container so the DOM is stable.
7172 */
7173 createItemButton(item) {
7174 const ctx = {
7175 ...this.buildHookContextBase(),
7176 item,
7177 isSystem: false
7178 };
7179 const tile2 = document.createElement("div");
7180 const baseClasses = ["desktop-mode-dock__item"];
7181 if (item.multi) {
7182 baseClasses.push("desktop-mode-dock__item--multi");
7183 }
7184 const filteredClasses = applyFilters(
7185 HOOKS.DOCK_TILE_CLASS,
7186 baseClasses,
7187 ctx
7188 );
7189 tile2.className = filteredClasses.join(" ");
7190 tile2.dataset.menuSlug = item.id;
7191 const primary = document.createElement("button");
7192 primary.className = "desktop-mode-dock__item-primary";
7193 primary.setAttribute("type", "button");
7194 primary.setAttribute("aria-label", item.title);
7195 const iconEl = this.resolveIcon(item.icon, item.title, item.url);
7196 primary.appendChild(iconEl);
7197 if (item.badge > 0) {
7198 const displayCount = item.badge > 99 ? "99+" : String(item.badge);
7199 const badge = document.createElement("span");
7200 badge.className = "desktop-mode-dock__badge";
7201 badge.textContent = displayCount;
7202 badge.setAttribute(
7203 "aria-label",
7204 sprintf(
7205 // translators: %d is the number of pending updates / items.
7206 _n("%d update", "%d updates", item.badge),
7207 item.badge
7208 )
7209 );
7210 primary.appendChild(badge);
7211 }
7212 primary.addEventListener("click", () => {
7213 this.openPage(item);
7214 });
7215 tile2.addEventListener("contextmenu", (ev) => {
7216 ev.preventDefault();
7217 openItemVisibilityMenu({
7218 x: ev.clientX,
7219 y: ev.clientY,
7220 id: item.id,
7221 title: item.title,
7222 surface: "dock",
7223 pluginFile: item.pluginFile ?? null,
7224 pluginName: item.pluginName ?? null
7225 });
7226 });
7227 tile2.appendChild(primary);
7228 this.bindTooltipFiltered(tile2, item.title, ctx);
7229 const baseId = this.resolveItemBaseId(item);
7230 const teardown = attachDockPeek({
7231 tile: tile2,
7232 item: {
7233 id: item.id,
7234 title: item.title,
7235 icon: item.icon,
7236 url: item.url
7237 },
7238 getInstances: () => {
7239 if (item.multi) {
7240 return this.windowManager.getAllByBaseId(baseId);
7241 }
7242 const single = this.windowManager.getById(baseId) || this.windowManager.getById(item.id);
7243 return single ? [single] : [];
7244 },
7245 // Ghost Card on EVERY tile, regardless of `multi`. The
7246 // affordance reads consistently across the dock — every
7247 // hover-peek surfaces a "+ open another <Page>" card. For
7248 // multi-capable items, clicking it spawns a fresh
7249 // instance. For singletons it falls through to the same
7250 // open-or-focus path the tile click takes — usually a
7251 // no-op (focuses the existing window) but cheap and
7252 // visually consistent.
7253 enableGhost: true,
7254 windowManager: this.windowManager,
7255 getOrientation: () => this.orientation,
7256 openNew: () => this.openNewInstance(item),
7257 suppressTooltip: (on) => {
7258 if (on) {
7259 this.tooltip.classList.remove(
7260 "desktop-mode-dock__tooltip--visible"
7261 );
7262 }
7263 }
7264 });
7265 this.peekTeardowns.set(item.id, teardown);
7266 this.attachDragReorder(tile2, item.id);
7267 return applyFilters(
7268 HOOKS.DOCK_TILE_ELEMENT,
7269 tile2,
7270 ctx
7271 );
7272 }
7273 /**
7274 * Drag-to-reorder for menu tiles. Fixed slots — no interpolated
7275 * positioning. While dragging:
7276 *
7277 * 1. Pointer down on the primary button starts a tentative drag.
7278 * Click handling is preserved by requiring movement past a
7279 * small threshold before we claim the gesture.
7280 * 2. Once claimed, the tile gets a `--dragging` modifier so CSS
7281 * can lift it visually. Every `pointermove` checks which other
7282 * menu tile the cursor is currently over; if it's a different
7283 * tile, we splice the dragged tile in front of it (so adjacent
7284 * tiles slide into the vacated slot).
7285 * 3. On `pointerup` we read the resulting DOM order, persist the
7286 * new id list to `dockOrder` via the public settings writer,
7287 * and the layout-dispatcher subscriber re-applies. Cancellation
7288 * (Escape, pointercancel) reverts to the original order.
7289 *
7290 * @since 0.25.0
7291 */
7292 attachDragReorder(tile2, itemId) {
7293 const THRESHOLD = 5;
7294 const FLIP_MS = 200;
7295 let active2 = false;
7296 let startX = 0;
7297 let startY = 0;
7298 let originalOrder = [];
7299 let originalNext = null;
7300 let pointerId = -1;
7301 let originRect = null;
7302 let justDragged = false;
7303 const hardReset = () => {
7304 active2 = false;
7305 tile2.classList.remove("desktop-mode-dock__item--dragging");
7306 tile2.style.transform = "";
7307 tile2.style.transition = "";
7308 document.removeEventListener("pointermove", onMove);
7309 document.removeEventListener("pointerup", onUp);
7310 document.removeEventListener("pointercancel", onCancel);
7311 document.removeEventListener("keydown", onKey, true);
7312 window.removeEventListener("blur", onBlur);
7313 document.removeEventListener("visibilitychange", onVisibility);
7314 pointerId = -1;
7315 originRect = null;
7316 };
7317 const isMenuTile = (el) => {
7318 return !!el && el instanceof HTMLElement && el.classList.contains("desktop-mode-dock__item") && !el.classList.contains("desktop-mode-dock__item--system") && !!el.dataset.menuSlug;
7319 };
7320 const eachSiblingTile = (fn) => {
7321 for (const child of Array.from(this.container.children)) {
7322 if (child instanceof HTMLElement && child !== tile2 && isMenuTile(child)) {
7323 fn(child);
7324 }
7325 }
7326 };
7327 const snapshotMenuOrder = () => {
7328 const ids = [];
7329 for (const child of Array.from(this.container.children)) {
7330 if (isMenuTile(child)) {
7331 ids.push(child.dataset.menuSlug);
7332 }
7333 }
7334 return ids;
7335 };
7336 const flipSiblings = (prevRects) => {
7337 eachSiblingTile((sib) => {
7338 const prev = prevRects.get(sib);
7339 if (!prev) {
7340 return;
7341 }
7342 const now = sib.getBoundingClientRect();
7343 const dx = prev.left - now.left;
7344 const dy = prev.top - now.top;
7345 if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) {
7346 return;
7347 }
7348 sib.style.transition = "none";
7349 sib.style.transform = `translate(${dx}px, ${dy}px)`;
7350 void sib.offsetHeight;
7351 sib.style.transition = `transform ${FLIP_MS}ms cubic-bezier(0.2, 0.7, 0.3, 1)`;
7352 sib.style.transform = "";
7353 const onEnd = () => {
7354 sib.style.transition = "";
7355 sib.style.transform = "";
7356 sib.removeEventListener("transitionend", onEnd);
7357 };
7358 sib.addEventListener("transitionend", onEnd);
7359 });
7360 };
7361 const onMove = (ev) => {
7362 if (pointerId !== -1 && ev.pointerId !== pointerId) {
7363 return;
7364 }
7365 if (!active2) {
7366 const dx2 = ev.clientX - startX;
7367 const dy2 = ev.clientY - startY;
7368 if (dx2 * dx2 + dy2 * dy2 < THRESHOLD * THRESHOLD) {
7369 return;
7370 }
7371 active2 = true;
7372 originalOrder = snapshotMenuOrder();
7373 originalNext = tile2.nextSibling;
7374 originRect = tile2.getBoundingClientRect();
7375 tile2.classList.add("desktop-mode-dock__item--dragging");
7376 this.tooltip.classList.remove(
7377 "desktop-mode-dock__tooltip--visible"
7378 );
7379 }
7380 if (!originRect) {
7381 return;
7382 }
7383 const dx = ev.clientX - startX;
7384 const dy = ev.clientY - startY;
7385 tile2.style.transform = `translate(${dx}px, ${dy}px)`;
7386 const under = document.elementFromPoint(ev.clientX, ev.clientY);
7387 const targetTile = under?.closest(
7388 ".desktop-mode-dock__item"
7389 );
7390 if (!targetTile || targetTile === tile2) {
7391 return;
7392 }
7393 if (!isMenuTile(targetTile)) {
7394 return;
7395 }
7396 const rect = targetTile.getBoundingClientRect();
7397 let insertBefore;
7398 if (this.orientation === "bottom") {
7399 insertBefore = ev.clientX < rect.left + rect.width / 2;
7400 } else {
7401 insertBefore = ev.clientY < rect.top + rect.height / 2;
7402 }
7403 const prevRects = /* @__PURE__ */ new Map();
7404 eachSiblingTile((sib) => {
7405 prevRects.set(sib, sib.getBoundingClientRect());
7406 });
7407 let reordered = false;
7408 if (insertBefore) {
7409 if (targetTile !== tile2.nextSibling) {
7410 this.container.insertBefore(tile2, targetTile);
7411 reordered = true;
7412 }
7413 } else if (targetTile.nextSibling !== tile2) {
7414 this.container.insertBefore(tile2, targetTile.nextSibling);
7415 reordered = true;
7416 }
7417 if (reordered) {
7418 tile2.style.transform = "";
7419 const fresh = tile2.getBoundingClientRect();
7420 startX = fresh.left + fresh.width / 2;
7421 startY = fresh.top + fresh.height / 2;
7422 tile2.style.transform = `translate(${ev.clientX - startX}px, ${ev.clientY - startY}px)`;
7423 flipSiblings(prevRects);
7424 }
7425 };
7426 const cleanup = () => {
7427 tile2.classList.remove("desktop-mode-dock__item--dragging");
7428 tile2.style.transform = "";
7429 tile2.style.transition = "";
7430 document.removeEventListener("pointermove", onMove);
7431 document.removeEventListener("pointerup", onUp);
7432 document.removeEventListener("pointercancel", onCancel);
7433 document.removeEventListener("keydown", onKey, true);
7434 window.removeEventListener("blur", onBlur);
7435 document.removeEventListener("visibilitychange", onVisibility);
7436 pointerId = -1;
7437 originRect = null;
7438 active2 = false;
7439 if (_Dock.activeDragReset === hardReset) {
7440 _Dock.activeDragReset = null;
7441 }
7442 };
7443 const animateHome = () => {
7444 tile2.style.transition = `transform ${FLIP_MS}ms cubic-bezier(0.2, 0.7, 0.3, 1)`;
7445 tile2.style.transform = "";
7446 const onEnd = () => {
7447 tile2.style.transition = "";
7448 tile2.removeEventListener("transitionend", onEnd);
7449 };
7450 tile2.addEventListener("transitionend", onEnd);
7451 };
7452 const persistDockOrder = (finalOrder) => {
7453 const api = window.wp?.desktop;
7454 if (!api?.getOsSettings || !api?.updateOsSettings) {
7455 return;
7456 }
7457 const existing = api.getOsSettings().dockOrder;
7458 const finalSet = new Set(finalOrder);
7459 const merged = [];
7460 let injected = false;
7461 for (const id of existing) {
7462 if (finalSet.has(id)) {
7463 if (!injected) {
7464 merged.push(...finalOrder);
7465 injected = true;
7466 }
7467 continue;
7468 }
7469 merged.push(id);
7470 }
7471 if (!injected) {
7472 merged.push(...finalOrder);
7473 }
7474 api.updateOsSettings({ dockOrder: merged });
7475 };
7476 const onUp = (ev) => {
7477 if (pointerId !== -1 && ev.pointerId !== pointerId) {
7478 return;
7479 }
7480 if (!active2) {
7481 cleanup();
7482 return;
7483 }
7484 justDragged = true;
7485 const finalOrder = snapshotMenuOrder();
7486 animateHome();
7487 cleanup();
7488 const same = finalOrder.length === originalOrder.length && finalOrder.every((id, i) => id === originalOrder[i]);
7489 if (!same) {
7490 persistDockOrder(finalOrder);
7491 }
7492 setTimeout(() => {
7493 justDragged = false;
7494 }, 200);
7495 };
7496 const onCancel = (ev) => {
7497 if (ev && pointerId !== -1 && ev.pointerId !== pointerId) {
7498 return;
7499 }
7500 if (active2 && originalNext !== void 0) {
7501 const prevRects = /* @__PURE__ */ new Map();
7502 eachSiblingTile((sib) => {
7503 prevRects.set(sib, sib.getBoundingClientRect());
7504 });
7505 this.container.insertBefore(tile2, originalNext);
7506 flipSiblings(prevRects);
7507 }
7508 animateHome();
7509 cleanup();
7510 };
7511 const onKey = (ev) => {
7512 if (ev.key === "Escape") {
7513 onCancel();
7514 }
7515 };
7516 const onBlur = () => onCancel();
7517 const onVisibility = () => {
7518 if (document.visibilityState !== "visible") {
7519 onCancel();
7520 }
7521 };
7522 tile2.addEventListener("pointerdown", (ev) => {
7523 if (ev.button !== 0) {
7524 return;
7525 }
7526 if (_Dock.activeDragReset) {
7527 const prev = _Dock.activeDragReset;
7528 _Dock.activeDragReset = null;
7529 prev();
7530 }
7531 if (active2 || pointerId !== -1) {
7532 hardReset();
7533 }
7534 startX = ev.clientX;
7535 startY = ev.clientY;
7536 pointerId = ev.pointerId;
7537 active2 = false;
7538 _Dock.activeDragReset = hardReset;
7539 document.addEventListener("pointermove", onMove);
7540 document.addEventListener("pointerup", onUp);
7541 document.addEventListener("pointercancel", onCancel);
7542 document.addEventListener("keydown", onKey, true);
7543 window.addEventListener("blur", onBlur);
7544 document.addEventListener("visibilitychange", onVisibility);
7545 });
7546 tile2.addEventListener(
7547 "click",
7548 (ev) => {
7549 if (justDragged) {
7550 ev.preventDefault();
7551 ev.stopImmediatePropagation();
7552 }
7553 },
7554 true
7555 );
7556 }
7557 /**
7558 * Resolve a registered icon value into a DOM element.
7559 *
7560 * Priority: dashicons class → inline SVG data URI → image URL →
7561 * letter badge derived from the item's title. The letter fallback is
7562 * important for plugin tiles: plugin authors routinely register
7563 * top-level menus with `add_menu_page()` and omit the icon argument
7564 * (defaulting to `'div'` or empty), which would otherwise render as
7565 * an indistinguishable wall of generic wrenches. A colored letter
7566 * tile gives each plugin a stable, unique-ish visual identity with
7567 * zero plugin-side effort — the hue derives deterministically from
7568 * the title so the same plugin always gets the same color.
7569 *
7570 * @param icon The icon value from the menu entry.
7571 * @param title Human-readable title, used when falling back to a
7572 * letter badge.
7573 */
7574 resolveIcon(icon, title, url) {
7575 if (icon.startsWith("dashicons-") && icon !== "dashicons-admin-generic") {
7576 const el = document.createElement("span");
7577 el.className = `dashicons ${icon}`;
7578 el.setAttribute("aria-hidden", "true");
7579 return el;
7580 }
7581 if (icon.startsWith("data:image/svg+xml;base64,")) {
7582 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
7583 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
7584 return this._makeSvgIcon(icon);
7585 }
7586 }
7587 if (icon.startsWith("url(")) {
7588 return this._makeSvgIcon(icon);
7589 }
7590 if (icon.startsWith("http://") || icon.startsWith("https://")) {
7591 const img = document.createElement("img");
7592 img.className = "desktop-mode-dock__item-img";
7593 img.src = icon;
7594 img.alt = "";
7595 img.setAttribute("aria-hidden", "true");
7596 return img;
7597 }
7598 if (url) {
7599 const native = this._extractNativeMenuIcon(url);
7600 if (native) {
7601 return native;
7602 }
7603 }
7604 if (icon === "dashicons-admin-generic") {
7605 const el = document.createElement("span");
7606 el.className = "dashicons dashicons-admin-generic";
7607 el.setAttribute("aria-hidden", "true");
7608 return el;
7609 }
7610 return this.createLetterBadge(title);
7611 }
7612 /**
7613 * Build an SVG-background icon tile. Shared between the data-URI
7614 * branch of {@link resolveIcon} and the native-menu extractor.
7615 */
7616 _makeSvgIcon(bgValue) {
7617 const el = document.createElement("span");
7618 el.className = "desktop-mode-dock__item-svg";
7619 el.style.backgroundImage = bgValue.startsWith("url(") ? bgValue : `url("${bgValue}")`;
7620 el.style.backgroundSize = "contain";
7621 el.style.backgroundRepeat = "no-repeat";
7622 el.style.backgroundPosition = "center";
7623 el.setAttribute("aria-hidden", "true");
7624 return el;
7625 }
7626 /**
7627 * Extract a plugin's icon from the hidden `#adminmenu` that still
7628 * exists in the parent shell DOM (display:none'd by desktop.css).
7629 * Handles the three shapes plugins commonly use when the menu-page
7630 * icon_url is 'none' or 'div':
7631 *
7632 * (a) `<img src="...">` nested inside `.wp-menu-image`
7633 * (b) a dashicon class on `.wp-menu-image` itself
7634 * (c) a CSS background-image on `.wp-menu-image::before` (the
7635 * `menu-icon-XYZ` pattern Yoast, WooCommerce, Jetpack, etc. use)
7636 *
7637 * Returns null when the URL doesn't match any admin-menu entry or
7638 * none of the three shapes are detectable.
7639 */
7640 _extractNativeMenuIcon(url) {
7641 const adminMenu = document.getElementById("adminmenu");
7642 if (!adminMenu) {
7643 return null;
7644 }
7645 let target;
7646 try {
7647 const u = new URL(url, window.location.href);
7648 const filename = u.pathname.split("/").pop() || "";
7649 target = filename + u.search;
7650 } catch {
7651 return null;
7652 }
7653 if (!target) {
7654 return null;
7655 }
7656 const links = adminMenu.querySelectorAll("li.menu-top > a");
7657 let matchLi = null;
7658 for (const link of Array.from(links)) {
7659 if (link.href.endsWith(target)) {
7660 matchLi = link.closest("li.menu-top");
7661 break;
7662 }
7663 }
7664 if (!matchLi) {
7665 return null;
7666 }
7667 const imgWrap = matchLi.querySelector(".wp-menu-image");
7668 if (!imgWrap) {
7669 return null;
7670 }
7671 const img = imgWrap.querySelector("img");
7672 if (img && img.src) {
7673 const el = document.createElement("img");
7674 el.className = "desktop-mode-dock__item-img";
7675 el.src = img.src;
7676 el.alt = "";
7677 el.setAttribute("aria-hidden", "true");
7678 return el;
7679 }
7680 const dashMatch = imgWrap.className.match(/\bdashicons-[\w-]+\b/);
7681 if (dashMatch && dashMatch[0] !== "dashicons-before") {
7682 const el = document.createElement("span");
7683 el.className = `dashicons ${dashMatch[0]}`;
7684 el.setAttribute("aria-hidden", "true");
7685 return el;
7686 }
7687 const before = window.getComputedStyle(imgWrap, "::before");
7688 const bg = before.backgroundImage;
7689 if (bg && bg !== "none" && !bg.includes('url("")')) {
7690 return this._makeSvgIcon(bg);
7691 }
7692 const bgWrap = window.getComputedStyle(imgWrap).backgroundImage;
7693 if (bgWrap && bgWrap !== "none" && !bgWrap.includes('url("")')) {
7694 return this._makeSvgIcon(bgWrap);
7695 }
7696 return null;
7697 }
7698 /**
7699 * Create a letter-badge icon — a rounded square tinted with a
7700 * deterministic hue derived from the title, displaying the first
7701 * letter of the title. Mirrors the "app icon placeholder" look
7702 * macOS uses when an app ships without artwork.
7703 *
7704 * The title always drives both the letter and the hue — same plugin,
7705 * same color across reloads. An empty title falls through to a `?`
7706 * on a neutral gray tile, but the menu builder upstream guards
7707 * against empty titles, so this is a defensive branch.
7708 */
7709 createLetterBadge(title) {
7710 const el = document.createElement("span");
7711 el.className = "desktop-mode-dock__item-letter";
7712 el.setAttribute("aria-hidden", "true");
7713 const trimmed = title.trim();
7714 const firstCodePoint = trimmed ? Array.from(trimmed)[0] : "?";
7715 el.textContent = firstCodePoint.toUpperCase();
7716 const hue = hashTitleToHue(trimmed);
7717 el.style.background = `linear-gradient(135deg, hsl(${hue} 62% 55%), hsl(${(hue + 24) % 360} 58% 42%))`;
7718 return el;
7719 }
7720 /**
7721 * Bind tooltip show/hide on hover. Tooltip anchor differs per
7722 * orientation: left dock → tile's right side, right dock → tile's
7723 * left side, bottom dock → above the tile. We set the relevant
7724 * coordinate inline each enter; the CSS takes care of the rest.
7725 */
7726 /**
7727 * Resolves the tooltip text through {@link HOOKS.DOCK_TILE_TOOLTIP}
7728 * once at bind time (so the dock doesn't re-filter on every
7729 * pointerenter) and stashes the resolved text on
7730 * `tile.dataset.dockTooltip` so the multi-instance chip can
7731 * restore it on its own pointerleave without going through the
7732 * filter again.
7733 *
7734 * Returning an empty string from the filter suppresses the
7735 * tooltip — the listener short-circuits and never adds the
7736 * `--visible` class.
7737 */
7738 bindTooltipFiltered(tile2, text, ctx) {
7739 const filtered = applyFilters(
7740 HOOKS.DOCK_TILE_TOOLTIP,
7741 text,
7742 ctx
7743 );
7744 tile2.dataset.dockTooltip = filtered;
7745 if (filtered === "") {
7746 return;
7747 }
7748 tile2.addEventListener("pointerenter", () => {
7749 this.positionTooltip(tile2, filtered);
7750 this.tooltip.classList.add("desktop-mode-dock__tooltip--visible");
7751 });
7752 tile2.addEventListener("pointerleave", () => {
7753 this.tooltip.classList.remove("desktop-mode-dock__tooltip--visible");
7754 });
7755 }
7756 /**
7757 * Write the tooltip text + anchor coordinate for `el`. Split out
7758 * because the multi-instance chip's pointerenter handler also
7759 * needs to anchor to a specific element (the chip, not the tile).
7760 */
7761 positionTooltip(el, text) {
7762 const rect = el.getBoundingClientRect();
7763 this.tooltip.textContent = text;
7764 if (this.orientation === "bottom") {
7765 this.tooltip.style.left = `${rect.left + rect.width / 2}px`;
7766 this.tooltip.style.top = `${rect.top - 14}px`;
7767 } else if (this.orientation === "right") {
7768 this.tooltip.style.top = `${rect.top + rect.height / 2 - 14}px`;
7769 this.tooltip.style.left = `${rect.left}px`;
7770 } else {
7771 this.tooltip.style.top = `${rect.top + rect.height / 2 - 14}px`;
7772 this.tooltip.style.left = `${rect.right + 8}px`;
7773 }
7774 }
7775 /**
7776 * Open an admin page in a window (or focus if already open).
7777 *
7778 * Consults the native URL-remap registry first — when an opt-in
7779 * native window has registered itself as the replacement for this
7780 * admin URL (e.g. the native Posts window for `edit.php` when the
7781 * user has flipped `nativePostsEnabled`), the click is rerouted
7782 * to that window and the iframe path is skipped. The dock item
7783 * itself is untouched: same icon, same tooltip, same position —
7784 * only the destination changes.
7785 */
7786 openPage(item) {
7787 if (item.id.startsWith("dock:")) {
7788 const iconId = item.id.slice(5);
7789 const cfg = window.desktopModeConfig;
7790 const icon = cfg?.desktopIcons?.find((i) => i.id === iconId);
7791 if (icon?.window) {
7792 const wp = window.wp?.desktop;
7793 wp?.openWindow?.(icon.window);
7794 return;
7795 }
7796 if (icon?.url) {
7797 if (tryOpenExternalUrl(icon.url)) {
7798 return;
7799 }
7800 const baseId2 = this.deriveWindowId(icon.url);
7801 this.windowManager.open({
7802 id: baseId2,
7803 baseId: baseId2,
7804 url: icon.url,
7805 parentUrl: icon.url,
7806 title: icon.title,
7807 icon: icon.icon.startsWith("dashicons-") ? icon.icon : "dashicons-admin-generic",
7808 submenu: [],
7809 multi: false
7810 });
7811 return;
7812 }
7813 return;
7814 }
7815 if (tryOpenExternalUrl(item.url)) {
7816 return;
7817 }
7818 if (tryNativeUrlRemap(item.url)) {
7819 return;
7820 }
7821 const baseId = this.deriveWindowId(item.url);
7822 this.windowManager.open({
7823 id: baseId,
7824 baseId,
7825 url: item.url,
7826 parentUrl: item.url,
7827 title: item.title,
7828 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
7829 submenu: item.submenu,
7830 multi: !!item.multi
7831 });
7832 }
7833 /**
7834 * Open a brand-new instance of a page, even if one is already
7835 * open. Invoked by the "+" ghost card in the dock peek.
7836 *
7837 * The user explicitly asked for "another window of this thing,"
7838 * so we honour the request even when {@link tryNativeUrlRemap}
7839 * would otherwise route the click into a native-window
7840 * singleton. Result: clicking + while a native Posts window is
7841 * open opens a fresh iframe of `edit.php` alongside it. Two
7842 * windows of Posts is the explicit ask — that's what + is for.
7843 */
7844 openNewInstance(item) {
7845 if (tryOpenExternalUrl(item.url)) {
7846 return;
7847 }
7848 const openNewWindow = window.wp?.desktop?.openNewWindow;
7849 if (item.windowId && !item.url) {
7850 if (openNewWindow?.(item.windowId, { source: "dock-peek" })) {
7851 return;
7852 }
7853 }
7854 const remappedId = resolveNativeUrlRemap(item.url);
7855 if (remappedId) {
7856 if (openNewWindow?.(remappedId, { source: "dock-peek" })) {
7857 return;
7858 }
7859 }
7860 const baseId = this.deriveWindowId(item.url);
7861 void this.windowManager.openNew({
7862 id: baseId,
7863 baseId,
7864 url: item.url,
7865 parentUrl: item.url,
7866 title: item.title,
7867 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
7868 submenu: item.submenu,
7869 multi: true
7870 });
7871 }
7872 /**
7873 * Derive a window ID from an admin page URL.
7874 */
7875 deriveWindowId(url) {
7876 return deriveWindowId(url, this.adminUrl);
7877 }
7878 /**
7879 * Resolve the window-manager key for a dock tile, in this order:
7880 *
7881 * 1. `item.windowId` — set by `applyDockPlacement` when the tile
7882 * is synthesized from a `desktop_mode_register_icon()` entry
7883 * whose target is a native window. Native-window ids never
7884 * pass through the URL → native-window remap layer, so we
7885 * short-circuit before touching it.
7886 * 2. {@link resolveNativeUrlRemap} on `item.url` — captures the
7887 * `nativePostsEnabled` / `nativePagesEnabled` opt-ins that
7888 * repoint a URL-based tile at a native window.
7889 * 3. {@link deriveWindowId} on `item.url` — the URL-based
7890 * fallback for ordinary admin-menu tiles.
7891 *
7892 * Shared by the hover-peek card and the active/focused-dot
7893 * indicator; the two stayed in lockstep before this method existed
7894 * by hand-rolling the same chain at each call site.
7895 */
7896 resolveItemBaseId(item) {
7897 if (item.windowId) {
7898 return item.windowId;
7899 }
7900 const remapped = resolveNativeUrlRemap(item.url);
7901 return remapped ?? this.deriveWindowId(item.url);
7902 }
7903 /**
7904 * Listen to window events to update active/focused indicators on dock items.
7905 *
7906 * The event detail isn't used — we just need to re-query the
7907 * window manager on every change — so the handlers take no
7908 * argument and the type cast is gone with it.
7909 */
7910 bindWindowEvents() {
7911 const refresh = () => this.updateActiveStates();
7912 this.boundRefresh = refresh;
7913 document.addEventListener("desktop-mode-window-opened", refresh);
7914 document.addEventListener("desktop-mode-window-closed", refresh);
7915 document.addEventListener("desktop-mode-window-focused", refresh);
7916 window.wp?.hooks?.addAction?.(
7917 "desktop-mode.desktop.switched",
7918 this.hooksNamespace,
7919 refresh
7920 );
7921 window.wp?.hooks?.addAction?.(
7922 "desktop-mode.desktop.closed",
7923 this.hooksNamespace,
7924 refresh
7925 );
7926 }
7927 /**
7928 * Tear the dock down: detach window-lifecycle listeners, clear
7929 * pending attention timers, remove the floating tooltip from
7930 * `document.body`, and empty the container's children. Used by
7931 * the layout dispatcher when the user switches `desktopLayout`
7932 * in OS Settings — old dock(s) get destroyed and a fresh set is
7933 * constructed for the new layout.
7934 *
7935 * Idempotent: calling twice is safe.
7936 */
7937 destroy() {
7938 document.removeEventListener(
7939 "desktop-mode-window-opened",
7940 this.boundRefresh
7941 );
7942 document.removeEventListener(
7943 "desktop-mode-window-closed",
7944 this.boundRefresh
7945 );
7946 document.removeEventListener(
7947 "desktop-mode-window-focused",
7948 this.boundRefresh
7949 );
7950 window.wp?.hooks?.removeAction?.(
7951 "desktop-mode.desktop.switched",
7952 this.hooksNamespace
7953 );
7954 window.wp?.hooks?.removeAction?.(
7955 "desktop-mode.desktop.closed",
7956 this.hooksNamespace
7957 );
7958 for (const handle of this.attentionTimers.values()) {
7959 window.clearTimeout(handle);
7960 }
7961 this.attentionTimers.clear();
7962 for (const teardown of this.peekTeardowns.values()) {
7963 teardown();
7964 }
7965 this.peekTeardowns.clear();
7966 this.tooltip.remove();
7967 while (this.container.firstChild) {
7968 this.container.removeChild(this.container.firstChild);
7969 }
7970 this.itemElements.clear();
7971 this.systemItemElements.clear();
7972 this.systemItems = [];
7973 this.systemSeparator = null;
7974 this.container.removeAttribute("data-desktop-mode-dock-placement");
7975 }
7976 /**
7977 * Update the active/focused classes and multi-instance rail on every
7978 * dock item in response to a window lifecycle event.
7979 *
7980 * For singletons the rail is absent; "active" means "the one window
7981 * is open". For multi-capable items, active means "≥1 instance is
7982 * open" and focused means "the focused window belongs to this item".
7983 */
7984 updateActiveStates() {
7985 const focused = this.windowManager.getFocused();
7986 const focusedBaseId = focused ? focused.config.baseId || focused.id : null;
7987 const activeDesktopId = this.windowManager.getActiveDesktopId();
7988 const onActiveDesktop = (w) => (w.config.desktopId || activeDesktopId) === activeDesktopId;
7989 for (const item of this.items) {
7990 const tile2 = this.itemElements.get(item.id);
7991 if (!tile2) {
7992 continue;
7993 }
7994 const baseId = this.resolveItemBaseId(item);
7995 const instances = item.multi ? this.windowManager.getAllByBaseId(baseId).filter(onActiveDesktop) : [];
7996 const single = this.windowManager.getById(baseId);
7997 const singleOpen = !item.multi && !!single && onActiveDesktop(single);
7998 const isOpen = item.multi ? instances.length > 0 : singleOpen;
7999 const isFocused = focusedBaseId === baseId && !!focused && onActiveDesktop(focused);
8000 tile2.classList.toggle("desktop-mode-dock__item--active", isOpen);
8001 tile2.classList.toggle("desktop-mode-dock__item--focused", isFocused);
8002 }
8003 for (const sys of this.systemItems) {
8004 const tile2 = this.systemItemElements.get(sys.id);
8005 if (!tile2) {
8006 continue;
8007 }
8008 const isOpen = sys.isOpen ? sys.isOpen() : false;
8009 const isFocused = !!focused && focused.id === sys.id;
8010 tile2.classList.toggle("desktop-mode-dock__item--active", isOpen);
8011 tile2.classList.toggle("desktop-mode-dock__item--focused", isFocused);
8012 }
8013 }
8014 };
8015 _Dock.instanceCounter = 0;
8016 _Dock.activeDragReset = null;
8017 let Dock = _Dock;
8018 function _applyBadgeNode(host, count) {
8019 const existing = host.querySelector(
8020 ":scope > .desktop-mode-dock__badge"
8021 );
8022 if (count <= 0) {
8023 existing?.remove();
8024 return;
8025 }
8026 const display = count > 99 ? "99+" : String(count);
8027 if (existing) {
8028 if (existing.textContent !== display) {
8029 existing.textContent = display;
8030 }
8031 existing.setAttribute(
8032 "aria-label",
8033 sprintf(
8034 // translators: %d is the number of pending items in a dock badge.
8035 _n("%d notification", "%d notifications", count),
8036 count
8037 )
8038 );
8039 return;
8040 }
8041 const badge = document.createElement("span");
8042 badge.className = "desktop-mode-dock__badge";
8043 badge.textContent = display;
8044 badge.setAttribute(
8045 "aria-label",
8046 sprintf(
8047 // translators: %d is the number of pending items in a dock badge.
8048 _n("%d notification", "%d notifications", count),
8049 count
8050 )
8051 );
8052 host.appendChild(badge);
8053 }
8054 const DEFAULT_RENDERER_DOCK = Symbol.for(
8055 "desktop-mode/default-dock-rail-renderer/dock"
8056 );
8057 const defaultDockRailRenderer = {
8058 id: "default",
8059 label: "Icon strip",
8060 description: "The shipped baseline — icon tiles with badges, tooltips, multi-instance chips, and attention animations.",
8061 icon: "dashicons-menu-alt",
8062 apiVersion: 1,
8063 mount(deps2) {
8064 const dock = new Dock(
8065 deps2.container,
8066 deps2.windowManager,
8067 deps2.items,
8068 deps2.adminUrl,
8069 deps2.orientation
8070 );
8071 const controller = {
8072 [DEFAULT_RENDERER_DOCK]: dock,
8073 replaceItems: (items) => dock.replaceItems(items),
8074 appendSystemItem: (item) => dock.appendSystemItem(item),
8075 removeSystemItem: (id) => dock.removeSystemItem(id),
8076 setBadge: (itemId, count) => dock.setBadge(itemId, count),
8077 setAttention: (itemId, mode, opts) => dock.setAttention(itemId, mode, opts),
8078 setOrientation: (orientation) => dock.setOrientation(orientation),
8079 destroy: () => dock.destroy()
8080 };
8081 return controller;
8082 }
8083 };
8084 function unwrapDefaultDock(controller) {
8085 if (!controller) {
8086 return null;
8087 }
8088 const probe = controller;
8089 const dock = probe[DEFAULT_RENDERER_DOCK];
8090 return dock instanceof Dock ? dock : null;
8091 }
8092 function installDefaultDockRailRenderer() {
8093 register$1(defaultDockRailRenderer);
8094 }
8095 function customGradientCss(state2) {
8096 const { from, to, angle } = state2.customGradient;
8097 return `linear-gradient(${angle}deg, ${from}, ${to})`;
8098 }
8099 function registerCustomGradient(ctx) {
8100 register$2({
8101 id: CUSTOM_GRADIENT_ID,
8102 label: __("Custom gradient"),
8103 type: "css",
8104 preview: customGradientCss(ctx.state),
8105 resolveValue: () => customGradientCss(ctx.state)
8106 });
8107 }
8108 function registerCustomImageIfPresent(state2) {
8109 if (!state2.customImage) {
8110 unregister$2(CUSTOM_IMAGE_ID);
8111 return;
8112 }
8113 const safeUrl = encodeURI(state2.customImage.url);
8114 const value = `url("${safeUrl}") center/cover no-repeat, #1d2327`;
8115 register$2({
8116 id: CUSTOM_IMAGE_ID,
8117 label: __("Custom image"),
8118 type: "css",
8119 value,
8120 preview: value
8121 });
8122 }
8123 let _panelLoadPromise = null;
8124 function loadOsSettingsPanelBundle(scriptUrl) {
8125 if (window.desktopModeRenderOsSettingsPanel) {
8126 return Promise.resolve(window.desktopModeRenderOsSettingsPanel);
8127 }
8128 if (_panelLoadPromise) {
8129 return _panelLoadPromise;
8130 }
8131 _panelLoadPromise = new Promise((resolve2, reject) => {
8132 const existing = document.querySelector(
8133 'script[data-desktop-mode-os-settings-panel="1"]'
8134 );
8135 const finish = () => {
8136 const fn = window.desktopModeRenderOsSettingsPanel;
8137 if (!fn) {
8138 reject(
8139 new Error(
8140 "[desktop-mode] os-settings-panel bundle loaded but did not register desktopModeRenderOsSettingsPanel"
8141 )
8142 );
8143 return;
8144 }
8145 resolve2(fn);
8146 };
8147 if (existing) {
8148 if (window.desktopModeRenderOsSettingsPanel) {
8149 finish();
8150 } else {
8151 existing.addEventListener("load", finish);
8152 existing.addEventListener(
8153 "error",
8154 () => reject(new Error("failed to load os-settings-panel bundle"))
8155 );
8156 }
8157 return;
8158 }
8159 const s = document.createElement("script");
8160 s.src = scriptUrl;
8161 s.async = true;
8162 s.dataset.desktopModeOsSettingsPanel = "1";
8163 s.addEventListener("load", finish);
8164 s.addEventListener(
8165 "error",
8166 () => reject(new Error("failed to load os-settings-panel bundle"))
8167 );
8168 document.head.appendChild(s);
8169 });
8170 return _panelLoadPromise;
8171 }
8172 class OsSettings {
8173 constructor(config, layer) {
8174 this.activeEditorTeardown = null;
8175 this.tabRegistryUnsubscribe = null;
8176 this.activeTabId = null;
8177 this.osSettingsListeners = /* @__PURE__ */ new Set();
8178 this._lastRenderedBody = null;
8179 this.config = config;
8180 this.layer = layer;
8181 this.state = loadState();
8182 setLastConfirmedState(this.state);
8183 document.addEventListener(
8184 "desktop-mode-os-settings-save-lifecycle",
8185 (e) => {
8186 const detail = e.detail;
8187 if (!detail || detail.phase !== "failed" || !detail.rolledBackTo) {
8188 return;
8189 }
8190 this.state = detail.rolledBackTo;
8191 this.apply();
8192 if (this._lastRenderedBody?.isConnected) {
8193 this.renderPanel(this._lastRenderedBody);
8194 }
8195 }
8196 );
8197 registerCustomGradient(this);
8198 registerCustomImageIfPresent(this.state);
8199 }
8200 /** Project the private state into the public snapshot shape. */
8201 getOsSettingsSnapshot() {
8202 return {
8203 wallpaper: this.state.wallpaper,
8204 accent: this.state.accent,
8205 dockSize: this.state.dockSize,
8206 desktopLayout: this.state.desktopLayout,
8207 dockRailRenderer: this.state.dockRailRenderer,
8208 ai: { ...this.state.ai },
8209 nativePostsEnabled: this.state.nativePostsEnabled,
8210 nativePostsHiddenColumns: this.state.nativePostsHiddenColumns.slice(),
8211 nativePagesEnabled: this.state.nativePagesEnabled,
8212 nativeUsersEnabled: this.state.nativeUsersEnabled,
8213 nativePluginsEnabled: this.state.nativePluginsEnabled,
8214 nativeCommentsEnabled: this.state.nativeCommentsEnabled,
8215 foldersSharingEnabled: this.state.foldersSharingEnabled,
8216 itemVisibility: { ...this.state.itemVisibility },
8217 dockOrder: this.state.dockOrder.slice(),
8218 dockPromotedPositions: Object.fromEntries(
8219 Object.entries(this.state.dockPromotedPositions).map(
8220 ([k, v]) => [k, { ...v }]
8221 )
8222 )
8223 };
8224 }
8225 subscribeOsSettings(cb) {
8226 this.osSettingsListeners.add(cb);
8227 return () => {
8228 this.osSettingsListeners.delete(cb);
8229 };
8230 }
8231 /**
8232 * Apply the current state: wallpaper via the layer, accent + dock
8233 * size as CSS custom properties on the shell.
8234 *
8235 * Safe to call repeatedly — calls into `layer.apply` dedupe via
8236 * generation counter; CSS property writes are idempotent.
8237 */
8238 apply() {
8239 const shell = document.getElementById("desktop-mode-shell");
8240 if (!shell) {
8241 return;
8242 }
8243 const def = get$1(this.state.wallpaper) || get$1(getDefaultWallpaperId()) || get$1(DEFAULT_WALLPAPER_ID) || all$1()[0];
8244 if (def) {
8245 this.layer.apply(def);
8246 }
8247 const accents = getAccents();
8248 const accent = accents.find((a) => a.id === this.state.accent) ?? accents[0];
8249 const dockSize = DOCK_SIZES.find((d) => d.id === this.state.dockSize) ?? DOCK_SIZES[1];
8250 const root = document.documentElement;
8251 root.style.setProperty("--wp-admin-theme-color", accent.value);
8252 root.style.setProperty("--desktop-mode-dock-width", `${dockSize.width}px`);
8253 root.style.setProperty("--desktop-mode-dock-icon-size", `${dockSize.icon}px`);
8254 shell.setAttribute(
8255 "data-desktop-mode-layout",
8256 this.state.desktopLayout
8257 );
8258 setActiveRenderer(this.state.dockRailRenderer);
8259 }
8260 save(opts = {}) {
8261 saveState(this.state, opts);
8262 if (this.osSettingsListeners.size > 0) {
8263 const snapshot = this.getOsSettingsSnapshot();
8264 const listeners2 = Array.from(this.osSettingsListeners);
8265 for (const cb of listeners2) {
8266 try {
8267 cb(snapshot);
8268 } catch (err) {
8269 if (typeof console !== "undefined") {
8270 console.error(
8271 "[desktop-mode] os-settings listener threw:",
8272 err
8273 );
8274 }
8275 }
8276 }
8277 }
8278 }
8279 /**
8280 * Render the settings panel into the given native-window body.
8281 *
8282 * Builds three sections (wallpaper, accent, dock size) and wires
8283 * each to save/apply on change. The panel is a one-shot build per
8284 * window open — closing and re-opening renders a fresh tree.
8285 */
8286 /**
8287 * Render the settings panel into the given native-window body.
8288 *
8289 * Lazy since 0.8.4 — the actual rendering logic plus every
8290 * `<wpd-*>` component the panel uses lives in
8291 * `src/settings/panel.ts`, compiled into its own Vite target
8292 * `os-settings-panel[.min].js`. The script is injected on the
8293 * first call below and the matching
8294 * `window.desktopModeRenderOsSettingsPanel( ctx, body )` global
8295 * is then invoked. Subsequent calls (registry-driven re-render,
8296 * save-failure rollback) skip the load and forward immediately.
8297 *
8298 * Why this is a `<script>`-injected sibling bundle rather than
8299 * an in-bundle dynamic import: Vite IIFE lib mode inlines
8300 * `import()` calls, so an in-bundle lazy import would give zero
8301 * byte savings. A separate Vite target is the only mechanism
8302 * that actually shrinks `desktop.min.js`. See the Stage 8
8303 * section of `BUNDLE-SIZE-REPORT.md` for the full picture.
8304 */
8305 renderPanel(body) {
8306 this._lastRenderedBody = body;
8307 const fn = window.desktopModeRenderOsSettingsPanel;
8308 if (fn) {
8309 fn(this, body);
8310 return;
8311 }
8312 void loadOsSettingsPanelBundle(
8313 this.config.osSettingsPanelBundleUrl ?? ""
8314 ).then((render2) => {
8315 if (!body.isConnected) {
8316 return;
8317 }
8318 render2(this, body);
8319 }).catch((err) => {
8320 if (typeof console !== "undefined") {
8321 console.error(
8322 "[desktop-mode] OS Settings panel failed to load:",
8323 err
8324 );
8325 }
8326 });
8327 }
8328 }
8329 const EXIT_DESKTOP_MODE_TILE_ID = "desktop-mode-exit";
8330 function getExitDesktopModeTileDef() {
8331 return {
8332 id: EXIT_DESKTOP_MODE_TILE_ID,
8333 title: __("Exit Desktop Mode"),
8334 // `dashicons-exit` (door with arrow) is the clearest "leave"
8335 // glyph in the WordPress set, distinct from `dashicons-desktop`
8336 // used by OS Settings.
8337 icon: "dashicons-exit",
8338 onOpen: () => {
8339 void exitDesktopMode();
8340 }
8341 };
8342 }
8343 async function exitDesktopMode() {
8344 const cfg = window.desktopModeAdminBar;
8345 const fallback = cfg?.classicUrl || "/wp-admin/";
8346 if (!cfg?.ajaxUrl || !cfg?.nonce) {
8347 navigateTop(fallback);
8348 return;
8349 }
8350 const body = new URLSearchParams();
8351 body.set("action", "save-desktop-mode");
8352 body.set("nonce", cfg.nonce);
8353 body.set("enabled", "");
8354 let target = fallback;
8355 try {
8356 const res = await fetch(cfg.ajaxUrl, {
8357 method: "POST",
8358 headers: {
8359 "Content-Type": "application/x-www-form-urlencoded"
8360 },
8361 body: body.toString(),
8362 credentials: "same-origin"
8363 });
8364 if (res.ok) {
8365 const json = await res.json();
8366 if (json?.success && json.data?.redirect) {
8367 target = json.data.redirect;
8368 }
8369 }
8370 } catch {
8371 }
8372 navigateTop(target);
8373 }
8374 function navigateTop(url) {
8375 try {
8376 window.top.location.href = url;
8377 } catch {
8378 window.location.href = url;
8379 }
8380 }
8381 const _initial$1 = {
8382 userId: null,
8383 requestedAt: 0,
8384 tabRequested: false
8385 };
8386 let _store$2 = null;
8387 function getStore$1() {
8388 if (_store$2) {
8389 return _store$2;
8390 }
8391 const w = window;
8392 const factory = w.wp?.desktop?.createSharedStore;
8393 if (typeof factory !== "function") {
8394 return null;
8395 }
8396 _store$2 = factory(
8397 "desktop-mode/user-edit/target",
8398 () => ({ ..._initial$1 })
8399 );
8400 return _store$2;
8401 }
8402 function setUserEditTarget(userId) {
8403 const store2 = getStore$1();
8404 if (store2) {
8405 store2.state.userId = userId;
8406 store2.state.requestedAt = Date.now();
8407 store2.state.tabRequested = true;
8408 store2.notify();
8409 return;
8410 }
8411 const w = window;
8412 w._wpdUserEditTarget = {
8413 userId,
8414 requestedAt: Date.now(),
8415 tabRequested: true
8416 };
8417 }
8418 const pending = /* @__PURE__ */ new Map();
8419 function loadVendorScript(url, extras) {
8420 const existing = pending.get(url);
8421 if (existing) {
8422 return existing;
8423 }
8424 const promise = new Promise((resolve2, reject) => {
8425 const selector = `script[data-desktop-mode-vendor="${cssEscape(url)}"]`;
8426 const preexisting = document.querySelector(selector);
8427 if (preexisting) {
8428 if (preexisting.dataset.loaded === "1") {
8429 resolve2();
8430 return;
8431 }
8432 preexisting.addEventListener("load", () => resolve2(), { once: true });
8433 preexisting.addEventListener(
8434 "error",
8435 () => reject(new Error(`Failed to load ${url}`)),
8436 { once: true }
8437 );
8438 return;
8439 }
8440 if (extras?.translations) {
8441 injectInline(extras.translations);
8442 }
8443 for (const code of extras?.l10n ?? []) {
8444 injectInline(code);
8445 }
8446 for (const code of extras?.before ?? []) {
8447 injectInline(code);
8448 }
8449 const script = document.createElement("script");
8450 script.src = url;
8451 script.async = true;
8452 script.dataset.desktopModeVendor = url;
8453 script.addEventListener(
8454 "load",
8455 () => {
8456 script.dataset.loaded = "1";
8457 for (const code of extras?.after ?? []) {
8458 injectInline(code);
8459 }
8460 resolve2();
8461 },
8462 { once: true }
8463 );
8464 script.addEventListener(
8465 "error",
8466 () => {
8467 pending.delete(url);
8468 script.remove();
8469 reject(new Error(`Failed to load ${url}`));
8470 },
8471 { once: true }
8472 );
8473 document.head.appendChild(script);
8474 });
8475 pending.set(url, promise);
8476 return promise;
8477 }
8478 function injectInline(code) {
8479 if (!code) {
8480 return;
8481 }
8482 const tag = document.createElement("script");
8483 tag.textContent = code;
8484 tag.dataset.desktopModeVendorInline = "1";
8485 document.head.appendChild(tag);
8486 }
8487 function cssEscape(value) {
8488 if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
8489 return CSS.escape(value);
8490 }
8491 return value.replace(/["\\]/g, "\\$&");
8492 }
8493 const registry$7 = /* @__PURE__ */ new Map();
8494 function registerModule(def) {
8495 if (!def || typeof def.id !== "string" || def.id === "") {
8496 if (typeof console !== "undefined") {
8497 console.warn("[desktop-mode] Ignored invalid module registration:", def);
8498 }
8499 return;
8500 }
8501 if (typeof def.url !== "string" || def.url === "") {
8502 if (typeof console !== "undefined") {
8503 console.warn(
8504 `[desktop-mode] Module "${def.id}" has no url; ignored.`
8505 );
8506 }
8507 return;
8508 }
8509 registry$7.set(def.id, def);
8510 }
8511 function moduleIds() {
8512 return Array.from(registry$7.keys());
8513 }
8514 async function loadModules(ids) {
8515 if (!ids || ids.length === 0) {
8516 return;
8517 }
8518 const unknown = ids.filter((id) => !registry$7.has(id));
8519 if (unknown.length > 0) {
8520 throw new Error(
8521 `[desktop-mode] Unknown module(s) in needs: ${unknown.map((id) => `"${id}"`).join(", ")}. Known modules: ${moduleIds().join(", ") || "(none)"}.`
8522 );
8523 }
8524 await Promise.all(
8525 ids.map((id) => {
8526 const def = registry$7.get(id);
8527 if (!def) {
8528 return Promise.resolve();
8529 }
8530 if (def.isReady && def.isReady()) {
8531 return Promise.resolve();
8532 }
8533 return loadVendorScript(def.url);
8534 })
8535 );
8536 }
8537 function createContext(id, pluginUrl) {
8538 return {
8539 id,
8540 pluginUrl,
8541 prefersReducedMotion: prefersReducedMotion(),
8542 visible: !document.hidden
8543 };
8544 }
8545 function prefersReducedMotion() {
8546 if (typeof window.matchMedia !== "function") {
8547 return false;
8548 }
8549 return window.matchMedia("( prefers-reduced-motion: reduce )").matches;
8550 }
8551 class WallpaperLayer {
8552 constructor(element, pluginUrl) {
8553 this.generation = 0;
8554 this.active = null;
8555 this.boundVisibilityChange = () => {
8556 if (!this.active) {
8557 return;
8558 }
8559 doAction(HOOKS.WALLPAPER_VISIBILITY, {
8560 id: this.active.id,
8561 state: document.hidden ? "hidden" : "visible"
8562 });
8563 };
8564 this.element = element;
8565 this.pluginUrl = pluginUrl;
8566 document.addEventListener("visibilitychange", this.boundVisibilityChange);
8567 }
8568 /**
8569 * Apply a wallpaper definition. Safe to call from any event
8570 * handler — handles type dispatch, teardown of the prior active
8571 * canvas, and race-safe async mounts.
8572 */
8573 apply(def) {
8574 const gen = ++this.generation;
8575 this.teardownActive();
8576 if (def.type === "css") {
8577 this.applyCss(def);
8578 return;
8579 }
8580 this.applyCanvas(def, gen);
8581 }
8582 /**
8583 * Imperative teardown entry point — called from desktop.ts on
8584 * `pagehide` so a canvas wallpaper's ticker doesn't compete with
8585 * the session-beacon flush at unload.
8586 */
8587 teardownActive() {
8588 if (!this.active) {
8589 return;
8590 }
8591 const { id, teardown } = this.active;
8592 this.active = null;
8593 doAction(HOOKS.WALLPAPER_UNMOUNTING, { id });
8594 try {
8595 teardown();
8596 } catch (err) {
8597 doAction(HOOKS.SHELL_ERROR, { scope: "wallpaper-teardown", id, error: err });
8598 if (typeof console !== "undefined") {
8599 console.error(
8600 `[desktop-mode] Wallpaper "${id}" teardown threw:`,
8601 err
8602 );
8603 }
8604 }
8605 this.element.innerHTML = "";
8606 }
8607 /** Remove listeners. Not called in normal flow — reserved for tests. */
8608 dispose() {
8609 this.teardownActive();
8610 document.removeEventListener("visibilitychange", this.boundVisibilityChange);
8611 }
8612 applyCss(def) {
8613 const value = def.resolveValue ? def.resolveValue(createContext(def.id, this.pluginUrl)) : def.value;
8614 if (typeof value === "string") {
8615 this.element.style.setProperty("--desktop-mode-bg", value);
8616 const shell = document.getElementById("desktop-mode-shell");
8617 shell?.style.setProperty("--desktop-mode-bg", value);
8618 }
8619 }
8620 applyCanvas(def, gen) {
8621 const ctx = createContext(def.id, this.pluginUrl);
8622 doAction(HOOKS.WALLPAPER_MOUNTING, { id: def.id, container: this.element, ctx });
8623 const depsReady = def.needs && def.needs.length > 0 ? loadModules(def.needs) : Promise.resolve();
8624 const onResolve = (teardown) => {
8625 if (gen !== this.generation) {
8626 try {
8627 teardown();
8628 } catch {
8629 }
8630 return;
8631 }
8632 this.active = { id: def.id, teardown };
8633 doAction(HOOKS.WALLPAPER_MOUNTED, { id: def.id, container: this.element, ctx });
8634 };
8635 depsReady.then(
8636 () => {
8637 if (gen !== this.generation) {
8638 return;
8639 }
8640 let result;
8641 try {
8642 result = def.mount(this.element, ctx);
8643 } catch (err) {
8644 this.handleMountFailure(def.id, err);
8645 return;
8646 }
8647 if (isThenable$1(result)) {
8648 result.then(onResolve, (err) => {
8649 if (gen !== this.generation) {
8650 return;
8651 }
8652 this.handleMountFailure(def.id, err);
8653 });
8654 return;
8655 }
8656 onResolve(result);
8657 },
8658 (err) => {
8659 if (gen !== this.generation) {
8660 return;
8661 }
8662 this.handleMountFailure(def.id, err);
8663 }
8664 );
8665 }
8666 handleMountFailure(id, err) {
8667 this.element.innerHTML = "";
8668 doAction(HOOKS.WALLPAPER_MOUNT_FAILED, { id, error: err });
8669 doAction(HOOKS.SHELL_ERROR, { scope: "wallpaper-mount", id, error: err });
8670 if (typeof console !== "undefined") {
8671 console.error(
8672 `[desktop-mode] Wallpaper "${id}" failed to mount:`,
8673 err
8674 );
8675 }
8676 }
8677 }
8678 function isThenable$1(value) {
8679 return !!value && typeof value === "object" && typeof value.then === "function";
8680 }
8681 function createWallpaperRegistrySync(deps2) {
8682 const { osSettings } = deps2;
8683 const registered = /* @__PURE__ */ new Set();
8684 const loadedScripts = /* @__PURE__ */ new Set();
8685 const ensureScript = async (entry) => {
8686 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
8687 return;
8688 }
8689 try {
8690 await loadVendorScript(entry.scriptUrl, {
8691 translations: entry.scriptTranslations,
8692 l10n: entry.scriptL10n,
8693 before: entry.scriptBefore,
8694 after: entry.scriptAfter
8695 });
8696 } catch (err) {
8697 doAction(HOOKS.SHELL_ERROR, {
8698 scope: "wallpaper-script-load",
8699 id: entry.id,
8700 error: err
8701 });
8702 }
8703 loadedScripts.add(entry.scriptUrl);
8704 };
8705 const readDef = (id) => {
8706 const globals = window.desktopModeWallpapers || {};
8707 return globals[id] ?? null;
8708 };
8709 const defFromCssEntry = (entry) => {
8710 if (entry.type !== "css" || entry.value === "") {
8711 return null;
8712 }
8713 return {
8714 id: entry.id,
8715 label: entry.label,
8716 type: "css",
8717 value: entry.value,
8718 preview: entry.preview !== "" ? entry.preview : entry.value
8719 };
8720 };
8721 const registerEntry = async (entry) => {
8722 if (registered.has(entry.id)) {
8723 return;
8724 }
8725 const cssDef = defFromCssEntry(entry);
8726 if (cssDef) {
8727 register$2(cssDef);
8728 registered.add(entry.id);
8729 osSettings.apply();
8730 return;
8731 }
8732 await ensureScript(entry);
8733 const def = readDef(entry.id);
8734 if (!def) {
8735 doAction(HOOKS.SHELL_ERROR, {
8736 scope: "wallpaper-missing-def",
8737 id: entry.id,
8738 error: new Error(
8739 `[desktop-mode] No wallpaper def on window.desktopModeWallpapers["${entry.id}"]. Script loaded but didn't publish a def — check the plugin's enqueue + global assignment.`
8740 )
8741 });
8742 return;
8743 }
8744 try {
8745 register$2(def);
8746 } catch (err) {
8747 doAction(HOOKS.SHELL_ERROR, {
8748 scope: "wallpaper-register",
8749 id: entry.id,
8750 error: err
8751 });
8752 return;
8753 }
8754 registered.add(entry.id);
8755 osSettings.apply();
8756 };
8757 const unregisterEntry = (id) => {
8758 if (!registered.has(id)) {
8759 return;
8760 }
8761 unregister$2(id);
8762 registered.delete(id);
8763 osSettings.apply();
8764 };
8765 return async (list2) => {
8766 const incoming = /* @__PURE__ */ new Set();
8767 for (const entry of list2) {
8768 incoming.add(entry.id);
8769 }
8770 for (const id of Array.from(registered)) {
8771 if (!incoming.has(id)) {
8772 unregisterEntry(id);
8773 }
8774 }
8775 for (const entry of list2) {
8776 if (!registered.has(entry.id)) {
8777 await registerEntry(entry);
8778 }
8779 }
8780 };
8781 }
8782 const COMMAND_SLUG = /^[a-z0-9_/-]+$/;
8783 const commandRegistryStore = createSharedStore(
8784 "desktop-mode/commands-registry",
8785 () => ({
8786 registry: /* @__PURE__ */ new Map(),
8787 listeners: /* @__PURE__ */ new Set()
8788 })
8789 );
8790 const registry$6 = commandRegistryStore.state.registry;
8791 const listeners$9 = commandRegistryStore.state.listeners;
8792 function registerCommand(cmd) {
8793 const errors = [];
8794 const slug = typeof cmd?.slug === "string" ? cmd.slug.trim().toLowerCase() : "";
8795 if (!cmd || typeof cmd !== "object") {
8796 errors.push("def (not an object)");
8797 } else {
8798 if (typeof cmd.slug !== "string" || cmd.slug.trim() === "") {
8799 errors.push("slug (missing)");
8800 } else if (!COMMAND_SLUG.test(slug)) {
8801 errors.push(
8802 `slug (must match ${COMMAND_SLUG} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
8803 );
8804 }
8805 if (typeof cmd.label !== "string" || cmd.label.trim() === "") {
8806 errors.push("label (missing)");
8807 }
8808 if (typeof cmd.run !== "function") {
8809 errors.push("run (must be a function)");
8810 }
8811 }
8812 throwOnRegistrationErrors("Command", errors, cmd);
8813 registry$6.set(slug, { ...cmd, slug });
8814 notify$b();
8815 }
8816 function unregisterCommand(slug) {
8817 if (registry$6.delete(slug.toLowerCase())) {
8818 notify$b();
8819 }
8820 }
8821 function unregisterByOwner(owner) {
8822 if (!owner) {
8823 return 0;
8824 }
8825 let removed = 0;
8826 for (const [slug, cmd] of Array.from(registry$6.entries())) {
8827 if (cmd.owner === owner) {
8828 registry$6.delete(slug);
8829 removed++;
8830 }
8831 }
8832 if (removed > 0) {
8833 notify$b();
8834 }
8835 return removed;
8836 }
8837 function listCommands() {
8838 return Array.from(registry$6.values());
8839 }
8840 function listAiCallableCommands() {
8841 const out = [];
8842 for (const cmd of registry$6.values()) {
8843 if (cmd.aiCallable !== true) {
8844 continue;
8845 }
8846 out.push({
8847 slug: cmd.slug,
8848 label: cmd.label,
8849 description: cmd.description ?? "",
8850 hint: cmd.hint ?? ""
8851 });
8852 }
8853 return out;
8854 }
8855 function findCommand(slug) {
8856 return registry$6.get(slug.toLowerCase()) ?? null;
8857 }
8858 function notify$b() {
8859 const snapshot = Array.from(listeners$9);
8860 for (const cb of snapshot) {
8861 try {
8862 cb();
8863 } catch (err) {
8864 if (typeof console !== "undefined") {
8865 console.error("[desktop-mode] command-registry listener threw:", err);
8866 }
8867 }
8868 }
8869 }
8870 function createCommandRegistrySync() {
8871 const loadedHandles = /* @__PURE__ */ new Set();
8872 const loadedUrls = /* @__PURE__ */ new Set();
8873 let prevSlugsByHandle = /* @__PURE__ */ new Map();
8874 const ensureScript = async (entry) => {
8875 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
8876 loadedHandles.add(entry.handle);
8877 return;
8878 }
8879 try {
8880 await loadVendorScript(entry.scriptUrl, {
8881 translations: entry.scriptTranslations,
8882 l10n: entry.scriptL10n,
8883 before: entry.scriptBefore,
8884 after: entry.scriptAfter
8885 });
8886 } catch (err) {
8887 doAction(HOOKS.SHELL_ERROR, {
8888 scope: "command-script-load",
8889 handle: entry.handle,
8890 url: entry.scriptUrl,
8891 error: err
8892 });
8893 return;
8894 }
8895 loadedUrls.add(entry.scriptUrl);
8896 loadedHandles.add(entry.handle);
8897 };
8898 const slugsByHandleFrom = (commands) => {
8899 const map = /* @__PURE__ */ new Map();
8900 if (!commands) {
8901 return map;
8902 }
8903 for (const entry of commands) {
8904 if (!entry.scriptHandle || !entry.slug) {
8905 continue;
8906 }
8907 let set = map.get(entry.scriptHandle);
8908 if (!set) {
8909 set = /* @__PURE__ */ new Set();
8910 map.set(entry.scriptHandle, set);
8911 }
8912 set.add(entry.slug);
8913 }
8914 return map;
8915 };
8916 const collectSlugsToRemove = (handle) => {
8917 const slugs = /* @__PURE__ */ new Set();
8918 for (const cmd of listCommands()) {
8919 if (cmd.owner === handle) {
8920 slugs.add(cmd.slug);
8921 }
8922 }
8923 const declared = prevSlugsByHandle.get(handle);
8924 if (declared) {
8925 for (const slug of declared) {
8926 slugs.add(slug);
8927 }
8928 }
8929 return slugs;
8930 };
8931 return async (scripts, commands) => {
8932 const incomingHandles = /* @__PURE__ */ new Set();
8933 for (const entry of scripts) {
8934 if (entry.handle) {
8935 incomingHandles.add(entry.handle);
8936 }
8937 }
8938 for (const handle of Array.from(loadedHandles)) {
8939 if (incomingHandles.has(handle)) {
8940 continue;
8941 }
8942 for (const slug of collectSlugsToRemove(handle)) {
8943 unregisterCommand(slug);
8944 }
8945 loadedHandles.delete(handle);
8946 }
8947 for (const entry of scripts) {
8948 if (!entry.handle || loadedHandles.has(entry.handle)) {
8949 continue;
8950 }
8951 await ensureScript(entry);
8952 }
8953 prevSlugsByHandle = slugsByHandleFrom(commands);
8954 };
8955 }
8956 const store$a = createSharedStore(
8957 "desktop-mode/settings-tab-registry",
8958 () => ({
8959 registry: /* @__PURE__ */ new Map(),
8960 listeners: /* @__PURE__ */ new Set()
8961 })
8962 );
8963 const registry$5 = store$a.state.registry;
8964 const listeners$8 = store$a.state.listeners;
8965 function registerSettingsTab(tab) {
8966 if (!tab || typeof tab.id !== "string" || tab.id.trim() === "") {
8967 return;
8968 }
8969 if (typeof tab.label !== "string" || tab.label.trim() === "") {
8970 return;
8971 }
8972 if (typeof tab.render !== "function") {
8973 return;
8974 }
8975 const id = tab.id.trim().toLowerCase();
8976 if (!/^[a-z0-9_\-]+$/.test(id)) {
8977 if (typeof console !== "undefined") {
8978 console.warn(
8979 "[desktop-mode] registerSettingsTab: id must be [a-z0-9_-]+, got",
8980 tab.id
8981 );
8982 }
8983 return;
8984 }
8985 registry$5.set(id, { ...tab, id });
8986 notify$a();
8987 }
8988 function unregisterSettingsTab(id) {
8989 if (registry$5.delete(id.toLowerCase())) {
8990 notify$a();
8991 }
8992 }
8993 function unregisterSettingsTabsByOwner(owner) {
8994 if (!owner) {
8995 return 0;
8996 }
8997 let removed = 0;
8998 for (const [id, tab] of Array.from(registry$5.entries())) {
8999 if (tab.owner === owner) {
9000 registry$5.delete(id);
9001 removed++;
9002 }
9003 }
9004 if (removed > 0) {
9005 notify$a();
9006 }
9007 return removed;
9008 }
9009 function listSettingsTabs() {
9010 return Array.from(registry$5.values()).sort(
9011 (a, b) => (a.order ?? 100) - (b.order ?? 100)
9012 );
9013 }
9014 function notify$a() {
9015 const snapshot = Array.from(listeners$8);
9016 for (const cb of snapshot) {
9017 try {
9018 cb();
9019 } catch (err) {
9020 if (typeof console !== "undefined") {
9021 console.error(
9022 "[desktop-mode] settings-tab-registry listener threw:",
9023 err
9024 );
9025 }
9026 }
9027 }
9028 }
9029 function createSettingsTabRegistrySync() {
9030 const loadedHandles = /* @__PURE__ */ new Set();
9031 const loadedUrls = /* @__PURE__ */ new Set();
9032 let prevIdsByHandle = /* @__PURE__ */ new Map();
9033 const ensureScript = async (entry) => {
9034 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9035 loadedHandles.add(entry.handle);
9036 return;
9037 }
9038 try {
9039 await loadVendorScript(entry.scriptUrl, {
9040 translations: entry.scriptTranslations,
9041 l10n: entry.scriptL10n,
9042 before: entry.scriptBefore,
9043 after: entry.scriptAfter
9044 });
9045 } catch (err) {
9046 doAction(HOOKS.SHELL_ERROR, {
9047 scope: "settings-tab-script-load",
9048 handle: entry.handle,
9049 url: entry.scriptUrl,
9050 error: err
9051 });
9052 return;
9053 }
9054 loadedUrls.add(entry.scriptUrl);
9055 loadedHandles.add(entry.handle);
9056 };
9057 const idsByHandleFrom = (tabs) => {
9058 const map = /* @__PURE__ */ new Map();
9059 if (!tabs) {
9060 return map;
9061 }
9062 for (const entry of tabs) {
9063 if (!entry.scriptHandle || !entry.id) {
9064 continue;
9065 }
9066 let set = map.get(entry.scriptHandle);
9067 if (!set) {
9068 set = /* @__PURE__ */ new Set();
9069 map.set(entry.scriptHandle, set);
9070 }
9071 set.add(entry.id);
9072 }
9073 return map;
9074 };
9075 const removeByHandle = (handle) => {
9076 unregisterSettingsTabsByOwner(handle);
9077 const declared = prevIdsByHandle.get(handle);
9078 if (declared) {
9079 const present = new Set(
9080 listSettingsTabs().map((t) => t.id)
9081 );
9082 for (const id of declared) {
9083 if (present.has(id)) {
9084 unregisterSettingsTab(id);
9085 }
9086 }
9087 }
9088 };
9089 return async (scripts, tabs) => {
9090 const incomingHandles = /* @__PURE__ */ new Set();
9091 for (const entry of scripts) {
9092 if (entry.handle) {
9093 incomingHandles.add(entry.handle);
9094 }
9095 }
9096 for (const handle of Array.from(loadedHandles)) {
9097 if (incomingHandles.has(handle)) {
9098 continue;
9099 }
9100 removeByHandle(handle);
9101 loadedHandles.delete(handle);
9102 }
9103 for (const entry of scripts) {
9104 if (!entry.handle || loadedHandles.has(entry.handle)) {
9105 continue;
9106 }
9107 await ensureScript(entry);
9108 }
9109 prevIdsByHandle = idsByHandleFrom(tabs);
9110 };
9111 }
9112 const store$9 = createSharedStore(
9113 "desktop-mode/title-bar-buttons-registry",
9114 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9115 );
9116 const registry$4 = store$9.state.registry;
9117 const listeners$7 = store$9.state.listeners;
9118 const TITLE_BAR_BUTTON_ID = /^[a-z0-9_/-]+$/;
9119 function registerTitleBarButton(def) {
9120 const errors = [];
9121 if (!def || typeof def !== "object") {
9122 errors.push("def (not an object)");
9123 } else {
9124 if (typeof def.id !== "string" || def.id.trim() === "") {
9125 errors.push("id (missing)");
9126 } else if (!TITLE_BAR_BUTTON_ID.test(def.id.trim().toLowerCase())) {
9127 errors.push(
9128 `id (must match ${TITLE_BAR_BUTTON_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9129 );
9130 }
9131 if (typeof def.label !== "string" || def.label.trim() === "") {
9132 errors.push("label (missing)");
9133 }
9134 if (typeof def.icon !== "string" || def.icon.trim() === "") {
9135 errors.push("icon (missing)");
9136 }
9137 if (typeof def.match !== "function") {
9138 errors.push("match (must be a function)");
9139 }
9140 if (typeof def.onClick !== "function" && typeof def.render !== "function") {
9141 errors.push("onClick|render (at least one must be a function)");
9142 }
9143 }
9144 throwOnRegistrationErrors("TitleBarButton", errors, def);
9145 const id = def.id.trim().toLowerCase();
9146 registry$4.set(id, { ...def, id });
9147 notify$9();
9148 }
9149 function unregisterTitleBarButton(id) {
9150 if (registry$4.delete(id.toLowerCase())) {
9151 notify$9();
9152 }
9153 }
9154 function unregisterTitleBarButtonsByOwner(owner) {
9155 if (!owner) {
9156 return 0;
9157 }
9158 let removed = 0;
9159 for (const [id, def] of Array.from(registry$4.entries())) {
9160 if (def.owner === owner) {
9161 registry$4.delete(id);
9162 removed++;
9163 }
9164 }
9165 if (removed > 0) {
9166 notify$9();
9167 }
9168 return removed;
9169 }
9170 function listTitleBarButtons() {
9171 return Array.from(registry$4.values()).sort(
9172 (a, b) => (a.order ?? 100) - (b.order ?? 100)
9173 );
9174 }
9175 function notify$9() {
9176 const snapshot = Array.from(listeners$7);
9177 for (const cb of snapshot) {
9178 try {
9179 cb();
9180 } catch (err) {
9181 if (typeof console !== "undefined") {
9182 console.error(
9183 "[desktop-mode] title-bar-button registry listener threw:",
9184 err
9185 );
9186 }
9187 }
9188 }
9189 }
9190 function createTitleBarButtonRegistrySync() {
9191 const loadedHandles = /* @__PURE__ */ new Set();
9192 const loadedUrls = /* @__PURE__ */ new Set();
9193 const ensureScript = async (entry) => {
9194 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9195 loadedHandles.add(entry.handle);
9196 return;
9197 }
9198 try {
9199 await loadVendorScript(entry.scriptUrl, {
9200 translations: entry.scriptTranslations,
9201 l10n: entry.scriptL10n,
9202 before: entry.scriptBefore,
9203 after: entry.scriptAfter
9204 });
9205 } catch (err) {
9206 doAction(HOOKS.SHELL_ERROR, {
9207 scope: "titlebar-button-script-load",
9208 handle: entry.handle,
9209 url: entry.scriptUrl,
9210 error: err
9211 });
9212 return;
9213 }
9214 loadedUrls.add(entry.scriptUrl);
9215 loadedHandles.add(entry.handle);
9216 };
9217 return async (scripts) => {
9218 const incomingHandles = /* @__PURE__ */ new Set();
9219 for (const entry of scripts) {
9220 if (entry.handle) {
9221 incomingHandles.add(entry.handle);
9222 }
9223 }
9224 for (const handle of Array.from(loadedHandles)) {
9225 if (incomingHandles.has(handle)) {
9226 continue;
9227 }
9228 unregisterTitleBarButtonsByOwner(handle);
9229 loadedHandles.delete(handle);
9230 }
9231 for (const entry of scripts) {
9232 if (!entry.handle || loadedHandles.has(entry.handle)) {
9233 continue;
9234 }
9235 await ensureScript(entry);
9236 }
9237 };
9238 }
9239 function createDockRailRendererSync() {
9240 const loadedHandles = /* @__PURE__ */ new Set();
9241 const loadedUrls = /* @__PURE__ */ new Set();
9242 const ensureScript = async (entry) => {
9243 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9244 loadedHandles.add(entry.handle);
9245 return;
9246 }
9247 try {
9248 await loadVendorScript(entry.scriptUrl, {
9249 translations: entry.scriptTranslations,
9250 l10n: entry.scriptL10n,
9251 before: entry.scriptBefore,
9252 after: entry.scriptAfter
9253 });
9254 } catch (err) {
9255 doAction(HOOKS.SHELL_ERROR, {
9256 scope: "dock-rail-renderer-script-load",
9257 handle: entry.handle,
9258 url: entry.scriptUrl,
9259 error: err
9260 });
9261 return;
9262 }
9263 loadedUrls.add(entry.scriptUrl);
9264 loadedHandles.add(entry.handle);
9265 };
9266 return async (scripts) => {
9267 const incomingHandles = /* @__PURE__ */ new Set();
9268 for (const entry of scripts) {
9269 if (entry.handle) {
9270 incomingHandles.add(entry.handle);
9271 }
9272 }
9273 for (const handle of Array.from(loadedHandles)) {
9274 if (incomingHandles.has(handle)) {
9275 continue;
9276 }
9277 unregisterByOwner$1(handle);
9278 loadedHandles.delete(handle);
9279 }
9280 for (const entry of scripts) {
9281 if (!entry.handle || loadedHandles.has(entry.handle)) {
9282 continue;
9283 }
9284 await ensureScript(entry);
9285 }
9286 };
9287 }
9288 const store$8 = createSharedStore(
9289 "desktop-mode/window-themes-registry",
9290 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9291 );
9292 const registry$3 = store$8.state.registry;
9293 const listeners$6 = store$8.state.listeners;
9294 const WINDOW_THEME_ID = /^[a-z0-9_/-]+$/;
9295 function registerWindowTheme(def) {
9296 const errors = [];
9297 if (!def || typeof def !== "object") {
9298 errors.push("def (not an object)");
9299 } else {
9300 if (typeof def.id !== "string" || def.id.trim() === "") {
9301 errors.push("id (missing)");
9302 } else if (!WINDOW_THEME_ID.test(def.id.trim().toLowerCase())) {
9303 errors.push(
9304 `id (must match ${WINDOW_THEME_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9305 );
9306 }
9307 if (!def.tokens || typeof def.tokens !== "object") {
9308 errors.push("tokens (must be an object of CSS custom-property → value)");
9309 } else {
9310 for (const key of Object.keys(def.tokens)) {
9311 if (!key.startsWith("--")) {
9312 errors.push(
9313 `tokens.${key} (CSS custom-property keys must start with "--")`
9314 );
9315 break;
9316 }
9317 }
9318 }
9319 if (typeof def.match !== "function") {
9320 errors.push("match (must be a function)");
9321 }
9322 }
9323 throwOnRegistrationErrors("WindowTheme", errors, def);
9324 const id = def.id.trim().toLowerCase();
9325 registry$3.set(id, { ...def, id });
9326 notify$8();
9327 }
9328 function unregisterWindowTheme(id) {
9329 if (registry$3.delete(id.toLowerCase())) {
9330 notify$8();
9331 }
9332 }
9333 function unregisterWindowThemesByOwner(owner) {
9334 if (!owner) {
9335 return 0;
9336 }
9337 let removed = 0;
9338 for (const [id, def] of Array.from(registry$3.entries())) {
9339 if (def.owner === owner) {
9340 registry$3.delete(id);
9341 removed++;
9342 }
9343 }
9344 if (removed > 0) {
9345 notify$8();
9346 }
9347 return removed;
9348 }
9349 function listWindowThemes() {
9350 return Array.from(registry$3.values()).sort(
9351 (a, b) => (a.priority ?? 100) - (b.priority ?? 100)
9352 );
9353 }
9354 function notify$8() {
9355 const snapshot = Array.from(listeners$6);
9356 for (const cb of snapshot) {
9357 try {
9358 cb();
9359 } catch (err) {
9360 if (typeof console !== "undefined") {
9361 console.error(
9362 "[desktop-mode] window-theme registry listener threw:",
9363 err
9364 );
9365 }
9366 }
9367 }
9368 }
9369 function createWindowThemeRegistrySync() {
9370 const loadedHandles = /* @__PURE__ */ new Set();
9371 const loadedUrls = /* @__PURE__ */ new Set();
9372 let prevIdsByHandle = /* @__PURE__ */ new Map();
9373 const shellRegistered = /* @__PURE__ */ new Set();
9374 const ensureScript = async (entry) => {
9375 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9376 loadedHandles.add(entry.handle);
9377 return;
9378 }
9379 try {
9380 await loadVendorScript(entry.scriptUrl, {
9381 translations: entry.scriptTranslations,
9382 l10n: entry.scriptL10n,
9383 before: entry.scriptBefore,
9384 after: entry.scriptAfter
9385 });
9386 } catch (err) {
9387 doAction(HOOKS.SHELL_ERROR, {
9388 scope: "window-theme-script-load",
9389 handle: entry.handle,
9390 url: entry.scriptUrl,
9391 error: err
9392 });
9393 return;
9394 }
9395 loadedUrls.add(entry.scriptUrl);
9396 loadedHandles.add(entry.handle);
9397 };
9398 const idsByHandleFrom = (themes) => {
9399 const map = /* @__PURE__ */ new Map();
9400 if (!themes) {
9401 return map;
9402 }
9403 for (const entry of themes) {
9404 if (!entry.scriptHandle || !entry.id) {
9405 continue;
9406 }
9407 let set = map.get(entry.scriptHandle);
9408 if (!set) {
9409 set = /* @__PURE__ */ new Set();
9410 map.set(entry.scriptHandle, set);
9411 }
9412 set.add(entry.id);
9413 }
9414 return map;
9415 };
9416 const collectIdsToRemove = (handle) => {
9417 const ids = /* @__PURE__ */ new Set();
9418 for (const def of listWindowThemes()) {
9419 if (def.owner === handle) {
9420 ids.add(def.id);
9421 }
9422 }
9423 const declared = prevIdsByHandle.get(handle);
9424 if (declared) {
9425 for (const id of declared) {
9426 ids.add(id);
9427 }
9428 }
9429 return ids;
9430 };
9431 const applyMetadata = (themes) => {
9432 if (!themes) {
9433 return;
9434 }
9435 for (const entry of themes) {
9436 if (!entry.id || !entry.tokens) {
9437 continue;
9438 }
9439 try {
9440 registerWindowTheme({
9441 id: entry.id,
9442 label: entry.label,
9443 tokens: entry.tokens,
9444 priority: entry.priority,
9445 match: () => true,
9446 owner: entry.scriptHandle || void 0
9447 });
9448 shellRegistered.add(entry.id);
9449 } catch (err) {
9450 doAction(HOOKS.SHELL_ERROR, {
9451 scope: "window-theme-shell-register",
9452 id: entry.id,
9453 error: err
9454 });
9455 }
9456 }
9457 };
9458 return async (scripts, themes) => {
9459 const incomingHandles = /* @__PURE__ */ new Set();
9460 for (const entry of scripts) {
9461 if (entry.handle) {
9462 incomingHandles.add(entry.handle);
9463 }
9464 }
9465 for (const handle of Array.from(loadedHandles)) {
9466 if (incomingHandles.has(handle)) {
9467 continue;
9468 }
9469 const ids = collectIdsToRemove(handle);
9470 for (const id of ids) {
9471 unregisterWindowTheme(id);
9472 shellRegistered.delete(id);
9473 }
9474 unregisterWindowThemesByOwner(handle);
9475 loadedHandles.delete(handle);
9476 }
9477 applyMetadata(themes);
9478 for (const entry of scripts) {
9479 if (!entry.handle || loadedHandles.has(entry.handle)) {
9480 continue;
9481 }
9482 await ensureScript(entry);
9483 }
9484 prevIdsByHandle = idsByHandleFrom(themes);
9485 };
9486 }
9487 const store$7 = createSharedStore(
9488 "desktop-mode/window-controls-registry",
9489 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9490 );
9491 const registry$2 = store$7.state.registry;
9492 const listeners$5 = store$7.state.listeners;
9493 const WINDOW_CONTROL_ID = /^[a-z0-9_/-]+$/;
9494 function registerWindowControl(def) {
9495 const errors = [];
9496 if (!def || typeof def !== "object") {
9497 errors.push("def (not an object)");
9498 } else {
9499 if (typeof def.id !== "string" || def.id.trim() === "") {
9500 errors.push("id (missing)");
9501 } else if (!WINDOW_CONTROL_ID.test(def.id.trim().toLowerCase())) {
9502 errors.push(
9503 `id (must match ${WINDOW_CONTROL_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9504 );
9505 }
9506 if (typeof def.label !== "string" || def.label.trim() === "") {
9507 errors.push("label (missing)");
9508 }
9509 if (typeof def.onClick !== "function" && typeof def.render !== "function") {
9510 errors.push("onClick|render (at least one must be a function)");
9511 }
9512 if (typeof def.render !== "function") {
9513 if (typeof def.icon !== "string" || def.icon.trim() === "") {
9514 errors.push("icon (required when render is omitted)");
9515 }
9516 }
9517 if (typeof def.match !== "function") {
9518 errors.push("match (must be a function)");
9519 }
9520 if (def.placement !== void 0 && def.placement !== "left" && def.placement !== "right" && def.placement !== "controls") {
9521 errors.push('placement (must be "left", "right", or "controls")');
9522 }
9523 }
9524 throwOnRegistrationErrors("WindowControl", errors, def);
9525 const id = def.id.trim().toLowerCase();
9526 registry$2.set(id, { ...def, id });
9527 notify$7();
9528 }
9529 function unregisterWindowControl(id) {
9530 if (registry$2.delete(id.toLowerCase())) {
9531 notify$7();
9532 }
9533 }
9534 function unregisterWindowControlsByOwner(owner) {
9535 if (!owner) {
9536 return 0;
9537 }
9538 let removed = 0;
9539 for (const [id, def] of Array.from(registry$2.entries())) {
9540 if (def.owner === owner) {
9541 registry$2.delete(id);
9542 removed++;
9543 }
9544 }
9545 if (removed > 0) {
9546 notify$7();
9547 }
9548 return removed;
9549 }
9550 function listWindowControls() {
9551 return Array.from(registry$2.values()).sort((a, b) => {
9552 const oa = a.order ?? 100;
9553 const ob = b.order ?? 100;
9554 if (oa !== ob) {
9555 return oa - ob;
9556 }
9557 return a.id.localeCompare(b.id);
9558 });
9559 }
9560 function notify$7() {
9561 const snapshot = Array.from(listeners$5);
9562 for (const cb of snapshot) {
9563 try {
9564 cb();
9565 } catch (err) {
9566 if (typeof console !== "undefined") {
9567 console.error(
9568 "[desktop-mode] window-control registry listener threw:",
9569 err
9570 );
9571 }
9572 }
9573 }
9574 }
9575 function registerBuiltInControls() {
9576 registerWindowControl({
9577 id: "core/minimize",
9578 label: __("Minimize"),
9579 icon: "minimize",
9580 placement: "controls",
9581 order: 10,
9582 core: true,
9583 match: () => true,
9584 onClick: (win) => {
9585 win.minimize();
9586 }
9587 });
9588 registerWindowControl({
9589 id: "core/maximize",
9590 label: __("Maximize"),
9591 icon: "maximize",
9592 placement: "controls",
9593 order: 20,
9594 core: true,
9595 match: () => true,
9596 onClick: (win) => {
9597 win.toggleMaximize();
9598 }
9599 });
9600 registerWindowControl({
9601 id: "core/focus-tab",
9602 label: __("Enter fullscreen"),
9603 icon: "fullscreen",
9604 placement: "controls",
9605 order: 30,
9606 core: true,
9607 match: () => true,
9608 onClick: (win) => {
9609 win.toggleFullscreen();
9610 }
9611 });
9612 registerWindowControl({
9613 id: "core/close",
9614 label: __("Close"),
9615 icon: "close",
9616 placement: "controls",
9617 order: 50,
9618 core: true,
9619 match: () => true,
9620 onClick: (win) => {
9621 win.close();
9622 }
9623 });
9624 }
9625 function createWindowControlRegistrySync() {
9626 const loadedHandles = /* @__PURE__ */ new Set();
9627 const loadedUrls = /* @__PURE__ */ new Set();
9628 let prevIdsByHandle = /* @__PURE__ */ new Map();
9629 const ensureScript = async (entry) => {
9630 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9631 loadedHandles.add(entry.handle);
9632 return;
9633 }
9634 try {
9635 await loadVendorScript(entry.scriptUrl, {
9636 translations: entry.scriptTranslations,
9637 l10n: entry.scriptL10n,
9638 before: entry.scriptBefore,
9639 after: entry.scriptAfter
9640 });
9641 } catch (err) {
9642 doAction(HOOKS.SHELL_ERROR, {
9643 scope: "window-control-script-load",
9644 handle: entry.handle,
9645 url: entry.scriptUrl,
9646 error: err
9647 });
9648 return;
9649 }
9650 loadedUrls.add(entry.scriptUrl);
9651 loadedHandles.add(entry.handle);
9652 };
9653 const idsByHandleFrom = (controls) => {
9654 const map = /* @__PURE__ */ new Map();
9655 if (!controls) {
9656 return map;
9657 }
9658 for (const entry of controls) {
9659 if (!entry.scriptHandle || !entry.id) {
9660 continue;
9661 }
9662 let set = map.get(entry.scriptHandle);
9663 if (!set) {
9664 set = /* @__PURE__ */ new Set();
9665 map.set(entry.scriptHandle, set);
9666 }
9667 set.add(entry.id);
9668 }
9669 return map;
9670 };
9671 const collectIdsToRemove = (handle) => {
9672 const ids = /* @__PURE__ */ new Set();
9673 for (const def of listWindowControls()) {
9674 if (def.owner === handle) {
9675 ids.add(def.id);
9676 }
9677 }
9678 const declared = prevIdsByHandle.get(handle);
9679 if (declared) {
9680 for (const id of declared) {
9681 ids.add(id);
9682 }
9683 }
9684 return ids;
9685 };
9686 return async (scripts, controls) => {
9687 const incomingHandles = /* @__PURE__ */ new Set();
9688 for (const entry of scripts) {
9689 if (entry.handle) {
9690 incomingHandles.add(entry.handle);
9691 }
9692 }
9693 for (const handle of Array.from(loadedHandles)) {
9694 if (incomingHandles.has(handle)) {
9695 continue;
9696 }
9697 for (const id of collectIdsToRemove(handle)) {
9698 unregisterWindowControl(id);
9699 }
9700 unregisterWindowControlsByOwner(handle);
9701 loadedHandles.delete(handle);
9702 }
9703 for (const entry of scripts) {
9704 if (!entry.handle || loadedHandles.has(entry.handle)) {
9705 continue;
9706 }
9707 await ensureScript(entry);
9708 }
9709 prevIdsByHandle = idsByHandleFrom(controls);
9710 };
9711 }
9712 const store$6 = createSharedStore(
9713 "desktop-mode/window-slots-registry",
9714 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9715 );
9716 const registry$1 = store$6.state.registry;
9717 const listeners$4 = store$6.state.listeners;
9718 const WINDOW_SLOT_ID = /^[a-z0-9_/-]+$/;
9719 const KNOWN_SLOTS = /* @__PURE__ */ new Set([
9720 "before-titlebar",
9721 "before-icon",
9722 "icon",
9723 "title",
9724 "after-title",
9725 "before-controls",
9726 "controls",
9727 "after-controls",
9728 "after-titlebar"
9729 ]);
9730 function registerWindowSlot(def) {
9731 const errors = [];
9732 if (!def || typeof def !== "object") {
9733 errors.push("def (not an object)");
9734 } else {
9735 if (typeof def.id !== "string" || def.id.trim() === "") {
9736 errors.push("id (missing)");
9737 } else if (!WINDOW_SLOT_ID.test(def.id.trim().toLowerCase())) {
9738 errors.push(
9739 `id (must match ${WINDOW_SLOT_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9740 );
9741 }
9742 if (typeof def.slot !== "string" || def.slot.trim() === "") {
9743 errors.push("slot (missing)");
9744 } else if (!KNOWN_SLOTS.has(def.slot)) {
9745 errors.push(
9746 `slot (must be one of ${Array.from(KNOWN_SLOTS).join(", ")})`
9747 );
9748 }
9749 if (typeof def.match !== "function") {
9750 errors.push("match (must be a function)");
9751 }
9752 if (typeof def.render !== "function") {
9753 errors.push("render (must be a function)");
9754 }
9755 }
9756 throwOnRegistrationErrors("WindowSlot", errors, def);
9757 const id = def.id.trim().toLowerCase();
9758 registry$1.set(id, { ...def, id });
9759 notify$6();
9760 }
9761 function unregisterWindowSlot(id) {
9762 if (registry$1.delete(id.toLowerCase())) {
9763 notify$6();
9764 }
9765 }
9766 function unregisterWindowSlotsByOwner(owner) {
9767 if (!owner) {
9768 return 0;
9769 }
9770 let removed = 0;
9771 for (const [id, def] of Array.from(registry$1.entries())) {
9772 if (def.owner === owner) {
9773 registry$1.delete(id);
9774 removed++;
9775 }
9776 }
9777 if (removed > 0) {
9778 notify$6();
9779 }
9780 return removed;
9781 }
9782 function listWindowSlots() {
9783 return Array.from(registry$1.values()).sort((a, b) => {
9784 const oa = a.order ?? 100;
9785 const ob = b.order ?? 100;
9786 if (oa !== ob) {
9787 return oa - ob;
9788 }
9789 return a.id.localeCompare(b.id);
9790 });
9791 }
9792 function notify$6() {
9793 const snapshot = Array.from(listeners$4);
9794 for (const cb of snapshot) {
9795 try {
9796 cb();
9797 } catch (err) {
9798 if (typeof console !== "undefined") {
9799 console.error(
9800 "[desktop-mode] window-slot registry listener threw:",
9801 err
9802 );
9803 }
9804 }
9805 }
9806 }
9807 function createWindowSlotRegistrySync() {
9808 const loadedHandles = /* @__PURE__ */ new Set();
9809 const loadedUrls = /* @__PURE__ */ new Set();
9810 let prevIdsByHandle = /* @__PURE__ */ new Map();
9811 const ensureScript = async (entry) => {
9812 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9813 loadedHandles.add(entry.handle);
9814 return;
9815 }
9816 try {
9817 await loadVendorScript(entry.scriptUrl, {
9818 translations: entry.scriptTranslations,
9819 l10n: entry.scriptL10n,
9820 before: entry.scriptBefore,
9821 after: entry.scriptAfter
9822 });
9823 } catch (err) {
9824 doAction(HOOKS.SHELL_ERROR, {
9825 scope: "window-slot-script-load",
9826 handle: entry.handle,
9827 url: entry.scriptUrl,
9828 error: err
9829 });
9830 return;
9831 }
9832 loadedUrls.add(entry.scriptUrl);
9833 loadedHandles.add(entry.handle);
9834 };
9835 const idsByHandleFrom = (slots) => {
9836 const map = /* @__PURE__ */ new Map();
9837 if (!slots) {
9838 return map;
9839 }
9840 for (const entry of slots) {
9841 if (!entry.scriptHandle || !entry.id) {
9842 continue;
9843 }
9844 let set = map.get(entry.scriptHandle);
9845 if (!set) {
9846 set = /* @__PURE__ */ new Set();
9847 map.set(entry.scriptHandle, set);
9848 }
9849 set.add(entry.id);
9850 }
9851 return map;
9852 };
9853 const collectIdsToRemove = (handle) => {
9854 const ids = /* @__PURE__ */ new Set();
9855 for (const def of listWindowSlots()) {
9856 if (def.owner === handle) {
9857 ids.add(def.id);
9858 }
9859 }
9860 const declared = prevIdsByHandle.get(handle);
9861 if (declared) {
9862 for (const id of declared) {
9863 ids.add(id);
9864 }
9865 }
9866 return ids;
9867 };
9868 return async (scripts, slots) => {
9869 const incomingHandles = /* @__PURE__ */ new Set();
9870 for (const entry of scripts) {
9871 if (entry.handle) {
9872 incomingHandles.add(entry.handle);
9873 }
9874 }
9875 for (const handle of Array.from(loadedHandles)) {
9876 if (incomingHandles.has(handle)) {
9877 continue;
9878 }
9879 for (const id of collectIdsToRemove(handle)) {
9880 unregisterWindowSlot(id);
9881 }
9882 unregisterWindowSlotsByOwner(handle);
9883 loadedHandles.delete(handle);
9884 }
9885 for (const entry of scripts) {
9886 if (!entry.handle || loadedHandles.has(entry.handle)) {
9887 continue;
9888 }
9889 await ensureScript(entry);
9890 }
9891 prevIdsByHandle = idsByHandleFrom(slots);
9892 };
9893 }
9894 const KEY_PREFIX = "desktop-mode-notice-dismissed";
9895 function currentUserSuffix() {
9896 const w = window.wp;
9897 const uid = w?.desktop?.config?.currentUserId;
9898 if (typeof uid === "number" && uid > 0) {
9899 return String(uid);
9900 }
9901 return "anon";
9902 }
9903 function storageKey() {
9904 return `${KEY_PREFIX}:${currentUserSuffix()}`;
9905 }
9906 function readMap() {
9907 try {
9908 const raw = window.localStorage.getItem(storageKey());
9909 if (!raw) {
9910 return {};
9911 }
9912 const parsed = JSON.parse(raw);
9913 if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
9914 return parsed;
9915 }
9916 } catch {
9917 }
9918 return {};
9919 }
9920 function writeMap(map) {
9921 try {
9922 window.localStorage.setItem(storageKey(), JSON.stringify(map));
9923 } catch {
9924 }
9925 }
9926 function isNoticeDismissed(id) {
9927 if (!id) {
9928 return false;
9929 }
9930 return readMap()[id] === true;
9931 }
9932 function markNoticeDismissed(id) {
9933 if (!id) {
9934 return;
9935 }
9936 const map = readMap();
9937 map[id] = true;
9938 writeMap(map);
9939 }
9940 function clearNoticeDismissed(id) {
9941 if (!id) {
9942 return;
9943 }
9944 const map = readMap();
9945 if (map[id]) {
9946 delete map[id];
9947 writeMap(map);
9948 }
9949 }
9950 const styles$4 = css`:host{display:flex;align-items:flex-start;gap:10px;width:100%;box-sizing:border-box;padding:10px 14px;font:var( --wpd-notice-font,13px/1.5 var( --desktop-mode-font,system-ui ) );color:var( --wpd-notice-color,var( --desktop-mode-text,#1d2327 ) );background:var( --wpd-notice-bg,rgba( 0,0,0,0.04 ) );border-block-end:1px solid var( --wpd-notice-border,rgba( 0,0,0,0.08 ) );border-inline-start:4px solid var( --wpd-notice-accent,#646970 )}:host( [ hidden ] ){display:none}.wpd-notice__icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;color:var( --wpd-notice-accent,#646970 )}.wpd-notice__icon[ hidden ]{display:none}.wpd-notice__label{flex:1;min-width:0;word-wrap:break-word}::slotted( a ){color:var( --wpd-notice-link,var( --wp-admin-theme-color,#2271b1 ) )}::slotted( p:first-child ){margin-block-start:0}::slotted( p:last-child ){margin-block-end:0}.wpd-notice__close{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;background:transparent;color:inherit;opacity:0.6;cursor:pointer;border-radius:4px;transition:opacity 0.12s ease,background-color 0.12s ease}.wpd-notice__close:hover{opacity:1;background:rgba( 0,0,0,0.06 )}.wpd-notice__close:focus-visible{opacity:1;outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:1px}.wpd-notice__close[ hidden ]{display:none}.wpd-notice__close svg{width:14px;height:14px}:host( [ tone='info' ] ){--wpd-notice-accent:var( --wpd-notice-info,#0969da );--wpd-notice-bg:var( --wpd-notice-info-bg,rgba( 9,105,218,0.08 ) );--wpd-notice-border:var( --wpd-notice-info-border,rgba( 9,105,218,0.16 ) )}:host( [ tone='success' ] ){--wpd-notice-accent:var( --wpd-notice-success,#1a7f37 );--wpd-notice-bg:var( --wpd-notice-success-bg,rgba( 26,127,55,0.08 ) );--wpd-notice-border:var( --wpd-notice-success-border,rgba( 26,127,55,0.16 ) )}:host( [ tone='warning' ] ){--wpd-notice-accent:var( --wpd-notice-warning,#9a6700 );--wpd-notice-bg:var( --wpd-notice-warning-bg,rgba( 154,103,0,0.08 ) );--wpd-notice-border:var( --wpd-notice-warning-border,rgba( 154,103,0,0.16 ) )}:host( [ tone='error' ] ),:host( [ tone='danger' ] ){--wpd-notice-accent:var( --wpd-notice-error,#cf222e );--wpd-notice-bg:var( --wpd-notice-error-bg,rgba( 207,34,46,0.08 ) );--wpd-notice-border:var( --wpd-notice-error-border,rgba( 207,34,46,0.16 ) )}:host( [ tone='neutral' ] ){--wpd-notice-accent:var( --wpd-notice-neutral,#57606a );--wpd-notice-bg:var( --wpd-notice-neutral-bg,rgba( 87,96,106,0.08 ) );--wpd-notice-border:var( --wpd-notice-neutral-border,rgba( 87,96,106,0.16 ) )}`;
9951 const _WpdNotice = class _WpdNotice extends Component {
9952 connectedCallback() {
9953 super.connectedCallback();
9954 if (!this.hasAttribute("role")) {
9955 this.setAttribute("role", "status");
9956 }
9957 if (!this.hasAttribute("tone")) {
9958 this.setAttribute("tone", "info");
9959 }
9960 const id = this.getAttribute("notice-id");
9961 if (id && isNoticeDismissed(id)) {
9962 this.hidden = true;
9963 }
9964 }
9965 /**
9966 * Imperatively dismiss the notice — hides the host and records
9967 * the dismissal in localStorage when `notice-id` is set.
9968 */
9969 dismiss() {
9970 this.hidden = true;
9971 const id = this.getAttribute("notice-id");
9972 if (id) {
9973 markNoticeDismissed(id);
9974 }
9975 this.emit("wpd-notice-dismiss", { noticeId: id ?? void 0 });
9976 }
9977 /**
9978 * Clear a previously recorded dismissal and re-show the notice.
9979 * Useful in tests and for "Show again" affordances.
9980 */
9981 undismiss() {
9982 const id = this.getAttribute("notice-id");
9983 if (id) {
9984 clearNoticeDismissed(id);
9985 }
9986 this.hidden = false;
9987 }
9988 render() {
9989 const icon = this.getAttribute("icon");
9990 const dismissible = !this.hasAttribute("not-dismissible");
9991 return html`
9992 <span
9993 class="wpd-notice__icon dashicons ${icon ?? ""}"
9994 ?hidden=${!icon}
9995 aria-hidden="true"
9996 ></span>
9997 <span class="wpd-notice__label"><slot></slot></span>
9998 <button
9999 type="button"
10000 class="wpd-notice__close"
10001 ?hidden=${!dismissible}
10002 aria-label=${__("Dismiss notice")}
10003 @click=${(e) => this._onDismiss(e)}
10004 >
10005 <svg viewBox="0 0 14 14" aria-hidden="true">
10006 <path
10007 d="M3 3 L11 11 M11 3 L3 11"
10008 stroke="currentColor"
10009 stroke-width="1.6"
10010 stroke-linecap="round"
10011 fill="none"
10012 ></path>
10013 </svg>
10014 </button>
10015 `;
10016 }
10017 _onDismiss(e) {
10018 e.preventDefault();
10019 e.stopPropagation();
10020 this.dismiss();
10021 }
10022 };
10023 _WpdNotice.props = ["tone", "notDismissible", "icon", "noticeId"];
10024 _WpdNotice.styles = [styles$4];
10025 _WpdNotice.help = {
10026 title: "Notice",
10027 summary: "Full-width banner placed inside a window (typically the after-titlebar slot). Tone-coded background + accent stripe, optional close button, optional dashicons leading glyph. Slotted content is HTML — links and basic formatting are supported.",
10028 status: "experimental",
10029 since: "0.22.0",
10030 props: [
10031 {
10032 name: "tone",
10033 type: '"info" | "success" | "warning" | "error" | "danger" | "neutral"',
10034 description: "Color palette. Defaults to `info`. `error` and `danger` are aliases."
10035 },
10036 {
10037 name: "not-dismissible",
10038 type: "boolean",
10039 description: "Suppress the trailing close button. Defaults to dismissible."
10040 },
10041 {
10042 name: "icon",
10043 type: "string",
10044 description: "Optional Dashicons class for a leading glyph (e.g. `dashicons-info`)."
10045 },
10046 {
10047 name: "notice-id",
10048 type: "string",
10049 description: "Persistence key. When set, the notice records its dismissed state in localStorage so it stays closed across reloads for the same user."
10050 }
10051 ],
10052 slots: [
10053 {
10054 name: "(default)",
10055 description: "Message HTML. Links, `<strong>`, `<em>`, and other inline formatting are allowed."
10056 }
10057 ],
10058 events: [
10059 {
10060 name: "wpd-notice-dismiss",
10061 description: "Fires after the user clicks the close button.",
10062 detail: "{ noticeId?: string }"
10063 }
10064 ],
10065 cssProps: [
10066 { name: "--wpd-notice-bg", description: "Background color." },
10067 { name: "--wpd-notice-accent", description: "Left-edge stripe + icon color." },
10068 { name: "--wpd-notice-color", description: "Text color." },
10069 { name: "--wpd-notice-border", description: "Bottom border color." },
10070 { name: "--wpd-notice-link", description: "Color for slotted <a> elements." }
10071 ],
10072 example: html`
10073 <wpd-notice tone="warning" notice-id="docs/example">
10074 Heads up — this is a demo notice.
10075 <a href="#">Learn more</a>.
10076 </wpd-notice>
10077 `
10078 };
10079 let WpdNotice = _WpdNotice;
10080 defineComponent("wpd-notice", WpdNotice);
10081 const store$5 = createSharedStore(
10082 "desktop-mode/window-notices",
10083 () => ({ entries: /* @__PURE__ */ new Map() })
10084 );
10085 const ID_PATTERN = /^[a-z0-9_/-]+$/;
10086 function slotIdFor(id) {
10087 return `desktop-mode-notice/${id.toLowerCase()}`;
10088 }
10089 function buildNoticeElement(entry) {
10090 const el = document.createElement("wpd-notice");
10091 el.setAttribute("tone", entry.tone ?? "info");
10092 el.setAttribute("notice-id", entry.id);
10093 if (entry.dismissible === false) {
10094 el.setAttribute("not-dismissible", "");
10095 }
10096 if (entry.icon) {
10097 el.setAttribute("icon", entry.icon);
10098 }
10099 el.innerHTML = entry.message;
10100 return el;
10101 }
10102 function registerWindowNotice(entry) {
10103 if (!entry || typeof entry !== "object") {
10104 return () => {
10105 };
10106 }
10107 const id = String(entry.id ?? "").trim().toLowerCase();
10108 if (!id || !ID_PATTERN.test(id)) {
10109 return () => {
10110 };
10111 }
10112 if (typeof entry.message !== "string" || entry.message === "") {
10113 return () => {
10114 };
10115 }
10116 const normalised = { ...entry, id };
10117 store$5.state.entries.set(id, normalised);
10118 const slotId = slotIdFor(id);
10119 registerWindowSlot({
10120 id: slotId,
10121 slot: "after-titlebar",
10122 order: normalised.order ?? 100,
10123 // Append rather than clear — every notice slot entry appends
10124 // its own `<wpd-notice>` so multiple notices stack.
10125 replace: false,
10126 owner: normalised.owner,
10127 match: (win) => {
10128 const def = store$5.state.entries.get(id);
10129 if (!def) {
10130 return false;
10131 }
10132 if (typeof def.match !== "function") {
10133 return true;
10134 }
10135 try {
10136 return def.match(win) === true;
10137 } catch {
10138 return false;
10139 }
10140 },
10141 render: (host) => {
10142 const def = store$5.state.entries.get(id);
10143 if (!def) {
10144 return;
10145 }
10146 host.appendChild(buildNoticeElement(def));
10147 }
10148 });
10149 return () => unregisterWindowNotice(id);
10150 }
10151 function unregisterWindowNotice(id) {
10152 const key = String(id ?? "").trim().toLowerCase();
10153 if (!key) {
10154 return;
10155 }
10156 if (store$5.state.entries.delete(key)) {
10157 unregisterWindowSlot(slotIdFor(key));
10158 }
10159 }
10160 function listWindowNotices() {
10161 return Array.from(store$5.state.entries.values()).sort((a, b) => {
10162 const oa = a.order ?? 100;
10163 const ob = b.order ?? 100;
10164 if (oa !== ob) {
10165 return oa - ob;
10166 }
10167 return a.id.localeCompare(b.id);
10168 });
10169 }
10170 function dismissWindowNotice(id) {
10171 const key = String(id ?? "").trim().toLowerCase();
10172 if (!key) {
10173 return;
10174 }
10175 markNoticeDismissed(key);
10176 }
10177 function undismissWindowNotice(id) {
10178 const key = String(id ?? "").trim().toLowerCase();
10179 if (!key) {
10180 return;
10181 }
10182 clearNoticeDismissed(key);
10183 }
10184 function buildMatcher(match) {
10185 if (!match) {
10186 return void 0;
10187 }
10188 const ids = /* @__PURE__ */ new Set();
10189 if (typeof match.window === "string" && match.window !== "") {
10190 ids.add(match.window);
10191 }
10192 if (Array.isArray(match.windows)) {
10193 for (const id of match.windows) {
10194 if (typeof id === "string" && id !== "") {
10195 ids.add(id);
10196 }
10197 }
10198 }
10199 const needle = typeof match.urlContains === "string" && match.urlContains !== "" ? match.urlContains.toLowerCase() : null;
10200 if (ids.size === 0 && needle === null) {
10201 return void 0;
10202 }
10203 return (w) => {
10204 if (ids.size > 0 && !ids.has(w.id)) {
10205 return false;
10206 }
10207 if (needle !== null) {
10208 const url = typeof w.config.url === "string" ? w.config.url.toLowerCase() : "";
10209 if (!url.includes(needle)) {
10210 return false;
10211 }
10212 }
10213 return true;
10214 };
10215 }
10216 function applyServerWindowNotices(entries) {
10217 const wanted = /* @__PURE__ */ new Set();
10218 for (const entry of entries) {
10219 if (!entry || typeof entry.id !== "string" || !entry.id) {
10220 continue;
10221 }
10222 wanted.add(entry.id.toLowerCase());
10223 registerWindowNotice({
10224 id: entry.id,
10225 message: entry.message,
10226 tone: entry.tone,
10227 dismissible: entry.dismissible !== false,
10228 icon: entry.icon,
10229 match: buildMatcher(entry.match),
10230 order: typeof entry.order === "number" ? entry.order : void 0,
10231 // `owner` tag marks every server-shipped notice so a
10232 // targeted cleanup is trivial if/when we surface a sweep
10233 // helper later. Matches the convention used by the
10234 // command / settings-tab sync modules.
10235 owner: "__server__"
10236 });
10237 }
10238 for (const existing of listWindowNotices()) {
10239 if (existing.owner !== "__server__") {
10240 continue;
10241 }
10242 if (!wanted.has(existing.id)) {
10243 unregisterWindowNotice(existing.id);
10244 }
10245 }
10246 }
10247 const store$4 = createSharedStore(
10248 "desktop-mode/window-chrome-registry",
10249 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
10250 );
10251 const registry = store$4.state.registry;
10252 const listeners$3 = store$4.state.listeners;
10253 const WINDOW_CHROME_ID = /^[a-z0-9_/-]+$/;
10254 function registerWindowChrome(def) {
10255 const errors = [];
10256 if (!def || typeof def !== "object") {
10257 errors.push("def (not an object)");
10258 } else {
10259 if (typeof def.id !== "string" || def.id.trim() === "") {
10260 errors.push("id (missing)");
10261 } else if (!WINDOW_CHROME_ID.test(def.id.trim().toLowerCase())) {
10262 errors.push(
10263 `id (must match ${WINDOW_CHROME_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
10264 );
10265 }
10266 if (typeof def.match !== "function") {
10267 errors.push("match (must be a function)");
10268 }
10269 if (typeof def.render !== "function") {
10270 errors.push("render (must be a function)");
10271 }
10272 }
10273 throwOnRegistrationErrors("WindowChrome", errors, def);
10274 const id = def.id.trim().toLowerCase();
10275 registry.set(id, { ...def, id });
10276 notify$5();
10277 }
10278 function unregisterWindowChrome(id) {
10279 if (registry.delete(id.toLowerCase())) {
10280 notify$5();
10281 }
10282 }
10283 function unregisterWindowChromesByOwner(owner) {
10284 if (!owner) {
10285 return 0;
10286 }
10287 let removed = 0;
10288 for (const [id, def] of Array.from(registry.entries())) {
10289 if (def.owner === owner) {
10290 registry.delete(id);
10291 removed++;
10292 }
10293 }
10294 if (removed > 0) {
10295 notify$5();
10296 }
10297 return removed;
10298 }
10299 function listWindowChromes() {
10300 return Array.from(registry.values()).sort(
10301 (a, b) => a.id.localeCompare(b.id)
10302 );
10303 }
10304 function notify$5() {
10305 const snapshot = Array.from(listeners$3);
10306 for (const cb of snapshot) {
10307 try {
10308 cb();
10309 } catch (err) {
10310 if (typeof console !== "undefined") {
10311 console.error(
10312 "[desktop-mode] window-chrome registry listener threw:",
10313 err
10314 );
10315 }
10316 }
10317 }
10318 }
10319 function createWindowChromeRegistrySync() {
10320 const loadedHandles = /* @__PURE__ */ new Set();
10321 const loadedUrls = /* @__PURE__ */ new Set();
10322 let prevIdsByHandle = /* @__PURE__ */ new Map();
10323 const ensureScript = async (entry) => {
10324 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
10325 loadedHandles.add(entry.handle);
10326 return;
10327 }
10328 try {
10329 await loadVendorScript(entry.scriptUrl, {
10330 translations: entry.scriptTranslations,
10331 l10n: entry.scriptL10n,
10332 before: entry.scriptBefore,
10333 after: entry.scriptAfter
10334 });
10335 } catch (err) {
10336 doAction(HOOKS.SHELL_ERROR, {
10337 scope: "window-chrome-script-load",
10338 handle: entry.handle,
10339 url: entry.scriptUrl,
10340 error: err
10341 });
10342 return;
10343 }
10344 loadedUrls.add(entry.scriptUrl);
10345 loadedHandles.add(entry.handle);
10346 };
10347 const idsByHandleFrom = (chromes) => {
10348 const map = /* @__PURE__ */ new Map();
10349 if (!chromes) {
10350 return map;
10351 }
10352 for (const entry of chromes) {
10353 if (!entry.scriptHandle || !entry.id) {
10354 continue;
10355 }
10356 let set = map.get(entry.scriptHandle);
10357 if (!set) {
10358 set = /* @__PURE__ */ new Set();
10359 map.set(entry.scriptHandle, set);
10360 }
10361 set.add(entry.id);
10362 }
10363 return map;
10364 };
10365 const collectIdsToRemove = (handle) => {
10366 const ids = /* @__PURE__ */ new Set();
10367 for (const def of listWindowChromes()) {
10368 if (def.owner === handle) {
10369 ids.add(def.id);
10370 }
10371 }
10372 const declared = prevIdsByHandle.get(handle);
10373 if (declared) {
10374 for (const id of declared) {
10375 ids.add(id);
10376 }
10377 }
10378 return ids;
10379 };
10380 return async (scripts, chromes) => {
10381 const incomingHandles = /* @__PURE__ */ new Set();
10382 for (const entry of scripts) {
10383 if (entry.handle) {
10384 incomingHandles.add(entry.handle);
10385 }
10386 }
10387 for (const handle of Array.from(loadedHandles)) {
10388 if (incomingHandles.has(handle)) {
10389 continue;
10390 }
10391 for (const id of collectIdsToRemove(handle)) {
10392 unregisterWindowChrome(id);
10393 }
10394 unregisterWindowChromesByOwner(handle);
10395 loadedHandles.delete(handle);
10396 }
10397 for (const entry of scripts) {
10398 if (!entry.handle || loadedHandles.has(entry.handle)) {
10399 continue;
10400 }
10401 await ensureScript(entry);
10402 }
10403 prevIdsByHandle = idsByHandleFrom(chromes);
10404 };
10405 }
10406 const INITIAL_ORIGIN$2 = window.location.origin;
10407 let _connSeq = 0;
10408 const _connections = /* @__PURE__ */ new Map();
10409 const _connectionsByTarget = /* @__PURE__ */ new Map();
10410 const _syntheticIframes = /* @__PURE__ */ new Map();
10411 function registerSyntheticIframe(windowId, iframe) {
10412 _syntheticIframes.set(windowId, iframe);
10413 return () => {
10414 if (_syntheticIframes.get(windowId) === iframe) {
10415 _syntheticIframes.delete(windowId);
10416 }
10417 };
10418 }
10419 function nextId() {
10420 return `desktop-mode-conn-${++_connSeq}`;
10421 }
10422 function createConnectionBridge(manager) {
10423 const sendToIframe = (win, message) => {
10424 try {
10425 win.contentWindow?.postMessage(message, INITIAL_ORIGIN$2);
10426 } catch (err) {
10427 if (typeof console !== "undefined") {
10428 console.error(
10429 "[desktop-mode] connection: postMessage failed",
10430 err
10431 );
10432 }
10433 }
10434 };
10435 const connect = (targetWindowId, opts = {}) => {
10436 const id = nextId();
10437 const topics = Array.isArray(opts.topics) ? [...opts.topics] : [];
10438 const subs = /* @__PURE__ */ new Map();
10439 const queue = [];
10440 let isOpen = false;
10441 let destroyed = false;
10442 const targetIframe = () => {
10443 const synth = _syntheticIframes.get(targetWindowId);
10444 if (synth) {
10445 return synth;
10446 }
10447 const w = manager.getById(targetWindowId);
10448 return w?.iframe ?? null;
10449 };
10450 const isNativeTarget = () => {
10451 if (targetIframe()) {
10452 return false;
10453 }
10454 const w = manager.getById(targetWindowId);
10455 return !!w && w.config?.native === true;
10456 };
10457 const nativeSubUnsubs = [];
10458 const flushQueue = () => {
10459 const iframe2 = targetIframe();
10460 if (!iframe2) {
10461 return;
10462 }
10463 while (queue.length) {
10464 const msg = queue.shift();
10465 sendToIframe(iframe2, {
10466 type: "desktop-mode-bridge-publish",
10467 connectionId: id,
10468 topic: msg.topic,
10469 payload: msg.payload
10470 });
10471 }
10472 };
10473 const conn = {
10474 id,
10475 target: targetWindowId,
10476 isOpen: () => isOpen,
10477 subscribe(topic, cb) {
10478 const wrapped = cb;
10479 if (isNativeTarget()) {
10480 const off = addParentSubscriber(
10481 targetWindowId,
10482 topic,
10483 (payload, meta) => {
10484 doAction(HOOKS.CONNECTION_MESSAGE, {
10485 connectionId: id,
10486 topic: meta.channel,
10487 direction: "in"
10488 });
10489 try {
10490 wrapped(payload, { topic: meta.channel });
10491 } catch (err) {
10492 if (typeof console !== "undefined") {
10493 console.error(
10494 "[desktop-mode] connection subscriber threw:",
10495 err
10496 );
10497 }
10498 }
10499 }
10500 );
10501 nativeSubUnsubs.push(off);
10502 return off;
10503 }
10504 let bucket22 = subs.get(topic);
10505 if (!bucket22) {
10506 bucket22 = /* @__PURE__ */ new Set();
10507 subs.set(topic, bucket22);
10508 }
10509 bucket22.add(wrapped);
10510 return () => {
10511 bucket22?.delete(wrapped);
10512 };
10513 },
10514 send(topic, payload) {
10515 if (destroyed) {
10516 return;
10517 }
10518 doAction(HOOKS.CONNECTION_MESSAGE, {
10519 connectionId: id,
10520 topic,
10521 direction: "out"
10522 });
10523 if (isNativeTarget()) {
10524 dispatchToNative(targetWindowId, topic, payload);
10525 return;
10526 }
10527 if (!isOpen) {
10528 queue.push({ topic, payload });
10529 return;
10530 }
10531 const iframe2 = targetIframe();
10532 if (!iframe2) {
10533 return;
10534 }
10535 sendToIframe(iframe2, {
10536 type: "desktop-mode-bridge-publish",
10537 connectionId: id,
10538 topic,
10539 payload
10540 });
10541 },
10542 disconnect() {
10543 conn._destroy("disconnect");
10544 },
10545 _targetWindow: targetIframe,
10546 _handleIframeMessage(data) {
10547 if (!data || typeof data !== "object") {
10548 return;
10549 }
10550 const msg = data;
10551 if (msg.type === "desktop-mode-bridge-handshake-ack") {
10552 if (isOpen) {
10553 return;
10554 }
10555 isOpen = true;
10556 doAction(HOOKS.CONNECTION_OPENED, {
10557 connectionId: id,
10558 targetWindowId,
10559 topics
10560 });
10561 try {
10562 opts.onOpen?.();
10563 } catch (err) {
10564 if (typeof console !== "undefined") {
10565 console.error(
10566 "[desktop-mode] connection.onOpen threw:",
10567 err
10568 );
10569 }
10570 }
10571 flushQueue();
10572 return;
10573 }
10574 if (msg.type === "desktop-mode-bridge-publish") {
10575 const m = data;
10576 const topic = typeof m.topic === "string" ? m.topic : "";
10577 if (!topic) {
10578 return;
10579 }
10580 doAction(HOOKS.CONNECTION_MESSAGE, {
10581 connectionId: id,
10582 topic,
10583 direction: "in"
10584 });
10585 const exact = subs.get(topic);
10586 if (exact) {
10587 for (const cb of Array.from(exact)) {
10588 try {
10589 cb(m.payload, { topic });
10590 } catch (err) {
10591 if (typeof console !== "undefined") {
10592 console.error(
10593 "[desktop-mode] connection subscriber threw:",
10594 err
10595 );
10596 }
10597 }
10598 }
10599 }
10600 const wildcard = subs.get("*");
10601 if (wildcard) {
10602 for (const cb of Array.from(wildcard)) {
10603 try {
10604 cb(m.payload, { topic });
10605 } catch (err) {
10606 if (typeof console !== "undefined") {
10607 console.error(
10608 "[desktop-mode] connection wildcard subscriber threw:",
10609 err
10610 );
10611 }
10612 }
10613 }
10614 }
10615 return;
10616 }
10617 if (msg.type === "desktop-mode-bridge-disconnect") {
10618 conn._destroy("disconnect");
10619 }
10620 },
10621 _destroy(reason) {
10622 if (destroyed) {
10623 return;
10624 }
10625 destroyed = true;
10626 const wasOpen = isOpen;
10627 isOpen = false;
10628 _connections.delete(id);
10629 const targetSet = _connectionsByTarget.get(targetWindowId);
10630 if (targetSet) {
10631 targetSet.delete(id);
10632 if (targetSet.size === 0) {
10633 _connectionsByTarget.delete(targetWindowId);
10634 }
10635 }
10636 for (const off of nativeSubUnsubs.splice(0)) {
10637 try {
10638 off();
10639 } catch {
10640 }
10641 }
10642 if (wasOpen) {
10643 const iframe2 = targetIframe();
10644 if (iframe2) {
10645 sendToIframe(iframe2, {
10646 type: "desktop-mode-bridge-disconnect",
10647 connectionId: id
10648 });
10649 }
10650 }
10651 doAction(HOOKS.CONNECTION_CLOSED, {
10652 connectionId: id,
10653 reason
10654 });
10655 try {
10656 opts.onClose?.(reason);
10657 } catch (err) {
10658 if (typeof console !== "undefined") {
10659 console.error(
10660 "[desktop-mode] connection.onClose threw:",
10661 err
10662 );
10663 }
10664 }
10665 }
10666 };
10667 _connections.set(id, conn);
10668 let bucket2 = _connectionsByTarget.get(targetWindowId);
10669 if (!bucket2) {
10670 bucket2 = /* @__PURE__ */ new Set();
10671 _connectionsByTarget.set(targetWindowId, bucket2);
10672 }
10673 bucket2.add(id);
10674 if (isNativeTarget()) {
10675 Promise.resolve().then(() => {
10676 if (destroyed || isOpen) {
10677 return;
10678 }
10679 isOpen = true;
10680 doAction(HOOKS.CONNECTION_OPENED, {
10681 connectionId: id,
10682 targetWindowId,
10683 topics
10684 });
10685 try {
10686 opts.onOpen?.();
10687 } catch (err) {
10688 if (typeof console !== "undefined") {
10689 console.error(
10690 "[desktop-mode] connection.onOpen threw:",
10691 err
10692 );
10693 }
10694 }
10695 });
10696 return conn;
10697 }
10698 const iframe = targetIframe();
10699 if (iframe) {
10700 sendToIframe(iframe, {
10701 type: "desktop-mode-bridge-handshake",
10702 connectionId: id,
10703 topics
10704 });
10705 }
10706 return conn;
10707 };
10708 const routeIncomingFromIframe = (data, windowId) => {
10709 if (!data || typeof data !== "object") {
10710 return;
10711 }
10712 const msg = data;
10713 if (typeof msg.type !== "string" || !msg.type.startsWith("desktop-mode-bridge-")) {
10714 return;
10715 }
10716 if (msg.type === "desktop-mode-bridge-connection-request" && typeof msg.requestId === "string" && typeof windowId === "string" && windowId !== "") {
10717 handleConnectionRequest(windowId, msg.requestId, Array.isArray(msg.topics) ? msg.topics : []);
10718 return;
10719 }
10720 if (typeof msg.connectionId !== "string") {
10721 return;
10722 }
10723 const conn = _connections.get(msg.connectionId);
10724 conn?._handleIframeMessage(data);
10725 };
10726 const handleConnectionRequest = (windowId, requestId, topics) => {
10727 const synth = _syntheticIframes.get(windowId);
10728 const iframe = synth ?? manager.getById(windowId)?.iframe ?? null;
10729 if (!iframe) {
10730 return;
10731 }
10732 const decision = applyFilters(
10733 HOOKS.IFRAME_CONNECTION_REQUEST,
10734 true,
10735 { windowId, requestId, topics: topics.slice() }
10736 );
10737 if (decision === false) {
10738 try {
10739 iframe.contentWindow?.postMessage({
10740 type: "desktop-mode-bridge-connection-ack",
10741 requestId,
10742 accepted: false,
10743 reason: "rejected"
10744 }, INITIAL_ORIGIN$2);
10745 } catch {
10746 }
10747 return;
10748 }
10749 const finalTopics = decision && typeof decision === "object" && Array.isArray(decision.topics) ? decision.topics : topics;
10750 const conn = connect(windowId, { topics: finalTopics });
10751 try {
10752 iframe.contentWindow?.postMessage({
10753 type: "desktop-mode-bridge-connection-ack",
10754 requestId,
10755 accepted: true,
10756 connectionId: conn.id
10757 }, INITIAL_ORIGIN$2);
10758 } catch {
10759 }
10760 };
10761 const onIframeReady = (windowId) => {
10762 const bucket2 = _connectionsByTarget.get(windowId);
10763 if (!bucket2) {
10764 return;
10765 }
10766 for (const connId of Array.from(bucket2)) {
10767 const conn = _connections.get(connId);
10768 if (!conn || conn.isOpen()) {
10769 continue;
10770 }
10771 const iframe = conn._targetWindow();
10772 if (!iframe) {
10773 continue;
10774 }
10775 sendToIframe(iframe, {
10776 type: "desktop-mode-bridge-handshake",
10777 connectionId: conn.id,
10778 topics: []
10779 // already negotiated client-side; iframe re-uses
10780 });
10781 }
10782 };
10783 const onWindowClosed = (windowId) => {
10784 const bucket2 = _connectionsByTarget.get(windowId);
10785 if (!bucket2) {
10786 return;
10787 }
10788 for (const connId of Array.from(bucket2)) {
10789 const conn = _connections.get(connId);
10790 conn?._destroy("window-closed");
10791 }
10792 };
10793 return { connect, routeIncomingFromIframe, onIframeReady, onWindowClosed };
10794 }
10795 const __vite_import_meta_env__ = {};
10796 function devLog(...args) {
10797 const mode = typeof { url: _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("desktop.js", document.baseURI).href } !== "undefined" && __vite_import_meta_env__ ? "development" : void 0;
10798 if (mode !== "production") {
10799 console.log(...args);
10800 }
10801 }
10802 const OWNER_PREFIX = "iframe:";
10803 function ownerFor(windowId) {
10804 return OWNER_PREFIX + windowId;
10805 }
10806 function iconFor(harvested) {
10807 if (harvested.icon && typeof harvested.icon === "string" && harvested.icon.startsWith("dashicons-")) {
10808 return harvested.icon;
10809 }
10810 return harvested.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
10811 }
10812 function slugFor(windowId, name) {
10813 const safeName = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
10814 const safeWin = windowId.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
10815 return `win-${safeWin}-${safeName}`;
10816 }
10817 class IframeCommandBridge {
10818 constructor(opts) {
10819 this.subscribedWindowId = null;
10820 this.manager = opts.manager;
10821 this.adminUrl = opts.adminUrl;
10822 }
10823 /** Wire up the focus / close / message listeners. Idempotent. */
10824 install() {
10825 document.addEventListener("desktop-mode-window-focused", (e) => {
10826 const detail = e.detail;
10827 if (detail && typeof detail.windowId === "string") {
10828 this.onFocused(detail.windowId);
10829 }
10830 });
10831 document.addEventListener("desktop-mode-window-closed", (e) => {
10832 const detail = e.detail;
10833 if (detail && typeof detail.windowId === "string") {
10834 unregisterByOwner(ownerFor(detail.windowId));
10835 if (this.subscribedWindowId === detail.windowId) {
10836 this.subscribedWindowId = null;
10837 }
10838 }
10839 });
10840 document.addEventListener("desktop-mode-window-changed", (e) => {
10841 const detail = e.detail;
10842 if (!detail || typeof detail.windowId !== "string") {
10843 return;
10844 }
10845 if (detail.reason !== "state") {
10846 return;
10847 }
10848 if (detail.state !== "minimized") {
10849 return;
10850 }
10851 if (this.subscribedWindowId === detail.windowId) {
10852 this.subscribedWindowId = null;
10853 }
10854 });
10855 window.addEventListener("message", (e) => {
10856 if (e.origin !== window.location.origin) {
10857 return;
10858 }
10859 const data = e.data;
10860 if (!data || typeof data.type !== "string") {
10861 return;
10862 }
10863 if (data.type === "desktop-mode-bridge-ready") {
10864 const win2 = this.manager.findByIframeSource(e.source);
10865 if (win2 && win2.id === this.subscribedWindowId) {
10866 this.sendSubscribe(win2.id);
10867 }
10868 return;
10869 }
10870 if (data.type !== "desktop-mode-commands-list") {
10871 return;
10872 }
10873 if (!Array.isArray(data.commands)) {
10874 return;
10875 }
10876 const win = this.manager.findByIframeSource(e.source);
10877 if (!win) {
10878 return;
10879 }
10880 if (win.id !== this.subscribedWindowId) {
10881 return;
10882 }
10883 this.applyList(win.id, data.commands);
10884 });
10885 const focused = this.manager.getFocused();
10886 if (focused) {
10887 this.onFocused(focused.id);
10888 }
10889 }
10890 onFocused(windowId) {
10891 if (this.subscribedWindowId === windowId) {
10892 return;
10893 }
10894 if (this.subscribedWindowId) {
10895 const prev = this.manager.getById(this.subscribedWindowId);
10896 if (prev && prev.iframe && prev.iframe.contentWindow) {
10897 try {
10898 prev.iframe.contentWindow.postMessage(
10899 { type: "desktop-mode-commands-unsubscribe" },
10900 window.location.origin
10901 );
10902 } catch {
10903 }
10904 }
10905 unregisterByOwner(ownerFor(this.subscribedWindowId));
10906 }
10907 this.subscribedWindowId = windowId;
10908 this.sendSubscribe(windowId);
10909 }
10910 sendSubscribe(windowId) {
10911 const win = this.manager.getById(windowId);
10912 if (!win) {
10913 return;
10914 }
10915 if (!win.iframe) {
10916 return;
10917 }
10918 if (!win.iframe.contentWindow) {
10919 return;
10920 }
10921 try {
10922 win.iframe.contentWindow.postMessage(
10923 { type: "desktop-mode-commands-subscribe" },
10924 window.location.origin
10925 );
10926 } catch (err) {
10927 devLog("[wpd-cmd:parent] sendSubscribe: postMessage threw", err);
10928 }
10929 }
10930 applyList(windowId, commands) {
10931 const owner = ownerFor(windowId);
10932 unregisterByOwner(owner);
10933 for (const cmd of commands) {
10934 if (!cmd || !cmd.name || !cmd.label) {
10935 continue;
10936 }
10937 const slug = slugFor(windowId, cmd.name);
10938 const safeSvg = typeof cmd.iconSvg === "string" && cmd.iconSvg !== "" ? sanitizeIconSvg(cmd.iconSvg) : "";
10939 const def = {
10940 slug,
10941 label: cmd.label,
10942 icon: iconFor(cmd),
10943 iconSvg: safeSvg !== "" ? safeSvg : void 0,
10944 owner,
10945 // Harvested commands are contextual by construction —
10946 // they come from whichever window has focus. Surface
10947 // them eagerly so the user sees "Duplicate block" /
10948 // "Toggle distraction free" without having to type `/`
10949 // first.
10950 eager: true,
10951 run: cmd.kind === "navigate" && cmd.url ? this.runNavigate(cmd.url, cmd.label, iconFor(cmd)) : this.runProxy(windowId, cmd.name)
10952 };
10953 try {
10954 registerCommand(def);
10955 } catch (err) {
10956 console.error(
10957 "[desktop-mode] iframe-bridge: dropping bad command",
10958 def,
10959 err
10960 );
10961 }
10962 }
10963 }
10964 runNavigate(url, title, icon) {
10965 return (_args, ctx) => {
10966 ctx.close();
10967 if (tryNativeUrlRemap(url)) {
10968 return;
10969 }
10970 const id = deriveWindowId(url, this.adminUrl);
10971 this.manager.open({ id, baseId: id, url, title, icon });
10972 };
10973 }
10974 runProxy(windowId, name) {
10975 return (_args, ctx) => {
10976 ctx.close();
10977 const win = this.manager.getById(windowId);
10978 if (!win || !win.iframe || !win.iframe.contentWindow) {
10979 return;
10980 }
10981 try {
10982 win.iframe.contentWindow.postMessage(
10983 { type: "desktop-mode-commands-invoke", name },
10984 window.location.origin
10985 );
10986 } catch {
10987 }
10988 this.manager.focus(win);
10989 };
10990 }
10991 }
10992 const OWNER = "global";
10993 const NAV_HREF_LITERAL_RE = /(?:document\.location\.href|window\.location\.href|location\.href)\s*=\s*['"]([^'"$]+?)['"]/;
10994 const NAV_ASSIGN_LITERAL_RE = /(?:document\.location|window\.location|location)\s*=\s*['"]([^'"$]+?)['"]/;
10995 const NAV_CALL_LITERAL_RE = /location\.(?:assign|replace)\s*\(\s*['"]([^'"$]+?)['"]\s*\)/;
10996 const NAV_INTENT_RE = /(?:document\.location|window\.location|location)\s*(?:\.href\s*)?=|location\.(?:assign|replace)\s*\(/;
10997 const SITE_EDITOR_INTENT_RE = /getSiteEditorPage\s*\(|site-editor\.php/;
10998 const SITE_EDITOR_NAME_RE = /^(wp_template_part|wp_template|wp_navigation|wp_block)-(.+)$/;
10999 function lookupMenuCommand(name) {
11000 const list2 = window.__desktopModeMenuCommands;
11001 if (!Array.isArray(list2)) {
11002 return null;
11003 }
11004 for (const entry of list2) {
11005 if (entry && typeof entry === "object" && entry.name === name && typeof entry.url === "string" && entry.url !== "") {
11006 return {
11007 label: typeof entry.label === "string" ? entry.label : "",
11008 url: entry.url
11009 };
11010 }
11011 }
11012 return null;
11013 }
11014 class ShellCommandHarvester {
11015 constructor(opts) {
11016 this.mounted = false;
11017 this.host = null;
11018 this.root = null;
11019 this.kindCache = /* @__PURE__ */ Object.create(null);
11020 this.callbackCache = /* @__PURE__ */ Object.create(null);
11021 this.lastFingerprint = "";
11022 this.manager = opts.manager;
11023 this.adminUrl = opts.adminUrl;
11024 }
11025 /** Mount the harvester. Idempotent. Safe to call before `wp.data` loads. */
11026 install() {
11027 this.tryMount(0);
11028 }
11029 tryMount(attempt) {
11030 if (this.mounted) {
11031 return;
11032 }
11033 const wp = window.wp;
11034 if (!wp || !wp.data || !wp.element || typeof wp.data.subscribe !== "function") {
11035 if (attempt < 40) {
11036 window.setTimeout(() => this.tryMount(attempt + 1), 150);
11037 }
11038 return;
11039 }
11040 this.mount();
11041 }
11042 mount() {
11043 const wp = window.wp;
11044 const el = wp.element;
11045 const data = wp.data;
11046 const createEl = el.createElement;
11047 const useEffect = el.useEffect;
11048 const useRef = el.useRef;
11049 const useMemo = el.useMemo;
11050 const useSelect = data.useSelect;
11051 if (typeof createEl !== "function" || typeof useEffect !== "function" || typeof useRef !== "function" || typeof useMemo !== "function" || typeof useSelect !== "function" || typeof el.createRoot !== "function") {
11052 return;
11053 }
11054 this.mounted = true;
11055 const host = document.createElement("div");
11056 host.setAttribute("aria-hidden", "true");
11057 host.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;left:-9999px;top:-9999px;";
11058 (document.body || document.documentElement).appendChild(host);
11059 this.host = host;
11060 const bucket2 = {
11061 perLoader: {},
11062 statics: [],
11063 loadersList: []
11064 };
11065 const fingerprint2 = (cmds) => {
11066 if (!Array.isArray(cmds) || cmds.length === 0) {
11067 return "";
11068 }
11069 const keys = new Array(cmds.length);
11070 for (let i = 0; i < cmds.length; i++) {
11071 const c = cmds[i];
11072 keys[i] = c && c.name ? c.name : "";
11073 }
11074 return keys.join("|");
11075 };
11076 const mergeAndPublish = () => {
11077 let merged = [];
11078 for (const name of bucket2.loadersList) {
11079 const slice = bucket2.perLoader[name];
11080 if (Array.isArray(slice)) {
11081 merged = merged.concat(slice);
11082 }
11083 }
11084 if (Array.isArray(bucket2.statics)) {
11085 merged = merged.concat(bucket2.statics);
11086 }
11087 this.callbackCache = /* @__PURE__ */ Object.create(null);
11088 for (const cc of merged) {
11089 if (cc && cc.name && typeof cc.callback === "function") {
11090 this.callbackCache[cc.name] = cc.callback;
11091 }
11092 }
11093 this.publish(merged);
11094 };
11095 const LoaderSlot = (props) => {
11096 const loader = props.loader;
11097 let result = null;
11098 try {
11099 result = loader.hook({ search: "" });
11100 } catch {
11101 }
11102 const cmds = result && Array.isArray(result.commands) ? result.commands : [];
11103 const key = useMemo(() => fingerprint2(cmds), [cmds]);
11104 useEffect(() => {
11105 bucket2.perLoader[loader.name] = cmds;
11106 mergeAndPublish();
11107 }, [key]);
11108 useEffect(() => {
11109 return () => {
11110 delete bucket2.perLoader[loader.name];
11111 mergeAndPublish();
11112 };
11113 }, []);
11114 return null;
11115 };
11116 const Harvester = () => {
11117 const loaders = useSelect((s) => {
11118 const ss = s("core/commands");
11119 if (!ss || typeof ss.getCommandLoaders !== "function") {
11120 return [];
11121 }
11122 return [
11123 ...ss.getCommandLoaders(false) || [],
11124 ...ss.getCommandLoaders(true) || []
11125 ];
11126 }, []);
11127 const staticCmds = useSelect((s) => {
11128 const ss = s("core/commands");
11129 if (!ss || typeof ss.getCommands !== "function") {
11130 return [];
11131 }
11132 return [
11133 ...ss.getCommands(false) || [],
11134 ...ss.getCommands(true) || []
11135 ];
11136 }, []);
11137 const loadersNames = useMemo(() => {
11138 return Array.isArray(loaders) ? loaders.map((l) => l ? l.name || "" : "") : [];
11139 }, [loaders]);
11140 const loadersKey = loadersNames.join("|");
11141 useEffect(() => {
11142 bucket2.loadersList = loadersNames;
11143 mergeAndPublish();
11144 }, [loadersKey]);
11145 const staticKey = useMemo(
11146 () => fingerprint2(Array.isArray(staticCmds) ? staticCmds : []),
11147 [staticCmds]
11148 );
11149 useEffect(() => {
11150 bucket2.statics = Array.isArray(staticCmds) ? staticCmds : [];
11151 mergeAndPublish();
11152 }, [staticKey]);
11153 if (!Array.isArray(loaders) || loaders.length === 0) {
11154 return null;
11155 }
11156 const children = [];
11157 for (const loader of loaders) {
11158 if (!loader || typeof loader.hook !== "function") {
11159 continue;
11160 }
11161 children.push(
11162 createEl(LoaderSlot, { key: loader.name, loader })
11163 );
11164 }
11165 return createEl(el.Fragment || "div", null, children);
11166 };
11167 try {
11168 this.root = el.createRoot(host);
11169 this.root.render(createEl(Harvester));
11170 } catch {
11171 this.mounted = false;
11172 this.root = null;
11173 if (this.host && this.host.parentNode) {
11174 this.host.parentNode.removeChild(this.host);
11175 }
11176 this.host = null;
11177 }
11178 }
11179 publish(raw) {
11180 const seen = /* @__PURE__ */ Object.create(null);
11181 const classified = [];
11182 for (const cmd of raw) {
11183 if (!cmd || !cmd.name || !cmd.label) {
11184 continue;
11185 }
11186 if (cmd.disabled) {
11187 continue;
11188 }
11189 if (seen[cmd.name]) {
11190 continue;
11191 }
11192 seen[cmd.name] = true;
11193 classified.push(this.classify(cmd));
11194 }
11195 let key = "";
11196 for (const c of classified) {
11197 key += `${c.name}|${c.kind}|${c.url || ""}
11198 `;
11199 }
11200 if (key === this.lastFingerprint) {
11201 return;
11202 }
11203 this.lastFingerprint = key;
11204 unregisterByOwner(OWNER);
11205 for (const c of classified) {
11206 if (c.kind === "skip") {
11207 continue;
11208 }
11209 const slug = `global-${c.name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-")}`;
11210 const icon = this.iconFor(c);
11211 const def = {
11212 slug,
11213 label: c.label,
11214 icon,
11215 iconSvg: c.iconSvg && c.iconSvg !== "" ? sanitizeIconSvg(c.iconSvg) : void 0,
11216 owner: OWNER,
11217 // NOT eager. The palette splits the registry into two
11218 // disjoint surfaces: `eager` commands show on empty
11219 // input (and are excluded from slash search at
11220 // `src/ai-assistant/impl.ts:494`); non-eager commands
11221 // show when the user types `/<query>`. The WP baseline
11222 // is large (~150 entries) and meant to be searched —
11223 // surfacing it eagerly would drown the iframe-harvested
11224 // contextual shortcuts on every open. Slash-search is
11225 // the right surface for it, matching the native WP
11226 // palette UX (open, type, find).
11227 run: c.kind === "navigate" && c.url ? this.runNavigate(c.url, c.windowTitle || c.label, icon) : this.runInvoke(c.name, c.label, icon)
11228 };
11229 try {
11230 registerCommand(def);
11231 } catch (err) {
11232 console.error(
11233 "[desktop-mode] shell-harvester: dropping bad command",
11234 def,
11235 err
11236 );
11237 }
11238 }
11239 }
11240 classify(cmd) {
11241 const out = {
11242 name: String(cmd.name),
11243 label: String(cmd.label),
11244 icon: typeof cmd.icon === "string" ? cmd.icon : void 0,
11245 iconSvg: void 0,
11246 kind: "action",
11247 url: void 0,
11248 callback: typeof cmd.callback === "function" ? cmd.callback : void 0
11249 };
11250 const cached = this.kindCache[out.name];
11251 if (cached) {
11252 out.kind = cached.kind;
11253 out.url = cached.url;
11254 out.iconSvg = cached.iconSvg;
11255 return out;
11256 }
11257 if (cmd.icon && typeof cmd.icon !== "string") {
11258 out.iconSvg = this.renderIcon(cmd.icon);
11259 }
11260 const menuEntry = lookupMenuCommand(out.name);
11261 if (menuEntry) {
11262 try {
11263 out.url = new URL(menuEntry.url, this.adminUrl).toString();
11264 out.kind = "navigate";
11265 if (menuEntry.label !== "") {
11266 out.windowTitle = menuEntry.label;
11267 }
11268 } catch {
11269 out.kind = "skip";
11270 }
11271 this.kindCache[out.name] = {
11272 kind: out.kind,
11273 url: out.url,
11274 iconSvg: out.iconSvg
11275 };
11276 return out;
11277 }
11278 if (typeof cmd.callback === "function") {
11279 let src = "";
11280 try {
11281 src = Function.prototype.toString.call(cmd.callback);
11282 } catch {
11283 src = "";
11284 }
11285 const literal = src.match(NAV_HREF_LITERAL_RE) || src.match(NAV_ASSIGN_LITERAL_RE) || src.match(NAV_CALL_LITERAL_RE);
11286 if (literal && literal[1]) {
11287 try {
11288 out.url = new URL(literal[1], window.location.href).toString();
11289 out.kind = "navigate";
11290 } catch {
11291 out.kind = "action";
11292 }
11293 } else if (NAV_INTENT_RE.test(src)) {
11294 const isSiteEditorIntent = SITE_EDITOR_INTENT_RE.test(src);
11295 const nameMatch = isSiteEditorIntent ? out.name.match(SITE_EDITOR_NAME_RE) : null;
11296 if (nameMatch) {
11297 const entityType = nameMatch[1];
11298 const entityId = nameMatch[2];
11299 const p = `/${entityType}/${entityId}`;
11300 try {
11301 const siteEditor = new URL("site-editor.php", this.adminUrl);
11302 siteEditor.searchParams.set("p", p);
11303 siteEditor.searchParams.set("canvas", "edit");
11304 out.url = siteEditor.toString();
11305 out.kind = "navigate";
11306 } catch {
11307 out.kind = "skip";
11308 }
11309 } else {
11310 out.kind = "skip";
11311 }
11312 }
11313 }
11314 this.kindCache[out.name] = {
11315 kind: out.kind,
11316 url: out.url,
11317 iconSvg: out.iconSvg
11318 };
11319 return out;
11320 }
11321 renderIcon(icon) {
11322 const wp = window.wp;
11323 if (!wp || !wp.element || typeof wp.element.renderToString !== "function") {
11324 return "";
11325 }
11326 try {
11327 const rendered = wp.element.renderToString(icon);
11328 if (typeof rendered === "string" && rendered.toLowerCase().startsWith("<svg")) {
11329 return rendered;
11330 }
11331 } catch {
11332 }
11333 return "";
11334 }
11335 iconFor(c) {
11336 if (c.icon && c.icon.startsWith("dashicons-")) {
11337 return c.icon;
11338 }
11339 return c.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
11340 }
11341 runNavigate(url, title, icon) {
11342 return (_args, ctx) => {
11343 ctx.close();
11344 if (tryNativeUrlRemap(url)) {
11345 return;
11346 }
11347 const id = deriveWindowId(url, this.adminUrl);
11348 this.manager.open({ id, baseId: id, url, title, icon });
11349 };
11350 }
11351 runInvoke(name, title, icon) {
11352 return (_args, ctx) => {
11353 ctx.close();
11354 const cb = this.callbackCache[name];
11355 if (typeof cb !== "function") {
11356 return;
11357 }
11358 const captured = this.runWithNavCapture(cb);
11359 if (captured) {
11360 const id = deriveWindowId(captured, this.adminUrl);
11361 this.manager.open({ id, baseId: id, url: captured, title, icon });
11362 }
11363 };
11364 }
11365 /**
11366 * Invoke `cb` with navigation sinks (`document.location`,
11367 * `window.location`, `location.assign`, `location.replace`)
11368 * shadowed so any assignment is captured instead of navigating
11369 * the shell. Returns the captured URL or `null` if the callback
11370 * was a pure JS action.
11371 *
11372 * The shadow uses `Object.defineProperty` on the document /
11373 * window instance to override the prototype's accessor for the
11374 * duration of the call. `delete` afterwards unshadows so the
11375 * native setter is restored.
11376 */
11377 runWithNavCapture(cb) {
11378 let captured = null;
11379 const setCaptured = (v) => {
11380 if (captured === null && typeof v === "string" && v !== "") {
11381 captured = v;
11382 }
11383 };
11384 const realLocation = window.location;
11385 const locationProxy = new Proxy(realLocation, {
11386 get(target, prop) {
11387 const value = target[prop];
11388 if (prop === "assign" || prop === "replace") {
11389 return (url) => setCaptured(url);
11390 }
11391 if (typeof value === "function") {
11392 return value.bind(target);
11393 }
11394 return value;
11395 },
11396 set(_target, prop, value) {
11397 if (prop === "href") {
11398 setCaptured(value);
11399 return true;
11400 }
11401 return true;
11402 }
11403 });
11404 const shadowed = [];
11405 const installShadow = (obj) => {
11406 try {
11407 Object.defineProperty(obj, "location", {
11408 configurable: true,
11409 get: () => locationProxy,
11410 set: (v) => setCaptured(v)
11411 });
11412 shadowed.push({ obj, key: "location" });
11413 } catch {
11414 }
11415 };
11416 installShadow(document);
11417 installShadow(window);
11418 try {
11419 cb({ close: () => {
11420 } });
11421 } catch {
11422 } finally {
11423 for (const s of shadowed) {
11424 try {
11425 delete s.obj[s.key];
11426 } catch {
11427 }
11428 }
11429 }
11430 return captured;
11431 }
11432 }
11433 const seed$2 = [];
11434 function register(def) {
11435 throwOnRegistrationErrors(
11436 "Widget",
11437 collectRegistrationErrors(def, WIDGET_CHECKS),
11438 def
11439 );
11440 const idx = seed$2.findIndex((w) => w.id === def.id);
11441 if (idx >= 0) {
11442 seed$2[idx] = def;
11443 } else {
11444 seed$2.push(def);
11445 }
11446 }
11447 function unregister(id) {
11448 const idx = seed$2.findIndex((w) => w.id === id);
11449 if (idx >= 0) {
11450 seed$2.splice(idx, 1);
11451 }
11452 }
11453 function all() {
11454 const copy = seed$2.slice();
11455 const filtered = applyFilters(HOOKS.WIDGETS, copy);
11456 if (!Array.isArray(filtered)) {
11457 if (typeof console !== "undefined") {
11458 console.warn(
11459 "[desktop-mode] `desktop-mode.widgets` filter returned a non-array; falling back to seed list."
11460 );
11461 }
11462 return copy;
11463 }
11464 return filtered.filter(isValidDef);
11465 }
11466 function get(id) {
11467 return all().find((w) => w.id === id);
11468 }
11469 const WIDGET_CHECKS = [
11470 {
11471 field: "id",
11472 message: "missing or not a non-empty string",
11473 valid: (d) => typeof d.id === "string" && d.id !== ""
11474 },
11475 {
11476 field: "label",
11477 message: "missing or not a non-empty string",
11478 valid: (d) => typeof d.label === "string" && d.label !== ""
11479 },
11480 {
11481 field: "description",
11482 message: "not a string",
11483 valid: (d) => typeof d.description === "string"
11484 },
11485 {
11486 field: "icon",
11487 message: "missing or not a non-empty string",
11488 valid: (d) => typeof d.icon === "string" && d.icon !== ""
11489 },
11490 {
11491 field: "mount",
11492 message: "not a function",
11493 valid: (d) => typeof d.mount === "function"
11494 }
11495 ];
11496 function isValidDef(def) {
11497 return collectRegistrationErrors(def, WIDGET_CHECKS).length === 0;
11498 }
11499 let active$2 = null;
11500 function openWidgetPicker(options) {
11501 if (active$2) {
11502 return;
11503 }
11504 const panel2 = document.createElement("div");
11505 panel2.className = "desktop-mode-widget-picker";
11506 panel2.setAttribute("role", "menu");
11507 panel2.setAttribute("aria-label", __("Add widget"));
11508 const title = document.createElement("div");
11509 title.className = "desktop-mode-widget-picker__title";
11510 title.textContent = __("Add widget");
11511 panel2.appendChild(title);
11512 const list2 = document.createElement("div");
11513 list2.className = "desktop-mode-widget-picker__list";
11514 panel2.appendChild(list2);
11515 paintList(list2, options);
11516 document.body.appendChild(panel2);
11517 positionPanel(panel2, options.anchor);
11518 const onOutsidePointerDown = (e) => {
11519 const target = e.target;
11520 if (!target) {
11521 return;
11522 }
11523 if (panel2.contains(target) || options.anchor.contains(target)) {
11524 return;
11525 }
11526 closeWidgetPicker();
11527 };
11528 window.setTimeout(() => {
11529 document.addEventListener("pointerdown", onOutsidePointerDown, true);
11530 }, 0);
11531 const onKeyDown = (e) => {
11532 if (e.key === "Escape") {
11533 closeWidgetPicker();
11534 }
11535 };
11536 document.addEventListener("keydown", onKeyDown);
11537 active$2 = { panel: panel2, options, onOutsidePointerDown, onKeyDown };
11538 const first = list2.querySelector(
11539 "button:not([disabled])"
11540 );
11541 first?.focus();
11542 }
11543 function refreshWidgetPicker() {
11544 if (!active$2) {
11545 return;
11546 }
11547 const list2 = active$2.panel.querySelector(
11548 ".desktop-mode-widget-picker__list"
11549 );
11550 if (list2) {
11551 paintList(list2, active$2.options);
11552 }
11553 }
11554 function closeWidgetPicker() {
11555 if (!active$2) {
11556 return;
11557 }
11558 document.removeEventListener(
11559 "pointerdown",
11560 active$2.onOutsidePointerDown,
11561 true
11562 );
11563 document.removeEventListener("keydown", active$2.onKeyDown);
11564 active$2.panel.remove();
11565 active$2 = null;
11566 }
11567 function paintList(list2, options) {
11568 list2.innerHTML = "";
11569 const enabled = new Set(options.enabledIds());
11570 const defs = options.registry();
11571 if (defs.length === 0) {
11572 const empty = document.createElement("div");
11573 empty.className = "desktop-mode-widget-picker__empty";
11574 empty.textContent = __(
11575 "No widgets available. Activate a plugin that registers one, or see the docs for the registerWidget API."
11576 );
11577 list2.appendChild(empty);
11578 return;
11579 }
11580 for (const def of defs) {
11581 const entry = document.createElement("button");
11582 entry.type = "button";
11583 entry.className = "desktop-mode-widget-picker__entry";
11584 const isAdded = enabled.has(def.id);
11585 if (isAdded) {
11586 entry.classList.add(
11587 "desktop-mode-widget-picker__entry--added"
11588 );
11589 entry.disabled = true;
11590 entry.setAttribute("aria-disabled", "true");
11591 }
11592 entry.setAttribute("role", "menuitem");
11593 let ariaLabel;
11594 if (isAdded) {
11595 ariaLabel = sprintf(__("%s (already added)"), def.label);
11596 } else {
11597 ariaLabel = sprintf(__("Add %s"), def.label);
11598 }
11599 entry.setAttribute("aria-label", ariaLabel);
11600 const icon = document.createElement("span");
11601 icon.className = `desktop-mode-widget-picker__entry-icon dashicons ${def.icon}`;
11602 icon.setAttribute("aria-hidden", "true");
11603 entry.appendChild(icon);
11604 const textWrap = document.createElement("span");
11605 textWrap.className = "desktop-mode-widget-picker__entry-text";
11606 const label = document.createElement("span");
11607 label.className = "desktop-mode-widget-picker__entry-label";
11608 label.textContent = def.label;
11609 textWrap.appendChild(label);
11610 if (def.description) {
11611 const desc = document.createElement("span");
11612 desc.className = "desktop-mode-widget-picker__entry-description";
11613 desc.textContent = def.description;
11614 textWrap.appendChild(desc);
11615 }
11616 entry.appendChild(textWrap);
11617 if (isAdded) {
11618 const status = document.createElement("span");
11619 status.className = "desktop-mode-widget-picker__entry-status";
11620 status.textContent = __("Added");
11621 entry.appendChild(status);
11622 }
11623 if (!isAdded) {
11624 entry.addEventListener("click", (e) => {
11625 e.preventDefault();
11626 e.stopPropagation();
11627 options.onAdd(def.id);
11628 });
11629 }
11630 list2.appendChild(entry);
11631 }
11632 }
11633 function positionPanel(panel2, anchor) {
11634 const rect = anchor.getBoundingClientRect();
11635 panel2.style.position = "fixed";
11636 panel2.style.left = "0px";
11637 panel2.style.top = "0px";
11638 panel2.style.visibility = "hidden";
11639 const panelRect = panel2.getBoundingClientRect();
11640 const width = panelRect.width || 320;
11641 const height = panelRect.height || 200;
11642 const gap = 6;
11643 let left = rect.right - width;
11644 let top = rect.top - height - gap;
11645 if (left < 8) {
11646 left = 8;
11647 }
11648 if (top < 8) {
11649 top = rect.bottom + gap;
11650 }
11651 panel2.style.left = `${Math.round(left)}px`;
11652 panel2.style.top = `${Math.round(top)}px`;
11653 panel2.style.visibility = "";
11654 }
11655 const FLOATING_CLASS = "desktop-mode-widgets__card--floating";
11656 const MOVABLE_CLASS = "desktop-mode-widgets__card--movable";
11657 const RESIZABLE_CLASS = "desktop-mode-widgets__card--resizable";
11658 const DRAGGING_CLASS = "desktop-mode-widgets__card--dragging";
11659 const RESIZING_CLASS = "desktop-mode-widgets__card--resizing";
11660 const DEFAULT_MIN_WIDTH = 160;
11661 const DEFAULT_MIN_HEIGHT = 80;
11662 const DEFAULT_WIDTH = 280;
11663 const DEFAULT_HEIGHT = 180;
11664 const VIEWPORT_MARGIN = 20;
11665 const DRAG_THRESHOLD_PX$1 = 5;
11666 const DRAG_THRESHOLD_SQUARED = DRAG_THRESHOLD_PX$1 * DRAG_THRESHOLD_PX$1;
11667 const DRAG_EXCLUDED_SELECTORS = 'input, textarea, select, button, a, [contenteditable="true"]';
11668 function buildFrame(def, ctx, handlers) {
11669 const card = document.createElement("div");
11670 card.className = "desktop-mode-widgets__card";
11671 card.dataset.widgetId = def.id;
11672 const movable = def.movable === true;
11673 const resizable = def.resizable === true;
11674 if (movable) {
11675 card.classList.add(MOVABLE_CLASS);
11676 }
11677 if (resizable) {
11678 card.classList.add(RESIZABLE_CLASS);
11679 }
11680 if (movable) {
11681 card.appendChild(buildChrome(def, handlers.onRemove, handlers.onRedock));
11682 } else {
11683 card.appendChild(buildCornerClose(def, handlers.onRemove));
11684 }
11685 const body = document.createElement("div");
11686 body.className = "desktop-mode-widgets__card-body";
11687 card.appendChild(body);
11688 let isFloating = false;
11689 if (ctx.geometry) {
11690 applyGeometry(card, ctx.geometry);
11691 card.classList.add(FLOATING_CLASS);
11692 isFloating = true;
11693 }
11694 const resizeCleanups = [];
11695 if (resizable) {
11696 for (const dir of allHandleDirs()) {
11697 const handle = document.createElement("div");
11698 handle.className = `desktop-mode-widgets__resize desktop-mode-widgets__resize--${dir}`;
11699 handle.setAttribute("aria-hidden", "true");
11700 handle.dataset.dir = dir;
11701 card.appendChild(handle);
11702 resizeCleanups.push(
11703 attachResize(card, handle, dir, def, ctx, handlers, () => isFloating)
11704 );
11705 }
11706 }
11707 let dragCleanup = null;
11708 if (movable) {
11709 const chrome = card.querySelector(
11710 ".desktop-mode-widgets__chrome"
11711 );
11712 if (chrome) {
11713 dragCleanup = attachDrag(card, chrome, def, ctx, handlers, (next) => {
11714 isFloating = next;
11715 });
11716 }
11717 }
11718 return {
11719 card,
11720 body,
11721 dispose: () => {
11722 for (const fn of resizeCleanups) {
11723 try {
11724 fn();
11725 } catch {
11726 }
11727 }
11728 if (dragCleanup) {
11729 try {
11730 dragCleanup();
11731 } catch {
11732 }
11733 }
11734 card.remove();
11735 }
11736 };
11737 }
11738 function buildChrome(def, onRemove, onRedock) {
11739 const chrome = document.createElement("header");
11740 chrome.className = "desktop-mode-widgets__chrome";
11741 const grip = document.createElement("span");
11742 grip.className = "desktop-mode-widgets__grip";
11743 grip.setAttribute("aria-hidden", "true");
11744 chrome.appendChild(grip);
11745 const title = document.createElement("span");
11746 title.className = "desktop-mode-widgets__title";
11747 title.textContent = def.label;
11748 chrome.appendChild(title);
11749 chrome.appendChild(buildRedockButton(def, onRedock));
11750 const close = buildCloseButton(def, onRemove);
11751 chrome.appendChild(close);
11752 return chrome;
11753 }
11754 function buildRedockButton(def, onRedock) {
11755 const btn = document.createElement("button");
11756 btn.type = "button";
11757 btn.className = "desktop-mode-widgets__card-redock";
11758 btn.setAttribute(
11759 "aria-label",
11760 // translators: %s is the widget label (e.g., "Clock")
11761 sprintf(__("Dock %s back to widget column"), def.label)
11762 );
11763 btn.innerHTML = '<svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true"><path d="M2 6h6M5.5 3.5L8 6l-2.5 2.5M10 2.5v7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>';
11764 btn.addEventListener("click", (e) => {
11765 e.preventDefault();
11766 e.stopPropagation();
11767 onRedock();
11768 });
11769 btn.dataset.noDrag = "true";
11770 return btn;
11771 }
11772 function buildCornerClose(def, onRemove) {
11773 const close = buildCloseButton(def, onRemove);
11774 close.classList.add("desktop-mode-widgets__card-close--corner");
11775 return close;
11776 }
11777 function buildCloseButton(def, onRemove) {
11778 const close = document.createElement("button");
11779 close.type = "button";
11780 close.className = "desktop-mode-widgets__card-close";
11781 close.setAttribute("aria-label", sprintf(__("Remove %s"), def.label));
11782 close.innerHTML = '<svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true"><path d="M2.5 2.5l7 7M9.5 2.5l-7 7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
11783 close.addEventListener("click", (e) => {
11784 e.preventDefault();
11785 e.stopPropagation();
11786 onRemove();
11787 });
11788 return close;
11789 }
11790 function attachDrag(card, chrome, def, ctx, handlers, setFloating) {
11791 let pointerId = null;
11792 let startX = 0;
11793 let startY = 0;
11794 let initialLeft = 0;
11795 let initialTop = 0;
11796 let committed = false;
11797 const onDown = (e) => {
11798 if (e.button !== 0) {
11799 return;
11800 }
11801 const target = e.target;
11802 if (target && target.closest(DRAG_EXCLUDED_SELECTORS)) {
11803 return;
11804 }
11805 e.preventDefault();
11806 pointerId = e.pointerId;
11807 startX = e.clientX;
11808 startY = e.clientY;
11809 committed = false;
11810 initialLeft = parseFloat(card.style.left) || 0;
11811 initialTop = parseFloat(card.style.top) || 0;
11812 chrome.setPointerCapture(pointerId);
11813 };
11814 const commitDrag = () => {
11815 if (!card.classList.contains(FLOATING_CLASS)) {
11816 const parentRect = ctx.floatingParent.getBoundingClientRect();
11817 const rect = card.getBoundingClientRect();
11818 const initial = {
11819 x: rect.left - parentRect.left,
11820 y: rect.top - parentRect.top,
11821 width: rect.width || def.defaultWidth || DEFAULT_WIDTH,
11822 height: rect.height || def.defaultHeight || DEFAULT_HEIGHT
11823 };
11824 applyGeometry(card, initial);
11825 card.classList.add(FLOATING_CLASS);
11826 setFloating(true);
11827 handlers.onLiberate(initial);
11828 initialLeft = parseFloat(card.style.left) || 0;
11829 initialTop = parseFloat(card.style.top) || 0;
11830 }
11831 card.classList.add(DRAGGING_CLASS);
11832 };
11833 const onMove = (e) => {
11834 if (pointerId === null || e.pointerId !== pointerId) {
11835 return;
11836 }
11837 const dx = e.clientX - startX;
11838 const dy = e.clientY - startY;
11839 if (!committed) {
11840 if (dx * dx + dy * dy < DRAG_THRESHOLD_SQUARED) {
11841 return;
11842 }
11843 committed = true;
11844 commitDrag();
11845 }
11846 const clamped = clampToParent(
11847 initialLeft + dx,
11848 initialTop + dy,
11849 card.offsetWidth,
11850 card.offsetHeight,
11851 ctx.floatingParent
11852 );
11853 card.style.left = `${clamped.x}px`;
11854 card.style.top = `${clamped.y}px`;
11855 };
11856 const onUp = (e) => {
11857 if (pointerId === null || e.pointerId !== pointerId) {
11858 return;
11859 }
11860 try {
11861 chrome.releasePointerCapture(pointerId);
11862 } catch {
11863 }
11864 pointerId = null;
11865 if (!committed) {
11866 return;
11867 }
11868 committed = false;
11869 card.classList.remove(DRAGGING_CLASS);
11870 handlers.onGeometryChanged(currentGeometry(card));
11871 };
11872 chrome.addEventListener("pointerdown", onDown);
11873 chrome.addEventListener("pointermove", onMove);
11874 chrome.addEventListener("pointerup", onUp);
11875 chrome.addEventListener("pointercancel", onUp);
11876 return () => {
11877 chrome.removeEventListener("pointerdown", onDown);
11878 chrome.removeEventListener("pointermove", onMove);
11879 chrome.removeEventListener("pointerup", onUp);
11880 chrome.removeEventListener("pointercancel", onUp);
11881 };
11882 }
11883 function attachResize(card, handle, dir, def, ctx, handlers, isFloating) {
11884 let pointerId = null;
11885 let startX = 0;
11886 let startY = 0;
11887 let startLeft = 0;
11888 let startTop = 0;
11889 let startW = 0;
11890 let startH = 0;
11891 const onDown = (e) => {
11892 if (e.button !== 0) {
11893 return;
11894 }
11895 if (!isFloating() && !isHeightOnlyDir(dir)) {
11896 return;
11897 }
11898 e.preventDefault();
11899 e.stopPropagation();
11900 pointerId = e.pointerId;
11901 startX = e.clientX;
11902 startY = e.clientY;
11903 const rect = card.getBoundingClientRect();
11904 const parentRect = ctx.floatingParent.getBoundingClientRect();
11905 startLeft = rect.left - parentRect.left;
11906 startTop = rect.top - parentRect.top;
11907 startW = rect.width;
11908 startH = rect.height;
11909 handle.setPointerCapture(pointerId);
11910 card.classList.add(RESIZING_CLASS);
11911 };
11912 const onMove = (e) => {
11913 if (pointerId === null || e.pointerId !== pointerId) {
11914 return;
11915 }
11916 const dx = e.clientX - startX;
11917 const dy = e.clientY - startY;
11918 const next = computeResize(
11919 dir,
11920 dx,
11921 dy,
11922 startLeft,
11923 startTop,
11924 startW,
11925 startH,
11926 def,
11927 ctx.floatingParent,
11928 isFloating()
11929 );
11930 if (isFloating()) {
11931 card.style.left = `${next.x}px`;
11932 card.style.top = `${next.y}px`;
11933 card.style.width = `${next.width}px`;
11934 }
11935 card.style.height = `${next.height}px`;
11936 };
11937 const onUp = (e) => {
11938 if (pointerId === null || e.pointerId !== pointerId) {
11939 return;
11940 }
11941 try {
11942 handle.releasePointerCapture(pointerId);
11943 } catch {
11944 }
11945 pointerId = null;
11946 card.classList.remove(RESIZING_CLASS);
11947 handlers.onGeometryChanged(currentGeometry(card));
11948 };
11949 handle.addEventListener("pointerdown", onDown);
11950 handle.addEventListener("pointermove", onMove);
11951 handle.addEventListener("pointerup", onUp);
11952 handle.addEventListener("pointercancel", onUp);
11953 return () => {
11954 handle.removeEventListener("pointerdown", onDown);
11955 handle.removeEventListener("pointermove", onMove);
11956 handle.removeEventListener("pointerup", onUp);
11957 handle.removeEventListener("pointercancel", onUp);
11958 };
11959 }
11960 function allHandleDirs() {
11961 return ["n", "e", "s", "w", "ne", "nw", "se", "sw"];
11962 }
11963 function isHeightOnlyDir(dir) {
11964 return dir === "s";
11965 }
11966 function applyGeometry(card, geometry) {
11967 card.style.left = `${geometry.x}px`;
11968 card.style.top = `${geometry.y}px`;
11969 card.style.width = `${geometry.width}px`;
11970 card.style.height = `${geometry.height}px`;
11971 }
11972 function currentGeometry(card) {
11973 return {
11974 x: parseFloat(card.style.left) || 0,
11975 y: parseFloat(card.style.top) || 0,
11976 width: card.offsetWidth,
11977 height: card.offsetHeight
11978 };
11979 }
11980 function clampToParent(x, y, width, height, parent) {
11981 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
11982 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
11983 const maxX = Math.max(0, parentWidth - width - VIEWPORT_MARGIN);
11984 const maxY = Math.max(0, parentHeight - height - VIEWPORT_MARGIN);
11985 return {
11986 x: Math.min(Math.max(VIEWPORT_MARGIN, x), maxX),
11987 y: Math.min(Math.max(VIEWPORT_MARGIN, y), maxY)
11988 };
11989 }
11990 function computeResize(dir, dx, dy, startLeft, startTop, startW, startH, def, parent, floating) {
11991 const minW = def.minWidth ?? DEFAULT_MIN_WIDTH;
11992 const minH = def.minHeight ?? DEFAULT_MIN_HEIGHT;
11993 const maxW = def.maxWidth ?? Infinity;
11994 const maxH = def.maxHeight ?? Infinity;
11995 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
11996 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
11997 let x = startLeft;
11998 let y = startTop;
11999 let width = startW;
12000 let height = startH;
12001 if (dir === "e" || dir === "ne" || dir === "se") {
12002 width = clamp(startW + dx, minW, Math.min(maxW, parentWidth - startLeft));
12003 }
12004 if (dir === "w" || dir === "nw" || dir === "sw") {
12005 const nextWidth = clamp(startW - dx, minW, Math.min(maxW, startLeft + startW));
12006 x = startLeft + (startW - nextWidth);
12007 width = nextWidth;
12008 }
12009 if (dir === "s" || dir === "se" || dir === "sw") {
12010 height = clamp(
12011 startH + dy,
12012 minH,
12013 Math.min(maxH, parentHeight - startTop)
12014 );
12015 }
12016 if (dir === "n" || dir === "ne" || dir === "nw") {
12017 const nextHeight = clamp(startH - dy, minH, Math.min(maxH, startTop + startH));
12018 y = startTop + (startH - nextHeight);
12019 height = nextHeight;
12020 }
12021 if (!floating) {
12022 width = startW;
12023 x = startLeft;
12024 }
12025 return { x, y, width, height };
12026 }
12027 function clamp(value, min, max) {
12028 if (max < min) {
12029 return min;
12030 }
12031 return Math.min(Math.max(value, min), max);
12032 }
12033 const IDS_KEY = "desktop-mode-widgets";
12034 const GEOMETRY_KEY = "desktop-mode-widgets-geometry";
12035 function readRawEnabled() {
12036 try {
12037 return window.localStorage.getItem(IDS_KEY);
12038 } catch {
12039 return null;
12040 }
12041 }
12042 function loadEnabledIds() {
12043 const raw = readRawEnabled();
12044 if (raw === null) {
12045 return [];
12046 }
12047 try {
12048 const parsed = JSON.parse(raw);
12049 if (!Array.isArray(parsed)) {
12050 return [];
12051 }
12052 return parsed.filter((x) => typeof x === "string");
12053 } catch {
12054 return [];
12055 }
12056 }
12057 function saveEnabledIds(ids) {
12058 try {
12059 window.localStorage.setItem(IDS_KEY, JSON.stringify(ids));
12060 } catch {
12061 }
12062 }
12063 function loadGeometry() {
12064 try {
12065 const raw = window.localStorage.getItem(GEOMETRY_KEY);
12066 if (!raw) {
12067 return {};
12068 }
12069 const parsed = JSON.parse(raw);
12070 if (!parsed || typeof parsed !== "object") {
12071 return {};
12072 }
12073 const out = {};
12074 for (const [id, rawEntry] of Object.entries(parsed)) {
12075 const entry = sanitizeGeometry(rawEntry);
12076 if (entry) {
12077 out[id] = entry;
12078 }
12079 }
12080 return out;
12081 } catch {
12082 return {};
12083 }
12084 }
12085 function saveGeometry(geometry) {
12086 try {
12087 window.localStorage.setItem(GEOMETRY_KEY, JSON.stringify(geometry));
12088 } catch {
12089 }
12090 }
12091 function sanitizeGeometry(raw) {
12092 if (!raw || typeof raw !== "object") {
12093 return null;
12094 }
12095 const { x, y, width, height } = raw;
12096 if (typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y) || typeof width !== "number" || !Number.isFinite(width) || width <= 0 || typeof height !== "number" || !Number.isFinite(height) || height <= 0) {
12097 return null;
12098 }
12099 return { x, y, width, height };
12100 }
12101 function createWidgetStorage(widgetId) {
12102 const prefix = `desktop-mode.widget.${widgetId}.`;
12103 const safeGet = (key) => {
12104 try {
12105 return localStorage.getItem(prefix + key);
12106 } catch {
12107 return null;
12108 }
12109 };
12110 return {
12111 get(key) {
12112 const raw = safeGet(key);
12113 if (raw === null) {
12114 return null;
12115 }
12116 try {
12117 return JSON.parse(raw);
12118 } catch {
12119 return null;
12120 }
12121 },
12122 set(key, value) {
12123 try {
12124 localStorage.setItem(prefix + key, JSON.stringify(value));
12125 } catch {
12126 }
12127 },
12128 remove(key) {
12129 try {
12130 localStorage.removeItem(prefix + key);
12131 } catch {
12132 }
12133 },
12134 clear() {
12135 try {
12136 for (let i = localStorage.length - 1; i >= 0; i--) {
12137 const key = localStorage.key(i);
12138 if (key && key.startsWith(prefix)) {
12139 localStorage.removeItem(key);
12140 }
12141 }
12142 } catch {
12143 }
12144 }
12145 };
12146 }
12147 const DEFAULT_ENABLED_IDS = ["clock"];
12148 class WidgetLayer {
12149 /**
12150 * @param root The column element (`#desktop-mode-widgets`).
12151 * @param pluginUrl Absolute plugin URL — passed to widget ctx.
12152 * @param floatingHost Parent for liberated (floating) widgets.
12153 * Defaults to the column's parent (the desktop
12154 * area) so floats are bounded by the visible
12155 * desktop, not the 320 px-wide column.
12156 */
12157 constructor(root, pluginUrl, floatingHost) {
12158 this.mounted = /* @__PURE__ */ new Map();
12159 this.generation = 0;
12160 this.root = root;
12161 this.pluginUrl = pluginUrl;
12162 this.enabledIds = loadEnabledIds();
12163 this.geometry = loadGeometry();
12164 this.floatingHost = floatingHost ?? root.parentElement ?? root;
12165 this.listEl = document.createElement("div");
12166 this.listEl.className = "desktop-mode-widgets__list";
12167 this.root.appendChild(this.listEl);
12168 this.addTile = this.buildAddTile();
12169 this.root.appendChild(this.addTile);
12170 this.paintEmptyState();
12171 }
12172 /**
12173 * Mount every widget the user has enabled (per localStorage).
12174 * Called once during shell boot, AFTER the registry seed has run
12175 * so built-ins are available. Safe to call multiple times — the
12176 * `mounted` map dedupes.
12177 */
12178 hydrate() {
12179 if (readRawEnabled() === null) {
12180 this.enabledIds = DEFAULT_ENABLED_IDS.filter(
12181 (id) => !!get(id)
12182 );
12183 saveEnabledIds(this.enabledIds);
12184 }
12185 for (const id of this.enabledIds) {
12186 if (this.mounted.has(id)) {
12187 continue;
12188 }
12189 this.mountById(id);
12190 }
12191 this.paintEmptyState();
12192 }
12193 /**
12194 * Add a widget by id — called by the picker after the user
12195 * selects an available entry. Idempotent.
12196 */
12197 add(id) {
12198 if (this.enabledIds.includes(id)) {
12199 return;
12200 }
12201 if (!get(id)) {
12202 return;
12203 }
12204 this.enabledIds.push(id);
12205 saveEnabledIds(this.enabledIds);
12206 this.mountById(id);
12207 this.paintEmptyState();
12208 doAction(HOOKS.WIDGET_ADDED, { id });
12209 refreshWidgetPicker();
12210 }
12211 /**
12212 * Remove a widget by id — called from the card's × button and
12213 * from the picker. Idempotent.
12214 */
12215 remove(id) {
12216 const before = this.enabledIds.length;
12217 this.enabledIds = this.enabledIds.filter((e) => e !== id);
12218 if (this.enabledIds.length === before) {
12219 return;
12220 }
12221 saveEnabledIds(this.enabledIds);
12222 if (this.geometry[id]) {
12223 delete this.geometry[id];
12224 saveGeometry(this.geometry);
12225 }
12226 this.unmountById(id);
12227 this.paintEmptyState();
12228 doAction(HOOKS.WIDGET_REMOVED, { id });
12229 refreshWidgetPicker();
12230 }
12231 /** Public read for the picker / external callers. */
12232 getEnabledIds() {
12233 return [...this.enabledIds];
12234 }
12235 /**
12236 * Mount a widget ONLY if it's already in the user's enabled
12237 * list AND not currently mounted. No-op when the widget isn't
12238 * enabled (user never opted in) and no-op when it's already on
12239 * screen. Used by the server-driven sync: when a plugin
12240 * activates mid-session, its widget def registers via the
12241 * sync's path; if the user had previously enabled that widget
12242 * (in a prior session or before the plugin was deactivated),
12243 * we want to bring it back on screen without toggling the
12244 * "enabled" state or firing a `WIDGET_ADDED` action.
12245 *
12246 * The net behaviour is "rehydrate this one widget now that
12247 * its def is finally registered," which is subtly different
12248 * from `ensureMounted` (which OPT-INs the user into enabling
12249 * the widget for the first time).
12250 */
12251 mountIfEnabled(id) {
12252 if (!get(id)) {
12253 return;
12254 }
12255 if (!this.enabledIds.includes(id)) {
12256 return;
12257 }
12258 if (this.mounted.has(id)) {
12259 return;
12260 }
12261 this.mountById(id);
12262 this.paintEmptyState();
12263 }
12264 /**
12265 * Unmount a widget without touching the persisted enablement.
12266 * Used by the server-driven widget-registry sync: when a plugin
12267 * deactivates mid-session, its widget defs disappear from the
12268 * registry and we need to pull any mounted instance off the
12269 * screen — but we deliberately KEEP the id in the user's
12270 * enabled list so re-activating the plugin re-mounts it
12271 * automatically through `hydrate()`.
12272 *
12273 * Idempotent; a no-op when the widget isn't currently mounted.
12274 */
12275 unmount(id) {
12276 if (!this.mounted.has(id)) {
12277 return;
12278 }
12279 this.unmountById(id);
12280 this.paintEmptyState();
12281 }
12282 /**
12283 * Guarantee the widget identified by `id` is currently mounted,
12284 * adding it to the enabled list if it isn't. No-op when the
12285 * widget is already on screen. Intended for companion plugins
12286 * that want to pin their widget programmatically — a monitor
12287 * plugin that auto-pins itself on the first error burst, a
12288 * first-run onboarding flow that ensures the quick-start widget
12289 * is present, etc.
12290 *
12291 * Returns `true` when the widget is mounted (either newly added
12292 * or already present), `false` when the id isn't registered —
12293 * callers can branch on the failure without having to maintain
12294 * their own registry snapshot.
12295 */
12296 ensureMounted(id) {
12297 if (!get(id)) {
12298 return false;
12299 }
12300 if (this.enabledIds.includes(id)) {
12301 return true;
12302 }
12303 this.add(id);
12304 return true;
12305 }
12306 /**
12307 * Tear down every widget. Called on shell unload via `pagehide`
12308 * so intervals / RAF loops stop before the beacon flush.
12309 */
12310 disposeAll() {
12311 for (const id of Array.from(this.mounted.keys())) {
12312 this.unmountById(id);
12313 }
12314 }
12315 // --- Internal ---------------------------------------------------
12316 mountById(id) {
12317 const def = get(id);
12318 if (!def) {
12319 return;
12320 }
12321 const gen = ++this.generation;
12322 const initialGeometry = def.movable === true ? this.geometry[id] : void 0;
12323 const frame = buildFrame(
12324 def,
12325 { floatingParent: this.floatingHost, geometry: initialGeometry },
12326 {
12327 onRemove: () => this.remove(id),
12328 onGeometryChanged: (geom) => this.persistGeometry(id, geom),
12329 onLiberate: (geom) => this.liberate(id, geom),
12330 onRedock: () => this.redock(id)
12331 }
12332 );
12333 const floating = !!initialGeometry;
12334 const record = {
12335 id,
12336 frame,
12337 generation: gen,
12338 teardown: null,
12339 floating
12340 };
12341 this.mounted.set(id, record);
12342 this.placeCard(frame.card, floating);
12343 const ctx = {
12344 id,
12345 pluginUrl: this.pluginUrl,
12346 storage: createWidgetStorage(id)
12347 };
12348 doAction(HOOKS.WIDGET_MOUNTING, { id, container: frame.body, ctx });
12349 const onResolve = (teardown) => {
12350 const current = this.mounted.get(id);
12351 if (!current || current.generation !== gen) {
12352 try {
12353 teardown();
12354 } catch {
12355 }
12356 return;
12357 }
12358 current.teardown = teardown;
12359 doAction(HOOKS.WIDGET_MOUNTED, { id, container: frame.body, ctx });
12360 };
12361 let result;
12362 try {
12363 result = def.mount(frame.body, ctx);
12364 } catch (err) {
12365 this.handleMountFailure(id, err);
12366 return;
12367 }
12368 if (isThenable(result)) {
12369 result.then(onResolve, (err) => {
12370 if (this.mounted.get(id)?.generation === gen) {
12371 this.handleMountFailure(id, err);
12372 }
12373 });
12374 return;
12375 }
12376 onResolve(result);
12377 }
12378 unmountById(id) {
12379 const record = this.mounted.get(id);
12380 if (!record) {
12381 return;
12382 }
12383 doAction(HOOKS.WIDGET_UNMOUNTING, { id });
12384 try {
12385 record.teardown?.();
12386 } catch (err) {
12387 doAction(HOOKS.SHELL_ERROR, { scope: "widget-teardown", id, error: err });
12388 if (typeof console !== "undefined") {
12389 console.error(
12390 `[desktop-mode] Widget "${id}" teardown threw:`,
12391 err
12392 );
12393 }
12394 }
12395 this.generation++;
12396 record.frame.dispose();
12397 this.mounted.delete(id);
12398 }
12399 handleMountFailure(id, err) {
12400 const record = this.mounted.get(id);
12401 if (record) {
12402 record.frame.dispose();
12403 this.mounted.delete(id);
12404 }
12405 doAction(HOOKS.WIDGET_MOUNT_FAILED, { id, error: err });
12406 doAction(HOOKS.SHELL_ERROR, { scope: "widget-mount", id, error: err });
12407 if (typeof console !== "undefined") {
12408 console.error(
12409 `[desktop-mode] Widget "${id}" failed to mount:`,
12410 err
12411 );
12412 }
12413 }
12414 buildAddTile() {
12415 const tile2 = document.createElement("button");
12416 tile2.type = "button";
12417 tile2.className = "desktop-mode-widgets__add";
12418 tile2.setAttribute("aria-label", __("Add widget"));
12419 const plus = document.createElement("span");
12420 plus.className = "desktop-mode-widgets__add-plus";
12421 plus.setAttribute("aria-hidden", "true");
12422 plus.textContent = "+";
12423 const label = document.createElement("span");
12424 label.className = "desktop-mode-widgets__add-label";
12425 label.textContent = __("Add widget");
12426 tile2.appendChild(plus);
12427 tile2.appendChild(label);
12428 tile2.addEventListener("click", (e) => {
12429 e.preventDefault();
12430 e.stopPropagation();
12431 openWidgetPicker({
12432 anchor: tile2,
12433 registry: () => all(),
12434 enabledIds: () => [...this.enabledIds],
12435 onAdd: (id) => this.add(id)
12436 });
12437 });
12438 return tile2;
12439 }
12440 /**
12441 * Drop a card into the right parent based on its floating state.
12442 * Docked cards append to the column list above the `+` tile;
12443 * floating cards append to the desktop-area-level host so they
12444 * sit above the wallpaper and can range across the viewport.
12445 */
12446 placeCard(card, floating) {
12447 if (floating) {
12448 this.floatingHost.appendChild(card);
12449 } else {
12450 this.listEl.appendChild(card);
12451 }
12452 }
12453 /**
12454 * Move a widget from the column into the floating host. Called by
12455 * the frame on the user's first drag of a movable widget.
12456 */
12457 liberate(id, geometry) {
12458 const record = this.mounted.get(id);
12459 if (!record || record.floating) {
12460 return;
12461 }
12462 record.floating = true;
12463 this.floatingHost.appendChild(record.frame.card);
12464 applyGeometry(record.frame.card, geometry);
12465 this.persistGeometry(id, geometry);
12466 this.paintEmptyState();
12467 }
12468 /**
12469 * Inverse of {@link liberate}: move a floating card back into
12470 * the column and drop its persisted geometry so a subsequent
12471 * shell boot brings it up docked. Called when the user clicks
12472 * the re-dock button in the card's chrome header, or
12473 * programmatically by companion plugins via
12474 * `wp.desktop.widgets.redock( id )` /
12475 * `wp.desktop.widgetLayer.redock( id )`.
12476 *
12477 * Idempotent — a docked widget silently no-ops, an unknown id
12478 * silently no-ops. The `--floating` class on the card is
12479 * removed as part of the same write so CSS rules that depend
12480 * on it (re-dock button visibility, absolute positioning) flip
12481 * back in one paint.
12482 *
12483 * @since 0.7.0 (private)
12484 * @since 0.25.0 (public)
12485 */
12486 redock(id) {
12487 const record = this.mounted.get(id);
12488 if (!record || !record.floating) {
12489 return;
12490 }
12491 record.floating = false;
12492 if (this.geometry[id]) {
12493 delete this.geometry[id];
12494 saveGeometry(this.geometry);
12495 }
12496 const card = record.frame.card;
12497 card.classList.remove("desktop-mode-widgets__card--floating");
12498 card.style.left = "";
12499 card.style.top = "";
12500 card.style.width = "";
12501 card.style.height = "";
12502 this.listEl.appendChild(card);
12503 this.paintEmptyState();
12504 }
12505 persistGeometry(id, geometry) {
12506 this.geometry[id] = geometry;
12507 saveGeometry(this.geometry);
12508 }
12509 /**
12510 * Toggle a `--has-widgets` modifier so CSS can hide the column's
12511 * decorative backdrop when nothing's mounted (keeps the empty
12512 * state clean — just the `+` tile floating in the corner).
12513 *
12514 * Floating widgets don't count toward "has widgets" in the column
12515 * sense — if every enabled widget is floating, the column itself
12516 * shows only the empty state + add tile.
12517 */
12518 paintEmptyState() {
12519 let docked = 0;
12520 for (const record of this.mounted.values()) {
12521 if (!record.floating) {
12522 docked++;
12523 }
12524 }
12525 this.root.classList.toggle(
12526 "desktop-mode-widgets--has-widgets",
12527 docked > 0
12528 );
12529 }
12530 }
12531 function isThenable(x) {
12532 return !!x && (typeof x === "object" || typeof x === "function") && typeof x.then === "function";
12533 }
12534 const DEFAULT_NATIVE_MIN_WIDTH = 280;
12535 const DEFAULT_NATIVE_MIN_HEIGHT = 220;
12536 const DEFAULT_NATIVE_WIDTH = 520;
12537 const DEFAULT_NATIVE_HEIGHT = 400;
12538 function buildIframeContentRender(cfg, cleanups, windowId) {
12539 return (body) => {
12540 const iframe = document.createElement("iframe");
12541 iframe.style.width = "100%";
12542 iframe.style.height = "100%";
12543 iframe.style.border = "0";
12544 iframe.setAttribute("src", cfg.url);
12545 if (typeof cfg.sandbox === "string" && cfg.sandbox !== "") {
12546 iframe.setAttribute("sandbox", cfg.sandbox);
12547 }
12548 body.style.padding = "0";
12549 body.appendChild(iframe);
12550 const unregisterSynth = registerSyntheticIframe(windowId, iframe);
12551 cleanups.push(unregisterSynth);
12552 let targetOrigin;
12553 try {
12554 targetOrigin = new URL(cfg.url, window.location.origin).origin;
12555 } catch {
12556 targetOrigin = window.location.origin;
12557 }
12558 let resolveReady = null;
12559 const readyPromise = new Promise((resolve2) => {
12560 resolveReady = resolve2;
12561 });
12562 const onLoad = () => {
12563 if (cfg.bridge) {
12564 try {
12565 const doc = iframe.contentDocument;
12566 if (doc && !doc.querySelector("script[data-desktop-mode-iframe-bridge]")) {
12567 const bridgeUrl = window.desktopModeConfig?.iframeBridgeUrl;
12568 if (bridgeUrl) {
12569 const s = doc.createElement("script");
12570 s.src = bridgeUrl;
12571 s.setAttribute("data-desktop-mode-iframe-bridge", "1");
12572 doc.head?.appendChild(s);
12573 }
12574 }
12575 } catch {
12576 }
12577 }
12578 markWindowContentReady(windowId);
12579 resolveReady?.();
12580 };
12581 iframe.addEventListener("load", onLoad);
12582 const onMessage = (e) => {
12583 if (!iframe.contentWindow || e.source !== iframe.contentWindow) {
12584 return;
12585 }
12586 if (e.origin !== targetOrigin && e.origin !== window.location.origin) {
12587 return;
12588 }
12589 const data = e.data;
12590 if (data && typeof data === "object" && typeof data.type === "string" && data.type.startsWith("desktop-mode-bridge-")) {
12591 const bridgeRouter = window.__desktopModeConnectionBridge;
12592 bridgeRouter?.routeIncomingFromIframe(data, windowId);
12593 }
12594 if (data && typeof data === "object" && data.type === "desktop-mode-window-publish" && typeof data.channel === "string" && data.channel !== "") {
12595 dispatchFromWindow(
12596 windowId,
12597 data.channel,
12598 data.payload
12599 );
12600 }
12601 try {
12602 cfg.onMessage?.(e.data);
12603 } catch (err) {
12604 if (typeof console !== "undefined") {
12605 console.error(
12606 "[desktop-mode] iframeContent.onMessage threw:",
12607 err
12608 );
12609 }
12610 }
12611 };
12612 window.addEventListener("message", onMessage);
12613 cleanups.push(() => {
12614 window.removeEventListener("message", onMessage);
12615 iframe.removeEventListener("load", onLoad);
12616 });
12617 return readyPromise;
12618 };
12619 }
12620 function createRegisterWindow(manager) {
12621 return async (def) => {
12622 const userRender = def.render;
12623 let render2 = userRender;
12624 const cleanups = [];
12625 if (def.iframeContent) {
12626 if (userRender && typeof console !== "undefined") {
12627 console.warn(
12628 "[desktop-mode] registerWindow: both `render` and `iframeContent` provided — ignoring `render` and using the iframe shorthand. Drop one."
12629 );
12630 }
12631 render2 = buildIframeContentRender(
12632 def.iframeContent,
12633 cleanups,
12634 def.id
12635 );
12636 }
12637 const userOnClose = def.onClose;
12638 const onClose = cleanups.length ? () => {
12639 for (const fn of cleanups) {
12640 try {
12641 fn();
12642 } catch {
12643 }
12644 }
12645 userOnClose?.();
12646 } : userOnClose;
12647 const win = await manager.open({
12648 id: def.id,
12649 baseId: def.baseId || def.id,
12650 native: true,
12651 url: def.url || `#${def.id}`,
12652 title: def.title,
12653 icon: def.icon,
12654 x: def.x ?? 0,
12655 y: def.y ?? 0,
12656 width: def.width ?? DEFAULT_NATIVE_WIDTH,
12657 height: def.height ?? DEFAULT_NATIVE_HEIGHT,
12658 minWidth: def.minWidth ?? DEFAULT_NATIVE_MIN_WIDTH,
12659 minHeight: def.minHeight ?? DEFAULT_NATIVE_MIN_HEIGHT,
12660 render: render2,
12661 onClose,
12662 onResize: def.onResize,
12663 autofocus: def.autofocus,
12664 initialState: def.initialState,
12665 ownerHandle: def.ownerHandle,
12666 multi: def.multi,
12667 desktopId: def.desktopId
12668 });
12669 return win;
12670 };
12671 }
12672 let onWindowInstanceCounter = 0;
12673 function onWindow(id, handlers, options = {}) {
12674 const namespace = `desktop-mode/on-window/${id}/${++onWindowInstanceCounter}`;
12675 const persistent = options.persistent === true;
12676 const bindings = [
12677 ["opened", HOOKS.WINDOW_OPENED],
12678 ["reopened", HOOKS.WINDOW_REOPENED],
12679 ["focused", HOOKS.WINDOW_FOCUSED],
12680 ["blurred", HOOKS.WINDOW_BLURRED],
12681 ["closing", HOOKS.WINDOW_CLOSING],
12682 ["closed", HOOKS.WINDOW_CLOSED],
12683 ["minimized", HOOKS.WINDOW_MINIMIZED],
12684 ["restored", HOOKS.WINDOW_RESTORED],
12685 ["maximized", HOOKS.WINDOW_MAXIMIZED],
12686 ["unmaximized", HOOKS.WINDOW_UNMAXIMIZED],
12687 ["fullscreenEntered", HOOKS.WINDOW_FULLSCREEN_ENTERED],
12688 ["fullscreenExited", HOOKS.WINDOW_FULLSCREEN_EXITED],
12689 ["resized", HOOKS.WINDOW_RESIZED],
12690 ["bodyResized", HOOKS.WINDOW_BODY_RESIZED],
12691 ["boundsChanged", HOOKS.WINDOW_BOUNDS_CHANGED]
12692 ];
12693 const registered = [];
12694 let disposed = false;
12695 const unsubscribe = () => {
12696 if (disposed) {
12697 return;
12698 }
12699 disposed = true;
12700 for (const hookName2 of registered) {
12701 removeAction(hookName2, namespace);
12702 }
12703 };
12704 for (const [key, hookName2] of bindings) {
12705 const handler = handlers[key];
12706 if (!handler) {
12707 continue;
12708 }
12709 registered.push(hookName2);
12710 addAction(hookName2, namespace, (payload) => {
12711 const p = payload;
12712 if (p.windowId !== id) {
12713 return;
12714 }
12715 const { windowId: _w, ...rest } = p;
12716 handler(rest);
12717 if (key === "closed" && !persistent) {
12718 unsubscribe();
12719 }
12720 });
12721 }
12722 return unsubscribe;
12723 }
12724 function createNativeWindowSync(deps2) {
12725 const { manager, appendSystemTile, removeSystemTile } = deps2;
12726 const registered = /* @__PURE__ */ new Set();
12727 const injectedTemplates = /* @__PURE__ */ new Set();
12728 const loadedScripts = /* @__PURE__ */ new Set();
12729 const loadedStyles = /* @__PURE__ */ new Set();
12730 const entriesById = /* @__PURE__ */ new Map();
12731 const resolveSizeForEntry = (entry) => {
12732 const saved = loadNativeWindowGeometry(entry.id);
12733 if (!saved) {
12734 return { width: entry.width, height: entry.height };
12735 }
12736 return {
12737 width: Math.max(saved.width, entry.minWidth),
12738 height: Math.max(saved.height, entry.minHeight)
12739 };
12740 };
12741 const ensureTemplate = (entry) => {
12742 if (injectedTemplates.has(entry.templateId)) {
12743 return;
12744 }
12745 if (document.getElementById(entry.templateId)) {
12746 injectedTemplates.add(entry.templateId);
12747 return;
12748 }
12749 if (!entry.templateHtml) {
12750 return;
12751 }
12752 const tpl = document.createElement("template");
12753 tpl.id = entry.templateId;
12754 tpl.innerHTML = entry.templateHtml;
12755 document.body.appendChild(tpl);
12756 injectedTemplates.add(entry.templateId);
12757 };
12758 const ensureStyle = (entry) => {
12759 const url = entry.styleUrl;
12760 if (!url || loadedStyles.has(url)) {
12761 return;
12762 }
12763 const safeUrl = url.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
12764 const existing = document.head.querySelector(
12765 `link[rel="stylesheet"][href="${safeUrl}"]`
12766 );
12767 if (!existing) {
12768 const link = document.createElement("link");
12769 link.rel = "stylesheet";
12770 link.href = url;
12771 if (entry.styleHandle) {
12772 link.dataset.desktopModeStyleHandle = entry.styleHandle;
12773 }
12774 document.head.appendChild(link);
12775 }
12776 if (Array.isArray(entry.styleInline)) {
12777 for (const css2 of entry.styleInline) {
12778 if (typeof css2 !== "string" || css2 === "") {
12779 continue;
12780 }
12781 const style = document.createElement("style");
12782 if (entry.styleHandle) {
12783 style.dataset.desktopModeStyleHandle = entry.styleHandle;
12784 }
12785 style.textContent = css2;
12786 document.head.appendChild(style);
12787 }
12788 }
12789 loadedStyles.add(url);
12790 };
12791 const ensureScript = async (entry) => {
12792 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
12793 return;
12794 }
12795 try {
12796 await loadVendorScript(entry.scriptUrl, {
12797 translations: entry.scriptTranslations,
12798 l10n: entry.scriptL10n,
12799 before: entry.scriptBefore,
12800 after: entry.scriptAfter
12801 });
12802 } catch (err) {
12803 doAction(HOOKS.SHELL_ERROR, {
12804 scope: "native-window-script-load",
12805 id: entry.id,
12806 error: err
12807 });
12808 }
12809 loadedScripts.add(entry.scriptUrl);
12810 };
12811 const openFromEntry = (entry) => {
12812 const globalRegistry = window.desktopModeNativeWindows || {};
12813 const render2 = globalRegistry[entry.id];
12814 const finalRender = (body, ctx) => {
12815 body.appendChild(cloneTemplate(entry.templateId));
12816 return render2?.(body, ctx);
12817 };
12818 const size = resolveSizeForEntry(entry);
12819 void manager.open({
12820 id: entry.id,
12821 baseId: entry.id,
12822 native: true,
12823 url: `#${entry.id}`,
12824 title: entry.title,
12825 icon: entry.icon,
12826 width: size.width,
12827 height: size.height,
12828 minWidth: entry.minWidth,
12829 minHeight: entry.minHeight,
12830 render: finalRender,
12831 autofocus: entry.autofocus,
12832 ownerHandle: entry.ownerHandle || entry.scriptHandle
12833 });
12834 };
12835 const openNewFromEntry = (entry) => {
12836 const globalRegistry = window.desktopModeNativeWindows || {};
12837 const render2 = globalRegistry[entry.id];
12838 const finalRender = (body, ctx) => {
12839 body.appendChild(cloneTemplate(entry.templateId));
12840 return render2?.(body, ctx);
12841 };
12842 const size = resolveSizeForEntry(entry);
12843 void manager.openNew({
12844 id: entry.id,
12845 baseId: entry.id,
12846 native: true,
12847 url: `#${entry.id}`,
12848 title: entry.title,
12849 icon: entry.icon,
12850 width: size.width,
12851 height: size.height,
12852 minWidth: entry.minWidth,
12853 minHeight: entry.minHeight,
12854 initialState: "normal",
12855 render: finalRender,
12856 autofocus: entry.autofocus,
12857 ownerHandle: entry.ownerHandle || entry.scriptHandle
12858 });
12859 };
12860 const registerTile = async (entry) => {
12861 if (registered.has(entry.id)) {
12862 return;
12863 }
12864 if ("none" === entry.placement) {
12865 ensureTemplate(entry);
12866 ensureStyle(entry);
12867 await ensureScript(entry);
12868 registered.add(entry.id);
12869 return;
12870 }
12871 ensureTemplate(entry);
12872 ensureStyle(entry);
12873 await ensureScript(entry);
12874 appendSystemTile({
12875 id: entry.id,
12876 title: entry.title,
12877 icon: entry.icon,
12878 isOpen: () => !!manager.getById(entry.id),
12879 onOpen: () => openFromEntry(entry)
12880 });
12881 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: entry.id });
12882 registered.add(entry.id);
12883 };
12884 const unregisterTile = (id) => {
12885 if (!registered.has(id)) {
12886 return;
12887 }
12888 removeSystemTile(id);
12889 registered.delete(id);
12890 entriesById.delete(id);
12891 };
12892 const sync = async (list2) => {
12893 const incoming = /* @__PURE__ */ new Set();
12894 for (const entry of list2) {
12895 incoming.add(entry.id);
12896 entriesById.set(entry.id, entry);
12897 }
12898 for (const id of Array.from(registered)) {
12899 if (!incoming.has(id)) {
12900 unregisterTile(id);
12901 }
12902 }
12903 for (const entry of list2) {
12904 if (!registered.has(entry.id)) {
12905 await registerTile(entry);
12906 }
12907 }
12908 };
12909 const openById = (id, opts = {}) => {
12910 const entry = entriesById.get(id);
12911 if (!entry) {
12912 return false;
12913 }
12914 activity.publish("desktop-mode/open-requested", {
12915 windowId: id,
12916 source: opts.source ?? "api"
12917 });
12918 openFromEntry(entry);
12919 return true;
12920 };
12921 const openNewById = (id, opts = {}) => {
12922 const entry = entriesById.get(id);
12923 if (!entry) {
12924 return false;
12925 }
12926 activity.publish("desktop-mode/open-requested", {
12927 windowId: id,
12928 source: opts.source ?? "api"
12929 });
12930 openNewFromEntry(entry);
12931 return true;
12932 };
12933 addAction(
12934 HOOKS.WINDOW_RESIZE_END,
12935 "desktop-mode-native-window-geometry",
12936 (payload) => {
12937 const p = payload;
12938 const windowId = p?.windowId;
12939 const width = p?.width;
12940 const height = p?.height;
12941 if (!windowId || typeof width !== "number" || typeof height !== "number") {
12942 return;
12943 }
12944 const win = manager.getById(windowId);
12945 if (!win) {
12946 return;
12947 }
12948 if (win.state !== "normal") {
12949 return;
12950 }
12951 const baseId = win.config.baseId || win.id;
12952 saveNativeWindowGeometry(baseId, { width, height });
12953 if (win.element) {
12954 saveNativeWindowPosition(baseId, {
12955 x: win.element.offsetLeft,
12956 y: win.element.offsetTop
12957 });
12958 }
12959 }
12960 );
12961 addAction(
12962 HOOKS.WINDOW_DRAG_END,
12963 "desktop-mode-native-window-geometry",
12964 (payload) => {
12965 const windowId = payload?.windowId;
12966 if (!windowId) {
12967 return;
12968 }
12969 const win = manager.getById(windowId);
12970 if (!win) {
12971 return;
12972 }
12973 if (win.state !== "normal") {
12974 return;
12975 }
12976 if (!win.element) {
12977 return;
12978 }
12979 const baseId = win.config.baseId || win.id;
12980 saveNativeWindowGeometry(baseId, {
12981 width: win.element.offsetWidth,
12982 height: win.element.offsetHeight
12983 });
12984 saveNativeWindowPosition(baseId, {
12985 x: win.element.offsetLeft,
12986 y: win.element.offsetTop
12987 });
12988 }
12989 );
12990 addAction(
12991 HOOKS.WINDOW_MAXIMIZED,
12992 "desktop-mode-native-window-geometry",
12993 (payload) => {
12994 const windowId = payload?.windowId;
12995 if (!windowId) {
12996 return;
12997 }
12998 const win = manager.getById(windowId);
12999 if (!win) {
13000 return;
13001 }
13002 const baseId = win.config.baseId || win.id;
13003 const entry = entriesById.get(baseId);
13004 const defaults = entry ? { width: entry.width, height: entry.height } : { width: win.config.width, height: win.config.height };
13005 setNativeWindowSavedState(baseId, "maximized", defaults);
13006 }
13007 );
13008 addAction(
13009 HOOKS.WINDOW_UNMAXIMIZED,
13010 "desktop-mode-native-window-geometry",
13011 (payload) => {
13012 const windowId = payload?.windowId;
13013 if (!windowId) {
13014 return;
13015 }
13016 const win = manager.getById(windowId);
13017 if (!win) {
13018 return;
13019 }
13020 const baseId = win.config.baseId || win.id;
13021 setNativeWindowSavedState(baseId, null);
13022 }
13023 );
13024 return { sync, openById, openNewById };
13025 }
13026 function cloneTemplate(template) {
13027 let tpl = null;
13028 if (typeof template === "string") {
13029 const found = document.getElementById(template);
13030 if (found instanceof HTMLTemplateElement) {
13031 tpl = found;
13032 }
13033 } else {
13034 tpl = template;
13035 }
13036 if (!tpl) {
13037 throw new Error(
13038 `[desktop-mode] cloneTemplate: no <template> found for ${typeof template === "string" ? `#${template}` : "<reference>"}`
13039 );
13040 }
13041 return tpl.content.cloneNode(true);
13042 }
13043 function renderIcon(icon, opts) {
13044 const className = opts.className ?? "";
13045 const title = opts.title ?? "";
13046 if (typeof icon === "string" && icon.startsWith("dashicons-")) {
13047 const el = document.createElement("span");
13048 el.className = `dashicons ${icon} ${className}`.trim();
13049 el.setAttribute("aria-hidden", "true");
13050 return el;
13051 }
13052 if (typeof icon === "string" && icon.startsWith("data:image/svg+xml;base64,")) {
13053 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
13054 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
13055 const el = document.createElement("span");
13056 el.className = className;
13057 el.setAttribute("aria-hidden", "true");
13058 el.style.backgroundImage = `url("${icon}")`;
13059 el.style.backgroundRepeat = "no-repeat";
13060 el.style.backgroundPosition = "center";
13061 el.style.backgroundSize = "contain";
13062 el.style.display = "inline-block";
13063 return el;
13064 }
13065 }
13066 if (typeof icon === "string" && /^data:image\/(png|jpeg|jpg|gif|webp|x-icon|vnd\.microsoft\.icon);base64,/i.test(icon)) {
13067 const commaIdx = icon.indexOf(",");
13068 const payload = commaIdx >= 0 ? icon.slice(commaIdx + 1) : "";
13069 if (/^[A-Za-z0-9+/=]+$/.test(payload)) {
13070 return makeImgIcon(icon, className);
13071 }
13072 }
13073 if (typeof icon === "string" && (icon.startsWith("http://") || icon.startsWith("https://"))) {
13074 return makeImgIcon(icon, className);
13075 }
13076 const span = document.createElement("span");
13077 span.className = `${className} desktop-mode-icon-letter`.trim();
13078 span.setAttribute("aria-hidden", "true");
13079 const letters = letterFromTitle(title);
13080 span.textContent = letters;
13081 const hue = hashTitleToHue(title);
13082 span.style.backgroundColor = `hsl( ${hue}, 60%, 45% )`;
13083 span.style.color = "#fff";
13084 span.style.display = "inline-flex";
13085 span.style.alignItems = "center";
13086 span.style.justifyContent = "center";
13087 span.style.fontWeight = "600";
13088 span.style.borderRadius = "4px";
13089 return span;
13090 }
13091 function makeImgIcon(src, className) {
13092 const img = document.createElement("img");
13093 img.className = className;
13094 img.src = src;
13095 img.alt = "";
13096 img.setAttribute("aria-hidden", "true");
13097 img.draggable = false;
13098 return img;
13099 }
13100 function letterFromTitle(title) {
13101 const trimmed = (title ?? "").trim();
13102 if (trimmed === "") {
13103 return "?";
13104 }
13105 const words = trimmed.split(/\s+/);
13106 if (words.length >= 2) {
13107 return (words[0][0] + words[1][0]).toUpperCase();
13108 }
13109 const first = words[0];
13110 if (first.length >= 2) {
13111 return first.slice(0, 2).toUpperCase();
13112 }
13113 return first.toUpperCase();
13114 }
13115 const BADGE_CLASS = "desktop-mode-icon__badge";
13116 const _badges = /* @__PURE__ */ new Map();
13117 function _safeBadge(count) {
13118 return Math.max(0, Math.floor(Number(count) || 0));
13119 }
13120 function setIconBadge(iconId, count) {
13121 if (!iconId) {
13122 return;
13123 }
13124 const tile2 = _findIconTile(iconId);
13125 if (!tile2) {
13126 return;
13127 }
13128 const safe = _safeBadge(count);
13129 const previous = _badges.get(iconId) ?? 0;
13130 if (safe === previous) {
13131 return;
13132 }
13133 if (safe === 0) {
13134 _badges.delete(iconId);
13135 } else {
13136 _badges.set(iconId, safe);
13137 }
13138 _paintBadgeNode(tile2, safe);
13139 activity.publish("desktop-mode/badge-changed", {
13140 itemId: iconId,
13141 count: safe,
13142 rail: "icon"
13143 });
13144 doAction(HOOKS.ICON_BADGE_CHANGED, {
13145 iconId,
13146 count: safe,
13147 previousCount: previous
13148 });
13149 }
13150 function clearIconBadge(iconId) {
13151 setIconBadge(iconId, 0);
13152 }
13153 function getIconBadge(iconId) {
13154 return _badges.get(iconId) ?? 0;
13155 }
13156 const iconsApi = {
13157 setBadge: setIconBadge,
13158 clearBadge: clearIconBadge,
13159 getBadge: getIconBadge
13160 };
13161 function fingerprintIcons(icons) {
13162 if (!icons || icons.length === 0) {
13163 return "";
13164 }
13165 return icons.map(
13166 (i) => `${i.id}|${i.title}|${i.icon}|${i.window ?? ""}|${i.url ?? ""}|${i.position ?? 0}|${i.pinned ? 1 : 0}`
13167 ).join(";");
13168 }
13169 let _lastFingerprint = "";
13170 function renderDesktopIcons(host, icons, deps2) {
13171 const fp = fingerprintIcons(icons);
13172 if (fp === _lastFingerprint && host.querySelector(":scope > .desktop-mode-icons")) {
13173 return;
13174 }
13175 _lastFingerprint = fp;
13176 const existing = host.querySelector(":scope > .desktop-mode-icons");
13177 if (existing) {
13178 existing.remove();
13179 }
13180 if (!icons || icons.length === 0) {
13181 return;
13182 }
13183 const container = document.createElement("div");
13184 container.className = "desktop-mode-icons";
13185 container.setAttribute("role", "list");
13186 container.setAttribute("aria-label", __("Desktop icons"));
13187 const ordered = [...icons].sort((a, b) => {
13188 const ap = a.pinned ? 0 : 1;
13189 const bp = b.pinned ? 0 : 1;
13190 return ap - bp;
13191 });
13192 const tiles = /* @__PURE__ */ new Map();
13193 for (const entry of ordered) {
13194 const tile2 = buildIcon(entry, deps2);
13195 const stored = _badges.get(entry.id) ?? 0;
13196 if (stored > 0) {
13197 _paintBadgeNode(tile2, stored);
13198 }
13199 container.appendChild(tile2);
13200 tiles.set(entry.id, tile2);
13201 }
13202 host.appendChild(container);
13203 doAction(HOOKS.DESKTOP_ICONS_RENDERED, {
13204 ids: (icons ?? []).map((i) => i.id),
13205 container,
13206 tiles
13207 });
13208 }
13209 function _findIconTile(iconId) {
13210 if (!iconId) {
13211 return null;
13212 }
13213 const container = document.querySelector(
13214 ".desktop-mode-icons"
13215 );
13216 if (!container) {
13217 return null;
13218 }
13219 return container.querySelector(
13220 `[data-icon-id="${_cssEscape(iconId)}"]`
13221 );
13222 }
13223 function _paintBadgeNode(host, count) {
13224 const existing = host.querySelector(
13225 `:scope > .${BADGE_CLASS}`
13226 );
13227 if (count <= 0) {
13228 existing?.remove();
13229 return;
13230 }
13231 const display = count > 99 ? "99+" : String(count);
13232 const ariaLabel = sprintf(
13233 // translators: %d is the number of pending items in a desktop-icon badge.
13234 _n("%d notification", "%d notifications", count),
13235 count
13236 );
13237 if (existing) {
13238 if (existing.textContent !== display) {
13239 existing.textContent = display;
13240 }
13241 existing.setAttribute("aria-label", ariaLabel);
13242 return;
13243 }
13244 const badge = document.createElement("span");
13245 badge.className = BADGE_CLASS;
13246 badge.textContent = display;
13247 badge.setAttribute("aria-label", ariaLabel);
13248 host.appendChild(badge);
13249 }
13250 function _cssEscape(value) {
13251 const c = window.CSS;
13252 return c?.escape ? c.escape(value) : value;
13253 }
13254 function buildIcon(entry, deps2) {
13255 const tile2 = document.createElement("button");
13256 tile2.type = "button";
13257 tile2.className = entry.pinned ? "desktop-mode-icon desktop-mode-icon--pinned" : "desktop-mode-icon";
13258 tile2.dataset.iconId = entry.id;
13259 if (entry.pinned) {
13260 tile2.dataset.pinned = "1";
13261 }
13262 tile2.setAttribute("role", "listitem");
13263 tile2.setAttribute("aria-label", entry.title);
13264 const icon = renderIcon(entry.icon, {
13265 title: entry.title,
13266 className: "desktop-mode-icon__image"
13267 });
13268 tile2.appendChild(icon);
13269 const label = document.createElement("span");
13270 label.className = "desktop-mode-icon__label";
13271 label.textContent = entry.title;
13272 tile2.appendChild(label);
13273 tile2.addEventListener("click", (e) => {
13274 e.stopPropagation();
13275 doAction(HOOKS.DESKTOP_ICON_CLICKED, {
13276 id: entry.id,
13277 target: entry.window ? "window" : "url"
13278 });
13279 openTarget(entry, deps2);
13280 });
13281 tile2.addEventListener("contextmenu", (e) => {
13282 if (entry.pinned) {
13283 return;
13284 }
13285 e.preventDefault();
13286 e.stopPropagation();
13287 openItemVisibilityMenu({
13288 x: e.clientX,
13289 y: e.clientY,
13290 id: entry.id,
13291 title: entry.title,
13292 surface: "desktop"
13293 });
13294 });
13295 return tile2;
13296 }
13297 function openTarget(entry, deps2) {
13298 if (entry.window) {
13299 const opened = deps2.openWindow(entry.window);
13300 if (!opened) {
13301 return;
13302 }
13303 return;
13304 }
13305 if (entry.url) {
13306 if (tryOpenExternalUrl(entry.url)) {
13307 return;
13308 }
13309 try {
13310 const parsed = new URL(entry.url, window.location.origin);
13311 void deps2.manager.open({
13312 id: `desktop-icon-${entry.id}`,
13313 url: parsed.toString(),
13314 title: entry.title,
13315 icon: entry.icon
13316 });
13317 } catch {
13318 }
13319 }
13320 }
13321 const SIDE_DOCK_ID = "desktop-mode-side-dock";
13322 function coreItemToIconEntry(item, index2) {
13323 return {
13324 id: `dock-core:${item.id}`,
13325 title: item.title,
13326 icon: item.icon,
13327 window: "",
13328 url: item.url,
13329 // Synthesized icons render after server-registered ones; the
13330 // large offset leaves headroom for plugin authors who set
13331 // explicit `position` values.
13332 position: 1e3 + index2
13333 };
13334 }
13335 function createLayoutDispatcher(deps2, initialLayout, initialDockItems, initialServerIcons) {
13336 let layout = initialLayout;
13337 let items = initialDockItems;
13338 let serverIcons = initialServerIcons ?? [];
13339 let primary = null;
13340 let side = null;
13341 let primaryDock = null;
13342 let sideDock = null;
13343 let sideDockEl = null;
13344 const systemTiles = /* @__PURE__ */ new Map();
13345 const railFor = (affinity) => {
13346 if (affinity === "core" && side) {
13347 return side;
13348 }
13349 return primary;
13350 };
13351 const ensureSideDockEl = () => {
13352 const existing = document.getElementById(
13353 SIDE_DOCK_ID
13354 );
13355 if (existing) {
13356 return existing;
13357 }
13358 const el = document.createElement("nav");
13359 el.id = SIDE_DOCK_ID;
13360 el.className = "desktop-mode-dock";
13361 el.setAttribute("role", "toolbar");
13362 el.setAttribute("aria-label", "Core admin navigation");
13363 deps2.shellBody.insertBefore(el, deps2.shellBody.firstChild);
13364 return el;
13365 };
13366 const removeSideDockEl = () => {
13367 if (sideDockEl && sideDockEl.parentNode) {
13368 sideDockEl.parentNode.removeChild(sideDockEl);
13369 }
13370 sideDockEl = null;
13371 };
13372 const readSettings = () => deps2.getSettings?.() ?? { itemVisibility: {}, dockOrder: [] };
13373 const effectiveDockItems = () => {
13374 const dockedNativeWindows = /* @__PURE__ */ new Set();
13375 for (const entry of systemTiles.values()) {
13376 dockedNativeWindows.add(entry.item.id);
13377 }
13378 return applyDockPlacement(
13379 items,
13380 serverIcons,
13381 readSettings(),
13382 dockedNativeWindows
13383 );
13384 };
13385 const partition = () => {
13386 const effective = effectiveDockItems();
13387 const core = [];
13388 const plugin = [];
13389 for (const item of effective) {
13390 if (item.isCore) {
13391 core.push(item);
13392 } else {
13393 plugin.push(item);
13394 }
13395 }
13396 return { core, plugin };
13397 };
13398 const repaintIcons = () => {
13399 const settings = readSettings();
13400 if (layout !== "spatial") {
13401 deps2.renderIcons(
13402 applyDesktopPlacement(serverIcons, items, settings.itemVisibility)
13403 );
13404 return;
13405 }
13406 const { core } = partition();
13407 const synthesized = core.map(coreItemToIconEntry);
13408 const explicitlyPromoted = [];
13409 let synthIndex = 0;
13410 for (const item of items) {
13411 const placement = settings.itemVisibility[item.id];
13412 if (placement === "desktop" || placement === "both") {
13413 explicitlyPromoted.push({
13414 id: `dock:${item.id}`,
13415 title: item.title,
13416 icon: item.icon,
13417 window: "",
13418 url: item.url || "",
13419 position: 2e3 + synthIndex++
13420 });
13421 }
13422 }
13423 deps2.renderIcons([...synthesized, ...explicitlyPromoted]);
13424 };
13425 const tearDownDocks = () => {
13426 if (primary) {
13427 try {
13428 primary.destroy();
13429 } catch (err) {
13430 doAction(HOOKS.SHELL_ERROR, {
13431 scope: "dock-rail-renderer/destroy",
13432 error: err
13433 });
13434 }
13435 primary = null;
13436 primaryDock = null;
13437 }
13438 if (side) {
13439 try {
13440 side.destroy();
13441 } catch (err) {
13442 doAction(HOOKS.SHELL_ERROR, {
13443 scope: "dock-rail-renderer/destroy",
13444 error: err
13445 });
13446 }
13447 side = null;
13448 sideDock = null;
13449 }
13450 };
13451 const mountRail = (mountDeps) => {
13452 const renderer = resolveActive();
13453 if (!renderer) {
13454 doAction(HOOKS.SHELL_ERROR, {
13455 scope: "dock-rail-renderer",
13456 message: "No dock rail renderer is registered."
13457 });
13458 return null;
13459 }
13460 try {
13461 return renderer.mount(mountDeps);
13462 } catch (err) {
13463 doAction(HOOKS.SHELL_ERROR, {
13464 scope: "dock-rail-renderer/mount",
13465 rendererId: renderer.id,
13466 error: err
13467 });
13468 if (renderer === defaultDockRailRenderer) {
13469 return null;
13470 }
13471 try {
13472 return defaultDockRailRenderer.mount(mountDeps);
13473 } catch {
13474 return null;
13475 }
13476 }
13477 };
13478 const buildMountDeps = (container, railItems, orientation) => ({
13479 container,
13480 items: railItems,
13481 // `fullMenu` is the complete admin-menu list. Renderers that
13482 // want to ignore the layout's partitioning (e.g., paint
13483 // every menu item in one ring regardless of `isCore`) read
13484 // this instead of `items`. Snapshot per-mount so a renderer
13485 // holding the array sees a stable list; live updates flow
13486 // through `replaceItems`.
13487 fullMenu: items.slice(),
13488 // Same idea for system tiles — OS Settings, plugin-owned
13489 // native-window launchers, etc. Lets a renderer apply
13490 // uniform treatment across menu + system cohorts in one
13491 // pass. Live updates flow through `appendSystemItem` /
13492 // `removeSystemItem`.
13493 fullSystemTiles: Array.from(systemTiles.values()).map(
13494 (entry) => entry.item
13495 ),
13496 orientation,
13497 windowManager: deps2.windowManager,
13498 adminUrl: deps2.adminUrl,
13499 // `openItem` / `openSubmenuPick` / `openSystemItem` /
13500 // `requestSubmenu` are routing callbacks for custom
13501 // renderers. They mirror exactly what the default renderer
13502 // (`Dock.openPage` / `Dock.openSubmenuPick`) does internally
13503 // — same `deriveWindowId(url, adminUrl)` call, same window-
13504 // config shape — so a custom renderer addresses the same
13505 // window with the same id at runtime. Switching renderer
13506 // mid-session doesn't lose the user's open windows.
13507 openItem: (item) => {
13508 const baseId = deriveWindowId(item.url, deps2.adminUrl);
13509 deps2.windowManager.open({
13510 id: baseId,
13511 baseId,
13512 url: item.url,
13513 parentUrl: item.url,
13514 title: item.title,
13515 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
13516 submenu: item.submenu,
13517 multi: !!item.multi
13518 });
13519 },
13520 openSubmenuPick: (item, sub) => {
13521 deps2.windowManager.open({
13522 id: deriveWindowId(sub.url, deps2.adminUrl),
13523 baseId: deriveWindowId(item.url, deps2.adminUrl),
13524 url: sub.url,
13525 // Pin the synthetic parent tab to the dock landing
13526 // page, not to the sub-page the user picked. Without
13527 // this, a submenu-pick (e.g. clicking "Editor" inside
13528 // Appearance's submenu popover) would open at
13529 // site-editor.php with no way back to themes.php.
13530 parentUrl: item.url,
13531 title: item.title,
13532 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
13533 submenu: item.submenu,
13534 multi: !!item.multi
13535 });
13536 },
13537 openSystemItem: (item) => item.onOpen()
13538 });
13539 const buildDocksForCurrentLayout = () => {
13540 tearDownDocks();
13541 const { core, plugin } = partition();
13542 if (layout === "classic") {
13543 sideDockEl = ensureSideDockEl();
13544 side = mountRail(
13545 buildMountDeps(sideDockEl, core, "left")
13546 );
13547 sideDock = unwrapDefaultDock(side);
13548 primary = mountRail(
13549 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
13550 );
13551 primaryDock = unwrapDefaultDock(primary);
13552 } else if (layout === "unified") {
13553 removeSideDockEl();
13554 primary = mountRail(
13555 buildMountDeps(deps2.bottomDockEl, effectiveDockItems(), "bottom")
13556 );
13557 primaryDock = unwrapDefaultDock(primary);
13558 } else {
13559 removeSideDockEl();
13560 primary = mountRail(
13561 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
13562 );
13563 primaryDock = unwrapDefaultDock(primary);
13564 }
13565 for (const entry of systemTiles.values()) {
13566 railFor(entry.affinity)?.appendSystemItem(entry.item);
13567 }
13568 };
13569 const dispatcher = {
13570 getLayout: () => layout,
13571 getPrimary: () => primaryDock,
13572 getSide: () => sideDock,
13573 setLayout: (next) => {
13574 if (next === layout) {
13575 return;
13576 }
13577 layout = next;
13578 deps2.shellRoot.setAttribute("data-desktop-mode-layout", next);
13579 buildDocksForCurrentLayout();
13580 repaintIcons();
13581 document.dispatchEvent(
13582 new CustomEvent("desktop-mode-layout-changed", {
13583 detail: {
13584 layout: next,
13585 primary: primaryDock,
13586 side: sideDock
13587 }
13588 })
13589 );
13590 },
13591 applyDockItems: (nextItems) => {
13592 items = nextItems;
13593 const { core, plugin } = partition();
13594 if (layout === "classic") {
13595 side?.replaceItems(core);
13596 primary?.replaceItems(plugin);
13597 } else if (layout === "unified") {
13598 primary?.replaceItems(effectiveDockItems());
13599 } else {
13600 primary?.replaceItems(plugin);
13601 }
13602 repaintIcons();
13603 },
13604 applyDesktopIcons: (next) => {
13605 serverIcons = next ?? [];
13606 repaintIcons();
13607 },
13608 appendSystemTile: (item, affinity = "plugin") => {
13609 systemTiles.set(item.id, { item, affinity });
13610 railFor(affinity)?.appendSystemItem(item);
13611 },
13612 removeSystemTile: (id) => {
13613 const entry = systemTiles.get(id);
13614 if (!entry) {
13615 return;
13616 }
13617 systemTiles.delete(id);
13618 railFor(entry.affinity)?.removeSystemItem(id);
13619 },
13620 listSystemTiles: () => Array.from(systemTiles.values()).map((entry) => ({
13621 id: entry.item.id,
13622 title: entry.item.title,
13623 icon: entry.item.icon,
13624 affinity: entry.affinity
13625 })),
13626 getSystemTile: (id) => systemTiles.get(id)?.item ?? null,
13627 getMenuItems: () => items.slice(),
13628 refresh: () => {
13629 const { core, plugin } = partition();
13630 if (layout === "classic") {
13631 side?.replaceItems(core);
13632 primary?.replaceItems(plugin);
13633 } else if (layout === "unified") {
13634 primary?.replaceItems(effectiveDockItems());
13635 } else {
13636 primary?.replaceItems(plugin);
13637 }
13638 repaintIcons();
13639 },
13640 destroy: () => {
13641 tearDownDocks();
13642 removeSideDockEl();
13643 }
13644 };
13645 deps2.shellRoot.setAttribute("data-desktop-mode-layout", layout);
13646 buildDocksForCurrentLayout();
13647 repaintIcons();
13648 let lastResolvedId = resolveActive()?.id ?? null;
13649 subscribe$3(() => {
13650 const nextId2 = resolveActive()?.id ?? null;
13651 if (nextId2 === lastResolvedId) {
13652 return;
13653 }
13654 lastResolvedId = nextId2;
13655 buildDocksForCurrentLayout();
13656 repaintIcons();
13657 document.dispatchEvent(
13658 new CustomEvent("desktop-mode-layout-changed", {
13659 detail: {
13660 layout,
13661 primary: primaryDock,
13662 side: sideDock
13663 }
13664 })
13665 );
13666 });
13667 return dispatcher;
13668 }
13669 function loadImpl(scriptUrl) {
13670 if (window.desktopModeCreateAiAssistant) {
13671 return Promise.resolve(window.desktopModeCreateAiAssistant);
13672 }
13673 return new Promise((resolve2, reject) => {
13674 const existing = document.querySelector(
13675 `script[data-desktop-mode-ai="1"]`
13676 );
13677 const finish = () => {
13678 const factory = window.desktopModeCreateAiAssistant;
13679 if (!factory) {
13680 reject(
13681 new Error(
13682 "[desktop-mode] ai-assistant bundle loaded but did not register desktopModeCreateAiAssistant"
13683 )
13684 );
13685 return;
13686 }
13687 resolve2(factory);
13688 };
13689 if (existing) {
13690 if (window.desktopModeCreateAiAssistant) {
13691 finish();
13692 } else {
13693 existing.addEventListener("load", finish);
13694 existing.addEventListener(
13695 "error",
13696 () => reject(new Error("failed to load ai-assistant bundle"))
13697 );
13698 }
13699 return;
13700 }
13701 const s = document.createElement("script");
13702 s.src = scriptUrl;
13703 s.async = true;
13704 s.dataset.desktopModeAi = "1";
13705 s.addEventListener("load", finish);
13706 s.addEventListener(
13707 "error",
13708 () => reject(new Error("failed to load ai-assistant bundle"))
13709 );
13710 document.head.appendChild(s);
13711 });
13712 }
13713 class AiAssistantStub {
13714 constructor(config, scriptUrl) {
13715 this._real = null;
13716 this._loadPromise = null;
13717 this._pendingAsk = null;
13718 this._intendOpen = false;
13719 this.ask = (...args) => {
13720 return this._ensure().then((r) => r.ask(...args));
13721 };
13722 this._config = config;
13723 this._scriptUrl = scriptUrl;
13724 }
13725 _ensure() {
13726 if (this._loadPromise) {
13727 return this._loadPromise;
13728 }
13729 this._loadPromise = loadImpl(this._scriptUrl).then((factory) => {
13730 const real = factory(this._config);
13731 if (this._pendingAsk) {
13732 real.attachAsk(this._pendingAsk);
13733 }
13734 this._real = real;
13735 return real;
13736 });
13737 return this._loadPromise;
13738 }
13739 open() {
13740 this._intendOpen = true;
13741 void this._ensure().then((r) => r.open());
13742 }
13743 close() {
13744 this._intendOpen = false;
13745 if (this._real) {
13746 this._real.close();
13747 }
13748 }
13749 toggle() {
13750 if (this.isOpen) {
13751 this.close();
13752 } else {
13753 this.open();
13754 }
13755 }
13756 get isOpen() {
13757 return this._real ? this._real.isOpen : this._intendOpen;
13758 }
13759 /**
13760 * Late-bind the programmatic `ask` callback. Mirrors the real
13761 * class's `attachAsk` signature so `desktop.ts`'s call site is
13762 * identical whether it's wiring the stub or the impl.
13763 */
13764 attachAsk(fn) {
13765 this._pendingAsk = fn;
13766 if (this._real) {
13767 this._real.attachAsk(fn);
13768 }
13769 }
13770 }
13771 const isAbortError = (err) => {
13772 if (!err || typeof err !== "object") {
13773 return false;
13774 }
13775 return err.name === "AbortError";
13776 };
13777 const normaliseToolsOpt = (tools) => {
13778 if (!tools) {
13779 return [];
13780 }
13781 const all2 = listAiCallableCommands();
13782 if (tools === true || tools === "aiCallable") {
13783 return all2;
13784 }
13785 if (Array.isArray(tools)) {
13786 const allowed = new Set(tools.map((s) => s.toLowerCase()));
13787 return all2.filter((c) => allowed.has(c.slug));
13788 }
13789 if (typeof tools === "function") {
13790 return all2.filter((c) => {
13791 try {
13792 return tools(c.slug) === true;
13793 } catch {
13794 return false;
13795 }
13796 });
13797 }
13798 return [];
13799 };
13800 const normaliseSystemPrompt = (sp) => {
13801 if (!sp) {
13802 return null;
13803 }
13804 if (typeof sp === "string") {
13805 return { text: sp, mode: "append" };
13806 }
13807 if (typeof sp === "object" && typeof sp.text === "string" && sp.text !== "") {
13808 return {
13809 text: sp.text,
13810 mode: sp.mode === "replace" ? "replace" : "append"
13811 };
13812 }
13813 return null;
13814 };
13815 function liftMessage(payloadMessage, result) {
13816 const seed2 = payloadMessage ?? "";
13817 if (seed2 !== "") {
13818 return seed2;
13819 }
13820 if (typeof result === "string" && result !== "") {
13821 return result;
13822 }
13823 if (result && typeof result === "object" && "message" in result && typeof result.message === "string") {
13824 return result.message;
13825 }
13826 return "";
13827 }
13828 function serialiseOutcome(result) {
13829 if (result === void 0) {
13830 return { value: null };
13831 }
13832 if (typeof result === "object" && result !== null) {
13833 return result;
13834 }
13835 return { value: result };
13836 }
13837 function createAsk(deps2) {
13838 const postToSearch = async (body, signal) => {
13839 const config = deps2.config();
13840 const url = config.aiSearchUrl ?? "";
13841 const nonce = config.restNonce ?? "";
13842 if (!url || !nonce) {
13843 throw new Error(
13844 "[desktop-mode] wp.desktop.ai.ask: aiSearchUrl / restNonce missing from config. AI Copilot may not be enabled."
13845 );
13846 }
13847 try {
13848 return await trackedFetch$1(
13849 url,
13850 {
13851 method: "POST",
13852 credentials: "same-origin",
13853 headers: {
13854 "Content-Type": "application/json",
13855 "X-WP-Nonce": nonce
13856 },
13857 body: JSON.stringify(body),
13858 signal
13859 },
13860 { source: "desktop-mode/ai-ask" }
13861 );
13862 } catch (err) {
13863 if (isAbortError(err)) {
13864 throw err;
13865 }
13866 throw new Error(
13867 `[desktop-mode] wp.desktop.ai.ask: network error — ${String(
13868 err?.message ?? err
13869 )}`
13870 );
13871 }
13872 };
13873 const dispatchToolCall = async (payload, opts) => {
13874 const slug = payload.tool?.slug ?? "";
13875 const args = payload.tool?.args ?? "";
13876 const cmd = findCommand(slug);
13877 if (!cmd) {
13878 return {
13879 ok: false,
13880 response: {
13881 answer_type: "tool_call",
13882 message: `Command /${slug} was not registered on this page.`,
13883 entity: null,
13884 admin_links: null,
13885 toolCall: {
13886 slug,
13887 args,
13888 result: { error: "command_not_found" }
13889 },
13890 request_id: payload.request_id
13891 }
13892 };
13893 }
13894 const ctx = opts.commandContext ?? deps2.fallbackContext();
13895 let result;
13896 try {
13897 result = await Promise.resolve(cmd.run(args, ctx));
13898 } catch (err) {
13899 result = { error: String(err?.message ?? err) };
13900 }
13901 return { ok: true, slug, args, result };
13902 };
13903 const composeFollowUp = async (text, slug, args, result, sp, signal) => {
13904 const body = {
13905 query: text,
13906 follow_up: {
13907 tool: { slug, args },
13908 result: serialiseOutcome(result)
13909 }
13910 };
13911 if (sp) {
13912 body.system_prompt_text = sp.text;
13913 body.system_prompt_mode = sp.mode;
13914 }
13915 let res;
13916 try {
13917 res = await postToSearch(body, signal);
13918 } catch (err) {
13919 if (isAbortError(err)) {
13920 throw err;
13921 }
13922 return null;
13923 }
13924 if (!res.ok) {
13925 return null;
13926 }
13927 const payload = await res.json().catch(() => ({}));
13928 const message = typeof payload.message === "string" ? payload.message.trim() : "";
13929 return message !== "" ? payload.message ?? null : null;
13930 };
13931 return async function ask(query, opts = {}) {
13932 const text = (query ?? "").trim();
13933 if (text === "") {
13934 const hasMeaningfulOpts = opts.tools !== void 0 || opts.systemPrompt !== void 0 || opts.followUp === true || opts.resumeTool !== void 0 || opts.commandContext !== void 0;
13935 if (hasMeaningfulOpts) {
13936 throw new Error(
13937 "[desktop-mode] wp.desktop.ai.ask: empty query passed with non-default options — likely a caller bug. Provide a query or call without options."
13938 );
13939 }
13940 return {
13941 answer_type: "chat",
13942 message: "",
13943 entity: null,
13944 admin_links: null
13945 };
13946 }
13947 const commandTools = normaliseToolsOpt(opts.tools);
13948 const sp = normaliseSystemPrompt(opts.systemPrompt);
13949 const body = { query: text };
13950 if (opts.resumeTool) {
13951 body.resume_tool = opts.resumeTool;
13952 }
13953 if (typeof opts.startOffset === "number") {
13954 body.start_offset = opts.startOffset;
13955 }
13956 if (commandTools.length > 0) {
13957 body.command_tools = commandTools;
13958 }
13959 if (sp) {
13960 body.system_prompt_text = sp.text;
13961 body.system_prompt_mode = sp.mode;
13962 }
13963 const res = await postToSearch(body, opts.signal);
13964 if (!res.ok) {
13965 const detail = await res.json().catch(() => ({ message: res.statusText }));
13966 throw new Error(
13967 `[desktop-mode] wp.desktop.ai.ask: HTTP ${res.status} — ${detail.message ?? res.statusText}`
13968 );
13969 }
13970 const payload = await res.json();
13971 if (payload.answer_type !== "tool_call" || !payload.tool) {
13972 return {
13973 answer_type: payload.answer_type,
13974 message: payload.message ?? "",
13975 entity: payload.entity ?? null,
13976 admin_links: payload.admin_links ?? null,
13977 request_id: payload.request_id,
13978 continue: payload.continue ?? null
13979 };
13980 }
13981 const dispatch2 = await dispatchToolCall(payload, opts);
13982 if (!dispatch2.ok) {
13983 return dispatch2.response;
13984 }
13985 const { slug, args, result } = dispatch2;
13986 let message = liftMessage(payload.message, result);
13987 if (opts.followUp === true) {
13988 const composed = await composeFollowUp(
13989 text,
13990 slug,
13991 args,
13992 result,
13993 sp,
13994 opts.signal
13995 );
13996 if (composed !== null) {
13997 message = composed;
13998 }
13999 }
14000 return {
14001 answer_type: "tool_call",
14002 message,
14003 entity: null,
14004 admin_links: null,
14005 toolCall: { slug, args, result },
14006 request_id: payload.request_id
14007 };
14008 };
14009 }
14010 const EVENT_NAME = "desktop-mode-broadcast";
14011 const POSTMESSAGE_TYPE = "desktop-mode-broadcast";
14012 const ORIGIN = window.location.origin;
14013 let _manager = null;
14014 function attachBroadcastBus(manager) {
14015 _manager = manager;
14016 }
14017 function broadcast(topic, payload) {
14018 const filteredTopic = String(
14019 applyFilters("desktop-mode.broadcast.topic", topic, { payload }) ?? topic
14020 );
14021 const filteredPayload = applyFilters(
14022 "desktop-mode.broadcast.payload",
14023 payload,
14024 { topic: filteredTopic }
14025 );
14026 const detail = {
14027 topic: filteredTopic,
14028 payload: filteredPayload
14029 };
14030 document.dispatchEvent(new CustomEvent(EVENT_NAME, { detail }));
14031 doAction(HOOKS.BROADCAST, detail);
14032 activity.publish(
14033 filteredTopic,
14034 filteredPayload
14035 );
14036 if (!_manager) {
14037 return;
14038 }
14039 const message = {
14040 type: POSTMESSAGE_TYPE,
14041 topic: filteredTopic,
14042 payload: filteredPayload
14043 };
14044 for (const win of _manager._stack) {
14045 const target = win.iframe?.contentWindow;
14046 if (!target) {
14047 continue;
14048 }
14049 try {
14050 target.postMessage(message, ORIGIN);
14051 } catch (err) {
14052 }
14053 }
14054 }
14055 function subscribe$2(topic, cb) {
14056 const handler = (e) => {
14057 const detail = e.detail;
14058 if (!detail) {
14059 return;
14060 }
14061 if (topic !== "*" && detail.topic !== topic) {
14062 return;
14063 }
14064 try {
14065 cb(detail.payload, { topic: detail.topic });
14066 } catch (err) {
14067 doAction(HOOKS.SHELL_ERROR, {
14068 scope: "broadcast-subscriber",
14069 topic: detail.topic,
14070 error: err
14071 });
14072 }
14073 };
14074 document.addEventListener(EVENT_NAME, handler);
14075 return () => document.removeEventListener(EVENT_NAME, handler);
14076 }
14077 function installBroadcastReceiver() {
14078 window.addEventListener("message", (e) => {
14079 if (e.origin !== ORIGIN) {
14080 return;
14081 }
14082 const data = e.data;
14083 if (!data || data.type !== POSTMESSAGE_TYPE) {
14084 return;
14085 }
14086 if (data._fromParent) {
14087 return;
14088 }
14089 if (typeof data.topic !== "string") {
14090 return;
14091 }
14092 broadcast(data.topic, data.payload);
14093 });
14094 }
14095 const LOG_PREFIX = "[desktop-mode-bin badge]";
14096 function log(...args) {
14097 try {
14098 if (window.localStorage?.getItem("desktopModeBinDebug")) {
14099 console.info(LOG_PREFIX, ...args);
14100 }
14101 } catch {
14102 }
14103 }
14104 function warn(...args) {
14105 console.warn(LOG_PREFIX, ...args);
14106 }
14107 const TARGET_ID = "desktop-mode-recycle-bin";
14108 const HEARTBEAT_FIELD$1 = "desktop_mode_recycle_bin_seen_ts";
14109 function getDesktopApi() {
14110 return window.wp?.desktop;
14111 }
14112 const store$3 = createSharedStore(
14113 "desktop-mode/recycle-bin/badge",
14114 () => ({
14115 current: 0,
14116 seenTs: 0,
14117 started: false,
14118 countUrl: ""
14119 })
14120 );
14121 function setRecycleBinBadge(next) {
14122 const safe = Math.max(0, Math.floor(next));
14123 const prev = store$3.state.current;
14124 store$3.state.current = safe;
14125 log("setRecycleBinBadge", { prev, next: safe });
14126 paintBadge(safe);
14127 }
14128 function adjustRecycleBinBadge(delta) {
14129 setRecycleBinBadge(store$3.state.current + delta);
14130 }
14131 function _currentRecycleBinBadge() {
14132 return store$3.state.current;
14133 }
14134 function paintBadge(count) {
14135 const desktop = getDesktopApi();
14136 const active2 = isBinWindowActive();
14137 const visible = active2 ? 0 : count;
14138 log("paintBadge", { count, visible, active: active2 });
14139 desktop?.dock?.setBadge?.(TARGET_ID, visible);
14140 desktop?.taskbar?.setBadge?.(TARGET_ID, visible);
14141 desktop?.icons?.setBadge?.(TARGET_ID, visible);
14142 }
14143 function isBinWindowActive() {
14144 return !!getDesktopApi()?.windowManager?.isActive?.(TARGET_ID);
14145 }
14146 function startRecycleBinBadge(initialRaw, countUrl = "") {
14147 const initial = Number(initialRaw) || 0;
14148 const cfg = window.desktopModeConfig;
14149 const cfgCount = cfg?.recycleBinCount;
14150 const cfgUrl = cfg?.recycleBinCountUrl;
14151 const cfgDebug = cfg?.desktopModeBinDebug;
14152 log("startRecycleBinBadge entry", {
14153 initial,
14154 countUrl,
14155 alreadyStarted: store$3.state.started,
14156 cfgCount,
14157 cfgUrl,
14158 cfgDebug,
14159 readyState: document.readyState
14160 });
14161 const cfgCountNum = Number(cfgCount);
14162 const cfgCountIsHealthy = (typeof cfgCount === "number" || typeof cfgCount === "string") && Number.isFinite(cfgCountNum);
14163 if (!cfgCountIsHealthy) {
14164 warn(
14165 "desktopModeConfig.recycleBinCount is missing — PHP filter `desktop_mode_shell_config` did not deliver. Check your PHP error log for `[desktop-mode-bin debug]` lines.",
14166 { cfg }
14167 );
14168 }
14169 if (store$3.state.started) {
14170 setRecycleBinBadge(initial);
14171 return;
14172 }
14173 store$3.state.started = true;
14174 store$3.state.countUrl = countUrl;
14175 store$3.state.seenTs = Date.now();
14176 setRecycleBinBadge(initial);
14177 wireDockTileSignal();
14178 wireDesktopIconsSignal();
14179 wireBroadcastDeltas();
14180 wirePostMessageFastPath();
14181 wireHeartbeatProbe();
14182 wireWindowLifecycleSignals();
14183 }
14184 function wireWindowLifecycleSignals() {
14185 const ns = "desktop-mode/recycle-bin/badge-lifecycle";
14186 const repaint = (payload) => {
14187 const detail = payload;
14188 if (detail?.windowId !== TARGET_ID) {
14189 return;
14190 }
14191 paintBadge(store$3.state.current);
14192 };
14193 addAction(HOOKS.WINDOW_OPENED, ns, repaint);
14194 addAction(HOOKS.WINDOW_FOCUSED, ns, repaint);
14195 addAction(HOOKS.WINDOW_BLURRED, ns, repaint);
14196 addAction(HOOKS.WINDOW_MINIMIZED, ns, repaint);
14197 addAction(HOOKS.WINDOW_RESTORED, ns, repaint);
14198 addAction(HOOKS.WINDOW_CLOSED, ns, repaint);
14199 addAction(HOOKS.WINDOW_REOPENED, ns, repaint);
14200 }
14201 function wireDockTileSignal() {
14202 addAction(
14203 HOOKS.DOCK_ITEM_APPENDED,
14204 "desktop-mode/recycle-bin/badge",
14205 (payload) => {
14206 if (payload?.id === TARGET_ID) {
14207 paintBadge(store$3.state.current);
14208 }
14209 }
14210 );
14211 }
14212 function wireDesktopIconsSignal() {
14213 addAction(
14214 HOOKS.DESKTOP_ICONS_RENDERED,
14215 "desktop-mode/recycle-bin/badge",
14216 (payload) => {
14217 if (payload?.ids?.includes(TARGET_ID)) {
14218 paintBadge(store$3.state.current);
14219 }
14220 }
14221 );
14222 }
14223 function wireBroadcastDeltas() {
14224 const onDomain = (payload) => {
14225 const detail = payload;
14226 if (!detail) {
14227 return;
14228 }
14229 const ids = Array.isArray(detail.ids) ? detail.ids.length : 0;
14230 switch (detail.action) {
14231 case "trashed":
14232 adjustRecycleBinBadge(+ids);
14233 break;
14234 case "untrashed":
14235 case "deleted":
14236 adjustRecycleBinBadge(-ids);
14237 break;
14238 }
14239 };
14240 subscribe$2("desktop-mode.post.changed", onDomain);
14241 subscribe$2("desktop-mode.page.changed", onDomain);
14242 subscribe$2("desktop-mode.attachment.changed", onDomain);
14243 subscribe$2("desktop-mode.comment.changed", onDomain);
14244 subscribe$2("desktop-mode.placement.changed", onDomain);
14245 subscribe$2("desktop-mode.shortcut.changed", onDomain);
14246 subscribe$2("desktop-mode.folder.changed", onDomain);
14247 }
14248 function wirePostMessageFastPath() {
14249 const expectedOrigin = window.location.origin;
14250 window.addEventListener("message", (e) => {
14251 if (e.origin !== expectedOrigin) {
14252 return;
14253 }
14254 const data = e.data;
14255 if (!data || data.type !== "desktop-mode-recycle-bin-changed") {
14256 return;
14257 }
14258 const ts = typeof data.ts === "number" ? data.ts : Date.now();
14259 if (ts <= store$3.state.seenTs) {
14260 log("postMessage skipped (ts <= seenTs)", { ts, seenTs: store$3.state.seenTs });
14261 return;
14262 }
14263 log("postMessage triggers refetch", { ts, prevSeenTs: store$3.state.seenTs });
14264 store$3.state.seenTs = ts;
14265 void refetchCount();
14266 });
14267 }
14268 function wireHeartbeatProbe() {
14269 const $ = window.jQuery;
14270 if (!$) {
14271 warn("wireHeartbeatProbe: window.jQuery not available — heartbeat path disabled");
14272 return;
14273 }
14274 log("wireHeartbeatProbe: jQuery + heartbeat hooks attached");
14275 $(document).on("heartbeat-send", (...args) => {
14276 const data = args[1];
14277 if (data) {
14278 data[HEARTBEAT_FIELD$1] = store$3.state.seenTs;
14279 }
14280 });
14281 $(document).on("heartbeat-tick", (...args) => {
14282 const response = args[1];
14283 const block = response?.desktop_mode_recycle_bin;
14284 log("heartbeat-tick", { hasBlock: !!block, block });
14285 if (!block) {
14286 return;
14287 }
14288 if (typeof block.ts === "number" && block.ts > store$3.state.seenTs) {
14289 store$3.state.seenTs = block.ts;
14290 }
14291 if (typeof block.count === "number") {
14292 setRecycleBinBadge(block.count);
14293 }
14294 });
14295 }
14296 async function refetchCount() {
14297 if (!store$3.state.countUrl) {
14298 log("refetchCount: no countUrl, skip");
14299 return;
14300 }
14301 log("refetchCount: hitting", store$3.state.countUrl);
14302 try {
14303 const response = await fetch(store$3.state.countUrl, {
14304 credentials: "same-origin",
14305 headers: { Accept: "application/json" }
14306 });
14307 if (!response.ok) {
14308 warn("refetchCount: non-OK", response.status, response.statusText);
14309 return;
14310 }
14311 const json = await response.json();
14312 log("refetchCount: response", json);
14313 if (typeof json.count === "number") {
14314 setRecycleBinBadge(json.count);
14315 }
14316 } catch (err) {
14317 warn("refetchCount: fetch failed", err);
14318 }
14319 }
14320 const OS_SETTINGS_ID = "desktop-mode-os-settings";
14321 const RECYCLE_BIN_ID = "desktop-mode-recycle-bin";
14322 function registerBuiltInPeekRenderers(opts) {
14323 const wpHooks = getWpHooks();
14324 if (!wpHooks) {
14325 return;
14326 }
14327 wpHooks.addFilter(
14328 "desktop-mode.dock.peek-card-content",
14329 "desktop-mode/built-in-peek-renderers",
14330 (body, ctx) => {
14331 const context = ctx;
14332 const id = context.window.id;
14333 if (id === OS_SETTINGS_ID) {
14334 return renderOsSettings();
14335 }
14336 if (id === RECYCLE_BIN_ID) {
14337 return renderRecycleBin(context, opts.getRecycleBinCount);
14338 }
14339 return body;
14340 }
14341 );
14342 }
14343 function renderOsSettings(_ctx) {
14344 const root = document.createElement("span");
14345 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--os-settings";
14346 root.setAttribute("aria-hidden", "true");
14347 const hero = document.createElement("span");
14348 hero.className = "desktop-mode-dock-peek__os-hero dashicons dashicons-admin-generic";
14349 root.appendChild(hero);
14350 const subtitle = document.createElement("span");
14351 subtitle.className = "desktop-mode-dock-peek__os-subtitle";
14352 subtitle.textContent = __("System Preferences");
14353 root.appendChild(subtitle);
14354 const tabs = document.createElement("span");
14355 tabs.className = "desktop-mode-dock-peek__os-tabs";
14356 for (const cls of [
14357 "dashicons-art",
14358 "dashicons-admin-customizer",
14359 "dashicons-editor-help"
14360 ]) {
14361 const tab = document.createElement("span");
14362 tab.className = `desktop-mode-dock-peek__os-tab dashicons ${cls}`;
14363 tabs.appendChild(tab);
14364 }
14365 root.appendChild(tabs);
14366 return root;
14367 }
14368 function renderRecycleBin(_ctx, getCount) {
14369 const root = document.createElement("span");
14370 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--recycle-bin";
14371 root.setAttribute("aria-hidden", "true");
14372 const count = Math.max(0, Math.floor(getCount() || 0));
14373 root.dataset.empty = count === 0 ? "true" : "false";
14374 const stage = document.createElement("span");
14375 stage.className = "desktop-mode-dock-peek__bin-stage";
14376 const stack = document.createElement("span");
14377 stack.className = "desktop-mode-dock-peek__bin-stack";
14378 for (let i = 0; i < 3; i++) {
14379 const slip = document.createElement("span");
14380 slip.className = "desktop-mode-dock-peek__bin-slip";
14381 stack.appendChild(slip);
14382 }
14383 stage.appendChild(stack);
14384 const icon = document.createElement("span");
14385 icon.className = `desktop-mode-dock-peek__bin-icon dashicons ${count === 0 ? "dashicons-trash" : "dashicons-trash"}`;
14386 stage.appendChild(icon);
14387 root.appendChild(stage);
14388 const label = document.createElement("span");
14389 label.className = "desktop-mode-dock-peek__bin-label";
14390 if (count === 0) {
14391 label.textContent = __("Recycle Bin — empty");
14392 } else if (count === 1) {
14393 label.textContent = __("1 item");
14394 } else if (count > 99) {
14395 label.textContent = "99+ items";
14396 } else {
14397 label.textContent = `${count} items`;
14398 }
14399 root.appendChild(label);
14400 return root;
14401 }
14402 function getWpHooks() {
14403 const wp = window.wp;
14404 return wp?.hooks ?? null;
14405 }
14406 const BUG_REPORT_WINDOW_ID = "desktop-mode-bug-report";
14407 const REPO_OWNER = "WordPress";
14408 const REPO_NAME = "desktop-mode";
14409 const MAX_BODY_LENGTH = 6e3;
14410 function renderBugReport(body) {
14411 body.classList.add("desktop-mode-bug-report");
14412 body.replaceChildren();
14413 const form = document.createElement("form");
14414 form.className = "desktop-mode-bug-report__form";
14415 form.setAttribute("novalidate", "");
14416 const intro = document.createElement("p");
14417 intro.className = "desktop-mode-bug-report__intro";
14418 intro.textContent = __(
14419 "Found a bug or have a feature idea? Fill this in and we will open a pre-filled GitHub issue for you to review and submit."
14420 );
14421 form.appendChild(intro);
14422 form.appendChild(buildTypeField());
14423 form.appendChild(buildTextField("title", __("Title"), {
14424 placeholder: __("A short summary"),
14425 required: true
14426 }));
14427 form.appendChild(buildTextareaField("description", __("What happened? What did you expect?"), {
14428 placeholder: __("Describe the issue or the feature you have in mind."),
14429 rows: 5,
14430 required: true
14431 }));
14432 form.appendChild(buildTextareaField("steps", __("Steps to reproduce (bug only)"), {
14433 placeholder: __("One step per line"),
14434 rows: 4
14435 }));
14436 const meta = buildMetadataPreview();
14437 form.appendChild(meta);
14438 const actions = document.createElement("div");
14439 actions.className = "desktop-mode-bug-report__actions";
14440 const submit = document.createElement("button");
14441 submit.type = "submit";
14442 submit.className = "desktop-mode-bug-report__submit";
14443 submit.textContent = __("Open issue on GitHub");
14444 actions.appendChild(submit);
14445 const hint = document.createElement("span");
14446 hint.className = "desktop-mode-bug-report__hint";
14447 hint.textContent = __("You will review and submit on GitHub.");
14448 actions.appendChild(hint);
14449 form.appendChild(actions);
14450 form.addEventListener("submit", (e) => {
14451 e.preventDefault();
14452 const state2 = readFormState(form);
14453 if (!state2.title.trim() || !state2.description.trim()) {
14454 showInlineError(form, __("Title and description are both required."));
14455 return;
14456 }
14457 const url = buildGithubIssueUrl(state2);
14458 window.open(url, "_blank", "noopener");
14459 });
14460 body.appendChild(form);
14461 }
14462 function buildTypeField() {
14463 const wrap = document.createElement("div");
14464 wrap.className = "desktop-mode-bug-report__field desktop-mode-bug-report__field--type";
14465 const label = document.createElement("span");
14466 label.className = "desktop-mode-bug-report__label";
14467 label.textContent = __("Type");
14468 wrap.appendChild(label);
14469 const group = document.createElement("div");
14470 group.className = "desktop-mode-bug-report__radio-group";
14471 group.setAttribute("role", "radiogroup");
14472 const options = [
14473 { value: "bug", label: __("Bug"), checked: true },
14474 { value: "feature", label: __("Feature request") },
14475 { value: "question", label: __("Question") }
14476 ];
14477 for (const opt of options) {
14478 const radioLabel = document.createElement("label");
14479 radioLabel.className = "desktop-mode-bug-report__radio";
14480 const input = document.createElement("input");
14481 input.type = "radio";
14482 input.name = "type";
14483 input.value = opt.value;
14484 if (opt.checked) {
14485 input.checked = true;
14486 }
14487 radioLabel.appendChild(input);
14488 const text = document.createElement("span");
14489 text.textContent = opt.label;
14490 radioLabel.appendChild(text);
14491 group.appendChild(radioLabel);
14492 }
14493 wrap.appendChild(group);
14494 return wrap;
14495 }
14496 function buildTextField(name, labelText, opts = {}) {
14497 const wrap = document.createElement("div");
14498 wrap.className = "desktop-mode-bug-report__field";
14499 const label = document.createElement("label");
14500 label.className = "desktop-mode-bug-report__label";
14501 label.textContent = labelText;
14502 wrap.appendChild(label);
14503 const input = document.createElement("input");
14504 input.type = "text";
14505 input.name = name;
14506 input.className = "desktop-mode-bug-report__input";
14507 if (opts.placeholder) {
14508 input.placeholder = opts.placeholder;
14509 }
14510 if (opts.required) {
14511 input.setAttribute("aria-required", "true");
14512 }
14513 label.appendChild(input);
14514 return wrap;
14515 }
14516 function buildTextareaField(name, labelText, opts = {}) {
14517 const wrap = document.createElement("div");
14518 wrap.className = "desktop-mode-bug-report__field";
14519 const label = document.createElement("label");
14520 label.className = "desktop-mode-bug-report__label";
14521 label.textContent = labelText;
14522 wrap.appendChild(label);
14523 const textarea = document.createElement("textarea");
14524 textarea.name = name;
14525 textarea.className = "desktop-mode-bug-report__textarea";
14526 textarea.rows = opts.rows ?? 4;
14527 if (opts.placeholder) {
14528 textarea.placeholder = opts.placeholder;
14529 }
14530 if (opts.required) {
14531 textarea.setAttribute("aria-required", "true");
14532 }
14533 label.appendChild(textarea);
14534 return wrap;
14535 }
14536 function buildMetadataPreview() {
14537 const details = document.createElement("details");
14538 details.className = "desktop-mode-bug-report__metadata";
14539 const summary = document.createElement("summary");
14540 summary.textContent = __("Environment included with the report");
14541 details.appendChild(summary);
14542 const pre = document.createElement("pre");
14543 pre.className = "desktop-mode-bug-report__metadata-body";
14544 pre.textContent = formatMetadata(collectMetadata());
14545 details.appendChild(pre);
14546 return details;
14547 }
14548 function showInlineError(form, msg) {
14549 let banner = form.querySelector(".desktop-mode-bug-report__error");
14550 if (!banner) {
14551 banner = document.createElement("div");
14552 banner.className = "desktop-mode-bug-report__error";
14553 banner.setAttribute("role", "alert");
14554 form.prepend(banner);
14555 }
14556 banner.textContent = msg;
14557 }
14558 function readFormState(form) {
14559 const data = new FormData(form);
14560 return {
14561 type: data.get("type") ?? "bug",
14562 title: data.get("title") ?? "",
14563 description: data.get("description") ?? "",
14564 steps: data.get("steps") ?? ""
14565 };
14566 }
14567 function buildGithubIssueUrl(state2) {
14568 const labels = labelsForType(state2.type);
14569 const body = composeIssueBody(state2);
14570 const params = new URLSearchParams();
14571 params.set("title", state2.title.trim());
14572 params.set("body", body);
14573 if (labels.length) {
14574 params.set("labels", labels.join(","));
14575 }
14576 return `https://github.com/${REPO_OWNER}/${REPO_NAME}/issues/new?${params.toString()}`;
14577 }
14578 function labelsForType(type) {
14579 switch (type) {
14580 case "bug":
14581 return ["bug"];
14582 case "feature":
14583 return ["enhancement"];
14584 case "question":
14585 return ["question"];
14586 default:
14587 return [];
14588 }
14589 }
14590 function composeIssueBody(state2) {
14591 const parts = [];
14592 parts.push(state2.description.trim());
14593 if (state2.type === "bug" && state2.steps.trim()) {
14594 parts.push("");
14595 parts.push("## Steps to reproduce");
14596 parts.push("");
14597 parts.push(state2.steps.trim());
14598 }
14599 parts.push("");
14600 parts.push("<details><summary>Environment</summary>");
14601 parts.push("");
14602 parts.push("```");
14603 parts.push(formatMetadata(collectMetadata()));
14604 parts.push("```");
14605 parts.push("");
14606 parts.push("</details>");
14607 let out = parts.join("\n");
14608 if (out.length > MAX_BODY_LENGTH) {
14609 out = out.slice(0, MAX_BODY_LENGTH) + "\n\n…(truncated to fit GitHub URL length limit)";
14610 }
14611 return out;
14612 }
14613 function collectMetadata() {
14614 const cfg = window.wp?.desktop?.config;
14615 return {
14616 pluginVersion: cfg?.pluginVersion ?? "unknown",
14617 wordpressVersion: cfg?.wordpressVersion ?? "unknown",
14618 userAgent: navigator.userAgent,
14619 viewport: `${window.innerWidth}x${window.innerHeight}`,
14620 platform: navigator.platform || "unknown",
14621 currentUrl: window.location.href
14622 };
14623 }
14624 function formatMetadata(m) {
14625 return [
14626 `Plugin version: ${m.pluginVersion}`,
14627 `WordPress version: ${m.wordpressVersion}`,
14628 `User agent: ${m.userAgent}`,
14629 `Viewport: ${m.viewport}`,
14630 `Platform: ${m.platform}`,
14631 `Current URL: ${m.currentUrl}`
14632 ].join("\n");
14633 }
14634 let _config = null;
14635 let _state = {
14636 installHintDismissed: false,
14637 notificationsEnabled: false
14638 };
14639 const _listeners = /* @__PURE__ */ new Set();
14640 function initPwaState(config) {
14641 if (!config) {
14642 _config = null;
14643 return;
14644 }
14645 _config = config;
14646 _state = { ...config.state };
14647 notify$4();
14648 }
14649 function getPwaState() {
14650 return { ..._state };
14651 }
14652 function updatePwaState(patch) {
14653 _state = { ..._state, ...patch };
14654 notify$4();
14655 if (!_config) {
14656 return getPwaState();
14657 }
14658 const body = JSON.stringify(patch);
14659 const nonce = readRestNonce$2();
14660 void fetch(_config.stateUrl, {
14661 method: "POST",
14662 credentials: "same-origin",
14663 headers: {
14664 "Content-Type": "application/json",
14665 ...nonce ? { "X-WP-Nonce": nonce } : {}
14666 },
14667 body
14668 }).catch((err) => {
14669 if (typeof console !== "undefined") {
14670 console.warn("[desktop-mode] pwa-state write failed:", err);
14671 }
14672 });
14673 return getPwaState();
14674 }
14675 function subscribePwaState(cb) {
14676 _listeners.add(cb);
14677 return () => {
14678 _listeners.delete(cb);
14679 };
14680 }
14681 function notify$4() {
14682 const snapshot = getPwaState();
14683 for (const cb of Array.from(_listeners)) {
14684 try {
14685 cb(snapshot);
14686 } catch (err) {
14687 if (typeof console !== "undefined") {
14688 console.error(
14689 "[desktop-mode] pwa-state listener threw:",
14690 err
14691 );
14692 }
14693 }
14694 }
14695 }
14696 function readRestNonce$2() {
14697 const cfg = window.desktopModeConfig;
14698 return cfg?.restNonce ?? "";
14699 }
14700 const state = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
14701 __proto__: null,
14702 getPwaState,
14703 initPwaState,
14704 subscribePwaState,
14705 updatePwaState
14706 }, Symbol.toStringTag, { value: "Module" }));
14707 let _registration = null;
14708 let _registrationFailed = false;
14709 let _controllerChangeBound = false;
14710 let _reloadingForSwUpdate = false;
14711 let _status = "pending";
14712 function bindControllerChangeReload() {
14713 if (_controllerChangeBound) {
14714 return;
14715 }
14716 _controllerChangeBound = true;
14717 const hadInitialController = !!navigator.serviceWorker.controller;
14718 navigator.serviceWorker.addEventListener("controllerchange", () => {
14719 if (!hadInitialController) {
14720 return;
14721 }
14722 if (_reloadingForSwUpdate) {
14723 return;
14724 }
14725 if (wasRecentlyReloadedForSwUpdate()) {
14726 return;
14727 }
14728 markReloadedForSwUpdate();
14729 _reloadingForSwUpdate = true;
14730 setTimeout(() => window.location.reload(), 0);
14731 });
14732 }
14733 const SW_RELOAD_THROTTLE_KEY = "wpd-sw-reload-ts";
14734 const SW_RELOAD_THROTTLE_MS = 3e4;
14735 function wasRecentlyReloadedForSwUpdate() {
14736 try {
14737 const raw = sessionStorage.getItem(SW_RELOAD_THROTTLE_KEY);
14738 const last = raw ? Number.parseInt(raw, 10) : 0;
14739 if (!Number.isFinite(last) || last <= 0) {
14740 return false;
14741 }
14742 return Date.now() - last < SW_RELOAD_THROTTLE_MS;
14743 } catch {
14744 return false;
14745 }
14746 }
14747 function markReloadedForSwUpdate() {
14748 try {
14749 sessionStorage.setItem(SW_RELOAD_THROTTLE_KEY, String(Date.now()));
14750 } catch {
14751 }
14752 }
14753 async function registerServiceWorker(config, options = {}) {
14754 if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
14755 _status = "unsupported";
14756 return null;
14757 }
14758 if (!config?.swUrl) {
14759 _status = "unsupported";
14760 return null;
14761 }
14762 if (!window.isSecureContext) {
14763 _status = "unsupported";
14764 return null;
14765 }
14766 if (_registration || _registrationFailed) {
14767 return _registration;
14768 }
14769 if (!options.forceReplace) {
14770 const existing = await navigator.serviceWorker.getRegistrations().catch(() => []);
14771 const foreign = existing.find((reg) => {
14772 const url = reg.active?.scriptURL ?? reg.installing?.scriptURL ?? "";
14773 return url !== "" && url !== config.swUrl;
14774 });
14775 if (foreign) {
14776 _status = "foreign-sw";
14777 if (typeof console !== "undefined") {
14778 console.warn(
14779 "[desktop-mode] another service worker is already registered (" + foreign.scope + "); skipping desktop-mode SW. Set desktop_mode_pwa_force_replace_sw=true to override."
14780 );
14781 }
14782 return null;
14783 }
14784 }
14785 try {
14786 _registration = await navigator.serviceWorker.register(config.swUrl, {
14787 scope: "/",
14788 updateViaCache: "none"
14789 });
14790 _status = "registered";
14791 bindControllerChangeReload();
14792 return _registration;
14793 } catch (err) {
14794 _registrationFailed = true;
14795 _status = "failed";
14796 if (typeof console !== "undefined") {
14797 console.warn("[desktop-mode] SW registration failed:", err);
14798 }
14799 return null;
14800 }
14801 }
14802 function getSwRegistrationStatus() {
14803 return _status;
14804 }
14805 const PWA_INSTALL_TILE_ID = "desktop-mode-pwa-install";
14806 function isStandaloneDisplay() {
14807 if (typeof window === "undefined") {
14808 return false;
14809 }
14810 if (window.matchMedia?.("(display-mode: standalone)").matches) {
14811 return true;
14812 }
14813 const nav = window.navigator;
14814 return nav.standalone === true;
14815 }
14816 async function isLikelyInstalled() {
14817 if (isStandaloneDisplay()) {
14818 return true;
14819 }
14820 const nav = window.navigator;
14821 if (typeof nav.getInstalledRelatedApps !== "function") {
14822 return false;
14823 }
14824 try {
14825 const apps = await nav.getInstalledRelatedApps();
14826 return Array.isArray(apps) && apps.length > 0;
14827 } catch {
14828 return false;
14829 }
14830 }
14831 let _deferred = null;
14832 function installPwaInstallAffordance(siteName, showToast2) {
14833 if (typeof window === "undefined") {
14834 return;
14835 }
14836 window.removeEventListener(
14837 "beforeinstallprompt",
14838 _handleBeforeInstall
14839 );
14840 window.addEventListener(
14841 "beforeinstallprompt",
14842 _handleBeforeInstall
14843 );
14844 window.removeEventListener("appinstalled", _handleAppInstalled);
14845 window.addEventListener("appinstalled", _handleAppInstalled);
14846 function _handleBeforeInstall(ev) {
14847 ev.preventDefault();
14848 _deferred = ev;
14849 }
14850 function _handleAppInstalled() {
14851 _deferred = null;
14852 showToast2({
14853 message: sprintf(
14854 /* translators: %s: site name */
14855 __("Installed %s as an app."),
14856 siteName
14857 )
14858 });
14859 }
14860 }
14861 function getInstallTileDef(siteName, showToast2) {
14862 return {
14863 id: PWA_INSTALL_TILE_ID,
14864 title: sprintf(
14865 /* translators: %s: site name */
14866 __("Install %s as an app"),
14867 siteName
14868 ),
14869 // Dashicons class — the dock renderer prefers Dashicons
14870 // strings. `dashicons-download` is the closest match for
14871 // "install" in the WordPress glyph set without shipping
14872 // bespoke artwork.
14873 icon: "dashicons-download",
14874 onOpen: () => {
14875 void onTileClick(siteName, showToast2);
14876 }
14877 };
14878 }
14879 async function onTileClick(siteName, showToast2) {
14880 if (_deferred) {
14881 const event = _deferred;
14882 _deferred = null;
14883 try {
14884 await event.prompt();
14885 const choice = await event.userChoice;
14886 if (choice.outcome === "dismissed") {
14887 showToast2({
14888 message: __("Install cancelled.")
14889 });
14890 }
14891 } catch (err) {
14892 if (typeof console !== "undefined") {
14893 console.warn(
14894 "[desktop-mode] install prompt failed:",
14895 err
14896 );
14897 }
14898 }
14899 return;
14900 }
14901 if (await isLikelyInstalled()) {
14902 showToast2({
14903 message: sprintf(
14904 /* translators: %s: site name */
14905 __(
14906 "%s is already installed. Open it from your apps menu or home screen."
14907 ),
14908 siteName
14909 )
14910 });
14911 return;
14912 }
14913 if (getSwRegistrationStatus() === "foreign-sw") {
14914 showToast2({
14915 message: __(
14916 "Install isn't available — another plugin's service worker is active on this site. A site admin can opt in by setting the desktop_mode_pwa_force_replace_sw filter to true."
14917 )
14918 });
14919 return;
14920 }
14921 showToast2({
14922 message: __(
14923 "Install isn't available right now. Keep using the page; if it still doesn't appear, the app may already be installed in this browser."
14924 )
14925 });
14926 }
14927 async function promptInstall() {
14928 if (!_deferred) {
14929 return "unavailable";
14930 }
14931 const event = _deferred;
14932 _deferred = null;
14933 try {
14934 await event.prompt();
14935 const choice = await event.userChoice;
14936 return choice.outcome;
14937 } catch {
14938 return "unavailable";
14939 }
14940 }
14941 function undismissInstallHint() {
14942 Promise.resolve().then(() => state).then((m) => {
14943 m.updatePwaState({ installHintDismissed: false });
14944 });
14945 }
14946 function notify$3(options) {
14947 const intent = activity.filter(
14948 "desktop-mode/notification-requested",
14949 { ...options }
14950 );
14951 if (!intent || intent.cancel === true || !intent.title) {
14952 return () => void 0;
14953 }
14954 let dismissed = false;
14955 let dismissNative = null;
14956 let dismissToast = null;
14957 const dismiss = () => {
14958 if (dismissed) {
14959 return;
14960 }
14961 dismissed = true;
14962 if (dismissNative) {
14963 dismissNative();
14964 }
14965 if (dismissToast) {
14966 dismissToast();
14967 }
14968 };
14969 const fallback = () => {
14970 dismissToast = showToast({
14971 message: intent.body ? intent.title + " — " + intent.body : intent.title
14972 });
14973 activity.publish("desktop-mode/notification-shown", {
14974 ...intent,
14975 fallback: "toast"
14976 });
14977 };
14978 if (typeof window === "undefined" || typeof Notification === "undefined") {
14979 fallback();
14980 return dismiss;
14981 }
14982 const perm = Notification.permission;
14983 if (perm === "granted") {
14984 dismissNative = renderNative(intent);
14985 return dismiss;
14986 }
14987 if (perm === "denied") {
14988 fallback();
14989 return dismiss;
14990 }
14991 void Notification.requestPermission().then((result) => {
14992 if (dismissed) {
14993 return;
14994 }
14995 if (result === "granted") {
14996 updatePwaState({ notificationsEnabled: true });
14997 dismissNative = renderNative(intent);
14998 return;
14999 }
15000 fallback();
15001 });
15002 return dismiss;
15003 }
15004 function renderNative(intent) {
15005 let n = null;
15006 try {
15007 n = new Notification(intent.title, {
15008 body: intent.body,
15009 icon: intent.icon,
15010 tag: intent.tag,
15011 requireInteraction: intent.requireInteraction
15012 });
15013 } catch (err) {
15014 if (typeof console !== "undefined") {
15015 console.warn("[desktop-mode] Notification ctor threw:", err);
15016 }
15017 return () => void 0;
15018 }
15019 if (intent.onClick) {
15020 const handler = intent.onClick;
15021 n.onclick = () => {
15022 try {
15023 handler(n);
15024 } catch (hErr) {
15025 if (typeof console !== "undefined") {
15026 console.error(
15027 "[desktop-mode] notification onClick threw:",
15028 hErr
15029 );
15030 }
15031 }
15032 };
15033 }
15034 activity.publish("desktop-mode/notification-shown", {
15035 ...intent,
15036 fallback: null
15037 });
15038 return () => {
15039 if (n) {
15040 n.close();
15041 }
15042 };
15043 }
15044 async function requestNotificationPermission() {
15045 if (typeof Notification === "undefined") {
15046 return "unsupported";
15047 }
15048 if (Notification.permission !== "default") {
15049 return Notification.permission;
15050 }
15051 const result = await Notification.requestPermission();
15052 if (result === "granted") {
15053 updatePwaState({ notificationsEnabled: true });
15054 }
15055 return result;
15056 }
15057 function getNotificationPermission() {
15058 if (typeof Notification === "undefined") {
15059 return "unsupported";
15060 }
15061 return Notification.permission;
15062 }
15063 function bootstrapPwa(config, showToast2) {
15064 if (!config.pwa) {
15065 return;
15066 }
15067 initPwaState(config.pwa);
15068 installPwaInstallAffordance(
15069 config.pwa.appName || "WordPress",
15070 showToast2
15071 );
15072 void registerServiceWorker(config.pwa, {
15073 forceReplace: !!config.pwa.forceReplaceSw
15074 });
15075 }
15076 const DRAG_BRIDGE_EVENTS = {
15077 START: "desktop-mode-cross-frame-drag-start",
15078 END: "desktop-mode-cross-frame-drag-end"
15079 };
15080 function isStart(m) {
15081 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-start" && !!m.payload && typeof m.payload === "object";
15082 }
15083 function isEnd(m) {
15084 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-end";
15085 }
15086 function isPayloadRequest(m) {
15087 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-payload-request";
15088 }
15089 class DragBridge {
15090 constructor() {
15091 this._payload = null;
15092 this._onMessage = (e) => {
15093 if (e.origin !== this._origin) {
15094 return;
15095 }
15096 const msg = e.data;
15097 if (isStart(msg)) {
15098 this._startDrag(msg.payload);
15099 return;
15100 }
15101 if (isEnd(msg)) {
15102 this._endDrag();
15103 return;
15104 }
15105 if (isPayloadRequest(msg) && this._payload && e.source) {
15106 try {
15107 e.source.postMessage(
15108 { type: "desktop-mode-drag-payload", payload: this._payload },
15109 this._origin
15110 );
15111 } catch {
15112 }
15113 }
15114 };
15115 this._origin = window.location.origin;
15116 window.addEventListener("message", this._onMessage);
15117 }
15118 getPayload() {
15119 return this._payload;
15120 }
15121 isDragging() {
15122 return this._payload !== null;
15123 }
15124 start(payload) {
15125 if (this._payload === payload) {
15126 return;
15127 }
15128 this._startDrag(payload);
15129 }
15130 end() {
15131 this._endDrag();
15132 }
15133 _startDrag(payload) {
15134 this._payload = payload;
15135 document.dispatchEvent(
15136 new CustomEvent(DRAG_BRIDGE_EVENTS.START, { detail: { payload } })
15137 );
15138 }
15139 _endDrag() {
15140 if (this._payload === null) {
15141 return;
15142 }
15143 const payload = this._payload;
15144 this._payload = null;
15145 document.dispatchEvent(
15146 new CustomEvent(DRAG_BRIDGE_EVENTS.END, { detail: { payload } })
15147 );
15148 }
15149 }
15150 class DropTargetRegistry {
15151 constructor() {
15152 this._targets = /* @__PURE__ */ new Map();
15153 this._byElement = /* @__PURE__ */ new Map();
15154 }
15155 register(target) {
15156 const prev = this._targets.get(target.id);
15157 if (prev) {
15158 this._byElement.delete(prev.element);
15159 }
15160 this._targets.set(target.id, target);
15161 this._byElement.set(target.element, target);
15162 return () => {
15163 const cur = this._targets.get(target.id);
15164 if (cur === target) {
15165 this._targets.delete(target.id);
15166 this._byElement.delete(target.element);
15167 }
15168 };
15169 }
15170 list() {
15171 return Array.from(this._targets.values());
15172 }
15173 clear() {
15174 this._targets.clear();
15175 this._byElement.clear();
15176 }
15177 /**
15178 * Find the deepest registered target whose element is `el` or an
15179 * ancestor of `el`. Walks the DOM tree once (O(depth)).
15180 *
15181 * Window claim boundary: if the walk crosses a `.desktop-mode-window`
15182 * element BEFORE finding a registered target, hit-testing stops
15183 * there and returns null. This is the rule that makes "drag over
15184 * a Gutenberg admin window" produce reject feedback instead of
15185 * silently routing the drop to the wallpaper canvas underneath.
15186 *
15187 * A window can opt INTO accepting drops by registering a target
15188 * on its own body (e.g. Recycle Bin's `[data-desktop-mode-recycle-bin-root]`):
15189 * since that element sits inside the window, the walk hits it
15190 * before reaching the window boundary and the body's target wins.
15191 */
15192 hitTest(el) {
15193 let cur = el;
15194 while (cur) {
15195 if (cur instanceof HTMLElement) {
15196 const t = this._byElement.get(cur);
15197 if (t) {
15198 return t;
15199 }
15200 if (cur.classList.contains("desktop-mode-window")) {
15201 return null;
15202 }
15203 }
15204 cur = cur.parentElement;
15205 }
15206 return null;
15207 }
15208 /**
15209 * Convenience: pick the target at viewport `(clientX, clientY)`.
15210 * Caller is responsible for hiding any obscuring ghost element
15211 * before calling — see `GhostHandle.withHidden()`.
15212 */
15213 hitTestPoint(clientX, clientY) {
15214 const el = document.elementFromPoint(clientX, clientY);
15215 const target = this.hitTest(el);
15216 return { target, element: el, accepted: false };
15217 }
15218 }
15219 const GHOST_CLASS = "desktop-mode-drag-ghost";
15220 const GHOST_ACCEPT_CLASS = "desktop-mode-drag-ghost--accept";
15221 const GHOST_REJECT_CLASS = "desktop-mode-drag-ghost--reject";
15222 const HINT_CLASS = "desktop-mode-drag-hint";
15223 const HINT_ACCEPT_CLASS = "desktop-mode-drag-hint--accept";
15224 const HINT_REJECT_CLASS = "desktop-mode-drag-hint--reject";
15225 const HINT_NEUTRAL_CLASS = "desktop-mode-drag-hint--neutral";
15226 const HINT_OFFSET_X = 16;
15227 const HINT_OFFSET_Y = 18;
15228 function mountGhost(payload, clientX, clientY) {
15229 const ghost = buildGhost(payload);
15230 const offsetX = payload.ghost?.offsetX ?? defaultOffsetX(payload.source);
15231 const offsetY = payload.ghost?.offsetY ?? defaultOffsetY(payload.source);
15232 ghost.classList.add(GHOST_CLASS);
15233 ghost.setAttribute("aria-hidden", "true");
15234 ghost.style.position = "fixed";
15235 ghost.style.left = "0";
15236 ghost.style.top = "0";
15237 ghost.style.margin = "0";
15238 ghost.style.pointerEvents = "none";
15239 ghost.style.zIndex = "2147483647";
15240 ghost.style.willChange = "transform";
15241 document.body.appendChild(ghost);
15242 const labels = resolveHintLabels(payload);
15243 const hint = labels ? buildHintChip() : null;
15244 if (hint) {
15245 document.body.appendChild(hint);
15246 }
15247 const handle = {
15248 get element() {
15249 return ghost;
15250 },
15251 moveTo(cx, cy) {
15252 ghost.style.transform = `translate3d(${cx - offsetX}px, ${cy - offsetY}px, 0)`;
15253 if (hint) {
15254 hint.style.transform = `translate3d(${cx + HINT_OFFSET_X}px, ${cy + HINT_OFFSET_Y}px, 0)`;
15255 }
15256 },
15257 setMode(mode) {
15258 ghost.classList.remove(GHOST_ACCEPT_CLASS, GHOST_REJECT_CLASS);
15259 if (mode === "accept") {
15260 ghost.classList.add(GHOST_ACCEPT_CLASS);
15261 } else if (mode === "reject") {
15262 ghost.classList.add(GHOST_REJECT_CLASS);
15263 }
15264 if (hint && labels) {
15265 hint.classList.remove(
15266 HINT_ACCEPT_CLASS,
15267 HINT_REJECT_CLASS,
15268 HINT_NEUTRAL_CLASS
15269 );
15270 if (mode === "accept") {
15271 hint.classList.add(HINT_ACCEPT_CLASS);
15272 hint.textContent = labels.accept;
15273 } else if (mode === "reject") {
15274 hint.classList.add(HINT_REJECT_CLASS);
15275 hint.textContent = labels.reject;
15276 } else {
15277 hint.classList.add(HINT_NEUTRAL_CLASS);
15278 hint.textContent = labels.neutral;
15279 }
15280 hint.hidden = !hint.textContent;
15281 }
15282 },
15283 withHidden(fn) {
15284 const prevG = ghost.style.visibility;
15285 const prevH = hint?.style.visibility ?? "";
15286 ghost.style.visibility = "hidden";
15287 if (hint) {
15288 hint.style.visibility = "hidden";
15289 }
15290 try {
15291 return fn();
15292 } finally {
15293 ghost.style.visibility = prevG;
15294 if (hint) {
15295 hint.style.visibility = prevH;
15296 }
15297 }
15298 },
15299 dispose() {
15300 if (ghost.isConnected) {
15301 ghost.remove();
15302 }
15303 if (hint?.isConnected) {
15304 hint.remove();
15305 }
15306 }
15307 };
15308 handle.moveTo(clientX, clientY);
15309 handle.setMode("neutral");
15310 return handle;
15311 }
15312 function buildHintChip() {
15313 const chip = document.createElement("div");
15314 chip.className = HINT_CLASS;
15315 chip.setAttribute("aria-hidden", "true");
15316 chip.setAttribute("role", "presentation");
15317 chip.style.position = "fixed";
15318 chip.style.left = "0";
15319 chip.style.top = "0";
15320 chip.style.margin = "0";
15321 chip.style.pointerEvents = "none";
15322 chip.style.zIndex = "2147483647";
15323 chip.style.willChange = "transform";
15324 return chip;
15325 }
15326 function resolveHintLabels(payload) {
15327 const cfg = payload.ghost?.hint;
15328 if (cfg?.hidden) {
15329 return null;
15330 }
15331 return {
15332 accept: cfg?.accept ?? defaultAcceptLabel(payload),
15333 reject: cfg?.reject ?? defaultRejectLabel(),
15334 neutral: cfg?.neutral ?? defaultNeutralLabel(payload)
15335 };
15336 }
15337 function defaultAcceptLabel(payload) {
15338 if (payload.type === "shortcut") {
15339 return __("Drop here to create shortcut", "desktop-mode");
15340 }
15341 if (payload.type === "desktop-file") {
15342 return __("Drop here to move", "desktop-mode");
15343 }
15344 return __("Drop here", "desktop-mode");
15345 }
15346 function defaultRejectLabel(_payload) {
15347 return __("Can’t drop here", "desktop-mode");
15348 }
15349 function defaultNeutralLabel(payload) {
15350 if (payload.type === "shortcut") {
15351 return __(
15352 "Drop on the desktop or a folder",
15353 "desktop-mode"
15354 );
15355 }
15356 if (payload.type === "desktop-file") {
15357 return __("Drop in a folder", "desktop-mode");
15358 }
15359 return "";
15360 }
15361 function buildGhost(payload) {
15362 if (payload.ghost?.element) {
15363 return payload.ghost.element;
15364 }
15365 const clone = payload.source.cloneNode(true);
15366 clone.removeAttribute("id");
15367 const rect = payload.source.getBoundingClientRect();
15368 clone.style.width = `${rect.width}px`;
15369 clone.style.height = `${rect.height}px`;
15370 return clone;
15371 }
15372 function defaultOffsetX(source) {
15373 return source.offsetWidth / 2;
15374 }
15375 function defaultOffsetY(source) {
15376 return source.offsetHeight / 2;
15377 }
15378 let _installed$2 = false;
15379 function installRecovery(cancelActive) {
15380 if (_installed$2) {
15381 return;
15382 }
15383 _installed$2 = true;
15384 document.addEventListener("keydown", (e) => {
15385 if (e.key === "Escape") {
15386 cancelActive("escape");
15387 }
15388 });
15389 window.addEventListener("blur", () => {
15390 cancelActive("blur");
15391 });
15392 document.addEventListener("visibilitychange", () => {
15393 if (document.hidden) {
15394 cancelActive("visibility");
15395 }
15396 });
15397 }
15398 const DRAG_THRESHOLD_PX = 4;
15399 const DRAG_EVENTS = {
15400 START: "desktop-mode.drag.start",
15401 MOVE: "desktop-mode.drag.move",
15402 ENTER: "desktop-mode.drag.enter",
15403 LEAVE: "desktop-mode.drag.leave",
15404 REJECTED: "desktop-mode.drag.rejected",
15405 COMMIT: "desktop-mode.drag.commit",
15406 CANCEL: "desktop-mode.drag.cancel",
15407 END: "desktop-mode.drag.end"
15408 };
15409 const SOURCE_DRAGGING_CLASS = "desktop-mode-file-tile--dragging";
15410 const TARGET_DROP_ACTIVE_CLASS = "desktop-mode-file-tile--drop-target";
15411 const TRASH_DROP_ACTIVE_ATTR$1 = "data-desktop-mode-trash-drop-active";
15412 const FILES_DROP_ACTIVE_ATTR = "data-files-drop-active";
15413 const BODY_DRAGGING_ATTR = "data-desktop-mode-dragging";
15414 const BODY_DRAG_TYPE_ATTR = "data-desktop-mode-drag-type";
15415 const BODY_DRAG_MODE_ATTR = "data-desktop-mode-drag-mode";
15416 class DragManager {
15417 constructor() {
15418 this._registry = new DropTargetRegistry();
15419 this._active = null;
15420 this._docListenersAttached = false;
15421 this._lastLiftedEndAt = 0;
15422 this._onPointerMove = (e) => {
15423 const session = this._active;
15424 if (!session || session._pointerId !== e.pointerId) {
15425 return;
15426 }
15427 const dx = e.clientX - session._origin.clientX;
15428 const dy = e.clientY - session._origin.clientY;
15429 if (!session._lifted) {
15430 if (Math.abs(dx) < DRAG_THRESHOLD_PX && Math.abs(dy) < DRAG_THRESHOLD_PX) {
15431 return;
15432 }
15433 this._lift(session, e);
15434 }
15435 if (!session._ghost) {
15436 return;
15437 }
15438 session._ghost.moveTo(e.clientX, e.clientY);
15439 this._updateHover(session, e.clientX, e.clientY);
15440 dispatchOnDocument(DRAG_EVENTS.MOVE, {
15441 payload: session.payload,
15442 clientX: e.clientX,
15443 clientY: e.clientY
15444 });
15445 };
15446 this._onPointerUp = (e) => {
15447 const session = this._active;
15448 if (!session || session._pointerId !== e.pointerId) {
15449 return;
15450 }
15451 if (!session._lifted) {
15452 session._finished = true;
15453 this._active = null;
15454 try {
15455 session._callbacks.onClickOnly?.();
15456 } catch (err) {
15457 console.error("[desktop-mode] drag onClickOnly threw:", err);
15458 }
15459 return;
15460 }
15461 const hit = this._hitTestNow(session, e.clientX, e.clientY);
15462 if (hit && hit.accepted && hit.target) {
15463 this._commit(session, hit.target, e.clientX, e.clientY);
15464 return;
15465 }
15466 this._cancel(session, hit && hit.target ? "rejected" : "no-target");
15467 };
15468 this._onPointerCancel = (e) => {
15469 const session = this._active;
15470 if (!session || session._pointerId !== e.pointerId) {
15471 return;
15472 }
15473 this._cancel(session, "pointercancel");
15474 };
15475 }
15476 start(opts) {
15477 if (this._active) {
15478 return null;
15479 }
15480 if (opts.origin.button !== 0) {
15481 return null;
15482 }
15483 const session = {
15484 payload: opts.payload,
15485 isFinished: () => session._finished,
15486 cancel: (reason) => this._cancel(session, reason ?? "caller"),
15487 _origin: opts.origin,
15488 _pointerId: opts.origin.pointerId,
15489 _lifted: false,
15490 _finished: false,
15491 _callbacks: {
15492 onClickOnly: opts.onClickOnly,
15493 onCancel: opts.onCancel,
15494 onCommit: opts.onCommit
15495 },
15496 _ghost: null,
15497 _currentTarget: null,
15498 _currentAccepted: false
15499 };
15500 this._active = session;
15501 this._ensureDocListeners();
15502 installRecovery((reason) => {
15503 if (this._active) {
15504 this._cancel(this._active, reason);
15505 }
15506 });
15507 return session;
15508 }
15509 registerDropTarget(target) {
15510 return this._registry.register(target);
15511 }
15512 isDragging() {
15513 return this._active !== null && this._active._lifted;
15514 }
15515 /**
15516 * Whether a real (lifted) drag ended within `withinMs` of now.
15517 * Surfaces that bind plain `click` listeners use this to ignore
15518 * the synthesized click that fires after a drop. 500 ms is a
15519 * generous default — browsers fire the click within 10–50 ms of
15520 * pointerup, but plugins may chain post-drag work into a
15521 * `requestAnimationFrame` and call back into a click-driven API.
15522 *
15523 * @public
15524 * @since 0.18.x
15525 */
15526 recentlyEndedDrag(withinMs = 500) {
15527 if (this._lastLiftedEndAt === 0) {
15528 return false;
15529 }
15530 return Date.now() - this._lastLiftedEndAt < withinMs;
15531 }
15532 getActive() {
15533 return this._active;
15534 }
15535 debug() {
15536 return {
15537 findOrphans: () => findOrphans(),
15538 listTargets: () => this._registry.list()
15539 };
15540 }
15541 // -----------------------------------------------------------------
15542 // Internals
15543 // -----------------------------------------------------------------
15544 _ensureDocListeners() {
15545 if (this._docListenersAttached) {
15546 return;
15547 }
15548 this._docListenersAttached = true;
15549 document.addEventListener("pointermove", this._onPointerMove, true);
15550 document.addEventListener("pointerup", this._onPointerUp, true);
15551 document.addEventListener("pointercancel", this._onPointerCancel, true);
15552 }
15553 _lift(session, e) {
15554 session._lifted = true;
15555 session.payload.source.classList.add(SOURCE_DRAGGING_CLASS);
15556 session._ghost = mountGhost(session.payload, e.clientX, e.clientY);
15557 if (typeof document !== "undefined" && document.body) {
15558 document.body.setAttribute(BODY_DRAGGING_ATTR, "");
15559 document.body.setAttribute(
15560 BODY_DRAG_TYPE_ATTR,
15561 String(session.payload.type)
15562 );
15563 document.body.setAttribute(BODY_DRAG_MODE_ATTR, "neutral");
15564 }
15565 dispatchOnDocument(DRAG_EVENTS.START, { payload: session.payload });
15566 }
15567 _hitTestNow(session, clientX, clientY) {
15568 const run = () => {
15569 const el = document.elementFromPoint(clientX, clientY);
15570 const target = this._registry.hitTest(el);
15571 if (!target) {
15572 return { target: null, accepted: false };
15573 }
15574 let accepted = false;
15575 try {
15576 accepted = target.accept(session.payload);
15577 } catch (err) {
15578 console.error("[desktop-mode] drop target accept() threw:", target.id, err);
15579 }
15580 return { target, accepted };
15581 };
15582 if (session._ghost) {
15583 return session._ghost.withHidden(run);
15584 }
15585 return run();
15586 }
15587 _updateHover(session, clientX, clientY) {
15588 const next = this._hitTestNow(session, clientX, clientY);
15589 const prevTarget = session._currentTarget;
15590 if (next.target === prevTarget && next.accepted === session._currentAccepted) {
15591 return;
15592 }
15593 if (prevTarget) {
15594 fireLeave(prevTarget, session);
15595 }
15596 session._currentTarget = next.target;
15597 session._currentAccepted = next.accepted;
15598 let mode;
15599 if (next.target) {
15600 if (next.accepted) {
15601 fireEnter(next.target, session);
15602 session._ghost?.setMode("accept");
15603 mode = "accept";
15604 } else {
15605 session._ghost?.setMode("reject");
15606 dispatchOnDocument(DRAG_EVENTS.REJECTED, {
15607 payload: session.payload,
15608 targetId: next.target.id
15609 });
15610 mode = "reject";
15611 }
15612 } else {
15613 session._ghost?.setMode("reject");
15614 mode = "reject";
15615 }
15616 if (typeof document !== "undefined" && document.body) {
15617 document.body.setAttribute(BODY_DRAG_MODE_ATTR, mode);
15618 }
15619 }
15620 _commit(session, target, clientX, clientY) {
15621 session._finished = true;
15622 this._lastLiftedEndAt = Date.now();
15623 fireLeave(target, session);
15624 this._cleanupDom(session);
15625 const prevActive = this._active;
15626 this._active = null;
15627 try {
15628 void target.onDrop(session, { clientX, clientY });
15629 } catch (err) {
15630 console.error("[desktop-mode] drop target onDrop threw:", target.id, err);
15631 }
15632 try {
15633 session._callbacks.onCommit?.(target);
15634 } catch (err) {
15635 console.error("[desktop-mode] drag onCommit threw:", err);
15636 }
15637 dispatchOnDocument(DRAG_EVENTS.COMMIT, {
15638 payload: session.payload,
15639 targetId: target.id
15640 });
15641 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason: "commit" });
15642 if (this._active === prevActive) {
15643 this._active = null;
15644 }
15645 }
15646 _cancel(session, reason) {
15647 if (session._finished) {
15648 return;
15649 }
15650 session._finished = true;
15651 if (session._lifted) {
15652 this._lastLiftedEndAt = Date.now();
15653 }
15654 if (session._currentTarget) {
15655 fireLeave(session._currentTarget, session);
15656 }
15657 this._cleanupDom(session);
15658 this._active = null;
15659 try {
15660 session._callbacks.onCancel?.(reason);
15661 } catch (err) {
15662 console.error("[desktop-mode] drag onCancel threw:", err);
15663 }
15664 dispatchOnDocument(DRAG_EVENTS.CANCEL, { payload: session.payload, reason });
15665 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason });
15666 }
15667 _cleanupDom(session) {
15668 try {
15669 session.payload.source.classList.remove(SOURCE_DRAGGING_CLASS);
15670 } catch {
15671 }
15672 session._ghost?.dispose();
15673 session._ghost = null;
15674 session._currentTarget = null;
15675 session._currentAccepted = false;
15676 if (typeof document !== "undefined" && document.body) {
15677 document.body.removeAttribute(BODY_DRAGGING_ATTR);
15678 document.body.removeAttribute(BODY_DRAG_TYPE_ATTR);
15679 document.body.removeAttribute(BODY_DRAG_MODE_ATTR);
15680 }
15681 scrubOrphans();
15682 }
15683 }
15684 function dispatchOnDocument(type, detail) {
15685 if (typeof document === "undefined") {
15686 return;
15687 }
15688 document.dispatchEvent(new CustomEvent(type, { detail }));
15689 }
15690 function fireEnter(target, session) {
15691 try {
15692 target.onEnter?.(session);
15693 } catch (err) {
15694 console.error("[desktop-mode] drop target onEnter threw:", target.id, err);
15695 }
15696 dispatchOnDocument(DRAG_EVENTS.ENTER, {
15697 payload: session.payload,
15698 targetId: target.id
15699 });
15700 }
15701 function fireLeave(target, session) {
15702 try {
15703 target.onLeave?.(session);
15704 } catch (err) {
15705 console.error("[desktop-mode] drop target onLeave threw:", target.id, err);
15706 }
15707 dispatchOnDocument(DRAG_EVENTS.LEAVE, {
15708 payload: session.payload,
15709 targetId: target.id
15710 });
15711 }
15712 function findOrphans() {
15713 if (typeof document === "undefined") {
15714 return [];
15715 }
15716 const out = [];
15717 for (const sel of [
15718 `.${SOURCE_DRAGGING_CLASS}`,
15719 `.${TARGET_DROP_ACTIVE_CLASS}`,
15720 `[${TRASH_DROP_ACTIVE_ATTR$1}]`,
15721 `[${FILES_DROP_ACTIVE_ATTR}]`
15722 ]) {
15723 document.querySelectorAll(sel).forEach((el) => out.push(el));
15724 }
15725 return out;
15726 }
15727 function scrubOrphans() {
15728 for (const el of findOrphans()) {
15729 el.classList.remove(SOURCE_DRAGGING_CLASS, TARGET_DROP_ACTIVE_CLASS);
15730 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR$1);
15731 el.removeAttribute(FILES_DROP_ACTIVE_ATTR);
15732 }
15733 }
15734 const TARGET_ID_PREFIX = "desktop-mode-iframe-drop-";
15735 const IFRAME_SELECTOR = "iframe.desktop-mode-window__iframe";
15736 const DROP_ACTIVE_ATTR = "data-desktop-mode-iframe-drop-active";
15737 let _installed$1 = false;
15738 let _dragManager = null;
15739 const _suppressedIframes = /* @__PURE__ */ new Map();
15740 const _activeRegistrations = /* @__PURE__ */ new Map();
15741 function extractBridgePayload(payload) {
15742 if (!payload || typeof payload !== "object") {
15743 return void 0;
15744 }
15745 const obj = payload;
15746 if (obj.type !== "shortcut" && obj.type !== "desktop-file") {
15747 return void 0;
15748 }
15749 const data = obj.data;
15750 return data?.bridgePayload;
15751 }
15752 function postIntoIframe(iframe, msg) {
15753 const w = iframe.contentWindow;
15754 if (!w) {
15755 return;
15756 }
15757 try {
15758 w.postMessage(msg, window.location.origin);
15759 } catch {
15760 }
15761 }
15762 function registerDropTargetFor(dragManager, iframe, target, windowId) {
15763 return dragManager.registerDropTarget({
15764 id: `${TARGET_ID_PREFIX}${windowId}`,
15765 element: target,
15766 accept: (payload) => !!extractBridgePayload(payload),
15767 onEnter: (session) => {
15768 const bridge = extractBridgePayload(session.payload);
15769 if (!bridge) {
15770 return;
15771 }
15772 target.setAttribute(DROP_ACTIVE_ATTR, "");
15773 postIntoIframe(iframe, {
15774 type: "desktop-mode-drag-over",
15775 payload: bridge
15776 });
15777 },
15778 onLeave: () => {
15779 target.removeAttribute(DROP_ACTIVE_ATTR);
15780 postIntoIframe(iframe, { type: "desktop-mode-drag-leave" });
15781 },
15782 onDrop: (session, ev) => {
15783 target.removeAttribute(DROP_ACTIVE_ATTR);
15784 const bridge = extractBridgePayload(session.payload);
15785 if (!bridge) {
15786 return;
15787 }
15788 const rect = iframe.getBoundingClientRect();
15789 postIntoIframe(iframe, {
15790 type: "desktop-mode-drop",
15791 payload: bridge,
15792 position: {
15793 x: ev.clientX - rect.left,
15794 y: ev.clientY - rect.top
15795 }
15796 });
15797 }
15798 });
15799 }
15800 function deriveWindowIdFromIframe(iframe) {
15801 let cur = iframe.parentElement;
15802 while (cur) {
15803 if (cur.id.startsWith("wp-window-")) {
15804 return cur.id.slice("wp-window-".length);
15805 }
15806 cur = cur.parentElement;
15807 }
15808 return `unknown-${Math.random().toString(36).slice(2, 10)}`;
15809 }
15810 function onDragStart(payload) {
15811 const dragManager = _dragManager;
15812 if (!dragManager) {
15813 return;
15814 }
15815 const iframes = document.querySelectorAll(IFRAME_SELECTOR);
15816 const isBridgeable = !!extractBridgePayload(payload);
15817 console.info(
15818 "[desktop-mode] drag-start: suppressing %d iframe(s); bridgeable=%s",
15819 iframes.length,
15820 isBridgeable,
15821 payload
15822 );
15823 iframes.forEach((iframe) => {
15824 if (_suppressedIframes.has(iframe)) {
15825 return;
15826 }
15827 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
15828 iframe.style.pointerEvents = "none";
15829 if (!isBridgeable) {
15830 return;
15831 }
15832 const dropTargetEl = iframe.parentElement;
15833 if (!dropTargetEl) {
15834 return;
15835 }
15836 const windowId = deriveWindowIdFromIframe(iframe);
15837 const deregister = registerDropTargetFor(
15838 dragManager,
15839 iframe,
15840 dropTargetEl,
15841 windowId
15842 );
15843 _activeRegistrations.set(iframe, deregister);
15844 });
15845 }
15846 function onDragEnd() {
15847 _suppressedIframes.forEach((prev, iframe) => {
15848 iframe.style.pointerEvents = prev;
15849 });
15850 _suppressedIframes.clear();
15851 _activeRegistrations.forEach((deregister) => {
15852 try {
15853 deregister();
15854 } catch {
15855 }
15856 });
15857 _activeRegistrations.clear();
15858 }
15859 function installIframeDropTargets(dragManager) {
15860 if (_installed$1) {
15861 return;
15862 }
15863 _installed$1 = true;
15864 _dragManager = dragManager;
15865 document.addEventListener(DRAG_EVENTS.START, (e) => {
15866 const detail = e.detail;
15867 onDragStart(detail?.payload);
15868 });
15869 document.addEventListener(DRAG_EVENTS.END, () => {
15870 onDragEnd();
15871 });
15872 addAction(
15873 HOOKS.WINDOW_CLOSED,
15874 "desktop-mode/drag/iframe-drop-targets-window-close",
15875 () => {
15876 for (const [iframe] of Array.from(_suppressedIframes)) {
15877 if (!iframe.isConnected) {
15878 _suppressedIframes.delete(iframe);
15879 }
15880 }
15881 for (const [iframe, deregister] of Array.from(_activeRegistrations)) {
15882 if (!iframe.isConnected) {
15883 try {
15884 deregister();
15885 } catch {
15886 }
15887 _activeRegistrations.delete(iframe);
15888 }
15889 }
15890 }
15891 );
15892 window.__desktopModeIframeDropDebug = () => ({
15893 installed: _installed$1,
15894 iframesInDom: document.querySelectorAll(IFRAME_SELECTOR).length,
15895 suppressedCount: _suppressedIframes.size,
15896 registeredCount: _activeRegistrations.size,
15897 suppressedIframeIds: Array.from(_suppressedIframes.keys()).map(
15898 deriveWindowIdFromIframe
15899 )
15900 });
15901 }
15902 function collectOpenables() {
15903 const desktop = window.wp?.desktop;
15904 if (!desktop) {
15905 return [];
15906 }
15907 const wm = desktop.windowManager;
15908 const config = desktop.config;
15909 if (!wm || !config) {
15910 return [];
15911 }
15912 const items = [];
15913 const fromMenu = (item, group) => ({
15914 id: item.id,
15915 label: item.title,
15916 description: group,
15917 icon: item.icon,
15918 open: () => wm.open({
15919 id: item.id,
15920 baseId: item.id,
15921 url: item.url,
15922 title: item.title,
15923 icon: item.icon
15924 })
15925 });
15926 for (const item of config.dockItems ?? []) {
15927 items.push(fromMenu(item, "Admin menu"));
15928 }
15929 const filtered = applyFilters(
15930 "desktop-mode.open-command.items",
15931 items
15932 );
15933 return Array.isArray(filtered) ? filtered : items;
15934 }
15935 const openCommand = {
15936 slug: "open",
15937 label: "Open",
15938 description: "Open an admin page or registered window.",
15939 hint: "[window]",
15940 icon: "dashicons-external",
15941 /**
15942 * Suggest matching windows as the user types args. Simple
15943 * case-insensitive substring match against label AND id so
15944 * "add" finds "Add New Post" and "jorvy" finds Jorvy whether
15945 * the plugin listed it with a friendly label or the slug.
15946 */
15947 suggest(args) {
15948 const q = args.trim().toLowerCase();
15949 const list2 = collectOpenables();
15950 const hits = q === "" ? list2 : list2.filter(
15951 (w) => w.label.toLowerCase().includes(q) || w.id.toLowerCase().includes(q)
15952 );
15953 return hits.slice(0, 12).map((w) => ({
15954 value: w.label,
15955 label: w.label,
15956 description: w.description,
15957 icon: w.icon ?? "dashicons-external"
15958 }));
15959 },
15960 run(args, ctx) {
15961 const q = args.trim();
15962 if (!q) {
15963 return "Type the name of a window to open, for example `/open Posts`.";
15964 }
15965 const list2 = collectOpenables();
15966 const ql = q.toLowerCase();
15967 const match = list2.find((w) => w.label.toLowerCase() === ql || w.id.toLowerCase() === ql) ?? list2.find(
15968 (w) => w.label.toLowerCase().includes(ql) || w.id.toLowerCase().includes(ql)
15969 );
15970 if (!match) {
15971 return `No window matching **${q}** — try \`/open\` alone to see available options.`;
15972 }
15973 match.open();
15974 ctx.close();
15975 }
15976 };
15977 function registerBuiltInCommands() {
15978 registerCommand(openCommand);
15979 }
15980 const palettes = [];
15981 const listeners$2 = /* @__PURE__ */ new Set();
15982 function registerPalette(p) {
15983 if (!p || typeof p.id !== "string" || p.id === "") {
15984 return () => {
15985 };
15986 }
15987 if (typeof p.open !== "function" || typeof p.close !== "function" || typeof p.isOpen !== "function") {
15988 return () => {
15989 };
15990 }
15991 const idx = palettes.findIndex((x) => x.id === p.id);
15992 if (idx >= 0) {
15993 palettes[idx] = p;
15994 } else {
15995 palettes.push(p);
15996 }
15997 notify$2();
15998 return () => {
15999 const i = palettes.findIndex((x) => x.id === p.id);
16000 if (i >= 0) {
16001 palettes.splice(i, 1);
16002 notify$2();
16003 }
16004 };
16005 }
16006 function unregisterPalette(id) {
16007 const idx = palettes.findIndex((x) => x.id === id);
16008 if (idx >= 0) {
16009 palettes.splice(idx, 1);
16010 notify$2();
16011 }
16012 }
16013 function listPalettes() {
16014 return palettes.slice();
16015 }
16016 function notify$2() {
16017 for (const cb of Array.from(listeners$2)) {
16018 try {
16019 cb();
16020 } catch (err) {
16021 if (typeof console !== "undefined") {
16022 console.error("[desktop-mode] palette-registry listener threw:", err);
16023 }
16024 }
16025 }
16026 }
16027 function cyclePalettes() {
16028 if (palettes.length === 0) {
16029 return;
16030 }
16031 const cur = palettes.findIndex((p) => {
16032 try {
16033 return p.isOpen();
16034 } catch {
16035 return false;
16036 }
16037 });
16038 if (cur === -1) {
16039 try {
16040 palettes[0].open();
16041 } catch {
16042 }
16043 return;
16044 }
16045 try {
16046 palettes[cur].close();
16047 } catch {
16048 }
16049 const next = cur + 1;
16050 if (next < palettes.length) {
16051 try {
16052 palettes[next].open();
16053 } catch {
16054 }
16055 }
16056 }
16057 function openPaletteOnly(id) {
16058 const target = palettes.find((p) => p.id === id);
16059 if (!target) {
16060 return;
16061 }
16062 for (const p of palettes) {
16063 if (p.id !== id) {
16064 try {
16065 if (p.isOpen()) {
16066 p.close();
16067 }
16068 } catch {
16069 }
16070 }
16071 }
16072 try {
16073 target.open();
16074 } catch {
16075 }
16076 }
16077 let installed$1 = false;
16078 function installPaletteShortcut() {
16079 if (installed$1) {
16080 return;
16081 }
16082 installed$1 = true;
16083 document.addEventListener(
16084 "keydown",
16085 (e) => {
16086 if (!(e.metaKey || e.ctrlKey) || e.key !== "k") {
16087 return;
16088 }
16089 if (e.shiftKey || e.altKey) {
16090 return;
16091 }
16092 e.preventDefault();
16093 e.stopImmediatePropagation();
16094 cyclePalettes();
16095 },
16096 true
16097 );
16098 const origin = window.location.origin;
16099 window.addEventListener("message", (e) => {
16100 if (e.origin !== origin) {
16101 return;
16102 }
16103 const data = e.data;
16104 if (data && data.type === "desktop-mode-palette-cycle") {
16105 cyclePalettes();
16106 }
16107 });
16108 }
16109 const suppliers = /* @__PURE__ */ new Map();
16110 const subscribers = /* @__PURE__ */ new Map();
16111 let booted$2 = false;
16112 const heartbeat = {
16113 contribute(field, supplier) {
16114 suppliers.set(field, supplier);
16115 return () => {
16116 if (suppliers.get(field) === supplier) {
16117 suppliers.delete(field);
16118 }
16119 };
16120 },
16121 subscribe(field, cb) {
16122 let set = subscribers.get(field);
16123 if (!set) {
16124 set = /* @__PURE__ */ new Set();
16125 subscribers.set(field, set);
16126 }
16127 set.add(cb);
16128 return () => {
16129 set.delete(cb);
16130 };
16131 }
16132 };
16133 function bootHeartbeatBus() {
16134 if (booted$2) {
16135 return;
16136 }
16137 booted$2 = true;
16138 const $ = window.jQuery;
16139 if (!$) {
16140 console.warn(
16141 "[desktop-mode/heartbeat] jQuery missing — Heartbeat bus disabled."
16142 );
16143 return;
16144 }
16145 $(document).on("heartbeat-send", (...args) => {
16146 const data = args[1];
16147 if (!data) {
16148 return;
16149 }
16150 for (const [field, supplier] of suppliers) {
16151 try {
16152 data[field] = supplier();
16153 } catch (err) {
16154 console.error(
16155 `[desktop-mode/heartbeat] supplier for "${field}" threw:`,
16156 err
16157 );
16158 }
16159 }
16160 });
16161 $(document).on("heartbeat-tick", (...args) => {
16162 const response = args[1];
16163 if (!response) {
16164 return;
16165 }
16166 for (const [field, set] of subscribers) {
16167 const value = response[field];
16168 if (value === void 0) {
16169 continue;
16170 }
16171 for (const cb of set) {
16172 try {
16173 cb(value);
16174 } catch (err) {
16175 console.error(
16176 `[desktop-mode/heartbeat] subscriber for "${field}" threw:`,
16177 err
16178 );
16179 }
16180 }
16181 }
16182 });
16183 }
16184 const store$2 = createSharedStore(
16185 "desktop-mode/presence",
16186 () => ({ byUser: /* @__PURE__ */ new Map(), serverTimeMs: 0 })
16187 );
16188 const ACTIVE_THRESHOLD_MS = 5 * 60 * 1e3;
16189 let lastInputMs = Date.now();
16190 let booted$1 = false;
16191 function noteUserActivity() {
16192 lastInputMs = Date.now();
16193 }
16194 function applySnapshot(block) {
16195 if (!block || !block.snapshot) {
16196 return;
16197 }
16198 const previous = store$2.state.byUser;
16199 const next = new Map(previous);
16200 const transitions = [];
16201 for (const [rawId, raw] of Object.entries(block.snapshot)) {
16202 const userId = Number(rawId);
16203 if (!Number.isFinite(userId) || userId <= 0) {
16204 continue;
16205 }
16206 const status = raw?.status ?? "offline";
16207 const entry = {
16208 status,
16209 lastSeenMs: Number(raw?.lastSeenMs ?? 0) || 0,
16210 lastActiveMs: Number(raw?.lastActiveMs ?? 0) || 0
16211 };
16212 const old = previous.get(userId);
16213 next.set(userId, entry);
16214 if (!old || old.status !== entry.status) {
16215 transitions.push({
16216 userId,
16217 oldStatus: old ? old.status : null,
16218 newStatus: entry.status,
16219 entry
16220 });
16221 }
16222 }
16223 store$2.state.byUser = next;
16224 if (typeof block.serverTimeMs === "number") {
16225 store$2.state.serverTimeMs = block.serverTimeMs;
16226 }
16227 store$2.notify();
16228 for (const t of transitions) {
16229 const detail = {
16230 userId: t.userId,
16231 oldStatus: t.oldStatus,
16232 newStatus: t.newStatus,
16233 lastSeenMs: t.entry.lastSeenMs,
16234 lastActiveMs: t.entry.lastActiveMs
16235 };
16236 document.dispatchEvent(
16237 new CustomEvent("desktop-mode-presence-changed", { detail })
16238 );
16239 activity.publish("desktop-mode/presence-changed", detail);
16240 }
16241 activity.publish("desktop-mode/presence-snapshot-applied", {
16242 applied: Object.keys(block.snapshot).length,
16243 transitions: transitions.length
16244 });
16245 }
16246 function bootPresenceProbe() {
16247 if (booted$1) {
16248 return;
16249 }
16250 booted$1 = true;
16251 document.addEventListener("pointerdown", noteUserActivity, {
16252 capture: true,
16253 passive: true
16254 });
16255 document.addEventListener("keydown", noteUserActivity, {
16256 capture: true,
16257 passive: true
16258 });
16259 document.addEventListener("visibilitychange", () => {
16260 if (!document.hidden) {
16261 noteUserActivity();
16262 }
16263 });
16264 heartbeat.contribute("desktop_mode_presence_active", () => true);
16265 heartbeat.contribute(
16266 "desktop_mode_user_active",
16267 () => Date.now() - lastInputMs < ACTIVE_THRESHOLD_MS
16268 );
16269 heartbeat.subscribe("desktop_mode_presence", (block) => {
16270 applySnapshot(block);
16271 });
16272 }
16273 function getStatus(userId) {
16274 const entry = store$2.state.byUser.get(userId);
16275 return entry ? entry.status : "offline";
16276 }
16277 function getAll() {
16278 return new Map(store$2.state.byUser);
16279 }
16280 function getEntry(userId) {
16281 return store$2.state.byUser.get(userId) ?? null;
16282 }
16283 function subscribe$1(cb) {
16284 return store$2.subscribe((s) => cb(s));
16285 }
16286 function markActive() {
16287 noteUserActivity();
16288 }
16289 function applyPresenceBatch(updates) {
16290 if (!Array.isArray(updates) || updates.length === 0) {
16291 return;
16292 }
16293 const previous = store$2.state.byUser;
16294 const next = new Map(previous);
16295 const transitions = [];
16296 for (const u of updates) {
16297 const userId = Number(u.userId);
16298 if (!Number.isFinite(userId) || userId <= 0) {
16299 continue;
16300 }
16301 const old = previous.get(userId);
16302 const entry = {
16303 status: u.status,
16304 lastSeenMs: typeof u.lastSeenMs === "number" ? u.lastSeenMs : old?.lastSeenMs ?? 0,
16305 lastActiveMs: typeof u.lastActiveMs === "number" ? u.lastActiveMs : old?.lastActiveMs ?? 0
16306 };
16307 next.set(userId, entry);
16308 if (!old || old.status !== entry.status) {
16309 transitions.push({
16310 userId,
16311 oldStatus: old ? old.status : null,
16312 newStatus: entry.status,
16313 entry
16314 });
16315 }
16316 }
16317 if (transitions.length === 0 && next.size === previous.size) {
16318 return;
16319 }
16320 store$2.state.byUser = next;
16321 store$2.notify();
16322 for (const t of transitions) {
16323 const detail = {
16324 userId: t.userId,
16325 oldStatus: t.oldStatus,
16326 newStatus: t.newStatus,
16327 lastSeenMs: t.entry.lastSeenMs,
16328 lastActiveMs: t.entry.lastActiveMs
16329 };
16330 document.dispatchEvent(
16331 new CustomEvent("desktop-mode-presence-changed", { detail })
16332 );
16333 activity.publish("desktop-mode/presence-changed", detail);
16334 }
16335 activity.publish("desktop-mode/presence-snapshot-applied", {
16336 applied: updates.length,
16337 transitions: transitions.length
16338 });
16339 }
16340 const presenceApi = Object.freeze({
16341 getStatus,
16342 getAll,
16343 getEntry,
16344 subscribe: subscribe$1,
16345 markActive,
16346 applyBatch: applyPresenceBatch
16347 });
16348 const HEARTBEAT_FIELD = "desktop_mode_nonces";
16349 const targets = /* @__PURE__ */ new Map();
16350 let booted = false;
16351 function registerNonceTarget(action, updater) {
16352 if (typeof action !== "string" || action === "") {
16353 return () => {
16354 };
16355 }
16356 let set = targets.get(action);
16357 if (!set) {
16358 set = /* @__PURE__ */ new Set();
16359 targets.set(action, set);
16360 }
16361 set.add(updater);
16362 return () => {
16363 set.delete(updater);
16364 };
16365 }
16366 function bootNonceRefresh() {
16367 if (booted) {
16368 return;
16369 }
16370 booted = true;
16371 heartbeat.subscribe(HEARTBEAT_FIELD, (payload) => {
16372 if (!payload || typeof payload !== "object") {
16373 return;
16374 }
16375 for (const [action, value] of Object.entries(payload)) {
16376 if (typeof value !== "string" || value === "") {
16377 continue;
16378 }
16379 const set = targets.get(action);
16380 if (!set) {
16381 continue;
16382 }
16383 for (const updater of set) {
16384 try {
16385 updater(value);
16386 } catch (err) {
16387 console.error(
16388 `[desktop-mode/nonce-refresh] updater for "${action}" threw:`,
16389 err
16390 );
16391 }
16392 }
16393 }
16394 });
16395 registerShellAndPluginsWindowTargets();
16396 }
16397 function registerShellAndPluginsWindowTargets() {
16398 registerNonceTarget("wp_rest", updateAllRestNonces);
16399 registerNonceTarget("desktop-mode-plugins", (fresh) => {
16400 writeWindowConfigField("desktop-mode-plugins", "ajaxNonce", fresh);
16401 });
16402 registerNonceTarget("updates", (fresh) => {
16403 writeWindowConfigField("desktop-mode-plugins", "updatesNonce", fresh);
16404 });
16405 }
16406 function updateAllRestNonces(fresh) {
16407 const cfg = readShellConfig();
16408 if (cfg && typeof cfg.restNonce === "string") {
16409 cfg.restNonce = fresh;
16410 }
16411 const windowConfigs = readWindowConfigs();
16412 if (!windowConfigs) {
16413 return;
16414 }
16415 for (const blob of Object.values(windowConfigs)) {
16416 if (blob && typeof blob === "object" && typeof blob.restNonce === "string") {
16417 blob.restNonce = fresh;
16418 }
16419 }
16420 }
16421 function writeWindowConfigField(windowId, field, value) {
16422 const blobs = readWindowConfigs();
16423 const blob = blobs?.[windowId];
16424 if (blob && typeof blob === "object") {
16425 blob[field] = value;
16426 }
16427 }
16428 function readShellConfig() {
16429 if (typeof window === "undefined") {
16430 return void 0;
16431 }
16432 return window.desktopModeConfig;
16433 }
16434 function readWindowConfigs() {
16435 if (typeof window === "undefined") {
16436 return void 0;
16437 }
16438 return window.desktopModeWindowConfig;
16439 }
16440 const VIEWPORT_CLAMP_MARGIN = 12;
16441 function findDockEntryForUrl(url, config) {
16442 const windowId = deriveWindowId(url, config.adminUrl);
16443 return (config.dockItems || []).find(
16444 (i) => deriveWindowId(i.url, config.adminUrl) === windowId || (i.submenu || []).some(
16445 (s) => deriveWindowId(s.url, config.adminUrl) === windowId
16446 )
16447 );
16448 }
16449 function clampGeometryToViewport(win, rect) {
16450 const maxW = Math.max(200, rect.width - VIEWPORT_CLAMP_MARGIN * 2);
16451 const maxH = Math.max(200, rect.height - VIEWPORT_CLAMP_MARGIN * 2);
16452 const width = Math.min(win.width, maxW);
16453 const height = Math.min(win.height, maxH);
16454 const maxX = Math.max(0, rect.width - width - VIEWPORT_CLAMP_MARGIN);
16455 const maxY = Math.max(0, rect.height - height - VIEWPORT_CLAMP_MARGIN);
16456 const x = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.x, maxX));
16457 const y = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.y, maxY));
16458 return { x, y, width, height };
16459 }
16460 const INITIAL_ORIGIN$1 = window.location.origin;
16461 function bindTopWindowLinkInterceptor(manager, config) {
16462 document.addEventListener(
16463 "click",
16464 (e) => {
16465 if (e.defaultPrevented) {
16466 return;
16467 }
16468 if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
16469 return;
16470 }
16471 const target = e.target;
16472 const link = target && target.closest ? target.closest("a[href]") : null;
16473 if (!link) {
16474 return;
16475 }
16476 const anchor = link;
16477 const linkTarget = anchor.getAttribute("target");
16478 if (linkTarget && linkTarget !== "" && linkTarget !== "_self") {
16479 return;
16480 }
16481 if (anchor.hasAttribute("download")) {
16482 return;
16483 }
16484 const rawHref = anchor.getAttribute("href");
16485 if (!rawHref || rawHref.charAt(0) === "#") {
16486 return;
16487 }
16488 if (/^(mailto:|tel:|javascript:|data:)/i.test(rawHref)) {
16489 return;
16490 }
16491 let url;
16492 try {
16493 url = new URL(rawHref, window.location.href);
16494 } catch (err) {
16495 if (typeof console !== "undefined") {
16496 console.warn(
16497 "[desktop-mode] Couldn’t parse href; letting the browser handle the click:",
16498 rawHref,
16499 err
16500 );
16501 }
16502 return;
16503 }
16504 if (url.origin !== INITIAL_ORIGIN$1) {
16505 return;
16506 }
16507 let adminPath;
16508 try {
16509 adminPath = new URL(config.adminUrl).pathname;
16510 } catch (err) {
16511 if (typeof console !== "undefined") {
16512 console.error(
16513 "[desktop-mode] config.adminUrl is not a valid URL; falling back to /wp-admin/:",
16514 config.adminUrl,
16515 err
16516 );
16517 }
16518 adminPath = "/wp-admin/";
16519 }
16520 if (!url.pathname.startsWith(adminPath)) {
16521 return;
16522 }
16523 if (/\/(admin-post|admin-ajax)\.php$/.test(url.pathname)) {
16524 return;
16525 }
16526 if (url.searchParams.has("action") && url.searchParams.get("action") === "logout") {
16527 return;
16528 }
16529 if (url.searchParams.has("desktop_mode_classic")) {
16530 return;
16531 }
16532 e.preventDefault();
16533 e.stopPropagation();
16534 if (tryNativeUrlRemap(url.href)) {
16535 return;
16536 }
16537 const windowId = deriveWindowId(url.href, config.adminUrl);
16538 const dockEntry = findDockEntryForUrl(url.href, config);
16539 const fallbackTitle = (anchor.textContent || "").trim() || dockEntry?.title || "";
16540 void manager.open({
16541 id: windowId,
16542 baseId: windowId,
16543 multi: !!dockEntry?.multi,
16544 url: url.href,
16545 parentUrl: dockEntry?.url ?? url.href,
16546 title: dockEntry?.title || fallbackTitle,
16547 icon: dockEntry?.icon || "dashicons-admin-generic",
16548 submenu: dockEntry?.submenu
16549 });
16550 },
16551 true
16552 );
16553 }
16554 const REGISTRY_CHANGED_EVENT = "desktop-mode-registry-changed";
16555 function diffIds(prev, next) {
16556 const prevIds = /* @__PURE__ */ new Set();
16557 if (Array.isArray(prev)) {
16558 for (const item of prev) {
16559 if (item && typeof item.id === "string") {
16560 prevIds.add(item.id);
16561 }
16562 }
16563 }
16564 const nextIds = /* @__PURE__ */ new Set();
16565 for (const item of next) {
16566 if (item && typeof item.id === "string") {
16567 nextIds.add(item.id);
16568 }
16569 }
16570 const added = [];
16571 for (const id of nextIds) {
16572 if (!prevIds.has(id)) {
16573 added.push(id);
16574 }
16575 }
16576 const removed = [];
16577 for (const id of prevIds) {
16578 if (!nextIds.has(id)) {
16579 removed.push(id);
16580 }
16581 }
16582 return { added, removed };
16583 }
16584 function emitRegistryChanged(registry2, prev, next) {
16585 const { added, removed } = diffIds(prev, next);
16586 if (added.length === 0 && removed.length === 0) {
16587 return;
16588 }
16589 if (typeof document === "undefined") {
16590 return;
16591 }
16592 const detail = { registry: registry2, added, removed };
16593 document.dispatchEvent(
16594 new CustomEvent(REGISTRY_CHANGED_EVENT, { detail })
16595 );
16596 }
16597 function createApplyPayload(deps2) {
16598 const {
16599 applyDockItems,
16600 config,
16601 syncNativeWindows,
16602 syncServerWidgets,
16603 syncServerWallpapers,
16604 syncServerCommands,
16605 syncServerSettingsTabs,
16606 syncServerTitleBarButtons,
16607 syncServerDockRailRenderers,
16608 renderIcons
16609 } = deps2;
16610 return function applyPayload(payload) {
16611 const dockItems = payload.dockItems;
16612 const nativeWindows = payload.nativeWindows;
16613 const serverWidgets = payload.serverWidgets;
16614 const serverWallpapers = payload.serverWallpapers;
16615 const serverCommandScripts = payload.serverCommandScripts;
16616 const serverCommands = payload.serverCommands;
16617 const serverSettingsTabScripts = payload.serverSettingsTabScripts;
16618 const serverSettingsTabs = payload.serverSettingsTabs;
16619 const serverDockRailRendererScripts = payload.serverDockRailRendererScripts;
16620 const serverTitleBarButtonScripts = payload.serverTitleBarButtonScripts;
16621 const serverWindowNotices = payload.serverWindowNotices;
16622 const desktopIcons = payload.desktopIcons;
16623 if (!Array.isArray(dockItems) || dockItems.length === 0) {
16624 return;
16625 }
16626 const prevDockItems = config.dockItems;
16627 applyDockItems(dockItems);
16628 config.dockItems = dockItems;
16629 emitRegistryChanged(
16630 "dock-items",
16631 prevDockItems,
16632 dockItems
16633 );
16634 if (Array.isArray(nativeWindows)) {
16635 const prevNativeWindows = config.nativeWindows;
16636 void syncNativeWindows(
16637 nativeWindows
16638 );
16639 config.nativeWindows = nativeWindows;
16640 emitRegistryChanged(
16641 "native-windows",
16642 prevNativeWindows,
16643 nativeWindows
16644 );
16645 }
16646 if (Array.isArray(serverWidgets)) {
16647 void syncServerWidgets(
16648 serverWidgets
16649 );
16650 config.serverWidgets = serverWidgets;
16651 }
16652 if (Array.isArray(serverWallpapers)) {
16653 void syncServerWallpapers(
16654 serverWallpapers
16655 );
16656 config.serverWallpapers = serverWallpapers;
16657 }
16658 if (Array.isArray(serverCommandScripts)) {
16659 void syncServerCommands(
16660 serverCommandScripts,
16661 Array.isArray(serverCommands) ? serverCommands : void 0
16662 );
16663 config.serverCommandScripts = serverCommandScripts;
16664 if (Array.isArray(serverCommands)) {
16665 config.serverCommands = serverCommands;
16666 }
16667 }
16668 if (Array.isArray(serverSettingsTabScripts)) {
16669 void syncServerSettingsTabs(
16670 serverSettingsTabScripts,
16671 Array.isArray(serverSettingsTabs) ? serverSettingsTabs : void 0
16672 );
16673 config.serverSettingsTabScripts = serverSettingsTabScripts;
16674 if (Array.isArray(serverSettingsTabs)) {
16675 config.serverSettingsTabs = serverSettingsTabs;
16676 }
16677 }
16678 if (Array.isArray(serverTitleBarButtonScripts)) {
16679 void syncServerTitleBarButtons(
16680 serverTitleBarButtonScripts
16681 );
16682 config.serverTitleBarButtonScripts = serverTitleBarButtonScripts;
16683 }
16684 if (Array.isArray(serverDockRailRendererScripts)) {
16685 void syncServerDockRailRenderers(
16686 serverDockRailRendererScripts
16687 );
16688 config.serverDockRailRendererScripts = serverDockRailRendererScripts;
16689 }
16690 if (Array.isArray(serverWindowNotices)) {
16691 applyServerWindowNotices(
16692 serverWindowNotices
16693 );
16694 config.serverWindowNotices = serverWindowNotices;
16695 }
16696 if (Array.isArray(desktopIcons)) {
16697 const prevDesktopIcons = config.desktopIcons;
16698 renderIcons(desktopIcons);
16699 config.desktopIcons = desktopIcons;
16700 emitRegistryChanged(
16701 "desktop-icons",
16702 prevDesktopIcons,
16703 desktopIcons
16704 );
16705 }
16706 };
16707 }
16708 const MENU_REFRESH_TIMEOUT_MS = 8e3;
16709 function bindMenuRefresh(deps2) {
16710 const {
16711 layoutDispatcher,
16712 config,
16713 syncNativeWindows,
16714 syncServerWidgets,
16715 syncServerWallpapers,
16716 syncServerCommands,
16717 syncServerSettingsTabs,
16718 syncServerTitleBarButtons,
16719 syncServerDockRailRenderers,
16720 renderIcons
16721 } = deps2;
16722 const applyPayload = createApplyPayload({
16723 applyDockItems: (items) => layoutDispatcher?.applyDockItems(items),
16724 config,
16725 syncNativeWindows,
16726 syncServerWidgets,
16727 syncServerWallpapers,
16728 syncServerCommands,
16729 syncServerSettingsTabs,
16730 syncServerTitleBarButtons,
16731 syncServerDockRailRenderers,
16732 renderIcons
16733 });
16734 window.addEventListener("message", (e) => {
16735 if (e.origin !== INITIAL_ORIGIN$1) {
16736 return;
16737 }
16738 const data = e.data;
16739 if (!data || data.type !== "desktop-mode-plugins-changed") {
16740 return;
16741 }
16742 if (data.payload) {
16743 applyPayload(data.payload);
16744 }
16745 });
16746 const refresh = () => {
16747 if (!config.adminUrl) {
16748 return Promise.resolve();
16749 }
16750 const probeUrl = (() => {
16751 try {
16752 const url = new URL("admin.php", config.adminUrl);
16753 url.searchParams.set("desktop_mode_chromeless", "1");
16754 url.searchParams.set("desktop_mode_menu_refresh", "1");
16755 return url.toString();
16756 } catch (_err) {
16757 return null;
16758 }
16759 })();
16760 if (!probeUrl) {
16761 return Promise.resolve();
16762 }
16763 return new Promise((resolve2) => {
16764 const iframe = document.createElement("iframe");
16765 iframe.setAttribute("aria-hidden", "true");
16766 iframe.tabIndex = -1;
16767 iframe.style.cssText = "position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;border:0;opacity:0;pointer-events:none;";
16768 iframe.src = probeUrl;
16769 let done = false;
16770 const cleanup = () => {
16771 if (done) {
16772 return;
16773 }
16774 done = true;
16775 window.clearTimeout(timeoutId);
16776 window.removeEventListener("message", onMessage);
16777 if (iframe.parentNode) {
16778 iframe.parentNode.removeChild(iframe);
16779 }
16780 resolve2();
16781 };
16782 const onMessage = (e) => {
16783 if (e.source !== iframe.contentWindow) {
16784 return;
16785 }
16786 const data = e.data;
16787 if (!data || data.type !== "desktop-mode-plugins-changed") {
16788 return;
16789 }
16790 cleanup();
16791 };
16792 const timeoutId = window.setTimeout(() => {
16793 doAction(HOOKS.SHELL_ERROR, {
16794 scope: "menu-refresh",
16795 error: new Error("menu refresh probe timed out")
16796 });
16797 cleanup();
16798 }, MENU_REFRESH_TIMEOUT_MS);
16799 window.addEventListener("message", onMessage);
16800 document.body.appendChild(iframe);
16801 });
16802 };
16803 return refresh;
16804 }
16805 async function restoreSession(manager, config, desktopArea) {
16806 const rect = desktopArea.getBoundingClientRect();
16807 if (Array.isArray(config.session.desktops) && config.session.desktops.length > 0) {
16808 manager.seedDesktops(
16809 config.session.desktops,
16810 config.session.activeDesktop || config.session.desktops[0].id
16811 );
16812 }
16813 for (const win of config.session.windows) {
16814 const clamped = clampGeometryToViewport(win, rect);
16815 const dockEntry = findDockEntryForUrl(win.url, config);
16816 const opened = await manager.open({
16817 id: win.id,
16818 baseId: win.baseId || win.id,
16819 desktopId: win.desktopId,
16820 multi: !!dockEntry?.multi,
16821 url: win.url,
16822 // `dockEntry?.url` is the parent menu's landing page —
16823 // recover it so the synthetic "back to parent" tab in
16824 // the in-window strip points at the dock URL even when
16825 // the saved `win.url` is a sub-page (e.g. theme-install.php
16826 // under Appearance, or a deep wc-admin route under
16827 // WooCommerce). Without this the dedup check in
16828 // `dom.ts` sees the iframe URL match a submenu entry
16829 // and suppresses the parent tab — losing the only
16830 // affordance to navigate back.
16831 parentUrl: dockEntry?.url ?? win.url,
16832 title: win.title,
16833 icon: win.icon || "dashicons-admin-generic",
16834 x: clamped.x,
16835 y: clamped.y,
16836 width: clamped.width,
16837 height: clamped.height,
16838 initialState: win.state,
16839 submenu: dockEntry?.submenu
16840 });
16841 if (Array.isArray(win.externalTabs)) {
16842 for (const ext of win.externalTabs) {
16843 if (ext && typeof ext.url === "string" && ext.url !== "") {
16844 opened.addExternalTab(
16845 ext.url,
16846 typeof ext.label === "string" && ext.label !== "" ? ext.label : ext.url
16847 );
16848 }
16849 }
16850 }
16851 }
16852 if (config.session.focused) {
16853 const focused = manager.getById(config.session.focused);
16854 if (focused) {
16855 manager.focus(focused);
16856 }
16857 }
16858 }
16859 async function openCurrentPage(manager, config) {
16860 if (tryNativeUrlRemap(config.currentPage)) {
16861 return;
16862 }
16863 const windowId = deriveWindowId(config.currentPage, config.adminUrl);
16864 const dockEntry = findDockEntryForUrl(config.currentPage, config);
16865 await manager.open({
16866 id: windowId,
16867 baseId: windowId,
16868 multi: !!dockEntry?.multi,
16869 url: config.currentPage,
16870 parentUrl: dockEntry?.url ?? config.currentPage,
16871 title: config.currentTitle,
16872 icon: config.currentIcon,
16873 submenu: dockEntry?.submenu
16874 });
16875 }
16876 function shouldAutoOpenCurrentPage(inputs) {
16877 const suppress = inputs.fromPortal && !inputs.fromPortalIntent && (inputs.hasSession || !inputs.defaultEnabled || inputs.isNativeDefault);
16878 return !suppress;
16879 }
16880 function trackedFetch(manager, input, requestInit, opts) {
16881 const finalInit = injectRestNonce(input, requestInit);
16882 const promise = window.fetch(input, finalInit);
16883 if (opts?.silent) {
16884 return promise;
16885 }
16886 let target = opts?.window;
16887 if (!target && opts?.windowId) {
16888 target = manager.getById(opts.windowId) ?? null;
16889 }
16890 if (!target) {
16891 target = manager.getFocused();
16892 }
16893 if (target && typeof target.trackActivity === "function") {
16894 void target.trackActivity(promise).catch(() => {
16895 });
16896 }
16897 return promise;
16898 }
16899 const SESSION_SAVE_DEBOUNCE_MS = 500;
16900 function createSessionSaver(manager, config) {
16901 let debounceTimer = null;
16902 let inFlight = false;
16903 const doSave = async () => {
16904 if (inFlight) {
16905 return;
16906 }
16907 const payload = manager.snapshot();
16908 inFlight = true;
16909 try {
16910 await trackedFetch(
16911 manager,
16912 config.sessionUrl,
16913 {
16914 method: "POST",
16915 credentials: "same-origin",
16916 headers: {
16917 "Content-Type": "application/json",
16918 "X-WP-Nonce": config.restNonce
16919 },
16920 body: JSON.stringify({ session: payload }),
16921 // Best-effort: we don't block the UI on persistence.
16922 keepalive: true
16923 },
16924 { silent: true }
16925 );
16926 } catch (err) {
16927 doAction(HOOKS.SHELL_ERROR, { scope: "session-save", error: err });
16928 } finally {
16929 inFlight = false;
16930 }
16931 };
16932 const flushImmediately = () => {
16933 if (debounceTimer !== null) {
16934 clearTimeout(debounceTimer);
16935 debounceTimer = null;
16936 }
16937 const payload = manager.snapshot();
16938 const body = new Blob(
16939 [JSON.stringify({ session: payload })],
16940 { type: "application/json" }
16941 );
16942 const beaconUrl = config.sessionUrl + (config.sessionUrl.includes("?") ? "&" : "?") + "_wpnonce=" + encodeURIComponent(config.restNonce);
16943 if (navigator.sendBeacon && navigator.sendBeacon(beaconUrl, body)) {
16944 return;
16945 }
16946 void doSave();
16947 };
16948 const schedule = () => {
16949 if (debounceTimer !== null) {
16950 clearTimeout(debounceTimer);
16951 }
16952 debounceTimer = window.setTimeout(() => {
16953 debounceTimer = null;
16954 void doSave();
16955 }, SESSION_SAVE_DEBOUNCE_MS);
16956 };
16957 window.addEventListener("pagehide", flushImmediately);
16958 document.addEventListener("visibilitychange", () => {
16959 if (document.visibilityState === "hidden") {
16960 flushImmediately();
16961 }
16962 });
16963 return schedule;
16964 }
16965 const SHELL_RESIZE_DEBOUNCE_MS = 120;
16966 function wireSessionEvents(save) {
16967 document.addEventListener("desktop-mode-window-opened", save);
16968 document.addEventListener("desktop-mode-window-closed", save);
16969 document.addEventListener("desktop-mode-window-focused", save);
16970 document.addEventListener("desktop-mode-window-changed", save);
16971 }
16972 function bindShellLifecycle() {
16973 const shellEl = document.getElementById("desktop-mode-shell");
16974 let resizeTimer = null;
16975 const fireShellResize = () => {
16976 resizeTimer = null;
16977 const rect = shellEl ? shellEl.getBoundingClientRect() : null;
16978 doAction(HOOKS.SHELL_RESIZED, {
16979 width: rect ? Math.round(rect.width) : window.innerWidth,
16980 height: rect ? Math.round(rect.height) : window.innerHeight
16981 });
16982 };
16983 window.addEventListener("resize", () => {
16984 if (resizeTimer !== null) {
16985 window.clearTimeout(resizeTimer);
16986 }
16987 resizeTimer = window.setTimeout(
16988 fireShellResize,
16989 SHELL_RESIZE_DEBOUNCE_MS
16990 );
16991 });
16992 document.addEventListener("visibilitychange", () => {
16993 doAction(HOOKS.SHELL_VISIBILITY, {
16994 state: document.hidden ? "hidden" : "visible"
16995 });
16996 });
16997 }
16998 function applyTileClasses(baseClasses, item, ctx) {
16999 const fullCtx = {
17000 rail: ctx.rail ?? "dock",
17001 orientation: ctx.orientation,
17002 dockId: ctx.dockId,
17003 container: ctx.container ?? document.body,
17004 item,
17005 isSystem: ctx.isSystem
17006 };
17007 return applyFilters(
17008 HOOKS.DOCK_TILE_CLASS,
17009 baseClasses,
17010 fullCtx
17011 );
17012 }
17013 function applyTileElement(tile2, item, ctx) {
17014 const fullCtx = {
17015 rail: ctx.rail ?? "dock",
17016 orientation: ctx.orientation,
17017 dockId: ctx.dockId,
17018 container: ctx.container ?? document.body,
17019 item,
17020 isSystem: ctx.isSystem
17021 };
17022 return applyFilters(
17023 HOOKS.DOCK_TILE_ELEMENT,
17024 tile2,
17025 fullCtx
17026 );
17027 }
17028 function applyTileTooltip(label, item, ctx) {
17029 const fullCtx = {
17030 rail: ctx.rail ?? "dock",
17031 orientation: ctx.orientation,
17032 dockId: ctx.dockId,
17033 container: ctx.container ?? document.body,
17034 item,
17035 isSystem: ctx.isSystem
17036 };
17037 return applyFilters(
17038 HOOKS.DOCK_TILE_TOOLTIP,
17039 label,
17040 fullCtx
17041 );
17042 }
17043 function dispatchTileRendered(el, item, ctx) {
17044 const fullCtx = {
17045 rail: ctx.rail ?? "dock",
17046 orientation: ctx.orientation,
17047 dockId: ctx.dockId,
17048 container: ctx.container ?? document.body,
17049 item,
17050 isSystem: ctx.isSystem
17051 };
17052 doAction(HOOKS.DOCK_TILE_RENDERED, { ...fullCtx, el });
17053 }
17054 const DEFAULT_DOCK_SELECTOR = [
17055 ".desktop-mode-dock",
17056 "#desktop-mode-dock",
17057 "#desktop-mode-side-dock",
17058 ".desktop-mode-dock__tooltip",
17059 ".desktop-mode-dock-submenu"
17060 ].join(",");
17061 const customSelectors = /* @__PURE__ */ new Set();
17062 function isDockElement(target) {
17063 if (!target || typeof target.closest !== "function") {
17064 return false;
17065 }
17066 const el = target;
17067 if (el.closest(DEFAULT_DOCK_SELECTOR)) {
17068 return true;
17069 }
17070 for (const selector of customSelectors) {
17071 if (el.closest(selector)) {
17072 return true;
17073 }
17074 }
17075 return false;
17076 }
17077 function registerDockSelector(selector) {
17078 if (typeof selector !== "string" || selector.trim() === "") {
17079 return () => void 0;
17080 }
17081 customSelectors.add(selector);
17082 return () => {
17083 customSelectors.delete(selector);
17084 };
17085 }
17086 const states = /* @__PURE__ */ new Map();
17087 const INITIAL_ORIGIN = window.location.origin;
17088 function ensureState(windowId) {
17089 let s = states.get(windowId);
17090 if (!s) {
17091 s = {
17092 headers: /* @__PURE__ */ new Map(),
17093 observers: /* @__PURE__ */ new Set(),
17094 observeCount: 0,
17095 loadHandler: null,
17096 loadHandlerTarget: null
17097 };
17098 states.set(windowId, s);
17099 }
17100 ensureLoadHandler(windowId, s);
17101 return s;
17102 }
17103 function ensureLoadHandler(windowId, s) {
17104 const iframe = findIframe(windowId);
17105 if (!iframe) {
17106 return;
17107 }
17108 if (s.loadHandlerTarget === iframe && s.loadHandler) {
17109 return;
17110 }
17111 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
17112 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
17113 }
17114 if (typeof iframe.addEventListener !== "function") {
17115 return;
17116 }
17117 const handler = () => {
17118 queueMicrotask(() => pushInstrumentation(windowId));
17119 };
17120 iframe.addEventListener("load", handler);
17121 s.loadHandler = handler;
17122 s.loadHandlerTarget = iframe;
17123 }
17124 function detachLoadHandler(s) {
17125 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
17126 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
17127 }
17128 s.loadHandler = null;
17129 s.loadHandlerTarget = null;
17130 }
17131 function findIframe(windowId) {
17132 const wpd = window.wp?.desktop?.windowManager;
17133 if (wpd && typeof wpd.getById === "function") {
17134 const win = wpd.getById(windowId);
17135 if (win?.iframe) {
17136 return win.iframe;
17137 }
17138 if (win?.element) {
17139 const synth = win.element.querySelector("iframe");
17140 if (synth) {
17141 return synth;
17142 }
17143 }
17144 }
17145 const fallback = document.getElementById(`wp-window-${windowId}`);
17146 return fallback?.querySelector("iframe") ?? null;
17147 }
17148 function snapshotHeaders(s) {
17149 const out = {};
17150 for (const [name, contributions] of s.headers) {
17151 const parts = [];
17152 for (const c of contributions) {
17153 let v;
17154 try {
17155 v = typeof c.value === "function" ? c.value() : c.value;
17156 } catch {
17157 continue;
17158 }
17159 if (typeof v === "string" && v !== "") {
17160 parts.push(v);
17161 }
17162 }
17163 if (parts.length > 0) {
17164 out[name] = parts.join(", ");
17165 }
17166 }
17167 return out;
17168 }
17169 function pushInstrumentation(windowId) {
17170 const iframe = findIframe(windowId);
17171 if (!iframe || !iframe.contentWindow) {
17172 return;
17173 }
17174 const s = states.get(windowId);
17175 const headers = s ? snapshotHeaders(s) : {};
17176 const observe = !!s && s.observeCount > 0;
17177 try {
17178 iframe.contentWindow.postMessage(
17179 {
17180 type: "desktop-mode-instrument-set",
17181 headers,
17182 observe
17183 },
17184 INITIAL_ORIGIN
17185 );
17186 } catch {
17187 }
17188 }
17189 addAction(HOOKS.IFRAME_READY, "desktop-mode/devtools/replay", (payload) => {
17190 const p = payload;
17191 if (p && typeof p.windowId === "string" && states.has(p.windowId)) {
17192 pushInstrumentation(p.windowId);
17193 }
17194 });
17195 addAction(
17196 HOOKS.IFRAME_NETWORK_COMPLETED,
17197 "desktop-mode/devtools/dispatch",
17198 (payload) => {
17199 const p = payload;
17200 if (!p || typeof p.windowId !== "string") {
17201 return;
17202 }
17203 const s = states.get(p.windowId);
17204 if (!s) {
17205 return;
17206 }
17207 for (const cb of s.observers) {
17208 try {
17209 cb(p);
17210 } catch {
17211 }
17212 }
17213 }
17214 );
17215 const sessions = /* @__PURE__ */ new Map();
17216 const POLL_INTERVAL_MS = 1e3;
17217 function pollOnce(sessionId, restUrl2, restNonce) {
17218 const sp = sessions.get(sessionId);
17219 if (!sp || sp.inflight) {
17220 return;
17221 }
17222 sp.inflight = true;
17223 const u = new URL(restUrl2 + "desktop-mode/v1/debug", window.location.origin);
17224 u.searchParams.set("sessionId", sessionId);
17225 u.searchParams.set("since", String(sp.cursor));
17226 for (const ch of sp.channels.keys()) {
17227 u.searchParams.append("channels[]", ch);
17228 }
17229 const url = u.toString();
17230 fetch(url, {
17231 credentials: "same-origin",
17232 headers: { "X-WP-Nonce": restNonce }
17233 }).then((r) => r.ok ? r.json() : { events: [], cursor: sp.cursor }).then((body) => {
17234 sp.inflight = false;
17235 if (!sessions.has(sessionId)) {
17236 return;
17237 }
17238 if (typeof body.cursor === "number") {
17239 sp.cursor = body.cursor;
17240 }
17241 for (const ev of body.events || []) {
17242 const bucket2 = sp.channels.get(ev.channel);
17243 if (!bucket2) {
17244 continue;
17245 }
17246 for (const cb of bucket2) {
17247 try {
17248 cb(ev);
17249 } catch {
17250 }
17251 }
17252 }
17253 }).catch(() => {
17254 sp.inflight = false;
17255 }).finally(() => {
17256 const stillThere = sessions.get(sessionId);
17257 if (stillThere && stillThere.channels.size > 0) {
17258 stillThere.timer = setTimeout(
17259 () => pollOnce(sessionId, restUrl2, restNonce),
17260 POLL_INTERVAL_MS
17261 );
17262 }
17263 });
17264 }
17265 function getRestEndpoint() {
17266 const cfg = window.desktopModeConfig;
17267 if (!cfg || !cfg.restUrl || !cfg.restNonce) {
17268 return null;
17269 }
17270 return { restUrl: cfg.restUrl, restNonce: cfg.restNonce };
17271 }
17272 function dispatchLocal(sessionId, ev) {
17273 const sp = sessions.get(sessionId);
17274 if (!sp) {
17275 return;
17276 }
17277 const bucket2 = sp.channels.get(ev.channel);
17278 if (!bucket2) {
17279 return;
17280 }
17281 for (const cb of bucket2) {
17282 try {
17283 cb(ev);
17284 } catch {
17285 }
17286 }
17287 }
17288 let _localEventCounter = 0;
17289 const debugBus = {
17290 startSession() {
17291 const cryptoApi = window.crypto;
17292 if (cryptoApi && typeof cryptoApi.randomUUID === "function") {
17293 return cryptoApi.randomUUID();
17294 }
17295 return "wpdbg-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
17296 },
17297 publish(sessionId, channel, payload) {
17298 dispatchLocal(sessionId, {
17299 id: ++_localEventCounter,
17300 t: Date.now(),
17301 channel,
17302 payload
17303 });
17304 },
17305 subscribe(sessionId, channel, cb) {
17306 let sp = sessions.get(sessionId);
17307 const startedFresh = !sp;
17308 if (!sp) {
17309 sp = {
17310 channels: /* @__PURE__ */ new Map(),
17311 cursor: 0,
17312 timer: null,
17313 inflight: false
17314 };
17315 sessions.set(sessionId, sp);
17316 }
17317 let bucket2 = sp.channels.get(channel);
17318 if (!bucket2) {
17319 bucket2 = /* @__PURE__ */ new Set();
17320 sp.channels.set(channel, bucket2);
17321 }
17322 bucket2.add(cb);
17323 if (startedFresh) {
17324 const ep = getRestEndpoint();
17325 if (ep) {
17326 pollOnce(sessionId, ep.restUrl, ep.restNonce);
17327 }
17328 }
17329 return () => {
17330 const cur = sessions.get(sessionId);
17331 if (!cur) {
17332 return;
17333 }
17334 const b = cur.channels.get(channel);
17335 if (b) {
17336 b.delete(cb);
17337 if (b.size === 0) {
17338 cur.channels.delete(channel);
17339 }
17340 }
17341 if (cur.channels.size === 0) {
17342 if (cur.timer) {
17343 clearTimeout(cur.timer);
17344 }
17345 sessions.delete(sessionId);
17346 }
17347 };
17348 }
17349 };
17350 const devtools = {
17351 addRequestHeader(windowId, name, value) {
17352 if (typeof windowId !== "string" || windowId === "") {
17353 return () => {
17354 };
17355 }
17356 if (typeof name !== "string" || name === "") {
17357 return () => {
17358 };
17359 }
17360 const s = ensureState(windowId);
17361 const contribution = { value };
17362 let bucket2 = s.headers.get(name);
17363 if (!bucket2) {
17364 bucket2 = [];
17365 s.headers.set(name, bucket2);
17366 }
17367 bucket2.push(contribution);
17368 pushInstrumentation(windowId);
17369 return () => {
17370 const cur = states.get(windowId);
17371 if (!cur) {
17372 return;
17373 }
17374 const b = cur.headers.get(name);
17375 if (!b) {
17376 return;
17377 }
17378 const i = b.indexOf(contribution);
17379 if (i >= 0) {
17380 b.splice(i, 1);
17381 }
17382 if (b.length === 0) {
17383 cur.headers.delete(name);
17384 }
17385 pushInstrumentation(windowId);
17386 gcWindowState(windowId);
17387 };
17388 },
17389 onRequest(windowId, cb, opts) {
17390 if (typeof windowId !== "string" || windowId === "") {
17391 return () => {
17392 };
17393 }
17394 if (typeof cb !== "function") {
17395 return () => {
17396 };
17397 }
17398 const s = ensureState(windowId);
17399 s.observers.add(cb);
17400 const wantsObserve = !!opts?.observe;
17401 if (wantsObserve) {
17402 s.observeCount++;
17403 pushInstrumentation(windowId);
17404 }
17405 return () => {
17406 const cur = states.get(windowId);
17407 if (!cur) {
17408 return;
17409 }
17410 cur.observers.delete(cb);
17411 if (wantsObserve) {
17412 cur.observeCount = Math.max(0, cur.observeCount - 1);
17413 pushInstrumentation(windowId);
17414 }
17415 gcWindowState(windowId);
17416 };
17417 },
17418 reloadWithDebugSession(windowId, sessionId, opts) {
17419 if (typeof windowId !== "string" || windowId === "" || typeof sessionId !== "string" || sessionId === "") {
17420 return null;
17421 }
17422 const iframe = findIframe(windowId);
17423 if (!iframe) {
17424 return null;
17425 }
17426 const headerName = opts?.headerName || "X-WP-Debug-Session";
17427 const queryArg = opts?.queryArg || "wp_debug_session";
17428 const stopHeader = devtools.addRequestHeader(windowId, headerName, sessionId);
17429 try {
17430 const currentSrc = iframe.getAttribute("src") || iframe.src || "";
17431 const u = new URL(currentSrc, window.location.origin);
17432 u.searchParams.set(queryArg, sessionId);
17433 iframe.src = u.toString();
17434 } catch {
17435 }
17436 return {
17437 dispose: () => {
17438 stopHeader();
17439 }
17440 };
17441 },
17442 debug: debugBus
17443 };
17444 function gcWindowState(windowId) {
17445 const s = states.get(windowId);
17446 if (!s) {
17447 return;
17448 }
17449 if (s.headers.size === 0 && s.observers.size === 0) {
17450 detachLoadHandler(s);
17451 states.delete(windowId);
17452 }
17453 }
17454 async function wpdConfirm(options) {
17455 await ensureShellOverlaysLoaded(shellOverlaysBundleUrl());
17456 return new Promise((resolve2) => {
17457 const dialog2 = document.createElement("wpd-confirm-dialog");
17458 dialog2.setAttribute("open", "");
17459 if (options.title) {
17460 dialog2.setAttribute("title", options.title);
17461 }
17462 dialog2.setAttribute("message", options.message);
17463 if (options.confirmLabel) {
17464 dialog2.setAttribute("confirm-label", options.confirmLabel);
17465 }
17466 if (options.cancelLabel) {
17467 dialog2.setAttribute("cancel-label", options.cancelLabel);
17468 }
17469 if (options.danger) {
17470 dialog2.setAttribute("danger", "");
17471 }
17472 if (options.hideCancel) {
17473 dialog2.setAttribute("hide-cancel", "");
17474 }
17475 if (options.dismissable) {
17476 dialog2.setAttribute("dismissable", "");
17477 }
17478 const cleanup = (ok) => {
17479 dialog2.remove();
17480 resolve2(ok);
17481 };
17482 dialog2.addEventListener("wpd-confirm", () => cleanup(true));
17483 dialog2.addEventListener("wpd-cancel", () => cleanup(false));
17484 document.body.appendChild(dialog2);
17485 const inner = dialog2.shadowRoot?.querySelector(".dialog");
17486 (inner ?? dialog2).focus?.();
17487 });
17488 }
17489 function collectWallpaperSurfaces(manager) {
17490 const seed2 = [];
17491 for (const w of manager.getVisibleRects()) {
17492 if (w.state === "minimized") {
17493 continue;
17494 }
17495 if (w.element.offsetParent === null) {
17496 continue;
17497 }
17498 const r = w.element.getBoundingClientRect();
17499 seed2.push({
17500 id: `window:${w.windowId}`,
17501 kind: "window",
17502 rect: rectFromDom(r),
17503 face: "top",
17504 element: w.element
17505 });
17506 }
17507 const shellEl = document.getElementById("desktop-mode-shell");
17508 if (shellEl) {
17509 const r = shellEl.getBoundingClientRect();
17510 seed2.push({
17511 id: "shell:floor",
17512 kind: "shell",
17513 rect: {
17514 x: r.left,
17515 y: r.bottom - 1,
17516 width: r.width,
17517 height: 1
17518 },
17519 face: "top",
17520 element: shellEl
17521 });
17522 }
17523 const dockEls = document.querySelectorAll(
17524 ".desktop-mode-dock"
17525 );
17526 let dockIndex = 0;
17527 for (const dockEl of Array.from(dockEls)) {
17528 const r = dockEl.getBoundingClientRect();
17529 if (r.width <= 0 || r.height <= 0) {
17530 continue;
17531 }
17532 const placement = dockEl.getAttribute("data-desktop-mode-dock-placement") ?? "bottom";
17533 const id = dockIndex === 0 ? "dock:edge" : `dock:edge:${dockIndex}`;
17534 dockIndex++;
17535 if (placement === "bottom") {
17536 seed2.push({
17537 id,
17538 kind: "dock",
17539 rect: { x: r.left, y: r.top, width: r.width, height: 1 },
17540 face: "top",
17541 element: dockEl
17542 });
17543 } else if (placement === "right") {
17544 seed2.push({
17545 id,
17546 kind: "dock",
17547 rect: { x: r.left, y: r.top, width: 1, height: r.height },
17548 face: "left",
17549 element: dockEl
17550 });
17551 } else {
17552 seed2.push({
17553 id,
17554 kind: "dock",
17555 rect: {
17556 x: r.right - 1,
17557 y: r.top,
17558 width: 1,
17559 height: r.height
17560 },
17561 face: "right",
17562 element: dockEl
17563 });
17564 }
17565 }
17566 const widgetCards = document.querySelectorAll(
17567 ".desktop-mode-widgets__card"
17568 );
17569 let widgetIndex = 0;
17570 widgetCards.forEach((card) => {
17571 const r = card.getBoundingClientRect();
17572 if (r.width === 0 && r.height === 0) {
17573 return;
17574 }
17575 const id = card.dataset.widgetId ?? String(widgetIndex++);
17576 seed2.push({
17577 id: `widget:${id}`,
17578 kind: "widget",
17579 rect: rectFromDom(r),
17580 face: "top",
17581 element: card
17582 });
17583 });
17584 const filtered = applyFilters(HOOKS.WALLPAPER_SURFACES, seed2);
17585 return Array.isArray(filtered) ? filtered : seed2;
17586 }
17587 function rectFromDom(r) {
17588 return {
17589 x: r.left,
17590 y: r.top,
17591 width: r.width,
17592 height: r.height
17593 };
17594 }
17595 const NODE_KEY_PROP = "__desktop_modeKeyedListKey";
17596 const NODE_DATA_PROP = "__desktop_modeKeyedListData";
17597 function getHostState(host) {
17598 const cached = host.__desktop_modeKeyedList;
17599 if (cached) {
17600 return cached;
17601 }
17602 const fresh = { byKey: /* @__PURE__ */ new Map() };
17603 host.__desktop_modeKeyedList = fresh;
17604 return fresh;
17605 }
17606 function renderKeyedList(host, items, opts) {
17607 const state2 = getHostState(host);
17608 const prev = state2.byKey;
17609 const next = /* @__PURE__ */ new Map();
17610 const ordered = [];
17611 const seenKeys = /* @__PURE__ */ new Set();
17612 for (const item of items) {
17613 const key = String(opts.keyOf(item));
17614 if (seenKeys.has(key)) {
17615 console.warn(
17616 "[desktop-mode/keyed-list] duplicate key — only the last item with this key will render:",
17617 key
17618 );
17619 }
17620 seenKeys.add(key);
17621 const reused = prev.get(key);
17622 if (reused) {
17623 const prevData = reused.data;
17624 opts.updateItem?.(reused.el, item, prevData);
17625 reused.data = item;
17626 next.set(key, reused);
17627 ordered.push(reused.el);
17628 continue;
17629 }
17630 const el = opts.buildItem(item);
17631 el[NODE_KEY_PROP] = key;
17632 el[NODE_DATA_PROP] = item;
17633 next.set(key, { el, data: item });
17634 ordered.push(el);
17635 }
17636 for (const [key, entry] of prev) {
17637 if (!next.has(key)) {
17638 entry.el.remove();
17639 }
17640 }
17641 for (let i = 0; i < ordered.length; i++) {
17642 const desired = ordered[i];
17643 const live = host.children[i];
17644 if (live === desired) {
17645 continue;
17646 }
17647 host.insertBefore(desired, live ?? null);
17648 }
17649 state2.byKey = next;
17650 }
17651 function clearKeyedList(host) {
17652 const cached = host.__desktop_modeKeyedList;
17653 if (!cached) {
17654 return;
17655 }
17656 for (const entry of cached.byKey.values()) {
17657 entry.el.remove();
17658 }
17659 cached.byKey.clear();
17660 delete host.__desktop_modeKeyedList;
17661 }
17662 function createInfiniteList(options) {
17663 const {
17664 root,
17665 fetchPage,
17666 getId,
17667 renderItem,
17668 rootMargin = "200px",
17669 initialCursor = null,
17670 onLoadingChange = () => void 0,
17671 onError = (err) => {
17672 if (typeof console !== "undefined") {
17673 console.error("[desktop-mode] createInfiniteList:", err);
17674 }
17675 }
17676 } = options;
17677 let sentinel = options.sentinel ?? null;
17678 if (!sentinel) {
17679 sentinel = document.createElement("div");
17680 sentinel.dataset.wpdInfiniteListSentinel = "";
17681 sentinel.style.height = "1px";
17682 root.appendChild(sentinel);
17683 }
17684 const seen = /* @__PURE__ */ new Set();
17685 let cursor = initialCursor;
17686 let hasMoreInternal = true;
17687 let loading = false;
17688 let controller = null;
17689 let renderedCount = 0;
17690 let destroyed = false;
17691 let observer = null;
17692 const setLoading = (next) => {
17693 if (loading === next) {
17694 return;
17695 }
17696 loading = next;
17697 try {
17698 onLoadingChange(next);
17699 } catch (err) {
17700 onError(err);
17701 }
17702 };
17703 const detachObserver = () => {
17704 if (observer) {
17705 observer.disconnect();
17706 observer = null;
17707 }
17708 };
17709 const ensureObserver = () => {
17710 if (observer || !sentinel || destroyed) {
17711 return;
17712 }
17713 observer = new IntersectionObserver(
17714 (entries) => {
17715 for (const entry of entries) {
17716 if (entry.isIntersecting) {
17717 void loadMore();
17718 }
17719 }
17720 },
17721 { rootMargin }
17722 );
17723 observer.observe(sentinel);
17724 };
17725 const loadMore = async () => {
17726 if (destroyed || loading || !hasMoreInternal) {
17727 return;
17728 }
17729 setLoading(true);
17730 controller = new AbortController();
17731 const localController = controller;
17732 try {
17733 const page = await fetchPage(cursor, localController.signal);
17734 if (destroyed || localController !== controller) {
17735 return;
17736 }
17737 let appended = 0;
17738 const frag = document.createDocumentFragment();
17739 for (const item of page.items ?? []) {
17740 const key = String(getId(item));
17741 if (seen.has(key)) {
17742 continue;
17743 }
17744 seen.add(key);
17745 const el = renderItem(item, renderedCount + appended);
17746 frag.appendChild(el);
17747 appended++;
17748 }
17749 if (appended > 0) {
17750 if (sentinel && sentinel.parentNode === root) {
17751 root.insertBefore(frag, sentinel);
17752 } else {
17753 root.appendChild(frag);
17754 }
17755 renderedCount += appended;
17756 }
17757 cursor = page.nextCursor ?? null;
17758 if (!cursor) {
17759 hasMoreInternal = false;
17760 detachObserver();
17761 }
17762 } catch (err) {
17763 if (err?.name === "AbortError") {
17764 return;
17765 }
17766 onError(err);
17767 } finally {
17768 if (localController === controller) {
17769 setLoading(false);
17770 controller = null;
17771 }
17772 }
17773 };
17774 const reset = () => {
17775 if (destroyed) {
17776 return;
17777 }
17778 controller?.abort();
17779 controller = null;
17780 seen.clear();
17781 cursor = initialCursor;
17782 hasMoreInternal = true;
17783 renderedCount = 0;
17784 const sentinelInRoot = sentinel && sentinel.parentNode === root;
17785 while (root.firstChild) {
17786 root.removeChild(root.firstChild);
17787 }
17788 if (sentinelInRoot && sentinel) {
17789 root.appendChild(sentinel);
17790 }
17791 setLoading(false);
17792 ensureObserver();
17793 void loadMore();
17794 };
17795 const destroy = () => {
17796 if (destroyed) {
17797 return;
17798 }
17799 destroyed = true;
17800 detachObserver();
17801 controller?.abort();
17802 controller = null;
17803 if (!options.sentinel && sentinel && sentinel.parentNode === root) {
17804 root.removeChild(sentinel);
17805 }
17806 sentinel = null;
17807 setLoading(false);
17808 };
17809 ensureObserver();
17810 void loadMore();
17811 return {
17812 reset,
17813 loadMore,
17814 hasMore: () => hasMoreInternal,
17815 isLoading: () => loading,
17816 destroy
17817 };
17818 }
17819 const POPUP_DEFAULT_WIDTH = 520;
17820 const POPUP_DEFAULT_HEIGHT = 720;
17821 const POPUP_CLOSE_POLL_MS = 500;
17822 function startOAuth(service, options = {}) {
17823 if (typeof service !== "string" || service === "") {
17824 return Promise.reject(
17825 new Error("[desktop-mode] startOAuth requires a non-empty service slug.")
17826 );
17827 }
17828 const restRoot = readRestRoot$1();
17829 const restNonce = readRestNonce$1();
17830 return trackedFetch$1(
17831 joinRestUrl(restRoot, "desktop-mode/v1/oauth/start"),
17832 {
17833 method: "POST",
17834 headers: {
17835 "Content-Type": "application/json",
17836 "X-WP-Nonce": restNonce ?? ""
17837 },
17838 body: JSON.stringify({ service })
17839 },
17840 { source: "desktop-mode/oauth-start" }
17841 ).then(async (res) => {
17842 if (!res.ok) {
17843 const text = await res.text().catch(() => "");
17844 throw new Error(
17845 `[desktop-mode] OAuth start failed (${res.status}): ${text}`
17846 );
17847 }
17848 return await res.json();
17849 }).then((startBody) => openPopupAndWait(startBody, service, options));
17850 }
17851 function openPopupAndWait(body, service, options) {
17852 return new Promise((resolve2, reject) => {
17853 const width = options.width ?? POPUP_DEFAULT_WIDTH;
17854 const height = options.height ?? POPUP_DEFAULT_HEIGHT;
17855 const left = Math.max(0, Math.floor((window.screen.width - width) / 2));
17856 const top = Math.max(0, Math.floor((window.screen.height - height) / 2));
17857 const features = [
17858 `width=${width}`,
17859 `height=${height}`,
17860 `left=${left}`,
17861 `top=${top}`,
17862 "menubar=no",
17863 "toolbar=no",
17864 "location=yes",
17865 "status=no",
17866 "resizable=yes",
17867 "scrollbars=yes"
17868 ].join(",");
17869 const popup = window.open(
17870 body.authorize_url,
17871 `desktop-mode-oauth-${service}`,
17872 features
17873 );
17874 if (!popup) {
17875 reject(
17876 new Error(
17877 "[desktop-mode] OAuth popup blocked. Tell users to allow popups for this site."
17878 )
17879 );
17880 return;
17881 }
17882 const expectedOrigin = window.location.origin;
17883 let pollTimer = null;
17884 let detached = false;
17885 const cleanup = () => {
17886 if (detached) {
17887 return;
17888 }
17889 detached = true;
17890 window.removeEventListener("message", onMessage);
17891 if (pollTimer !== null) {
17892 window.clearInterval(pollTimer);
17893 pollTimer = null;
17894 }
17895 };
17896 const onMessage = (e) => {
17897 if (e.origin !== expectedOrigin) {
17898 return;
17899 }
17900 const data = e.data;
17901 if (!data || data.type !== "desktop-mode-oauth-callback") {
17902 return;
17903 }
17904 const payload = data.payload;
17905 cleanup();
17906 if (payload && payload.ok) {
17907 resolve2(payload);
17908 } else {
17909 const reason = payload?.reason ?? "unknown";
17910 const message = payload?.message ?? "OAuth flow failed";
17911 const err = new Error(
17912 `[desktop-mode] startOAuth(${service}) failed: ${reason} — ${message}`
17913 );
17914 err.cause = payload;
17915 reject(err);
17916 }
17917 };
17918 window.addEventListener("message", onMessage);
17919 pollTimer = window.setInterval(() => {
17920 if (popup.closed) {
17921 cleanup();
17922 reject(
17923 new Error(
17924 `[desktop-mode] startOAuth(${service}) cancelled — popup closed before completing.`
17925 )
17926 );
17927 }
17928 }, POPUP_CLOSE_POLL_MS);
17929 });
17930 }
17931 function readDesktopConfig() {
17932 return window.desktopModeConfig ?? {};
17933 }
17934 function readRestRoot$1() {
17935 const root = readDesktopConfig().restRoot;
17936 if (typeof root === "string" && root !== "") {
17937 return root;
17938 }
17939 return `${window.location.origin}/wp-json/`;
17940 }
17941 function readRestNonce$1() {
17942 const nonce = readDesktopConfig().restNonce;
17943 return typeof nonce === "string" && nonce !== "" ? nonce : null;
17944 }
17945 const RESERVED_NAMESPACE_KEYS = /* @__PURE__ */ new Set([
17946 "windowManager",
17947 "dock",
17948 "taskbar",
17949 "icons",
17950 "saveSession",
17951 "hooks",
17952 "HOOKS",
17953 "isActive",
17954 "registerWallpaper",
17955 "registerWidget",
17956 "widgetLayer",
17957 "widgets",
17958 "registerSystemTile",
17959 "registerWindow",
17960 "openWindow",
17961 "cloneTemplate",
17962 "onWindow",
17963 "loadVendorScript",
17964 "getWallpaperSurfaces",
17965 "registerModule",
17966 "loadModules",
17967 "whenReady",
17968 "ready",
17969 "isReady",
17970 "setDefaultWindow",
17971 "refreshMenu",
17972 "config",
17973 "ai",
17974 "dragBridge",
17975 "dragManager",
17976 "registerCommand",
17977 "unregisterCommand",
17978 "listCommands",
17979 "registerDestructiveAdminAction",
17980 "unregisterDestructiveAdminAction",
17981 "listDestructiveAdminActions",
17982 "registerSettingsTab",
17983 "unregisterSettingsTab",
17984 "listSettingsTabs",
17985 "registerDockRailRenderer",
17986 "unregisterDockRailRenderer",
17987 "listDockRailRenderers",
17988 "openOsSettings",
17989 "getOsSettings",
17990 "subscribeOsSettings",
17991 "updateOsSettings",
17992 "deriveWindowId",
17993 "listSystemTiles",
17994 "getSystemTile",
17995 "getMenuItems",
17996 "renderIcon",
17997 "applyTileClasses",
17998 "applyTileElement",
17999 "applyTileTooltip",
18000 "dispatchTileRendered",
18001 "isDockElement",
18002 "registerDockSelector",
18003 "registerTitleBarButton",
18004 "unregisterTitleBarButton",
18005 "listTitleBarButtons",
18006 "registerWindowTheme",
18007 "unregisterWindowTheme",
18008 "listWindowThemes",
18009 "applyWindowTheme",
18010 "registerWindowControl",
18011 "unregisterWindowControl",
18012 "listWindowControls",
18013 "applyWindowControls",
18014 "registerWindowSlot",
18015 "unregisterWindowSlot",
18016 "listWindowSlots",
18017 "applyWindowSlot",
18018 "registerWindowNotice",
18019 "unregisterWindowNotice",
18020 "listWindowNotices",
18021 "dismissWindowNotice",
18022 "undismissWindowNotice",
18023 "registerWindowChrome",
18024 "unregisterWindowChrome",
18025 "listWindowChromes",
18026 "applyWindowChrome",
18027 "connect",
18028 "broadcast",
18029 "subscribe",
18030 "registerPalette",
18031 "unregisterPalette",
18032 "listPalettes",
18033 "openPalette",
18034 "devtools",
18035 "createSharedStore",
18036 "presence",
18037 "activity",
18038 "heartbeat",
18039 "showToast",
18040 "renderKeyedList",
18041 "clearKeyedList",
18042 "registerNamespace",
18043 "notify",
18044 "pwa",
18045 "getWindowConfig",
18046 "debug",
18047 "fetch"
18048 ]);
18049 function buildPublicApi(deps2) {
18050 const {
18051 manager,
18052 dock,
18053 layoutDispatcher,
18054 osSettings,
18055 iconsApi: iconsApi2,
18056 filesApi: filesApi2,
18057 saveSession,
18058 widgetLayer,
18059 registerWindow,
18060 openWindowById,
18061 openNewWindowById,
18062 placeSystemTile,
18063 setDefaultWindow,
18064 refreshMenu,
18065 openOsSettings,
18066 aiAssistant,
18067 dragBridge,
18068 dragManager,
18069 connect,
18070 config
18071 } = deps2;
18072 const desktopApi = {
18073 windowManager: manager,
18074 dock,
18075 sideDock: layoutDispatcher?.getSide() ?? null,
18076 desktopLayout: osSettings.getOsSettingsSnapshot().desktopLayout,
18077 icons: iconsApi2,
18078 files: filesApi2,
18079 confirm: wpdConfirm,
18080 saveSession,
18081 hooks: rawHooks(),
18082 HOOKS,
18083 isActive: () => !!document.getElementById("desktop-mode-shell"),
18084 registerWallpaper: (def) => {
18085 register$2(def);
18086 osSettings.apply();
18087 },
18088 registerWidget: (def) => {
18089 register(def);
18090 },
18091 widgetLayer,
18092 widgets: {
18093 redock: (id) => {
18094 widgetLayer?.redock(id);
18095 }
18096 },
18097 loadVendorScript,
18098 getWallpaperSurfaces: () => collectWallpaperSurfaces(manager),
18099 registerWindow,
18100 openWindow: openWindowById,
18101 openNewWindow: openNewWindowById,
18102 fetch: (input, requestInit, opts) => trackedFetch(manager, input, requestInit, opts),
18103 repaintLoadingOverlays,
18104 cloneTemplate,
18105 onWindow,
18106 createInfiniteList,
18107 startOAuth,
18108 registerSystemTile: (item) => {
18109 placeSystemTile(item);
18110 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: item.id });
18111 },
18112 registerModule,
18113 loadModules,
18114 whenReady,
18115 ready: whenReady,
18116 isReady,
18117 setDefaultWindow,
18118 refreshMenu,
18119 config,
18120 ai: aiAssistant,
18121 dragBridge,
18122 dragManager,
18123 registerCommand,
18124 unregisterCommand,
18125 listCommands,
18126 registerDestructiveAdminAction,
18127 unregisterDestructiveAdminAction,
18128 listDestructiveAdminActions,
18129 registerSettingsTab,
18130 unregisterSettingsTab,
18131 listSettingsTabs,
18132 registerDockRailRenderer: register$1,
18133 unregisterDockRailRenderer: unregister$1,
18134 listDockRailRenderers: list,
18135 openOsSettings,
18136 getOsSettings: () => osSettings.getOsSettingsSnapshot(),
18137 subscribeOsSettings: (cb) => osSettings.subscribeOsSettings(cb),
18138 updateOsSettings: (patch, opts = {}) => {
18139 if (typeof patch.wallpaper === "string") {
18140 osSettings.state.wallpaper = patch.wallpaper;
18141 }
18142 if (typeof patch.accent === "string") {
18143 osSettings.state.accent = patch.accent;
18144 }
18145 if (typeof patch.dockSize === "string") {
18146 osSettings.state.dockSize = patch.dockSize;
18147 }
18148 if (typeof patch.desktopLayout === "string") {
18149 osSettings.state.desktopLayout = patch.desktopLayout;
18150 }
18151 if (typeof patch.dockRailRenderer === "string") {
18152 osSettings.state.dockRailRenderer = patch.dockRailRenderer;
18153 }
18154 if (patch.ai && typeof patch.ai === "object") {
18155 osSettings.state.ai = { ...osSettings.state.ai, ...patch.ai };
18156 }
18157 if (typeof patch.nativePostsEnabled === "boolean") {
18158 osSettings.state.nativePostsEnabled = patch.nativePostsEnabled;
18159 }
18160 if (typeof patch.nativePagesEnabled === "boolean") {
18161 osSettings.state.nativePagesEnabled = patch.nativePagesEnabled;
18162 }
18163 if (typeof patch.nativeUsersEnabled === "boolean") {
18164 osSettings.state.nativeUsersEnabled = patch.nativeUsersEnabled;
18165 }
18166 if (typeof patch.nativePluginsEnabled === "boolean") {
18167 osSettings.state.nativePluginsEnabled = patch.nativePluginsEnabled;
18168 }
18169 if (typeof patch.nativeCommentsEnabled === "boolean") {
18170 osSettings.state.nativeCommentsEnabled = patch.nativeCommentsEnabled;
18171 }
18172 if (typeof patch.foldersSharingEnabled === "boolean") {
18173 osSettings.state.foldersSharingEnabled = patch.foldersSharingEnabled;
18174 }
18175 if (Array.isArray(patch.nativePostsHiddenColumns)) {
18176 osSettings.state.nativePostsHiddenColumns = patch.nativePostsHiddenColumns.filter(
18177 (v) => typeof v === "string" && v !== ""
18178 ).slice(0, 32);
18179 }
18180 if (patch.itemVisibility && typeof patch.itemVisibility === "object") {
18181 const allowed = ["both", "dock", "desktop", "hidden"];
18182 const next = {};
18183 for (const [k, v] of Object.entries(
18184 patch.itemVisibility
18185 )) {
18186 if (typeof k !== "string" || k === "") {
18187 continue;
18188 }
18189 if (typeof v !== "string" || !allowed.includes(v)) {
18190 continue;
18191 }
18192 next[k] = v;
18193 }
18194 osSettings.state.itemVisibility = next;
18195 }
18196 if (Array.isArray(patch.dockOrder)) {
18197 osSettings.state.dockOrder = patch.dockOrder.filter(
18198 (v) => typeof v === "string" && v !== ""
18199 ).slice(0, 256);
18200 }
18201 if (patch.dockPromotedPositions && typeof patch.dockPromotedPositions === "object") {
18202 const MAX_COORD = 1e5;
18203 const next = {};
18204 for (const [k, v] of Object.entries(
18205 patch.dockPromotedPositions
18206 )) {
18207 if (typeof k !== "string" || k === "") {
18208 continue;
18209 }
18210 if (!v || typeof v !== "object") {
18211 continue;
18212 }
18213 const pos = v;
18214 if (typeof pos.x !== "number" || typeof pos.y !== "number" || !Number.isFinite(pos.x) || !Number.isFinite(pos.y) || Math.abs(pos.x) > MAX_COORD || Math.abs(pos.y) > MAX_COORD) {
18215 continue;
18216 }
18217 next[k] = { x: pos.x, y: pos.y };
18218 if (Object.keys(next).length >= 256) {
18219 break;
18220 }
18221 }
18222 osSettings.state.dockPromotedPositions = next;
18223 }
18224 osSettings.save(opts);
18225 if (patch.itemVisibility || patch.dockOrder) {
18226 layoutDispatcher?.refresh();
18227 }
18228 },
18229 deriveWindowId: (url, overrideAdminUrl) => deriveWindowId(url, overrideAdminUrl ?? config.adminUrl),
18230 listSystemTiles: () => layoutDispatcher?.listSystemTiles() ?? [],
18231 getSystemTile: (id) => layoutDispatcher?.getSystemTile(id) ?? null,
18232 getMenuItems: () => layoutDispatcher?.getMenuItems() ?? [],
18233 renderIcon,
18234 applyTileClasses,
18235 applyTileElement,
18236 applyTileTooltip,
18237 dispatchTileRendered,
18238 isDockElement,
18239 registerDockSelector,
18240 registerTitleBarButton,
18241 unregisterTitleBarButton,
18242 listTitleBarButtons,
18243 registerWindowTheme,
18244 unregisterWindowTheme,
18245 listWindowThemes,
18246 applyWindowTheme: (windowId, override) => {
18247 const win = manager.getById(windowId);
18248 if (!win) {
18249 return;
18250 }
18251 win.setAppearanceTheme(override);
18252 },
18253 registerWindowControl,
18254 unregisterWindowControl,
18255 listWindowControls,
18256 applyWindowControls: (windowId, override) => {
18257 const win = manager.getById(windowId);
18258 if (!win) {
18259 return;
18260 }
18261 win.setAppearanceControls(override);
18262 },
18263 registerWindowSlot,
18264 unregisterWindowSlot,
18265 listWindowSlots,
18266 applyWindowSlot: (windowId, slot, slotConfig) => {
18267 const win = manager.getById(windowId);
18268 if (!win) {
18269 return;
18270 }
18271 win.setAppearanceSlot(slot, slotConfig);
18272 },
18273 registerWindowNotice,
18274 unregisterWindowNotice,
18275 listWindowNotices,
18276 dismissWindowNotice,
18277 undismissWindowNotice,
18278 registerWindowChrome,
18279 unregisterWindowChrome,
18280 listWindowChromes,
18281 applyWindowChrome: (windowId, chromeId) => {
18282 const win = manager.getById(windowId);
18283 if (!win) {
18284 return;
18285 }
18286 win.setAppearanceChrome(chromeId);
18287 },
18288 connect,
18289 broadcast,
18290 subscribe: subscribe$2,
18291 registerPalette,
18292 unregisterPalette,
18293 listPalettes,
18294 openPalette: openPaletteOnly,
18295 devtools,
18296 createSharedStore,
18297 presence: presenceApi,
18298 activity,
18299 heartbeat,
18300 showToast,
18301 notify: notify$3,
18302 pwa: {
18303 promptInstall,
18304 undismissInstallHint,
18305 getState: getPwaState,
18306 subscribe: subscribePwaState,
18307 requestNotificationPermission,
18308 getNotificationPermission
18309 },
18310 renderKeyedList,
18311 clearKeyedList,
18312 registerNamespace: (name, api) => {
18313 if (typeof name !== "string" || name === "") {
18314 console.warn(
18315 "[desktop-mode] registerNamespace: name must be a non-empty string"
18316 );
18317 return;
18318 }
18319 if (!api || typeof api !== "object") {
18320 console.warn(
18321 `[desktop-mode] registerNamespace("${name}"): api must be an object`
18322 );
18323 return;
18324 }
18325 if (RESERVED_NAMESPACE_KEYS.has(name)) {
18326 console.warn(
18327 `[desktop-mode] registerNamespace("${name}"): name is reserved by the shell — pick a plugin-specific key`
18328 );
18329 return;
18330 }
18331 desktopApi[name] = api;
18332 },
18333 getWindowConfig: (id) => {
18334 const store2 = window.desktopModeWindowConfig;
18335 if (!store2 || typeof store2 !== "object") {
18336 return void 0;
18337 }
18338 const value = store2[id];
18339 return value === void 0 ? void 0 : value;
18340 },
18341 debug: {
18342 window: (id) => {
18343 const entry = (config.nativeWindows ?? []).find(
18344 (e) => e.id === id
18345 );
18346 if (!entry) {
18347 return null;
18348 }
18349 const url = entry.scriptUrl || "";
18350 let loadPath = "unknown";
18351 let tagInDom = false;
18352 if (url) {
18353 const lazyTag = document.querySelector(
18354 `script[data-desktop-mode-vendor="${url.replace(/"/g, '\\"')}"]`
18355 );
18356 if (lazyTag) {
18357 loadPath = "lazy";
18358 tagInDom = true;
18359 } else {
18360 const eagerTag = Array.from(
18361 document.querySelectorAll(
18362 "script[src]"
18363 )
18364 ).find((s) => s.src === url);
18365 if (eagerTag) {
18366 loadPath = "eager";
18367 tagInDom = true;
18368 }
18369 }
18370 }
18371 const cfgStore = window.desktopModeWindowConfig;
18372 const configPresent = !!(cfgStore && typeof cfgStore === "object" && Object.prototype.hasOwnProperty.call(cfgStore, id));
18373 return {
18374 id,
18375 scriptHandle: entry.scriptHandle || "",
18376 scriptUrl: url,
18377 loadPath,
18378 tagInDom,
18379 configPresent,
18380 extras: {
18381 hasTranslations: !!entry.scriptTranslations,
18382 l10nCount: (entry.scriptL10n ?? []).length,
18383 beforeCount: (entry.scriptBefore ?? []).length,
18384 afterCount: (entry.scriptAfter ?? []).length
18385 }
18386 };
18387 }
18388 }
18389 };
18390 return desktopApi;
18391 }
18392 function installPublicApi(api) {
18393 if (!window.wp) {
18394 window.wp = {};
18395 }
18396 if (!window.wp.desktop) {
18397 window.wp.desktop = api;
18398 return;
18399 }
18400 Object.assign(
18401 window.wp.desktop,
18402 api
18403 );
18404 }
18405 const store$1 = createSharedStore("desktop-mode/layout", () => ({
18406 // Default mirrors the OsSettingsSnapshot default; the shell
18407 // re-publishes the persisted value as soon as it boots.
18408 layout: "classic"
18409 }));
18410 function setCurrentLayout(layout) {
18411 if (store$1.state.layout === layout) {
18412 return;
18413 }
18414 store$1.state.layout = layout;
18415 store$1.notify();
18416 }
18417 class DesktopFile {
18418 constructor(shape) {
18419 this.shape = shape;
18420 }
18421 /** Title shown under the tile. Defaults to `shape.title`. */
18422 title() {
18423 return this.shape.title;
18424 }
18425 /** Dashicon class or data URI. Defaults to `shape.icon`. */
18426 icon() {
18427 return this.shape.icon;
18428 }
18429 /** Optional preview-image URL. Defaults to `shape.previewUrl`. */
18430 previewUrl() {
18431 return this.shape.previewUrl;
18432 }
18433 /** Reference (id, URL, …). */
18434 ref() {
18435 return this.shape.ref;
18436 }
18437 /** Whether the underlying entity still exists. */
18438 exists() {
18439 return this.shape.exists;
18440 }
18441 }
18442 class DefaultDesktopFile extends DesktopFile {
18443 constructor(shape, typeSlug) {
18444 super(shape);
18445 this.typeSlug = typeSlug;
18446 }
18447 type() {
18448 return this.typeSlug;
18449 }
18450 }
18451 const seed$1 = /* @__PURE__ */ new Map();
18452 const listeners$1 = /* @__PURE__ */ new Set();
18453 function registerType(def) {
18454 if (!def.type) {
18455 throw new Error("[desktop-mode] registerType: `type` is required.");
18456 }
18457 if (!def.label) {
18458 throw new Error("[desktop-mode] registerType: `label` is required.");
18459 }
18460 seed$1.set(def.type, {
18461 type: def.type,
18462 label: def.label,
18463 sort: typeof def.sort === "number" ? def.sort : 100,
18464 DesktopFile: def.DesktopFile
18465 });
18466 doAction("desktop-mode.files.type-registered", def.type, def);
18467 notify$1();
18468 }
18469 function unregisterType(typeSlug) {
18470 if (seed$1.delete(typeSlug)) {
18471 doAction("desktop-mode.files.type-unregistered", typeSlug);
18472 notify$1();
18473 }
18474 }
18475 function getType(typeSlug) {
18476 const entry = seed$1.get(typeSlug);
18477 return entry ? entry : null;
18478 }
18479 function getTypes() {
18480 const list2 = Array.from(seed$1.values()).slice();
18481 const filtered = applyFilters(
18482 "desktop-mode.files.types",
18483 list2
18484 );
18485 const arr = Array.isArray(filtered) ? filtered : list2;
18486 arr.sort((a, b) => {
18487 if (a.sort !== b.sort) {
18488 return a.sort - b.sort;
18489 }
18490 return a.label.localeCompare(b.label);
18491 });
18492 return arr;
18493 }
18494 function resolve(shape) {
18495 const entry = seed$1.get(shape.type);
18496 if (entry?.DesktopFile) {
18497 return new entry.DesktopFile(shape);
18498 }
18499 return new DefaultDesktopFile(shape, shape.type);
18500 }
18501 function subscribe(cb) {
18502 listeners$1.add(cb);
18503 return () => listeners$1.delete(cb);
18504 }
18505 function notify$1() {
18506 for (const cb of listeners$1) {
18507 try {
18508 cb();
18509 } catch (err) {
18510 console.error("[desktop-mode] files registry subscriber threw:", err);
18511 }
18512 }
18513 }
18514 const seed = /* @__PURE__ */ new Map();
18515 const listeners = /* @__PURE__ */ new Set();
18516 let userAssociations = {};
18517 function setUserAssociations(map) {
18518 userAssociations = { ...map };
18519 notify();
18520 }
18521 function getUserAssociations() {
18522 return { ...userAssociations };
18523 }
18524 function registerOpener(def) {
18525 if (!def.id) {
18526 throw new Error("[desktop-mode] registerOpener: `id` is required.");
18527 }
18528 if (!def.label) {
18529 throw new Error("[desktop-mode] registerOpener: `label` is required.");
18530 }
18531 if (!Array.isArray(def.types) || def.types.length === 0) {
18532 throw new Error("[desktop-mode] registerOpener: `types` must be a non-empty array.");
18533 }
18534 if (!def.handler || typeof def.handler !== "object") {
18535 throw new Error("[desktop-mode] registerOpener: `handler` is required.");
18536 }
18537 seed.set(def.id, {
18538 id: def.id,
18539 label: def.label,
18540 types: def.types.slice(),
18541 isDefault: !!def.isDefault,
18542 sort: typeof def.sort === "number" ? def.sort : 100,
18543 handler: def.handler
18544 });
18545 doAction("desktop-mode.files.opener-registered", def.id, def);
18546 notify();
18547 }
18548 function unregisterOpener(id) {
18549 if (seed.delete(id)) {
18550 doAction("desktop-mode.files.opener-unregistered", id);
18551 notify();
18552 }
18553 }
18554 function getOpener(id) {
18555 return seed.get(id) ?? null;
18556 }
18557 function getOpeners() {
18558 const list2 = Array.from(seed.values()).slice();
18559 const filtered = applyFilters(
18560 "desktop-mode.files.openers",
18561 list2
18562 );
18563 const arr = Array.isArray(filtered) ? filtered : list2;
18564 arr.sort((a, b) => {
18565 const sa = typeof a.sort === "number" ? a.sort : 100;
18566 const sb = typeof b.sort === "number" ? b.sort : 100;
18567 if (sa !== sb) {
18568 return sa - sb;
18569 }
18570 return a.label.localeCompare(b.label);
18571 });
18572 return arr;
18573 }
18574 function getOpenersForType(type) {
18575 return getOpeners().filter((e) => e.types.includes(type));
18576 }
18577 function resolveOpener(type) {
18578 const candidates = getOpenersForType(type);
18579 if (candidates.length === 0) {
18580 return null;
18581 }
18582 const override = userAssociations[type];
18583 let resolved = null;
18584 if (override) {
18585 resolved = candidates.find((e) => e.id === override) ?? null;
18586 }
18587 if (!resolved) {
18588 resolved = candidates.find((e) => e.isDefault) ?? null;
18589 }
18590 if (!resolved) {
18591 resolved = candidates[0];
18592 }
18593 const filtered = applyFilters(
18594 "desktop-mode.files.resolve-opener",
18595 resolved,
18596 type
18597 );
18598 return filtered ?? null;
18599 }
18600 function subscribeOpeners(cb) {
18601 listeners.add(cb);
18602 return () => listeners.delete(cb);
18603 }
18604 function notify() {
18605 for (const cb of listeners) {
18606 try {
18607 cb();
18608 } catch (err) {
18609 console.error("[desktop-mode] openers subscriber threw:", err);
18610 }
18611 }
18612 }
18613 let deps$1 = null;
18614 function installOpenDeps(next) {
18615 deps$1 = next;
18616 }
18617 async function openFile(file, ctx) {
18618 if (!deps$1) {
18619 console.warn(
18620 "[desktop-mode] wp.desktop.files.open() called before the shell installed open deps. The file will not open."
18621 );
18622 return false;
18623 }
18624 const opener = resolveOpener(file.type());
18625 if (!opener) {
18626 doAction("desktop-mode.files.open-failed", {
18627 reason: "no-opener",
18628 type: file.type(),
18629 ref: file.ref()
18630 });
18631 return false;
18632 }
18633 doAction("desktop-mode.files.opening", { file, openerId: opener.id });
18634 try {
18635 const handler = opener.handler;
18636 if (handler.kind === "url") {
18637 const url = await handler.url(file);
18638 if (!url) {
18639 return false;
18640 }
18641 const id = handler.windowId ? handler.windowId(file) : deps$1.deriveWindowId(url);
18642 const title = handler.title ? handler.title(file) : file.title();
18643 const icon = file.icon();
18644 const opened = deps$1.openUrl({ id, url, title, icon });
18645 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "url" });
18646 return opened;
18647 }
18648 if (handler.kind === "window") {
18649 const config = handler.config ? handler.config(file) : void 0;
18650 const opened = deps$1.openNativeWindow(handler.windowId, config);
18651 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "window" });
18652 return opened;
18653 }
18654 await handler.open(file, ctx);
18655 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "js" });
18656 return true;
18657 } catch (err) {
18658 doAction("desktop-mode.files.open-failed", {
18659 reason: "handler-threw",
18660 type: file.type(),
18661 ref: file.ref(),
18662 openerId: opener.id,
18663 error: err
18664 });
18665 console.error("[desktop-mode] file opener threw:", err);
18666 return false;
18667 }
18668 }
18669 function registerBuiltInFileTypes() {
18670 registerType({ type: "shortcut", label: "Plugin shortcut", sort: 1 });
18671 registerType({ type: "folder", label: "Folder", sort: 5 });
18672 registerType({ type: "post", label: "Post", sort: 10 });
18673 registerType({ type: "attachment", label: "Media", sort: 20 });
18674 registerType({ type: "user", label: "User", sort: 30 });
18675 registerType({ type: "term", label: "Taxonomy term", sort: 40 });
18676 registerType({ type: "comment", label: "Comment", sort: 50 });
18677 registerType({ type: "bookmark", label: "Bookmark", sort: 60 });
18678 registerType({ type: "link", label: "Web link", sort: 70 });
18679 registerType({ type: "embed", label: "Embedded web window", sort: 80 });
18680 }
18681 let deps = null;
18682 function installRestDeps(next) {
18683 deps = next;
18684 }
18685 function ensureDeps() {
18686 if (!deps) {
18687 throw new Error("[desktop-mode] files REST client called before installRestDeps().");
18688 }
18689 return deps;
18690 }
18691 class FilesConflictError extends Error {
18692 constructor(detail) {
18693 super(
18694 `Row was changed by ${detail.actor.name || "another session"} (parent="${detail.current.parentName}")`
18695 );
18696 this.name = "FilesConflictError";
18697 this.status = 409;
18698 this.detail = detail;
18699 }
18700 }
18701 async function call(path, init2) {
18702 const { baseUrl, nonce } = ensureDeps();
18703 const url = joinRestUrl(baseUrl, path);
18704 const headers = new Headers(init2.headers ?? {});
18705 headers.set("X-WP-Nonce", nonce);
18706 if (init2.body && !headers.has("Content-Type")) {
18707 headers.set("Content-Type", "application/json");
18708 }
18709 const res = await trackedFetch$1(
18710 url,
18711 { ...init2, headers, credentials: "same-origin" },
18712 { source: "desktop-mode/files" }
18713 );
18714 const text = await res.text();
18715 let body = null;
18716 let parseError = null;
18717 if (text) {
18718 try {
18719 body = JSON.parse(text);
18720 } catch (e) {
18721 body = null;
18722 parseError = e;
18723 }
18724 }
18725 if (!res.ok) {
18726 if (res.status === 409) {
18727 const data = body?.data?.data ?? body?.data;
18728 if (data && typeof data === "object") {
18729 throw new FilesConflictError(data);
18730 }
18731 }
18732 const err = body;
18733 throw new Error(
18734 `[desktop-mode] files REST ${res.status}: ${err?.code ?? ""} ${err?.message ?? ""}`.trim()
18735 );
18736 }
18737 if (null === body) {
18738 if (parseError && text) {
18739 const head = text.slice(0, 120).replace(/\s+/g, " ");
18740 throw new Error(
18741 `[desktop-mode] files REST ${res.status} returned non-JSON body — ${parseError.message}. First 120 chars: ${head}`
18742 );
18743 }
18744 throw new Error(
18745 `[desktop-mode] files REST ${res.status}: empty or unparseable body.`
18746 );
18747 }
18748 return body;
18749 }
18750 function listPlacements(folderId = 0) {
18751 return call(
18752 `/placements?folder=${encodeURIComponent(String(folderId))}`,
18753 { method: "GET" }
18754 );
18755 }
18756 function createPlacement(body) {
18757 return call("/placements", {
18758 method: "POST",
18759 body: JSON.stringify(body)
18760 });
18761 }
18762 function updatePlacement(id, body, ifMatchMs) {
18763 const headers = {};
18764 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
18765 headers["If-Match"] = String(ifMatchMs);
18766 }
18767 return call(`/placements/${id}`, {
18768 method: "PATCH",
18769 body: JSON.stringify(body),
18770 headers
18771 });
18772 }
18773 function deletePlacement(id) {
18774 return call(`/placements/${id}`, { method: "DELETE" });
18775 }
18776 async function restoreTrashedItem(id, type) {
18777 const { baseUrl, nonce } = ensureDeps();
18778 const root = baseUrl.replace(/\/files\/?$/, "");
18779 const url = `${root}/recycle-bin/restore`;
18780 const res = await trackedFetch$1(
18781 url,
18782 {
18783 method: "POST",
18784 headers: {
18785 "Content-Type": "application/json",
18786 "X-WP-Nonce": nonce
18787 },
18788 credentials: "same-origin",
18789 body: JSON.stringify({ items: [{ id, type }] })
18790 },
18791 { source: "desktop-mode/files" }
18792 );
18793 if (!res.ok) {
18794 throw new Error(`[desktop-mode] restore ${res.status}`);
18795 }
18796 return await res.json();
18797 }
18798 function listFolders() {
18799 return call("/folders", { method: "GET" });
18800 }
18801 function createFolder(body) {
18802 return call("/folders", {
18803 method: "POST",
18804 body: JSON.stringify(body)
18805 });
18806 }
18807 function updateFolder(id, body, ifMatchMs) {
18808 const headers = {};
18809 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
18810 headers["If-Match"] = String(ifMatchMs);
18811 }
18812 return call(`/folders/${id}`, {
18813 method: "PATCH",
18814 body: JSON.stringify(body),
18815 headers
18816 });
18817 }
18818 function deleteFolder(id) {
18819 return call(`/folders/${id}`, { method: "DELETE" });
18820 }
18821 function saveAssociations(associations) {
18822 return call("/associations", {
18823 method: "PUT",
18824 body: JSON.stringify({ associations })
18825 });
18826 }
18827 function listShares(folderId) {
18828 return call(`/folders/${folderId}/shares`, { method: "GET" });
18829 }
18830 function inviteShare(folderId, body) {
18831 return call(`/folders/${folderId}/shares`, {
18832 method: "POST",
18833 body: JSON.stringify(body)
18834 });
18835 }
18836 function updateShareCapability(folderId, shareId, capability) {
18837 return call(`/folders/${folderId}/shares/${shareId}`, {
18838 method: "PATCH",
18839 body: JSON.stringify({ capability })
18840 });
18841 }
18842 function revokeShare(folderId, shareId) {
18843 return call(`/folders/${folderId}/shares/${shareId}`, {
18844 method: "DELETE"
18845 });
18846 }
18847 function acceptShare(folderId, shareId) {
18848 return call(`/folders/${folderId}/shares/${shareId}/accept`, {
18849 method: "POST"
18850 });
18851 }
18852 function denyShare(folderId, shareId) {
18853 return call(`/folders/${folderId}/shares/${shareId}/deny`, {
18854 method: "POST"
18855 });
18856 }
18857 function leaveShare(folderId) {
18858 return call(`/folders/${folderId}/leave`, {
18859 method: "POST"
18860 });
18861 }
18862 function purgeFolderSharingTables() {
18863 return call(
18864 "/folder-sharing-tables/purge",
18865 { method: "POST" }
18866 );
18867 }
18868 const filesRest = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
18869 __proto__: null,
18870 FilesConflictError,
18871 acceptShare,
18872 createFolder,
18873 createPlacement,
18874 deleteFolder,
18875 deletePlacement,
18876 denyShare,
18877 installRestDeps,
18878 inviteShare,
18879 leaveShare,
18880 listFolders,
18881 listPlacements,
18882 listShares,
18883 purgeFolderSharingTables,
18884 restoreTrashedItem,
18885 revokeShare,
18886 saveAssociations,
18887 updateFolder,
18888 updatePlacement,
18889 updateShareCapability
18890 }, Symbol.toStringTag, { value: "Module" }));
18891 const STORE_KEY = "desktop-mode/files";
18892 function getFilesStore() {
18893 return createSharedStore(STORE_KEY, () => ({
18894 placementsByFolder: /* @__PURE__ */ new Map(),
18895 folders: /* @__PURE__ */ new Map(),
18896 hydratedFolders: /* @__PURE__ */ new Set()
18897 }));
18898 }
18899 function fireChanged(detail) {
18900 if (typeof document === "undefined") {
18901 return;
18902 }
18903 document.dispatchEvent(
18904 new CustomEvent("desktop-mode-files-changed", {
18905 detail: { source: "local", ...detail }
18906 })
18907 );
18908 }
18909 function setFolderPlacements(folderId, placements) {
18910 const store2 = getFilesStore();
18911 const next = new Map(store2.state.placementsByFolder);
18912 next.set(folderId, placements.slice());
18913 const hydrated = new Set(store2.state.hydratedFolders);
18914 hydrated.add(folderId);
18915 store2.state = { ...store2.state, placementsByFolder: next, hydratedFolders: hydrated };
18916 store2.notify();
18917 fireChanged({ kind: "placements-set", folderId });
18918 }
18919 function upsertPlacement(placement, source = "local") {
18920 if (!placement || typeof placement.id !== "number") {
18921 console.warn(
18922 "[desktop-mode] upsertPlacement called with a non-placement value; ignoring.",
18923 placement
18924 );
18925 return;
18926 }
18927 const store2 = getFilesStore();
18928 const next = new Map(store2.state.placementsByFolder);
18929 for (const [folderId, list2] of next) {
18930 const idx2 = list2.findIndex((p) => p && p.id === placement.id);
18931 if (idx2 >= 0 && folderId !== placement.parentId) {
18932 const copy = list2.filter(Boolean);
18933 const removeAt = copy.findIndex((p) => p.id === placement.id);
18934 if (removeAt >= 0) {
18935 copy.splice(removeAt, 1);
18936 }
18937 next.set(folderId, copy);
18938 }
18939 }
18940 const rawTarget = next.get(placement.parentId)?.slice() ?? [];
18941 const target = rawTarget.filter(Boolean);
18942 const idx = target.findIndex((p) => p.id === placement.id);
18943 if (idx >= 0) {
18944 target[idx] = placement;
18945 } else {
18946 target.push(placement);
18947 }
18948 next.set(placement.parentId, target);
18949 store2.state = { ...store2.state, placementsByFolder: next };
18950 store2.notify();
18951 fireChanged({ kind: "placement-upserted", placementId: placement.id, folderId: placement.parentId, source });
18952 }
18953 function removePlacement(placementId, source = "local") {
18954 const store2 = getFilesStore();
18955 const next = new Map(store2.state.placementsByFolder);
18956 let touchedFolder;
18957 for (const [folderId, list2] of next) {
18958 const idx = list2.findIndex((p) => p && p.id === placementId);
18959 if (idx >= 0) {
18960 const copy = list2.filter(Boolean).filter(
18961 (p) => p.id !== placementId
18962 );
18963 next.set(folderId, copy);
18964 touchedFolder = folderId;
18965 }
18966 }
18967 if (touchedFolder === void 0) {
18968 return;
18969 }
18970 store2.state = { ...store2.state, placementsByFolder: next };
18971 store2.notify();
18972 fireChanged({ kind: "placement-removed", placementId, folderId: touchedFolder, source });
18973 }
18974 function setFolders(folders) {
18975 const store2 = getFilesStore();
18976 const next = /* @__PURE__ */ new Map();
18977 for (const f of folders) {
18978 next.set(f.id, f);
18979 }
18980 store2.state = { ...store2.state, folders: next };
18981 store2.notify();
18982 fireChanged({ kind: "folders-set" });
18983 }
18984 function upsertFolder(folder, source = "local") {
18985 const store2 = getFilesStore();
18986 const next = new Map(store2.state.folders);
18987 next.set(folder.id, folder);
18988 store2.state = { ...store2.state, folders: next };
18989 store2.notify();
18990 fireChanged({ kind: "folder-upserted", folderRowId: folder.id, source });
18991 }
18992 function removeFolder(folderId, source = "local") {
18993 const store2 = getFilesStore();
18994 const folders = new Map(store2.state.folders);
18995 folders.delete(folderId);
18996 const placements = new Map(store2.state.placementsByFolder);
18997 placements.delete(folderId);
18998 store2.state = { ...store2.state, folders, placementsByFolder: placements };
18999 store2.notify();
19000 fireChanged({ kind: "folder-removed", folderRowId: folderId, source });
19001 }
19002 function subscribeFilesStore(cb) {
19003 const store2 = getFilesStore();
19004 const off = store2.subscribe(cb);
19005 return off;
19006 }
19007 function getFilesState() {
19008 return getFilesStore().getState();
19009 }
19010 const store = {
19011 getState: getFilesState,
19012 subscribe: subscribeFilesStore,
19013 setFolderPlacements,
19014 upsertPlacement,
19015 upsertFolder,
19016 removePlacement,
19017 removeFolder
19018 };
19019 const styles$3 = css`:host{display:inline-block}`;
19020 const styles$2 = css`:host{position:absolute;width:var( --wpd-ribbon-size,90px );height:var( --wpd-ribbon-size,90px );overflow:hidden;pointer-events:none;z-index:var( --wpd-ribbon-z,2 )}:host( [ hidden ] ){display:none}.banner{position:absolute;display:block;width:var( --wpd-ribbon-banner-width,140px );padding:var( --wpd-ribbon-padding,4px 0 );text-align:center;font:var( --wpd-ribbon-font,700 10px/1.4 var( --desktop-mode-font,system-ui ) );letter-spacing:var( --wpd-ribbon-tracking,0.06em );text-transform:uppercase;color:var( --wpd-ribbon-fg,#fff );background:var( --wpd-ribbon-bg,var( --wp-admin-theme-color,#2271b1 ) );box-shadow:var( --wpd-ribbon-shadow,0 2px 4px rgba( 0,0,0,0.2 ) )}:host(:not( [ placement ] ) ),:host( [ placement='top-end' ] ){inset-block-start:0;inset-inline-end:0}:host(:not( [ placement ] ) ) .banner,:host( [ placement='top-end' ] ) .banner{inset-block-start:var( --wpd-ribbon-banner-offset,20px );inset-inline-end:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( 45deg )}:host( [ placement='top-start' ] ){inset-block-start:0;inset-inline-start:0}:host( [ placement='top-start' ] ) .banner{inset-block-start:var( --wpd-ribbon-banner-offset,20px );inset-inline-start:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( -45deg )}:host( [ placement='bottom-end' ] ){inset-block-end:0;inset-inline-end:0}:host( [ placement='bottom-end' ] ) .banner{inset-block-end:var( --wpd-ribbon-banner-offset,20px );inset-inline-end:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( -45deg )}:host( [ placement='bottom-start' ] ){inset-block-end:0;inset-inline-start:0}:host( [ placement='bottom-start' ] ) .banner{inset-block-end:var( --wpd-ribbon-banner-offset,20px );inset-inline-start:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( 45deg )}:host-context( [ dir='rtl' ] ):host(:not( [ placement ] ) ) .banner,:host-context( [ dir='rtl' ] ):host( [ placement='top-end' ] ) .banner{transform:rotate( -45deg )}:host-context( [ dir='rtl' ] ):host( [ placement='top-start' ] ) .banner{transform:rotate( 45deg )}:host-context( [ dir='rtl' ] ):host( [ placement='bottom-end' ] ) .banner{transform:rotate( 45deg )}:host-context( [ dir='rtl' ] ):host( [ placement='bottom-start' ] ) .banner{transform:rotate( -45deg )}:host( [ tone='success' ] ) .banner{background:var( --wpd-ribbon-success,#1a7f37 )}:host( [ tone='warning' ] ) .banner{background:var( --wpd-ribbon-warning,#9a6700 )}:host( [ tone='danger' ] ) .banner{background:var( --wpd-ribbon-danger,#cf222e )}:host( [ tone='info' ] ) .banner{background:var( --wpd-ribbon-info,#0969da )}:host( [ tone='neutral' ] ) .banner{background:var( --wpd-ribbon-neutral,#57606a )}`;
19021 const _WpdRibbon = class _WpdRibbon extends Component {
19022 render() {
19023 return html`<span class="banner" part="banner"><slot></slot></span>`;
19024 }
19025 };
19026 _WpdRibbon.props = ["placement", "tone"];
19027 _WpdRibbon.styles = [styles$2];
19028 _WpdRibbon.help = {
19029 title: "Ribbon",
19030 summary: "45° corner ribbon. Wraps the top-end (default), top-start, bottom-end, or bottom-start corner of its positioned parent. The host owns clipping + rotation; consumers only set position-relative on the parent and drop a label inside.",
19031 status: "experimental",
19032 since: "0.20.0",
19033 props: [
19034 {
19035 name: "placement",
19036 type: '"top-end" | "top-start" | "bottom-end" | "bottom-start"',
19037 description: "Which corner of the parent the ribbon hugs. Defaults to `top-end` (logical right in LTR, left in RTL)."
19038 },
19039 {
19040 name: "tone",
19041 type: '"primary" | "success" | "warning" | "danger" | "info" | "neutral"',
19042 description: "Background color tone. Defaults to `primary` (the admin theme accent)."
19043 }
19044 ],
19045 slots: [{ name: "(default)", description: "Ribbon label text. Keep short." }],
19046 cssProps: [
19047 { name: "--wpd-ribbon-size", default: "90px", description: "Square clipping window edge." },
19048 { name: "--wpd-ribbon-banner-width", default: "140px", description: "Width of the rotated strip." },
19049 { name: "--wpd-ribbon-banner-offset", default: "20px", description: "Distance from corner to strip center." },
19050 { name: "--wpd-ribbon-banner-pull", default: "-36px", description: "How far the strip overhangs the clip edge." },
19051 { name: "--wpd-ribbon-bg", default: "var(--wp-admin-theme-color, #2271b1)" },
19052 { name: "--wpd-ribbon-fg", default: "#fff" },
19053 { name: "--wpd-ribbon-shadow", default: "0 2px 4px rgba(0,0,0,0.2)" },
19054 { name: "--wpd-ribbon-padding", default: "4px 0" },
19055 { name: "--wpd-ribbon-font", default: "700 10px/1.4 system-ui" },
19056 { name: "--wpd-ribbon-tracking", default: "0.06em" },
19057 { name: "--wpd-ribbon-z", default: "2" }
19058 ],
19059 example: html`
19060 <div
19061 style="position: relative; width: 240px; height: 120px;
19062 border: 1px solid #ccc; border-radius: 8px;
19063 padding: 16px; box-sizing: border-box;"
19064 >
19065 <wpd-ribbon>Featured</wpd-ribbon>
19066 Card body…
19067 </div>
19068 `
19069 };
19070 let WpdRibbon = _WpdRibbon;
19071 defineComponent("wpd-ribbon", WpdRibbon);
19072 const TILE_CLASS = "desktop-mode-file-tile";
19073 const STATUS_LABEL = {
19074 draft: "Draft",
19075 pending: "Pending",
19076 private: "Private",
19077 future: "Scheduled"
19078 };
19079 function statusRibbonsEnabled() {
19080 const get2 = window.wp?.desktop?.getOsSettings;
19081 if (typeof get2 !== "function") {
19082 return true;
19083 }
19084 try {
19085 return get2()?.showPostStatusRibbons !== false;
19086 } catch {
19087 return true;
19088 }
19089 }
19090 function getDragManager$1() {
19091 const api = window.wp?.desktop?.dragManager;
19092 return api ?? null;
19093 }
19094 const REACTIVE_PROPS = [
19095 "type",
19096 "ref",
19097 "label",
19098 "icon",
19099 "thumbnail",
19100 "kind",
19101 "status",
19102 "selected",
19103 "missing",
19104 "access-gated",
19105 "drag-kind",
19106 "drag-title",
19107 "drag-icon"
19108 ];
19109 const _WpdTile = class _WpdTile extends Component {
19110 constructor() {
19111 super(...arguments);
19112 this._pointerdownHandler = null;
19113 this._keydownHandler = null;
19114 }
19115 connectedCallback() {
19116 super.connectedCallback();
19117 if (!this._keydownHandler) {
19118 this._keydownHandler = (e) => {
19119 if (e.key === "Enter" || e.key === " ") {
19120 e.preventDefault();
19121 this.click();
19122 }
19123 };
19124 this.addEventListener("keydown", this._keydownHandler);
19125 }
19126 this._paint();
19127 }
19128 disconnectedCallback() {
19129 if (this._pointerdownHandler) {
19130 this.removeEventListener(
19131 "pointerdown",
19132 this._pointerdownHandler
19133 );
19134 this._pointerdownHandler = null;
19135 }
19136 if (this._keydownHandler) {
19137 this.removeEventListener(
19138 "keydown",
19139 this._keydownHandler
19140 );
19141 this._keydownHandler = null;
19142 }
19143 }
19144 /**
19145 * Bypass the templated render loop. Lit-html's `render(template,
19146 * root)` would wipe the host's light-DOM children every tick —
19147 * including the visual / label / ribbon `_paint()` just
19148 * inserted. We override `requestUpdate` directly so attribute
19149 * changes call `_paint` (idempotent) without lit-html getting
19150 * involved.
19151 */
19152 requestUpdate() {
19153 if (!this.isConnected) {
19154 return;
19155 }
19156 this._paint();
19157 }
19158 render() {
19159 return html``;
19160 }
19161 _paint() {
19162 const type = this.getAttribute("type") ?? "";
19163 const ref = this.getAttribute("ref") ?? "";
19164 const label = this.getAttribute("label") ?? "";
19165 const icon = this.getAttribute("icon") ?? "";
19166 const thumbnail = this.getAttribute("thumbnail") ?? "";
19167 const kind = this.getAttribute("kind") ?? "entry";
19168 const status = this.getAttribute("status") ?? "";
19169 const selected = this.hasAttribute("selected");
19170 const missing = this.hasAttribute("missing");
19171 const accessGated = this.hasAttribute("access-gated");
19172 const ownedClasses = [
19173 TILE_CLASS,
19174 `${TILE_CLASS}--folder`,
19175 `${TILE_CLASS}--missing`,
19176 `${TILE_CLASS}--access-gated`,
19177 `${TILE_CLASS}--selected`
19178 ];
19179 for (const c of ownedClasses) {
19180 this.classList.remove(c);
19181 }
19182 this.classList.add(TILE_CLASS);
19183 if (kind === "folder") {
19184 this.classList.add(`${TILE_CLASS}--folder`);
19185 }
19186 if (missing) {
19187 this.classList.add(`${TILE_CLASS}--missing`);
19188 }
19189 if (accessGated) {
19190 this.classList.add(`${TILE_CLASS}--access-gated`);
19191 }
19192 if (selected) {
19193 this.classList.add(`${TILE_CLASS}--selected`);
19194 }
19195 this.dataset.fileType = type;
19196 this.dataset.fileRef = ref;
19197 if (kind) {
19198 this.dataset.role = kind;
19199 }
19200 this.setAttribute("role", "listitem");
19201 this.setAttribute("aria-label", label);
19202 if (!this.hasAttribute("tabindex")) {
19203 this.setAttribute("tabindex", "0");
19204 }
19205 const accessGatedTitle = "You don’t have permission to open this — ask the folder owner for access.";
19206 if (accessGated) {
19207 this.title = accessGatedTitle;
19208 this.setAttribute("aria-disabled", "true");
19209 } else {
19210 this.removeAttribute("aria-disabled");
19211 if (this.title === accessGatedTitle) {
19212 this.removeAttribute("title");
19213 }
19214 }
19215 const SLOTS = [
19216 `${TILE_CLASS}__visual`,
19217 `${TILE_CLASS}__label`,
19218 `${TILE_CLASS}__lock`
19219 ];
19220 for (const cls of SLOTS) {
19221 this.querySelectorAll(`:scope > .${cls}`).forEach(
19222 (n) => n.remove()
19223 );
19224 }
19225 this.querySelectorAll(":scope > wpd-ribbon").forEach(
19226 (n) => n.remove()
19227 );
19228 const visual = document.createElement("span");
19229 visual.className = `${TILE_CLASS}__visual`;
19230 if (thumbnail) {
19231 const img = document.createElement("img");
19232 img.src = thumbnail;
19233 img.alt = "";
19234 img.loading = "lazy";
19235 img.decoding = "async";
19236 img.className = `${TILE_CLASS}__preview`;
19237 img.draggable = false;
19238 visual.appendChild(img);
19239 } else if (icon) {
19240 const iconNode = renderIcon(icon, {
19241 title: label,
19242 className: `${TILE_CLASS}__icon`
19243 });
19244 visual.appendChild(iconNode);
19245 }
19246 this.appendChild(visual);
19247 const labelNode = document.createElement("span");
19248 labelNode.className = `${TILE_CLASS}__label`;
19249 labelNode.textContent = label;
19250 this.appendChild(labelNode);
19251 if (accessGated) {
19252 const lock = document.createElement("span");
19253 lock.className = `${TILE_CLASS}__lock dashicons dashicons-lock`;
19254 lock.setAttribute("aria-hidden", "true");
19255 this.appendChild(lock);
19256 }
19257 if (status && status !== "publish" && STATUS_LABEL[status] && statusRibbonsEnabled()) {
19258 const ribbon = document.createElement("wpd-ribbon");
19259 ribbon.setAttribute("placement", "top-end");
19260 ribbon.setAttribute("tone", ribbonToneFor(status));
19261 ribbon.textContent = STATUS_LABEL[status];
19262 this.appendChild(ribbon);
19263 }
19264 applyTileEntryStagger(this);
19265 doAction("desktop-mode.tile.rendered", { tile: this });
19266 this._wireDragOut();
19267 }
19268 _wireDragOut() {
19269 if (this._pointerdownHandler) {
19270 this.removeEventListener(
19271 "pointerdown",
19272 this._pointerdownHandler
19273 );
19274 this._pointerdownHandler = null;
19275 }
19276 const dragKind = this.getAttribute("drag-kind");
19277 if (!dragKind) {
19278 return;
19279 }
19280 const handler = (e) => {
19281 if (e.button !== 0) {
19282 return;
19283 }
19284 const dragManager = getDragManager$1();
19285 if (!dragManager) {
19286 return;
19287 }
19288 const ref = this.getAttribute("ref") ?? "";
19289 const title = this.getAttribute("drag-title") ?? this.getAttribute("label") ?? void 0;
19290 const icon = this.getAttribute("drag-icon") ?? this.getAttribute("icon") ?? void 0;
19291 const rect = this.getBoundingClientRect();
19292 dragManager.start({
19293 payload: {
19294 type: "shortcut",
19295 source: this,
19296 data: {
19297 kind: dragKind,
19298 ref,
19299 title,
19300 icon
19301 },
19302 ghost: {
19303 offsetX: e.clientX - rect.left,
19304 offsetY: e.clientY - rect.top
19305 }
19306 },
19307 origin: e
19308 });
19309 };
19310 this._pointerdownHandler = handler;
19311 this.addEventListener("pointerdown", handler);
19312 }
19313 };
19314 _WpdTile.shadow = false;
19315 _WpdTile.props = REACTIVE_PROPS;
19316 _WpdTile.styles = [styles$3];
19317 _WpdTile.help = {
19318 title: "Tile",
19319 summary: "Canonical file/entity tile. Used across the wallpaper, folder windows, every My WordPress section, and plugin surfaces. Renders the standard `.desktop-mode-file-tile` chrome + optional status ribbon and wires the shared drag-out helper.",
19320 status: "experimental",
19321 since: "0.21.0",
19322 props: [
19323 { name: "type", type: "string" },
19324 { name: "ref", type: "string" },
19325 { name: "label", type: "string" },
19326 { name: "icon", type: "string", description: "Dashicon class / URL / data URI. Ignored when `thumbnail` is set." },
19327 { name: "thumbnail", type: "string", description: "Preview image URL. Renders as `<img>` and wins over `icon`." },
19328 { name: "kind", type: "`entry` | `folder`" },
19329 { name: "status", type: "`draft` | `pending` | `private` | `future` | `publish`" },
19330 { name: "selected", type: "boolean" },
19331 { name: "missing", type: "boolean" },
19332 { name: "access-gated", type: "boolean" },
19333 { name: "drag-kind", type: "string", description: "When set, the component wires pointerdown → DragManager." },
19334 { name: "drag-title", type: "string" },
19335 { name: "drag-icon", type: "string" }
19336 ]
19337 };
19338 let WpdTile = _WpdTile;
19339 function ribbonToneFor(status) {
19340 switch (status) {
19341 case "draft":
19342 return "warning";
19343 case "pending":
19344 return "info";
19345 case "private":
19346 return "danger";
19347 case "future":
19348 return "primary";
19349 default:
19350 return "primary";
19351 }
19352 }
19353 defineComponent("wpd-tile", WpdTile);
19354 function buildTileFromSpec(spec) {
19355 const tile2 = document.createElement("wpd-tile");
19356 tile2.setAttribute("type", spec.type);
19357 tile2.setAttribute("ref", spec.ref);
19358 tile2.setAttribute("label", spec.label);
19359 if (spec.icon) {
19360 tile2.setAttribute("icon", spec.icon);
19361 }
19362 if (spec.thumbnail) {
19363 tile2.setAttribute("thumbnail", spec.thumbnail);
19364 }
19365 if (spec.role) {
19366 tile2.setAttribute("kind", spec.role);
19367 }
19368 if (spec.status) {
19369 tile2.setAttribute("status", spec.status);
19370 }
19371 if (spec.missing) {
19372 tile2.setAttribute("missing", "");
19373 }
19374 if (spec.accessGated) {
19375 tile2.setAttribute("access-gated", "");
19376 }
19377 if (spec.dataset) {
19378 for (const [key, raw] of Object.entries(spec.dataset)) {
19379 if (raw === void 0 || raw === null) {
19380 continue;
19381 }
19382 tile2.dataset[key] = String(raw);
19383 }
19384 }
19385 if (Array.isArray(spec.extraClasses)) {
19386 for (const c of spec.extraClasses) {
19387 if (c) {
19388 tile2.classList.add(c);
19389 }
19390 }
19391 }
19392 const classFiltered = applyFilters(
19393 "desktop-mode.tile.class",
19394 tile2.className,
19395 spec
19396 );
19397 if (classFiltered && classFiltered !== tile2.className) {
19398 tile2.className = classFiltered;
19399 }
19400 if (typeof spec.x === "number" && typeof spec.y === "number") {
19401 tile2.style.position = "absolute";
19402 tile2.style.left = `${spec.x}px`;
19403 tile2.style.top = `${spec.y}px`;
19404 }
19405 return tile2;
19406 }
19407 function placementToSpec(placement, folderId) {
19408 const file = resolve(placement.file);
19409 const previewUrl = file.previewUrl();
19410 const metaName = placement.meta && typeof placement.meta.name === "string" ? placement.meta.name.trim() : "";
19411 const label = metaName !== "" ? metaName : file.title();
19412 const metaIconUrl = placement.meta && typeof placement.meta.iconUrl === "string" ? placement.meta.iconUrl.trim() : "";
19413 return {
19414 type: placement.file.type,
19415 ref: placement.file.ref,
19416 label,
19417 // Preview wins over icon (matches the previous behavior).
19418 thumbnail: previewUrl || void 0,
19419 icon: previewUrl ? void 0 : metaIconUrl || file.icon(),
19420 x: placement.x,
19421 y: placement.y,
19422 dataset: {
19423 placementId: placement.id,
19424 folderId
19425 },
19426 meta: placement.meta,
19427 missing: !placement.file.exists,
19428 accessGated: Boolean(placement.accessGated),
19429 ariaLabel: label
19430 };
19431 }
19432 function buildTile(placement, folderId) {
19433 const file = resolve(placement.file);
19434 const tile2 = buildTileFromSpec(placementToSpec(placement, folderId));
19435 const classFiltered = applyFilters(
19436 "desktop-mode.files.tile-class",
19437 TILE_CLASS,
19438 placement
19439 );
19440 if (classFiltered && classFiltered !== TILE_CLASS) {
19441 tile2.className = classFiltered;
19442 }
19443 const extra = applyFilters(
19444 "desktop-mode.files.tile-element",
19445 null,
19446 placement
19447 );
19448 if (extra instanceof Element) {
19449 tile2.appendChild(extra);
19450 }
19451 tile2.addEventListener("dblclick", (e) => {
19452 e.preventDefault();
19453 e.stopPropagation();
19454 if (placement.accessGated) {
19455 showToast({
19456 message: `You don’t have permission to open "${placement.file.title || file.title()}". Ask the folder owner if you need access to this item.`,
19457 duration: 6e3
19458 });
19459 return;
19460 }
19461 void openFile(file, {
19462 placement: {
19463 id: placement.id,
19464 x: placement.x,
19465 y: placement.y,
19466 meta: placement.meta
19467 }
19468 });
19469 });
19470 doAction("desktop-mode.files.tile-rendered", { tile: tile2, placement });
19471 return tile2;
19472 }
19473 function setTilePosition(tile2, x, y) {
19474 tile2.style.left = `${x}px`;
19475 tile2.style.top = `${y}px`;
19476 }
19477 function attachDismissable(host, options) {
19478 const onAway = (e) => {
19479 if (e.target instanceof Node && host.contains(e.target)) {
19480 return;
19481 }
19482 if (e.target instanceof Node) {
19483 for (const sel of options.siblingSelectors ?? []) {
19484 const matches = Array.from(
19485 document.querySelectorAll(sel)
19486 );
19487 for (const m of matches) {
19488 if (m.contains(e.target)) {
19489 return;
19490 }
19491 }
19492 }
19493 }
19494 if (options.excludeOutsideTarget && e.target instanceof Node && options.excludeOutsideTarget.contains(e.target)) {
19495 return;
19496 }
19497 options.close();
19498 };
19499 const onKey = (e) => {
19500 if (e.key === "Escape") {
19501 options.close();
19502 }
19503 };
19504 document.addEventListener("mousedown", onAway, { capture: true });
19505 document.addEventListener("keydown", onKey);
19506 return () => {
19507 document.removeEventListener("mousedown", onAway, { capture: true });
19508 document.removeEventListener("keydown", onKey);
19509 };
19510 }
19511 const MENU_CLASS$2 = "desktop-mode-wallpaper-menu";
19512 let activeMenu$2 = null;
19513 function closeTileMenu() {
19514 if (!activeMenu$2) {
19515 return;
19516 }
19517 activeMenu$2.dispatchEvent(new CustomEvent("tile-menu-closed"));
19518 activeMenu$2.remove();
19519 activeMenu$2 = null;
19520 doAction("desktop-mode.files.tile-menu.closed", {});
19521 }
19522 let openGeneration$1 = 0;
19523 function openTileMenu(pos, opts) {
19524 closeTileMenu();
19525 const myGen = ++openGeneration$1;
19526 openWithShellOverlays(
19527 () => myGen === openGeneration$1,
19528 () => openTileMenuImmediate(pos, opts)
19529 );
19530 }
19531 function openTileMenuImmediate(pos, { placement, items }) {
19532 const list2 = applyFilters(
19533 "desktop-mode.files.tile-menu",
19534 items.slice(),
19535 placement
19536 );
19537 const sorted = (Array.isArray(list2) ? list2 : items).slice().sort((a, b) => {
19538 const sa = typeof a.sort === "number" ? a.sort : 100;
19539 const sb = typeof b.sort === "number" ? b.sort : 100;
19540 if (sa !== sb) {
19541 return sa - sb;
19542 }
19543 return a.label.localeCompare(b.label);
19544 });
19545 if (sorted.length === 0) {
19546 return;
19547 }
19548 const menu = document.createElement("wpd-context-menu");
19549 menu.setAttribute("open", "");
19550 menu.classList.add(MENU_CLASS$2);
19551 menu.dataset.placementId = String(placement.id);
19552 menu.style.left = `${pos.x}px`;
19553 menu.style.top = `${pos.y}px`;
19554 const itemById = /* @__PURE__ */ new Map();
19555 for (const item of sorted) {
19556 itemById.set(item.id, item);
19557 const opt = document.createElement("wpd-context-menu-option");
19558 opt.dataset.menuItemId = item.id;
19559 opt.setAttribute("value", item.id);
19560 if (item.danger) {
19561 opt.setAttribute("danger", "");
19562 }
19563 if (item.disabled) {
19564 opt.setAttribute("disabled", "");
19565 }
19566 if (item.icon) {
19567 opt.setAttribute("icon", sanitizeClass$2(item.icon));
19568 }
19569 opt.textContent = item.label;
19570 menu.appendChild(opt);
19571 }
19572 menu.addEventListener("wpd-context-menu-pick", (e) => {
19573 const detail = e.detail;
19574 const item = itemById.get(detail.id);
19575 if (!item) {
19576 return;
19577 }
19578 closeTileMenu();
19579 void item.onClick(new MouseEvent("click"));
19580 });
19581 document.body.appendChild(menu);
19582 activeMenu$2 = menu;
19583 const rect = menu.getBoundingClientRect();
19584 if (rect.right > window.innerWidth) {
19585 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
19586 }
19587 if (rect.bottom > window.innerHeight) {
19588 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
19589 }
19590 const detach = attachDismissable(menu, {
19591 close: () => closeTileMenu()
19592 });
19593 menu.addEventListener("tile-menu-closed", detach);
19594 doAction("desktop-mode.files.tile-menu.opened", {
19595 placementId: placement.id,
19596 items: sorted.map((i) => i.id)
19597 });
19598 }
19599 function sanitizeClass$2(raw) {
19600 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
19601 }
19602 const ROOT_CLASS$3 = "desktop-mode-create-folder-dialog";
19603 let active$1 = null;
19604 function closeCreateFolderDialog() {
19605 if (!active$1) {
19606 return;
19607 }
19608 active$1.dispatchEvent(new CustomEvent("create-folder-dialog-closed"));
19609 active$1.remove();
19610 active$1 = null;
19611 doAction("desktop-mode.files.create-folder.closed", {});
19612 }
19613 function openCreateFolderDialog(options) {
19614 closeCreateFolderDialog();
19615 const decision = applyFilters(
19616 "desktop-mode.files.create-folder.dialog",
19617 null,
19618 options
19619 );
19620 if (decision === false) {
19621 return;
19622 }
19623 const initial = (options.initialName ?? "Untitled folder").trim();
19624 const overlay = document.createElement("div");
19625 overlay.className = `${ROOT_CLASS$3}__overlay`;
19626 overlay.setAttribute("role", "presentation");
19627 const dialog2 = document.createElement("div");
19628 dialog2.className = ROOT_CLASS$3;
19629 dialog2.setAttribute("role", "dialog");
19630 dialog2.setAttribute("aria-modal", "true");
19631 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS$3}-title`);
19632 const title = document.createElement("h2");
19633 title.id = `${ROOT_CLASS$3}-title`;
19634 title.className = `${ROOT_CLASS$3}__title`;
19635 title.textContent = options.title ?? "New folder";
19636 dialog2.appendChild(title);
19637 const label = document.createElement("label");
19638 label.className = `${ROOT_CLASS$3}__label`;
19639 label.htmlFor = `${ROOT_CLASS$3}-input`;
19640 label.textContent = options.label ?? "Folder name";
19641 dialog2.appendChild(label);
19642 const input = document.createElement("input");
19643 input.type = "text";
19644 input.id = `${ROOT_CLASS$3}-input`;
19645 input.className = `${ROOT_CLASS$3}__input`;
19646 input.value = initial;
19647 input.setAttribute("autocomplete", "off");
19648 input.setAttribute("spellcheck", "false");
19649 dialog2.appendChild(input);
19650 const error = document.createElement("p");
19651 error.className = `${ROOT_CLASS$3}__error`;
19652 error.hidden = true;
19653 error.setAttribute("role", "alert");
19654 dialog2.appendChild(error);
19655 const actions = document.createElement("div");
19656 actions.className = `${ROOT_CLASS$3}__actions`;
19657 const cancel = document.createElement("button");
19658 cancel.type = "button";
19659 cancel.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--secondary`;
19660 cancel.textContent = "Cancel";
19661 const submit = document.createElement("button");
19662 submit.type = "button";
19663 submit.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--primary`;
19664 submit.textContent = options.submitLabel ?? "Create";
19665 actions.appendChild(cancel);
19666 actions.appendChild(submit);
19667 dialog2.appendChild(actions);
19668 overlay.appendChild(dialog2);
19669 document.body.appendChild(overlay);
19670 active$1 = overlay;
19671 input.focus();
19672 input.select();
19673 doAction("desktop-mode.files.create-folder.opened", {});
19674 const setBusy = (busy) => {
19675 input.disabled = busy;
19676 cancel.disabled = busy;
19677 submit.disabled = busy;
19678 dialog2.classList.toggle(`${ROOT_CLASS$3}--busy`, busy);
19679 };
19680 const showError = (msg) => {
19681 error.textContent = msg;
19682 error.hidden = false;
19683 };
19684 const doCancel = () => {
19685 closeCreateFolderDialog();
19686 options.onCancel?.();
19687 };
19688 const doSubmit = async () => {
19689 const name = input.value.trim();
19690 if (!name) {
19691 showError("Please enter a name.");
19692 input.focus();
19693 return;
19694 }
19695 error.hidden = true;
19696 setBusy(true);
19697 try {
19698 await options.onSubmit(name);
19699 closeCreateFolderDialog();
19700 } catch (err) {
19701 setBusy(false);
19702 showError(
19703 err instanceof Error ? err.message : "Could not create the folder."
19704 );
19705 input.focus();
19706 input.select();
19707 }
19708 };
19709 cancel.addEventListener("click", () => doCancel());
19710 submit.addEventListener("click", () => void doSubmit());
19711 overlay.addEventListener("click", (e) => {
19712 if (e.target === overlay) {
19713 doCancel();
19714 }
19715 });
19716 const onKey = (e) => {
19717 if (e.key === "Escape") {
19718 e.preventDefault();
19719 doCancel();
19720 } else if (e.key === "Enter" && !e.isComposing) {
19721 e.preventDefault();
19722 void doSubmit();
19723 }
19724 };
19725 dialog2.addEventListener("keydown", onKey);
19726 overlay.addEventListener("create-folder-dialog-closed", () => {
19727 dialog2.removeEventListener("keydown", onKey);
19728 });
19729 }
19730 const GRID_PADDING = 16;
19731 const GRID_CELL_W = 96;
19732 const GRID_CELL_H = 110;
19733 function pointToCell(x, y) {
19734 const col = Math.max(0, Math.round((x - GRID_PADDING) / GRID_CELL_W));
19735 const row = Math.max(0, Math.round((y - GRID_PADDING) / GRID_CELL_H));
19736 return cellToPos(col, row);
19737 }
19738 function cellToPos(col, row) {
19739 return {
19740 col,
19741 row,
19742 x: GRID_PADDING + col * GRID_CELL_W,
19743 y: GRID_PADDING + row * GRID_CELL_H
19744 };
19745 }
19746 function snapToEmptyCell(x, y, occupied, host) {
19747 const target = pointToCell(x, y);
19748 if (!occupied.has(cellKey(target.col, target.row))) {
19749 return target;
19750 }
19751 const maxRows = host ? Math.max(1, Math.floor((host.clientHeight - GRID_PADDING) / GRID_CELL_H)) : 999;
19752 for (let col = 0; col < 999; col++) {
19753 for (let row = 0; row < maxRows; row++) {
19754 if (!occupied.has(cellKey(col, row))) {
19755 return cellToPos(col, row);
19756 }
19757 }
19758 }
19759 return target;
19760 }
19761 function nextRowMajorCell(occupied, host) {
19762 const cols = host ? Math.max(
19763 1,
19764 Math.floor((host.clientWidth - GRID_PADDING) / GRID_CELL_W)
19765 ) : 4;
19766 const maxCols = Math.max(1, cols);
19767 for (let row = 0; row < 999; row++) {
19768 for (let col = 0; col < maxCols; col++) {
19769 if (!occupied.has(cellKey(col, row))) {
19770 return cellToPos(col, row);
19771 }
19772 }
19773 }
19774 return cellToPos(0, 0);
19775 }
19776 function buildOccupiedSet(placements, excludeId) {
19777 const out = /* @__PURE__ */ new Set();
19778 for (const p of placements) {
19779 const cell = pointToCell(p.x, p.y);
19780 out.add(cellKey(cell.col, cell.row));
19781 }
19782 return out;
19783 }
19784 function cellKey(col, row) {
19785 return `${col},${row}`;
19786 }
19787 function isConflict(err) {
19788 return err instanceof FilesConflictError;
19789 }
19790 function buildReason(err) {
19791 const actor = err.detail.actor.name || "Someone else";
19792 const where = err.detail.current.parentName || "another folder";
19793 if (err.detail.reason === "trashed") {
19794 return "This item is in the recycle bin.";
19795 }
19796 if (err.detail.reason === "forbidden") {
19797 return "You no longer have access.";
19798 }
19799 if (err.detail.reason === "gone") {
19800 return "This item was deleted.";
19801 }
19802 return `${actor} moved this to "${where}".`;
19803 }
19804 function showConflictToast(err) {
19805 const reason = buildReason(err);
19806 const targetParentId = err.detail.current.parentId;
19807 let action;
19808 if (targetParentId > 0) {
19809 action = {
19810 label: "View folder",
19811 onClick: () => {
19812 const winId = `desktop-mode-folder-${targetParentId}`;
19813 const mgr = window.desktopMode?.windowManager;
19814 if (mgr?.focus) {
19815 const w = mgr.focus(winId);
19816 if (w) {
19817 return;
19818 }
19819 }
19820 if (mgr?.open) {
19821 void mgr.open(winId);
19822 }
19823 }
19824 };
19825 }
19826 showToast({
19827 message: reason,
19828 action,
19829 duration: 7e3
19830 });
19831 }
19832 function broadcastFilesChange(kind, action, ids) {
19833 const api = window.wp?.desktop;
19834 api?.broadcast?.(`desktop-mode.${kind}.changed`, {
19835 source: "desktop-files",
19836 action,
19837 ids
19838 });
19839 }
19840 function showTrashErrorToast(err) {
19841 const api = window.wp?.desktop;
19842 if (!api?.showToast) {
19843 return;
19844 }
19845 const raw = err instanceof Error ? err.message : String(err);
19846 const friendly = raw.replace(/^\[desktop-mode\][^:]*:\s*/, "").replace(/^desktop_mode_files_[a-z_]+\s*/, "");
19847 api.showToast({
19848 message: friendly || "Could not move this item to the recycle bin.",
19849 duration: 5e3
19850 });
19851 }
19852 function showTrashedToast(message, onUndo) {
19853 const api = window.wp?.desktop;
19854 if (!api?.showToast) {
19855 return;
19856 }
19857 api.showToast({
19858 message,
19859 duration: 6e3,
19860 action: {
19861 label: "Undo",
19862 onClick: onUndo
19863 }
19864 });
19865 }
19866 async function trashPlacementWithUndo(placement) {
19867 const placementId = placement.id;
19868 const parentId = placement.parentId;
19869 const title = placement.file?.title ?? "Item";
19870 const kind = placement.file?.type === "shortcut" ? "shortcut" : "placement";
19871 store.removePlacement(placementId);
19872 try {
19873 await deletePlacement(placementId);
19874 broadcastFilesChange(kind, "trashed", [placementId]);
19875 showTrashedToast(`"${title}" moved to Trash`, async () => {
19876 try {
19877 await restoreTrashedItem(placementId, "placement");
19878 const res = await listPlacements(parentId);
19879 store.setFolderPlacements(parentId, res.placements);
19880 broadcastFilesChange(kind, "untrashed", [placementId]);
19881 } catch (err) {
19882 console.error("[desktop-mode] restore failed:", err);
19883 }
19884 });
19885 } catch (err) {
19886 console.error("[desktop-mode] deletePlacement failed:", err);
19887 showTrashErrorToast(err);
19888 void listPlacements(parentId).then((res) => {
19889 store.setFolderPlacements(parentId, res.placements);
19890 });
19891 }
19892 }
19893 async function trashFolderWithUndo(placement) {
19894 const folderId = parseInt(placement.file.ref, 10);
19895 if (!folderId) {
19896 return;
19897 }
19898 const placementId = placement.id;
19899 const parentId = placement.parentId;
19900 const title = placement.file?.title ?? "Folder";
19901 store.removePlacement(placementId);
19902 store.removeFolder(folderId);
19903 try {
19904 await deleteFolder(folderId);
19905 broadcastFilesChange("folder", "trashed", [folderId]);
19906 showTrashedToast(`"${title}" moved to Trash`, async () => {
19907 try {
19908 await restoreTrashedItem(folderId, "folder");
19909 const res = await listPlacements(parentId);
19910 store.setFolderPlacements(parentId, res.placements);
19911 broadcastFilesChange("folder", "untrashed", [folderId]);
19912 } catch (err) {
19913 console.error("[desktop-mode] restore folder failed:", err);
19914 }
19915 });
19916 } catch (err) {
19917 console.error("[desktop-mode] deleteFolder failed:", err);
19918 showTrashErrorToast(err);
19919 void listPlacements(parentId).then((res) => {
19920 store.setFolderPlacements(parentId, res.placements);
19921 });
19922 }
19923 }
19924 function trashByFileType(placement) {
19925 if (placement.file?.type === "folder") {
19926 return trashFolderWithUndo(placement);
19927 }
19928 return trashPlacementWithUndo(placement);
19929 }
19930 function buildBridgePayloadFromPlacement(placement) {
19931 const file = placement.file;
19932 if (!file) {
19933 return void 0;
19934 }
19935 const id = parseInt(String(file.ref ?? ""), 10);
19936 if (!Number.isFinite(id) || id <= 0) {
19937 return void 0;
19938 }
19939 const title = String(file.title ?? "");
19940 if (file.type === "attachment") {
19941 const url = String(file.sourceUrl ?? file.previewUrl ?? "");
19942 return {
19943 kind: "attachment",
19944 id,
19945 url,
19946 title,
19947 alt: String(file.alt ?? ""),
19948 mime: String(file.mime ?? ""),
19949 thumbnailUrl: file.previewUrl ? String(file.previewUrl) : void 0
19950 };
19951 }
19952 if (file.type === "post") {
19953 return {
19954 kind: "post",
19955 id,
19956 postType: String(file.postType ?? "post"),
19957 url: String(file.link ?? ""),
19958 title
19959 };
19960 }
19961 if (file.type === "user") {
19962 return {
19963 kind: "user",
19964 id,
19965 url: String(file.link ?? ""),
19966 title
19967 };
19968 }
19969 return void 0;
19970 }
19971 function getDragManager() {
19972 const api = window.wp?.desktop?.dragManager;
19973 return api ?? null;
19974 }
19975 const LAYER_CLASS = "desktop-mode-files-layer";
19976 function mountFilesLayer(host, folderId = 0) {
19977 const container = document.createElement("div");
19978 container.className = LAYER_CLASS;
19979 container.setAttribute("role", "list");
19980 container.dataset.folderId = String(folderId);
19981 host.appendChild(container);
19982 let lastFingerprint = "";
19983 let selectedId = null;
19984 const selectionListeners = /* @__PURE__ */ new Set();
19985 const notifySelection = (placement) => {
19986 for (const cb of selectionListeners) {
19987 try {
19988 cb(placement);
19989 } catch (err) {
19990 console.error(
19991 "[desktop-mode] files: selection listener threw:",
19992 err
19993 );
19994 }
19995 }
19996 };
19997 const setSelected = (placement) => {
19998 const newId = placement ? placement.id : null;
19999 if (newId === selectedId) {
20000 return;
20001 }
20002 container.querySelectorAll(`.${TILE_CLASS}--selected`).forEach((n) => n.removeAttribute("selected"));
20003 if (placement) {
20004 const tile2 = container.querySelector(
20005 `[data-placement-id="${placement.id}"]`
20006 );
20007 tile2?.setAttribute("selected", "");
20008 }
20009 selectedId = newId;
20010 notifySelection(placement);
20011 };
20012 const repaint = (state2) => {
20013 const raw = state2.placementsByFolder.get(folderId) ?? [];
20014 const list2 = raw.slice().sort((a, b) => {
20015 const ap = isPinned(a) ? 0 : 1;
20016 const bp = isPinned(b) ? 0 : 1;
20017 return ap - bp;
20018 });
20019 const fp = fingerprint(list2);
20020 if (fp === lastFingerprint) {
20021 return;
20022 }
20023 lastFingerprint = fp;
20024 if (tryPatchPositions(list2, container, host)) {
20025 return;
20026 }
20027 container.replaceChildren();
20028 for (const [, deregister] of folderDropDeregisters) {
20029 try {
20030 deregister();
20031 } catch {
20032 }
20033 }
20034 folderDropDeregisters.clear();
20035 for (const [, deregister] of tileRejectDeregisters) {
20036 try {
20037 deregister();
20038 } catch {
20039 }
20040 }
20041 tileRejectDeregisters.clear();
20042 const pinnedSlots = /* @__PURE__ */ new Map();
20043 const occupiedCells = /* @__PURE__ */ new Set();
20044 let pinnedIdx = 0;
20045 for (const placement of list2) {
20046 if (!isPinned(placement)) {
20047 continue;
20048 }
20049 const slot = cellToPos(0, pinnedIdx);
20050 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
20051 occupiedCells.add(cellKey(slot.col, slot.row));
20052 pinnedIdx += 1;
20053 }
20054 const displaced = /* @__PURE__ */ new Map();
20055 for (const placement of list2) {
20056 if (pinnedSlots.has(placement.id)) {
20057 continue;
20058 }
20059 const target = pointToCell(placement.x, placement.y);
20060 const key = cellKey(target.col, target.row);
20061 if (!occupiedCells.has(key)) {
20062 occupiedCells.add(key);
20063 continue;
20064 }
20065 const free = snapToEmptyCell(
20066 placement.x,
20067 placement.y,
20068 occupiedCells,
20069 host
20070 );
20071 occupiedCells.add(cellKey(free.col, free.row));
20072 displaced.set(placement.id, { x: free.x, y: free.y });
20073 }
20074 for (const placement of list2) {
20075 const tile2 = buildTile(placement, folderId);
20076 const pinnedSlot = pinnedSlots.get(placement.id);
20077 if (pinnedSlot) {
20078 setTilePosition(tile2, pinnedSlot.x, pinnedSlot.y);
20079 tile2.classList.add(`${TILE_CLASS}--pinned`);
20080 attachContextMenu(tile2, placement);
20081 attachSelectOnClick(tile2, placement);
20082 if (shouldRejectTileDrops(placement)) {
20083 const dragManager = getDragManager();
20084 if (dragManager) {
20085 const deregister = dragManager.registerDropTarget({
20086 id: `desktop-mode-files-tile-${placement.id}-reject`,
20087 element: tile2,
20088 accept: () => false,
20089 onDrop: () => {
20090 }
20091 });
20092 tileRejectDeregisters.set(placement.id, deregister);
20093 }
20094 }
20095 container.appendChild(tile2);
20096 continue;
20097 }
20098 const moved = displaced.get(placement.id);
20099 if (moved) {
20100 setTilePosition(tile2, moved.x, moved.y);
20101 }
20102 attachTileDrag(tile2, placement, folderId);
20103 attachContextMenu(tile2, placement);
20104 attachSelectOnClick(tile2, placement);
20105 if (placement.file.type === "folder") {
20106 const targetFolderId = parseInt(placement.file.ref, 10);
20107 if (targetFolderId > 0) {
20108 const dragManager = getDragManager();
20109 if (dragManager) {
20110 const deregister = registerFolderDropTarget(
20111 dragManager,
20112 tile2,
20113 targetFolderId
20114 );
20115 folderDropDeregisters.set(placement.id, deregister);
20116 }
20117 }
20118 } else if (shouldRejectTileDrops(placement)) {
20119 const dragManager = getDragManager();
20120 if (dragManager) {
20121 const deregister = dragManager.registerDropTarget({
20122 id: `desktop-mode-files-tile-${placement.id}-reject`,
20123 element: tile2,
20124 accept: () => false,
20125 onDrop: () => {
20126 }
20127 });
20128 tileRejectDeregisters.set(placement.id, deregister);
20129 }
20130 }
20131 container.appendChild(tile2);
20132 }
20133 if (selectedId !== null && !container.querySelector(`[data-placement-id="${selectedId}"]`)) {
20134 selectedId = null;
20135 notifySelection(null);
20136 } else if (selectedId !== null) {
20137 const tile2 = container.querySelector(
20138 `[data-placement-id="${selectedId}"]`
20139 );
20140 tile2?.setAttribute("selected", "");
20141 }
20142 doAction("desktop-mode.files.grid-rendered", {
20143 folderId,
20144 count: list2.length
20145 });
20146 };
20147 const dropTargetDeregisters = [];
20148 const folderDropDeregisters = /* @__PURE__ */ new Map();
20149 const tileRejectDeregisters = /* @__PURE__ */ new Map();
20150 let dropPreviewEl = null;
20151 let dropPreviewMoveHandler = null;
20152 const installCanvasDropPreview = (session) => {
20153 if (dropPreviewEl) {
20154 return;
20155 }
20156 if (session.payload.type !== "desktop-file") {
20157 return;
20158 }
20159 const previewEl = document.createElement("div");
20160 previewEl.className = "desktop-mode-files-drop-preview";
20161 previewEl.setAttribute("aria-hidden", "true");
20162 container.appendChild(previewEl);
20163 dropPreviewEl = previewEl;
20164 const ghost = session.payload.ghost;
20165 const offsetX = ghost?.offsetX ?? 0;
20166 const offsetY = ghost?.offsetY ?? 0;
20167 const data = session.payload.data;
20168 const movingId = data?.placement?.id;
20169 const updatePreview = (clientX, clientY) => {
20170 const rect = container.getBoundingClientRect();
20171 const rawX = Math.max(0, clientX - rect.left - offsetX);
20172 const rawY = Math.max(0, clientY - rect.top - offsetY);
20173 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
20174 const occupied = buildVisualOccupiedSet(peers, movingId);
20175 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
20176 previewEl.style.transform = `translate3d(${cell.x}px, ${cell.y}px, 0)`;
20177 };
20178 const sourceRect = session.payload.source.getBoundingClientRect();
20179 updatePreview(
20180 sourceRect.left + offsetX,
20181 sourceRect.top + offsetY
20182 );
20183 const moveHandler = (ev) => {
20184 updatePreview(ev.clientX, ev.clientY);
20185 };
20186 document.addEventListener("pointermove", moveHandler);
20187 dropPreviewMoveHandler = moveHandler;
20188 };
20189 const teardownCanvasDropPreview = () => {
20190 if (dropPreviewMoveHandler) {
20191 document.removeEventListener("pointermove", dropPreviewMoveHandler);
20192 dropPreviewMoveHandler = null;
20193 }
20194 if (dropPreviewEl) {
20195 dropPreviewEl.remove();
20196 dropPreviewEl = null;
20197 }
20198 };
20199 const canvasDropTarget = {
20200 id: `desktop-mode-files-canvas-${folderId}`,
20201 element: host,
20202 accept: (payload) => {
20203 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
20204 return false;
20205 }
20206 if (folderId > 0 && payload.type === "desktop-file") {
20207 const data = payload.data;
20208 if (data.placement.file?.type === "folder") {
20209 const movingFolderId = parseInt(data.placement.file.ref, 10);
20210 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, folderId)) {
20211 return false;
20212 }
20213 }
20214 }
20215 return true;
20216 },
20217 onEnter: (session) => {
20218 host.setAttribute("data-files-drop-active", "");
20219 installCanvasDropPreview(session);
20220 },
20221 onLeave: () => {
20222 host.removeAttribute("data-files-drop-active");
20223 teardownCanvasDropPreview();
20224 },
20225 onDrop: (session, ev) => {
20226 host.removeAttribute("data-files-drop-active");
20227 teardownCanvasDropPreview();
20228 const rect = container.getBoundingClientRect();
20229 const ghost = session.payload.ghost;
20230 const offsetX = ghost?.offsetX ?? 0;
20231 const offsetY = ghost?.offsetY ?? 0;
20232 const rawX = Math.max(0, ev.clientX - rect.left - offsetX);
20233 const rawY = Math.max(0, ev.clientY - rect.top - offsetY);
20234 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
20235 if (session.payload.type === "desktop-file") {
20236 const data = session.payload.data;
20237 const occupied = buildVisualOccupiedSet(peers, data.placement.id);
20238 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
20239 const next = {
20240 ...data.placement,
20241 x: cell.x,
20242 y: cell.y,
20243 parentId: folderId
20244 };
20245 store.upsertPlacement(next);
20246 doAction("desktop-mode.files.tile-manually-placed", {
20247 folderId,
20248 placementId: data.placement.id
20249 });
20250 if (isSyntheticPlacement(data.placement)) {
20251 const dockItemId = readSynthSource(data.placement);
20252 if (dockItemId) {
20253 persistDockPromotedPosition(
20254 dockItemId,
20255 cell.x,
20256 cell.y
20257 );
20258 }
20259 return;
20260 }
20261 void updatePlacement(
20262 data.placement.id,
20263 {
20264 x: cell.x,
20265 y: cell.y,
20266 parentId: folderId
20267 },
20268 data.placement.updatedAtMs
20269 ).then((server) => {
20270 store.upsertPlacement(server, "remote");
20271 }).catch((err) => {
20272 if (isConflict(err)) {
20273 showConflictToast(err);
20274 } else {
20275 console.error(
20276 "[desktop-mode] files: drag persist failed",
20277 err
20278 );
20279 }
20280 store.upsertPlacement(data.placement);
20281 });
20282 return;
20283 }
20284 if (session.payload.type === "shortcut") {
20285 const data = session.payload.data;
20286 const occupied = buildVisualOccupiedSet(peers);
20287 const cell = nextRowMajorCell(occupied, host);
20288 void createPlacement({
20289 parentId: folderId,
20290 type: data.kind,
20291 ref: data.ref,
20292 x: cell.x,
20293 y: cell.y
20294 }).then((placement) => {
20295 store.upsertPlacement(placement);
20296 doAction("desktop-mode.files.shortcut-dropped", {
20297 folderId,
20298 placement
20299 });
20300 }).catch((err) => {
20301 console.error(
20302 "[desktop-mode] shortcut drop failed:",
20303 err
20304 );
20305 });
20306 }
20307 }
20308 };
20309 const dragManagerForLayer = getDragManager();
20310 if (dragManagerForLayer) {
20311 dropTargetDeregisters.push(
20312 dragManagerForLayer.registerDropTarget(canvasDropTarget)
20313 );
20314 }
20315 const onCanvasClick = (e) => {
20316 if (e.target instanceof Element && e.target.closest(`.${TILE_CLASS}`)) {
20317 return;
20318 }
20319 setSelected(null);
20320 };
20321 host.addEventListener("click", onCanvasClick);
20322 function attachSelectOnClick(tile2, placement) {
20323 tile2.addEventListener("click", (e) => {
20324 e.stopPropagation();
20325 setSelected(placement);
20326 });
20327 }
20328 repaint(store.getState());
20329 const off = store.subscribe(repaint);
20330 let resolveHydrated = () => void 0;
20331 const hydrated = new Promise((resolve2) => {
20332 resolveHydrated = resolve2;
20333 });
20334 if (!store.getState().hydratedFolders.has(folderId)) {
20335 void listPlacements(folderId).then((res) => {
20336 store.setFolderPlacements(folderId, res.placements);
20337 }).catch((err) => {
20338 console.error("[desktop-mode] files: failed to hydrate folder", folderId, err);
20339 }).finally(() => {
20340 resolveHydrated();
20341 });
20342 } else {
20343 queueMicrotask(resolveHydrated);
20344 }
20345 const colsForWidth = () => {
20346 const w = host.clientWidth > 0 ? host.clientWidth : 4 * GRID_CELL_W;
20347 return Math.max(1, Math.floor((w - GRID_PADDING) / GRID_CELL_W));
20348 };
20349 const sortPlacements = (list2, mode) => {
20350 const sorted = list2.slice();
20351 switch (mode) {
20352 case "name-asc":
20353 sorted.sort(
20354 (a, b) => a.file.title.localeCompare(b.file.title)
20355 );
20356 break;
20357 case "name-desc":
20358 sorted.sort(
20359 (a, b) => b.file.title.localeCompare(a.file.title)
20360 );
20361 break;
20362 case "date-asc":
20363 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
20364 break;
20365 case "date-desc":
20366 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
20367 break;
20368 }
20369 return sorted;
20370 };
20371 const sort = (mode) => {
20372 const live = store.getState().placementsByFolder.get(folderId);
20373 if (!live || live.length === 0) {
20374 return;
20375 }
20376 const pinned = live.filter((p) => isPinned(p));
20377 const draggable = live.filter((p) => !isPinned(p));
20378 const sorted = sortPlacements(draggable, mode);
20379 const cols = colsForWidth();
20380 const occupied = /* @__PURE__ */ new Set();
20381 for (let i = 0; i < pinned.length; i += 1) {
20382 occupied.add(cellKey(0, i));
20383 }
20384 let idx = 0;
20385 const nextCell = () => {
20386 while (true) {
20387 const row = Math.floor(idx / cols);
20388 const col = idx % cols;
20389 idx += 1;
20390 if (!occupied.has(cellKey(col, row))) {
20391 return { col, row };
20392 }
20393 }
20394 };
20395 sorted.forEach((p, i) => {
20396 const cell = nextCell();
20397 const x = GRID_PADDING + cell.col * GRID_CELL_W;
20398 const y = GRID_PADDING + cell.row * GRID_CELL_H;
20399 const next = {
20400 ...p,
20401 x,
20402 y,
20403 sortOrder: i
20404 };
20405 store.upsertPlacement(next);
20406 if (isSyntheticPlacement(p)) {
20407 return;
20408 }
20409 void updatePlacement(p.id, { x, y, sortOrder: i }).catch((err) => {
20410 console.error(
20411 "[desktop-mode] files: sort persist failed",
20412 err
20413 );
20414 });
20415 });
20416 };
20417 const reflow = () => {
20418 const live = store.getState().placementsByFolder.get(folderId);
20419 if (!live || live.length === 0) {
20420 return;
20421 }
20422 const w = host.clientWidth > 0 ? host.clientWidth : Infinity;
20423 const overflowing = live.some((p) => {
20424 const right = p.x + GRID_CELL_W;
20425 return right > w;
20426 });
20427 if (!overflowing) {
20428 return;
20429 }
20430 const cols = colsForWidth();
20431 const pinned = live.filter((p) => isPinned(p));
20432 const draggable = live.filter((p) => !isPinned(p));
20433 const occupied = /* @__PURE__ */ new Set();
20434 for (let i = 0; i < pinned.length; i += 1) {
20435 occupied.add(cellKey(0, i));
20436 }
20437 let idx = 0;
20438 const nextCell = () => {
20439 while (true) {
20440 const row = Math.floor(idx / cols);
20441 const col = idx % cols;
20442 idx += 1;
20443 if (!occupied.has(cellKey(col, row))) {
20444 return { col, row };
20445 }
20446 }
20447 };
20448 for (const p of draggable) {
20449 const cell = nextCell();
20450 const x = GRID_PADDING + cell.col * GRID_CELL_W;
20451 const y = GRID_PADDING + cell.row * GRID_CELL_H;
20452 const tile2 = container.querySelector(
20453 `[data-placement-id="${p.id}"]`
20454 );
20455 if (tile2) {
20456 setTilePosition(tile2, x, y);
20457 }
20458 }
20459 };
20460 let lastWidth = host.clientWidth;
20461 let resizeObserver = null;
20462 if (typeof ResizeObserver !== "undefined") {
20463 resizeObserver = new ResizeObserver(() => {
20464 const w = host.clientWidth;
20465 if (w === lastWidth) {
20466 return;
20467 }
20468 lastWidth = w;
20469 reflow();
20470 });
20471 resizeObserver.observe(host);
20472 }
20473 return {
20474 host,
20475 folderId,
20476 onSelectionChange(cb) {
20477 selectionListeners.add(cb);
20478 return () => {
20479 selectionListeners.delete(cb);
20480 };
20481 },
20482 sort,
20483 reflow,
20484 hydrated,
20485 dispose() {
20486 off();
20487 resizeObserver?.disconnect();
20488 resizeObserver = null;
20489 for (const deregister of dropTargetDeregisters) {
20490 try {
20491 deregister();
20492 } catch {
20493 }
20494 }
20495 dropTargetDeregisters.length = 0;
20496 for (const deregister of folderDropDeregisters.values()) {
20497 try {
20498 deregister();
20499 } catch {
20500 }
20501 }
20502 folderDropDeregisters.clear();
20503 for (const deregister of tileRejectDeregisters.values()) {
20504 try {
20505 deregister();
20506 } catch {
20507 }
20508 }
20509 tileRejectDeregisters.clear();
20510 host.removeEventListener("click", onCanvasClick);
20511 selectionListeners.clear();
20512 container.remove();
20513 }
20514 };
20515 }
20516 function fingerprint(list2) {
20517 if (list2.length === 0) {
20518 return "0";
20519 }
20520 const parts = [];
20521 for (const p of list2) {
20522 parts.push(
20523 `${p.id}:${p.parentId}:${p.x}:${p.y}:${p.sortOrder}:${p.updatedAtMs}:${p.file.type}:${p.file.ref}:${p.file.title}:${p.file.icon}:${isPinned(p) ? 1 : 0}`
20524 );
20525 }
20526 return parts.join("|");
20527 }
20528 function isPinned(placement) {
20529 return Boolean(placement.file.pinned);
20530 }
20531 function readSynthSource(placement) {
20532 const meta = placement.meta;
20533 if (!meta || typeof meta !== "object") {
20534 return null;
20535 }
20536 const v = meta.__synthFromDockItem;
20537 return typeof v === "string" && v !== "" ? v : null;
20538 }
20539 function isSyntheticPlacement(placement) {
20540 return placement.id <= 0 || readSynthSource(placement) !== null;
20541 }
20542 const RECYCLE_BIN_REF = "desktop-mode-recycle-bin";
20543 function shouldRejectTileDrops(placement) {
20544 if (placement.file?.type === "folder") {
20545 return false;
20546 }
20547 if (placement.file?.ref === RECYCLE_BIN_REF) {
20548 return false;
20549 }
20550 return true;
20551 }
20552 function buildVisualOccupiedSet(placements, excludeId) {
20553 const sorted = placements.slice().sort((a, b) => {
20554 const ap = isPinned(a) ? 0 : 1;
20555 const bp = isPinned(b) ? 0 : 1;
20556 return ap - bp;
20557 });
20558 const set = /* @__PURE__ */ new Set();
20559 let pinnedIdx = 0;
20560 for (const p of sorted) {
20561 if (excludeId !== void 0 && p.id === excludeId) {
20562 continue;
20563 }
20564 if (isPinned(p)) {
20565 set.add(cellKey(0, pinnedIdx));
20566 pinnedIdx += 1;
20567 } else {
20568 const cell = pointToCell(p.x, p.y);
20569 set.add(cellKey(cell.col, cell.row));
20570 }
20571 }
20572 return set;
20573 }
20574 function wouldCreateFolderCycle(movingFolderId, targetParentId) {
20575 if (targetParentId <= 0 || movingFolderId <= 0) {
20576 return false;
20577 }
20578 if (movingFolderId === targetParentId) {
20579 return true;
20580 }
20581 const parentByFolderId = /* @__PURE__ */ new Map();
20582 const state2 = store.getState();
20583 for (const bucket2 of state2.placementsByFolder.values()) {
20584 for (const p of bucket2) {
20585 if (p.file?.type !== "folder") {
20586 continue;
20587 }
20588 const fid = parseInt(p.file.ref, 10);
20589 if (Number.isNaN(fid) || fid <= 0) {
20590 continue;
20591 }
20592 if (!parentByFolderId.has(fid)) {
20593 parentByFolderId.set(fid, p.parentId);
20594 }
20595 }
20596 }
20597 const visited = /* @__PURE__ */ new Set();
20598 let cursor = targetParentId;
20599 let maxDepth = 256;
20600 while (cursor > 0 && maxDepth-- > 0) {
20601 if (cursor === movingFolderId) {
20602 return true;
20603 }
20604 if (visited.has(cursor)) {
20605 return true;
20606 }
20607 visited.add(cursor);
20608 const next = parentByFolderId.get(cursor);
20609 if (next === void 0) {
20610 return false;
20611 }
20612 cursor = next;
20613 }
20614 return false;
20615 }
20616 function persistDockPromotedPosition(dockItemId, x, y) {
20617 const api = window.wp?.desktop;
20618 if (!api?.getOsSettings || !api?.updateOsSettings) {
20619 return;
20620 }
20621 const current = api.getOsSettings().dockPromotedPositions ?? {};
20622 api.updateOsSettings({
20623 dockPromotedPositions: {
20624 ...current,
20625 [dockItemId]: { x, y }
20626 }
20627 });
20628 }
20629 function tryPatchPositions(list2, container, host) {
20630 const tiles = Array.from(
20631 container.querySelectorAll("[data-placement-id]")
20632 );
20633 if (tiles.length !== list2.length) {
20634 return false;
20635 }
20636 const byId = /* @__PURE__ */ new Map();
20637 for (const tile2 of tiles) {
20638 const raw = tile2.dataset.placementId ?? "";
20639 const id = parseInt(raw, 10);
20640 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
20641 return false;
20642 }
20643 byId.set(id, tile2);
20644 }
20645 for (const placement of list2) {
20646 const tile2 = byId.get(placement.id);
20647 if (!tile2) {
20648 return false;
20649 }
20650 if (tile2.dataset.fileType !== placement.file.type) {
20651 return false;
20652 }
20653 if (tile2.dataset.fileRef !== placement.file.ref) {
20654 return false;
20655 }
20656 const wasPinned = tile2.classList.contains(`${TILE_CLASS}--pinned`);
20657 if (wasPinned !== isPinned(placement)) {
20658 return false;
20659 }
20660 }
20661 const pinnedSlots = /* @__PURE__ */ new Map();
20662 const occupiedCells = /* @__PURE__ */ new Set();
20663 let pinnedIdx = 0;
20664 for (const placement of list2) {
20665 if (!isPinned(placement)) {
20666 continue;
20667 }
20668 const slot = cellToPos(0, pinnedIdx);
20669 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
20670 occupiedCells.add(cellKey(slot.col, slot.row));
20671 pinnedIdx += 1;
20672 }
20673 const displaced = /* @__PURE__ */ new Map();
20674 for (const placement of list2) {
20675 if (pinnedSlots.has(placement.id)) {
20676 continue;
20677 }
20678 const target = pointToCell(placement.x, placement.y);
20679 const key = cellKey(target.col, target.row);
20680 if (!occupiedCells.has(key)) {
20681 occupiedCells.add(key);
20682 continue;
20683 }
20684 const free = snapToEmptyCell(
20685 placement.x,
20686 placement.y,
20687 occupiedCells,
20688 host
20689 );
20690 occupiedCells.add(cellKey(free.col, free.row));
20691 displaced.set(placement.id, { x: free.x, y: free.y });
20692 }
20693 for (const placement of list2) {
20694 const tile2 = byId.get(placement.id);
20695 if (!tile2) {
20696 continue;
20697 }
20698 const pinned = pinnedSlots.get(placement.id);
20699 const disp = displaced.get(placement.id);
20700 if (pinned) {
20701 setTilePosition(tile2, pinned.x, pinned.y);
20702 } else if (disp) {
20703 setTilePosition(tile2, disp.x, disp.y);
20704 } else {
20705 setTilePosition(tile2, placement.x, placement.y);
20706 }
20707 }
20708 return true;
20709 }
20710 function hidePromotedDockItem(dockItemId) {
20711 const api = window.wp?.desktop;
20712 if (!api?.getOsSettings || !api?.updateOsSettings) {
20713 return;
20714 }
20715 const current = api.getOsSettings().itemVisibility ?? {};
20716 const next = { ...current, [dockItemId]: "dock" };
20717 api.updateOsSettings({ itemVisibility: next });
20718 }
20719 function registerFolderDropTarget(dragManager, tile2, targetFolderId, currentFolderId) {
20720 const target = {
20721 id: `desktop-mode-files-folder-${targetFolderId}-tile-${tile2.dataset.placementId ?? "?"}`,
20722 element: tile2,
20723 accept: (payload) => {
20724 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
20725 return false;
20726 }
20727 if (payload.type === "desktop-file") {
20728 const data = payload.data;
20729 if (data.placement.file.type === "folder" && parseInt(data.placement.file.ref, 10) === targetFolderId) {
20730 return false;
20731 }
20732 if (data.placement.parentId === targetFolderId) {
20733 return false;
20734 }
20735 if (isSyntheticPlacement(data.placement)) {
20736 return false;
20737 }
20738 if (data.placement.file.type === "folder") {
20739 const movingFolderId = parseInt(data.placement.file.ref, 10);
20740 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, targetFolderId)) {
20741 return false;
20742 }
20743 }
20744 }
20745 return true;
20746 },
20747 onEnter: () => {
20748 tile2.classList.add(`${TILE_CLASS}--drop-target`);
20749 },
20750 onLeave: () => {
20751 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
20752 },
20753 onDrop: (session) => {
20754 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
20755 if (session.payload.type === "desktop-file") {
20756 const data = session.payload.data;
20757 const next = {
20758 ...data.placement,
20759 parentId: targetFolderId
20760 };
20761 store.upsertPlacement(next);
20762 void updatePlacement(
20763 data.placement.id,
20764 { parentId: targetFolderId },
20765 data.placement.updatedAtMs
20766 ).then((server) => {
20767 store.upsertPlacement(server, "remote");
20768 }).catch((err) => {
20769 if (isConflict(err)) {
20770 showConflictToast(err);
20771 } else {
20772 console.error(
20773 "[desktop-mode] files: move-into-folder persist failed",
20774 err
20775 );
20776 }
20777 store.upsertPlacement(data.placement);
20778 });
20779 return;
20780 }
20781 if (session.payload.type === "shortcut") {
20782 const data = session.payload.data;
20783 const peers = store.getState().placementsByFolder.get(targetFolderId) ?? [];
20784 const cell = nextRowMajorCell(buildVisualOccupiedSet(peers));
20785 void createPlacement({
20786 parentId: targetFolderId,
20787 type: data.kind,
20788 ref: data.ref,
20789 x: cell.x,
20790 y: cell.y
20791 }).then((placement) => {
20792 store.upsertPlacement(placement);
20793 doAction("desktop-mode.files.shortcut-dropped", {
20794 folderId: targetFolderId,
20795 placement
20796 });
20797 }).catch((err) => {
20798 console.error(
20799 "[desktop-mode] shortcut drop into folder failed:",
20800 err
20801 );
20802 });
20803 }
20804 }
20805 };
20806 return dragManager.registerDropTarget(target);
20807 }
20808 function attachTileDrag(tile2, placement, folderId) {
20809 tile2.addEventListener("pointerdown", (e) => {
20810 if (e.button !== 0) {
20811 return;
20812 }
20813 const dragManager = getDragManager();
20814 if (!dragManager) {
20815 return;
20816 }
20817 const liveBucket = store.getState().placementsByFolder.get(folderId);
20818 const livePlacement = liveBucket?.find((p) => p.id === placement.id) ?? placement;
20819 parseFloat(tile2.style.left) || livePlacement.x;
20820 parseFloat(tile2.style.top) || livePlacement.y;
20821 dragManager.start({
20822 payload: {
20823 type: "desktop-file",
20824 source: tile2,
20825 data: {
20826 placement: livePlacement,
20827 sourceFolderId: folderId,
20828 // Synthesize a cross-frame bridge payload from the
20829 // placement's file shape so a wallpaper-placed
20830 // shortcut can be dropped into an open Gutenberg
20831 // iframe and inserted as the matching block. The
20832 // PHP serialize() methods (`Desktop_Mode_Post_File`,
20833 // `Desktop_Mode_User_File`, `Desktop_Mode_Attachment_File`)
20834 // surface the URL fields this needs.
20835 bridgePayload: buildBridgePayloadFromPlacement(livePlacement)
20836 },
20837 ghost: {
20838 offsetX: e.clientX - tile2.getBoundingClientRect().left,
20839 offsetY: e.clientY - tile2.getBoundingClientRect().top
20840 }
20841 },
20842 origin: e
20843 // `onClickOnly` intentionally empty — a tile click is
20844 // handled by the dedicated `attachSelectOnClick` listener
20845 // below, which fires from the regular `click` event after
20846 // a sub-threshold pointerup. The manager won't fire a
20847 // `click` itself; the browser does.
20848 });
20849 });
20850 }
20851 function attachContextMenu(tile2, placement) {
20852 tile2.addEventListener("contextmenu", (e) => {
20853 e.preventDefault();
20854 e.stopPropagation();
20855 const items = [
20856 {
20857 id: "open",
20858 label: "Open",
20859 icon: "dashicons-external",
20860 sort: 10,
20861 onClick: () => {
20862 const file = resolve(placement.file);
20863 void openFile(file);
20864 }
20865 }
20866 ];
20867 if (placement.file.type === "post") {
20868 items.push({
20869 id: "navigate-into",
20870 label: "Navigate into",
20871 icon: "dashicons-category",
20872 sort: 20,
20873 onClick: () => {
20874 const postId = parseInt(placement.file.ref, 10);
20875 if (!postId) {
20876 return;
20877 }
20878 const api = window.wp?.desktop?.myWordpress;
20879 const postType = typeof placement.file.postType === "string" ? placement.file.postType : "post";
20880 const entityId = postType === "page" ? "pages" : "posts";
20881 api?.openDetail({
20882 entityId,
20883 postId,
20884 postTitle: placement.file.title || `#${postId}`
20885 });
20886 }
20887 });
20888 }
20889 const isFolder = placement.file.type === "folder";
20890 if (isFolder) {
20891 items.push({
20892 id: "rename-folder",
20893 label: "Rename…",
20894 icon: "dashicons-edit",
20895 sort: 30,
20896 onClick: () => {
20897 const folderId = parseInt(placement.file.ref, 10);
20898 if (!folderId) {
20899 return;
20900 }
20901 openCreateFolderDialog({
20902 title: "Rename folder",
20903 label: "New name",
20904 submitLabel: "Rename",
20905 initialName: placement.file.title,
20906 onSubmit: async (name) => {
20907 const trimmed = name.trim();
20908 if (!trimmed || trimmed === placement.file.title) {
20909 return;
20910 }
20911 const previousTitle = placement.file.title;
20912 const optimistic = {
20913 ...placement,
20914 file: { ...placement.file, title: trimmed }
20915 };
20916 store.upsertPlacement(optimistic);
20917 try {
20918 const folderUpdatedAtMs = store.getState().folders.get(folderId)?.updatedAtMs ?? 0;
20919 const updated = await updateFolder(
20920 folderId,
20921 { name: trimmed },
20922 folderUpdatedAtMs
20923 );
20924 store.upsertFolder(updated);
20925 const refreshed = await listPlacements(
20926 placement.parentId
20927 );
20928 store.setFolderPlacements(
20929 placement.parentId,
20930 refreshed.placements
20931 );
20932 } catch (err) {
20933 console.error(
20934 "[desktop-mode] rename folder failed:",
20935 err
20936 );
20937 store.upsertPlacement({
20938 ...placement,
20939 file: {
20940 ...placement.file,
20941 title: previousTitle
20942 }
20943 });
20944 }
20945 }
20946 });
20947 }
20948 });
20949 if (placement.canTrash !== false) {
20950 items.push({
20951 id: "delete-folder",
20952 label: "Move folder to Trash",
20953 icon: "dashicons-trash",
20954 sort: 90,
20955 danger: true,
20956 onClick: () => trashFolderWithUndo(placement)
20957 });
20958 }
20959 } else {
20960 const synthFromDockItem = readSynthSource(placement);
20961 const isRegisteredIcon = placement.file.type === "shortcut";
20962 if (synthFromDockItem || isRegisteredIcon) {
20963 const hideId = synthFromDockItem ?? placement.file.ref;
20964 items.push({
20965 id: "hide-from-desktop",
20966 label: "Hide from desktop",
20967 icon: "dashicons-hidden",
20968 sort: 90,
20969 onClick: () => hidePromotedDockItem(hideId)
20970 });
20971 } else if (placement.canTrash !== false) {
20972 items.push({
20973 id: "remove",
20974 label: "Move to Trash",
20975 icon: "dashicons-trash",
20976 sort: 90,
20977 danger: true,
20978 onClick: () => trashPlacementWithUndo(placement)
20979 });
20980 }
20981 }
20982 openTileMenu({ x: e.clientX, y: e.clientY }, { placement, items });
20983 });
20984 }
20985 const STATUS_BAR_CLASS = "desktop-mode-folder-status-bar";
20986 const ROOT_CLASS$2 = STATUS_BAR_CLASS;
20987 function mountFolderStatusBar(host, folderId) {
20988 const bar = document.createElement("div");
20989 bar.className = ROOT_CLASS$2;
20990 bar.setAttribute("role", "status");
20991 bar.dataset.folderId = String(folderId);
20992 host.appendChild(bar);
20993 const repaint = () => {
20994 const list2 = getFilesState().placementsByFolder.get(folderId) ?? [];
20995 const folders = list2.filter((p) => p.file.type === "folder").length;
20996 const files = list2.length - folders;
20997 const ctx = {
20998 folderId,
20999 totals: { files, folders, total: list2.length }
21000 };
21001 const segments = computeSegments(ctx);
21002 render(bar, segments);
21003 };
21004 repaint();
21005 const off = subscribeFilesStore(() => repaint());
21006 return {
21007 dispose() {
21008 off();
21009 bar.remove();
21010 }
21011 };
21012 }
21013 function computeSegments(ctx) {
21014 const { folders, files } = ctx.totals;
21015 const builtIns = [
21016 {
21017 id: "count",
21018 label: pluralize(files, "file", "files") + (folders > 0 ? `, ${pluralize(folders, "folder", "folders")}` : ""),
21019 align: "start",
21020 sort: 10
21021 }
21022 ];
21023 const filtered = applyFilters(
21024 "desktop-mode.files.folder-window.status-bar",
21025 builtIns,
21026 ctx
21027 );
21028 return Array.isArray(filtered) ? filtered : builtIns;
21029 }
21030 function render(bar, segments) {
21031 const sort = (a, b) => {
21032 const sa = typeof a.sort === "number" ? a.sort : 100;
21033 const sb = typeof b.sort === "number" ? b.sort : 100;
21034 if (sa !== sb) {
21035 return sa - sb;
21036 }
21037 return a.label.localeCompare(b.label);
21038 };
21039 const start = segments.filter((s) => (s.align ?? "start") === "start").sort(sort);
21040 const end = segments.filter((s) => s.align === "end").sort(sort);
21041 bar.replaceChildren();
21042 bar.appendChild(buildCluster("start", start));
21043 bar.appendChild(buildCluster("end", end));
21044 }
21045 function buildCluster(align, segs) {
21046 const cluster = document.createElement("div");
21047 cluster.className = `${ROOT_CLASS$2}__cluster ${ROOT_CLASS$2}__cluster--${align}`;
21048 for (const seg of segs) {
21049 cluster.appendChild(buildSegment(seg));
21050 }
21051 return cluster;
21052 }
21053 function buildSegment(seg) {
21054 const interactive = typeof seg.onClick === "function";
21055 const el = document.createElement(interactive ? "button" : "span");
21056 el.className = `${ROOT_CLASS$2}__segment`;
21057 el.dataset.segmentId = seg.id;
21058 if (interactive) {
21059 el.type = "button";
21060 el.addEventListener("click", (e) => seg.onClick(e));
21061 }
21062 if (seg.icon) {
21063 const icon = document.createElement("span");
21064 icon.className = `${ROOT_CLASS$2}__icon dashicons ${seg.icon.replace(/[^a-zA-Z0-9_-]/g, "")}`;
21065 icon.setAttribute("aria-hidden", "true");
21066 el.appendChild(icon);
21067 }
21068 const label = document.createElement("span");
21069 label.className = `${ROOT_CLASS$2}__label`;
21070 label.textContent = seg.label;
21071 el.appendChild(label);
21072 return el;
21073 }
21074 function pluralize(n, singular, plural) {
21075 return `${n} ${n === 1 ? singular : plural}`;
21076 }
21077 const MENU_CLASS$1 = "desktop-mode-icon-canvas-menu";
21078 let activeMenu$1 = null;
21079 let activeFlyout = null;
21080 let activeCanvas = null;
21081 let outsideHandler = null;
21082 let escHandler = null;
21083 function attachIconCanvasMenu(canvas, deps2) {
21084 deps2.openOnBackgroundClick !== false;
21085 const onContextMenu = (e) => {
21086 if (isInsideTile(e.target) || isInsideMenu(e.target)) {
21087 return;
21088 }
21089 e.preventDefault();
21090 toggle(e.clientX, e.clientY);
21091 };
21092 let toggleGen = 0;
21093 const toggle = (x, y) => {
21094 if (activeCanvas === canvas && activeMenu$1) {
21095 closeMenu();
21096 return;
21097 }
21098 const items = buildItems(deps2);
21099 const filtered = applyFilters(
21100 "desktop-mode.icon-canvas.menu",
21101 items,
21102 deps2.scope
21103 );
21104 const finalItems = Array.isArray(filtered) ? filtered : items;
21105 const myGen = ++toggleGen;
21106 openWithShellOverlays(
21107 () => myGen === toggleGen,
21108 () => openMenu(finalItems, { x, y }, canvas)
21109 );
21110 };
21111 canvas.addEventListener("contextmenu", onContextMenu);
21112 return {
21113 dispose: () => {
21114 canvas.removeEventListener("contextmenu", onContextMenu);
21115 closeMenu();
21116 }
21117 };
21118 }
21119 function isInsideTile(target) {
21120 if (!(target instanceof Element)) {
21121 return false;
21122 }
21123 return target.closest(".desktop-mode-file-tile") !== null;
21124 }
21125 function isInsideMenu(target) {
21126 if (!(target instanceof Element)) {
21127 return false;
21128 }
21129 return target.closest(`.${MENU_CLASS$1}`) !== null;
21130 }
21131 function buildItems(deps2) {
21132 const sortItem = {
21133 id: "sort-by",
21134 label: __("Sort by", "desktop-mode"),
21135 icon: "dashicons-sort",
21136 sort: 10,
21137 children: [
21138 {
21139 id: "sort-name-asc",
21140 label: __("Name (A → Z)", "desktop-mode"),
21141 sort: 10,
21142 onClick: () => deps2.onSort("name-asc")
21143 },
21144 {
21145 id: "sort-name-desc",
21146 label: __("Name (Z → A)", "desktop-mode"),
21147 sort: 20,
21148 onClick: () => deps2.onSort("name-desc")
21149 },
21150 {
21151 id: "sort-date-desc",
21152 label: __("Newest first", "desktop-mode"),
21153 sort: 30,
21154 onClick: () => deps2.onSort("date-desc")
21155 },
21156 {
21157 id: "sort-date-asc",
21158 label: __("Oldest first", "desktop-mode"),
21159 sort: 40,
21160 onClick: () => deps2.onSort("date-asc")
21161 }
21162 ]
21163 };
21164 const items = [sortItem];
21165 if (Array.isArray(deps2.extraItems)) {
21166 items.push(...deps2.extraItems);
21167 }
21168 return items;
21169 }
21170 function sortItems(items) {
21171 return items.slice().sort((a, b) => {
21172 const sa = typeof a.sort === "number" ? a.sort : 100;
21173 const sb = typeof b.sort === "number" ? b.sort : 100;
21174 if (sa !== sb) {
21175 return sa - sb;
21176 }
21177 return a.label.localeCompare(b.label);
21178 });
21179 }
21180 function openMenu(items, pos, canvas) {
21181 closeMenu();
21182 if (items.length === 0) {
21183 return;
21184 }
21185 activeCanvas = canvas;
21186 const sorted = sortItems(items);
21187 const menu = document.createElement("wpd-context-menu");
21188 menu.setAttribute("open", "");
21189 menu.classList.add(MENU_CLASS$1);
21190 menu.style.left = `${pos.x}px`;
21191 menu.style.top = `${pos.y}px`;
21192 const itemById = /* @__PURE__ */ new Map();
21193 for (const item of sorted) {
21194 itemById.set(item.id, item);
21195 const opt = appendOption(menu, item);
21196 if (hasChildren(item)) {
21197 opt.addEventListener("mouseenter", () => {
21198 openFlyout(item, opt);
21199 });
21200 }
21201 }
21202 menu.addEventListener("wpd-context-menu-pick", (e) => {
21203 const detail = e.detail;
21204 const item = itemById.get(detail.id);
21205 if (!item) {
21206 return;
21207 }
21208 if (hasChildren(item)) {
21209 e.stopPropagation();
21210 const anchor = menu.querySelector(
21211 `[data-menu-item-id="${item.id}"]`
21212 );
21213 if (anchor) {
21214 openFlyout(item, anchor);
21215 }
21216 return;
21217 }
21218 closeMenu();
21219 item.onClick?.();
21220 });
21221 document.body.appendChild(menu);
21222 activeMenu$1 = menu;
21223 clampToViewport(menu);
21224 queueMicrotask(() => {
21225 outsideHandler = (e) => {
21226 if (isInsideMenu(e.target)) {
21227 return;
21228 }
21229 closeMenu();
21230 };
21231 escHandler = (e) => {
21232 if (e.key === "Escape") {
21233 closeMenu();
21234 }
21235 };
21236 document.addEventListener("mousedown", outsideHandler);
21237 document.addEventListener("keydown", escHandler);
21238 });
21239 }
21240 function appendOption(host, item) {
21241 const opt = document.createElement("wpd-context-menu-option");
21242 opt.dataset.menuItemId = item.id;
21243 opt.setAttribute("value", item.id);
21244 if (item.heading) {
21245 opt.setAttribute("heading", "");
21246 }
21247 if (item.disabled) {
21248 opt.setAttribute("disabled", "");
21249 }
21250 if (item.icon) {
21251 opt.setAttribute("icon", sanitizeClass$1(item.icon));
21252 }
21253 if (hasChildren(item)) {
21254 opt.setAttribute("has-children", "");
21255 }
21256 opt.textContent = item.label;
21257 host.appendChild(opt);
21258 return opt;
21259 }
21260 function openFlyout(parent, anchor) {
21261 closeFlyout();
21262 if (!hasChildren(parent)) {
21263 return;
21264 }
21265 const fly = document.createElement("wpd-context-menu");
21266 fly.setAttribute("open", "");
21267 fly.classList.add(MENU_CLASS$1, `${MENU_CLASS$1}--flyout`);
21268 const childById = /* @__PURE__ */ new Map();
21269 for (const child of sortItems(parent.children ?? [])) {
21270 childById.set(child.id, child);
21271 appendOption(fly, child);
21272 }
21273 fly.addEventListener("wpd-context-menu-pick", (e) => {
21274 const detail = e.detail;
21275 const child = childById.get(detail.id);
21276 if (!child) {
21277 return;
21278 }
21279 e.stopPropagation();
21280 closeMenu();
21281 child.onClick?.();
21282 });
21283 document.body.appendChild(fly);
21284 activeFlyout = fly;
21285 positionFlyout(fly, anchor);
21286 }
21287 function positionFlyout(fly, anchor) {
21288 const ar = anchor.getBoundingClientRect();
21289 fly.style.position = "fixed";
21290 fly.style.left = `${ar.right}px`;
21291 fly.style.top = `${ar.top}px`;
21292 const fr = fly.getBoundingClientRect();
21293 if (fr.right > window.innerWidth) {
21294 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
21295 }
21296 if (fr.bottom > window.innerHeight) {
21297 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
21298 }
21299 }
21300 function clampToViewport(menu) {
21301 const rect = menu.getBoundingClientRect();
21302 if (rect.right > window.innerWidth) {
21303 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
21304 }
21305 if (rect.bottom > window.innerHeight) {
21306 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
21307 }
21308 }
21309 function hasChildren(item) {
21310 return Array.isArray(item.children) && item.children.length > 0;
21311 }
21312 function closeFlyout() {
21313 if (activeFlyout) {
21314 activeFlyout.remove();
21315 activeFlyout = null;
21316 }
21317 }
21318 function closeMenu() {
21319 closeFlyout();
21320 if (activeMenu$1) {
21321 activeMenu$1.remove();
21322 activeMenu$1 = null;
21323 }
21324 activeCanvas = null;
21325 if (outsideHandler) {
21326 document.removeEventListener("mousedown", outsideHandler);
21327 outsideHandler = null;
21328 }
21329 if (escHandler) {
21330 document.removeEventListener("keydown", escHandler);
21331 escHandler = null;
21332 }
21333 }
21334 function sanitizeClass$1(raw) {
21335 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
21336 }
21337 const ROOT_CLASS$1 = "desktop-mode-breadcrumbs";
21338 function renderBreadcrumbs(host, segments, opts = {}) {
21339 host.replaceChildren();
21340 host.classList.add(ROOT_CLASS$1);
21341 if (opts.onBack) {
21342 const back = document.createElement("button");
21343 back.type = "button";
21344 back.className = `${ROOT_CLASS$1}__back`;
21345 back.setAttribute("aria-label", __("Back", "desktop-mode"));
21346 back.title = __("Back", "desktop-mode");
21347 const arrow = document.createElement("span");
21348 arrow.className = "dashicons dashicons-arrow-left-alt2";
21349 arrow.setAttribute("aria-hidden", "true");
21350 back.appendChild(arrow);
21351 if (opts.backDisabled) {
21352 back.disabled = true;
21353 }
21354 const onBack = opts.onBack;
21355 back.addEventListener("click", () => {
21356 if (back.disabled) {
21357 return;
21358 }
21359 onBack();
21360 });
21361 host.appendChild(back);
21362 }
21363 const nav = document.createElement("nav");
21364 nav.className = `${ROOT_CLASS$1}__crumbs`;
21365 nav.setAttribute("aria-label", __("Breadcrumb", "desktop-mode"));
21366 segments.forEach((seg, idx) => {
21367 if (idx > 0) {
21368 const sep = document.createElement("span");
21369 sep.className = `${ROOT_CLASS$1}__sep`;
21370 sep.setAttribute("aria-hidden", "true");
21371 sep.textContent = "›";
21372 nav.appendChild(sep);
21373 }
21374 if (!seg.onClick) {
21375 const here = document.createElement("span");
21376 here.className = `${ROOT_CLASS$1}__crumb ${ROOT_CLASS$1}__crumb--current`;
21377 here.setAttribute("aria-current", "page");
21378 here.textContent = seg.label;
21379 nav.appendChild(here);
21380 return;
21381 }
21382 const btn = document.createElement("button");
21383 btn.type = "button";
21384 btn.className = `${ROOT_CLASS$1}__crumb`;
21385 btn.textContent = seg.label;
21386 const onClick = seg.onClick;
21387 btn.addEventListener("click", () => {
21388 onClick();
21389 });
21390 nav.appendChild(btn);
21391 });
21392 host.appendChild(nav);
21393 }
21394 async function getJson(url, init2 = {}) {
21395 const response = await trackedFetch$1(url, {
21396 credentials: "same-origin",
21397 headers: {
21398 Accept: "application/json",
21399 "X-WP-Nonce": readRestNonce(),
21400 ...init2.headers ?? {}
21401 },
21402 ...init2
21403 });
21404 if (!response.ok) {
21405 throw new Error(`${response.status} ${response.statusText}`);
21406 }
21407 return await response.json();
21408 }
21409 function readRestNonce() {
21410 const cfg = window.wp?.desktop?.config;
21411 return cfg?.restNonce ?? "";
21412 }
21413 function readRestRoot() {
21414 const cfg = window.wp?.desktop?.config;
21415 if (cfg?.restUrl) {
21416 return cfg.restUrl.endsWith("/") ? cfg.restUrl : cfg.restUrl + "/";
21417 }
21418 return `${window.location.origin}/wp-json/`;
21419 }
21420 function restUrl(path) {
21421 return joinRestUrl(readRestRoot(), path);
21422 }
21423 function renderPlacementPreview(placement, host) {
21424 const filtered = applyFilters(
21425 "desktop-mode.files.preview",
21426 null,
21427 placement
21428 );
21429 if (filtered instanceof HTMLElement) {
21430 host.replaceChildren(filtered);
21431 return;
21432 }
21433 if (placement.accessGated) {
21434 host.replaceChildren(renderAccessGated(placement));
21435 return;
21436 }
21437 host.replaceChildren(renderLoading());
21438 void renderByType(placement).then((node) => {
21439 host.replaceChildren(node);
21440 }).catch((err) => {
21441 host.replaceChildren(renderError(err));
21442 });
21443 }
21444 function renderAccessGated(placement) {
21445 const wrap = document.createElement("div");
21446 wrap.className = "desktop-mode-files__access-gated";
21447 const ring = document.createElement("div");
21448 ring.className = "desktop-mode-files__access-gated-ring";
21449 const glyph = document.createElement("span");
21450 glyph.className = "dashicons dashicons-lock desktop-mode-files__access-gated-glyph";
21451 glyph.setAttribute("aria-hidden", "true");
21452 ring.appendChild(glyph);
21453 wrap.appendChild(ring);
21454 const title = document.createElement("h2");
21455 title.className = "desktop-mode-files__access-gated-title";
21456 title.textContent = "No permission to view";
21457 wrap.appendChild(title);
21458 const sub = document.createElement("p");
21459 sub.className = "desktop-mode-files__access-gated-sub";
21460 const target = placement.file.title || placement.file.type;
21461 sub.textContent = `You don’t have access to "${target}". The folder owner shared this folder with you, but your role doesn’t include permission to open this item.`;
21462 wrap.appendChild(sub);
21463 const hint = document.createElement("p");
21464 hint.className = "desktop-mode-files__access-gated-hint";
21465 hint.textContent = "Ask the owner to grant access on the underlying item, or to remove it from the shared folder.";
21466 wrap.appendChild(hint);
21467 return wrap;
21468 }
21469 async function renderByType(placement) {
21470 const file = placement.file;
21471 switch (file.type) {
21472 case "post":
21473 return renderPostPreview(file.ref, file);
21474 case "folder":
21475 return renderFolderPreview(file);
21476 case "shortcut":
21477 return renderShortcutPreview(file);
21478 case "attachment":
21479 return renderAttachmentPreview(file.ref, file);
21480 case "user":
21481 return renderUserSummary(file.ref, file);
21482 case "term":
21483 return renderTermSummary(file);
21484 case "comment":
21485 return renderCommentSummary(file.ref, file);
21486 case "bookmark":
21487 return renderBookmarkPreview(file);
21488 default:
21489 return renderGenericPreview(file);
21490 }
21491 }
21492 async function renderPostPreview(ref, file) {
21493 const id = parseInt(ref, 10);
21494 if (!id) {
21495 return renderGenericPreview(file);
21496 }
21497 let data = null;
21498 for (const path of ["wp/v2/posts", "wp/v2/pages"]) {
21499 try {
21500 data = await getJson(
21501 restUrl(
21502 `${path}/${id}?_fields=id,title,content,date,link,status`
21503 )
21504 );
21505 break;
21506 } catch {
21507 }
21508 }
21509 if (!data) {
21510 return renderGenericPreview(file);
21511 }
21512 const wrap = articleShell();
21513 const h = document.createElement("h2");
21514 h.className = "desktop-mode-my-wordpress__article-title";
21515 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
21516 wrap.appendChild(h);
21517 const meta = document.createElement("p");
21518 meta.className = "desktop-mode-my-wordpress__article-meta";
21519 const parts = [];
21520 parts.push(formatDate(data.date));
21521 if (data.status && data.status !== "publish") {
21522 parts.push(data.status);
21523 }
21524 meta.textContent = parts.join(" · ");
21525 wrap.appendChild(meta);
21526 if (data.content?.rendered) {
21527 const body = document.createElement("div");
21528 body.className = "desktop-mode-my-wordpress__article-content";
21529 body.innerHTML = data.content.rendered;
21530 wrap.appendChild(body);
21531 }
21532 const footer = document.createElement("footer");
21533 footer.className = "desktop-mode-my-wordpress__article-footer";
21534 const myWordpressApi = window.wp?.desktop?.myWordpress;
21535 if (myWordpressApi) {
21536 const exploreBtn = document.createElement("wpd-button");
21537 exploreBtn.setAttribute("variant", "secondary");
21538 exploreBtn.textContent = __("Explore details", "desktop-mode");
21539 exploreBtn.title = __(
21540 "See author, comments, categories, tags, attached media, and revisions for this entry.",
21541 "desktop-mode"
21542 );
21543 exploreBtn.addEventListener("click", () => {
21544 const postType = typeof file.postType === "string" ? file.postType : "post";
21545 myWordpressApi.openDetail({
21546 entityId: postType === "page" ? "pages" : "posts",
21547 postId: id,
21548 postTitle: stripTags(data.title.rendered) || `#${id}`
21549 });
21550 });
21551 footer.appendChild(exploreBtn);
21552 }
21553 const editBtn = document.createElement("wpd-button");
21554 editBtn.setAttribute("variant", "primary");
21555 editBtn.textContent = __("Open in editor", "desktop-mode");
21556 editBtn.addEventListener("click", () => {
21557 const adminUrl = window.wp?.desktop?.config?.adminUrl;
21558 if (!adminUrl) {
21559 return;
21560 }
21561 const editUrl = `${adminUrl}post.php?post=${id}&action=edit`;
21562 const wm = window.wp?.desktop?.windowManager;
21563 const postType = typeof file.postType === "string" ? file.postType : "post";
21564 const entityId = postType === "page" ? "pages" : "posts";
21565 wm?.open({
21566 id: `${entityId}-edit-${id}`,
21567 url: editUrl,
21568 title: stripTags(data.title.rendered),
21569 icon: file.icon
21570 });
21571 });
21572 footer.appendChild(editBtn);
21573 wrap.appendChild(footer);
21574 return wrap;
21575 }
21576 async function renderUserSummary(ref, file) {
21577 const id = parseInt(ref, 10);
21578 if (!id) {
21579 return renderGenericPreview(file);
21580 }
21581 let data = null;
21582 try {
21583 data = await getJson(
21584 restUrl(`desktop-mode/v1/user-stats/${id}`)
21585 );
21586 } catch {
21587 return renderGenericPreview(file);
21588 }
21589 const wrap = articleShell("desktop-mode-my-wordpress__user");
21590 const header = document.createElement("header");
21591 header.className = "desktop-mode-my-wordpress__user-header";
21592 if (data.profile.avatarUrl) {
21593 const img = document.createElement("img");
21594 img.className = "desktop-mode-my-wordpress__user-avatar";
21595 img.src = data.profile.avatarUrl;
21596 img.alt = "";
21597 header.appendChild(img);
21598 }
21599 const head = document.createElement("div");
21600 head.className = "desktop-mode-my-wordpress__user-headline";
21601 const h = document.createElement("h2");
21602 h.className = "desktop-mode-my-wordpress__article-title";
21603 h.textContent = data.profile.name || file.title || `#${id}`;
21604 head.appendChild(h);
21605 if (data.profile.roleLabels && data.profile.roleLabels.length > 0) {
21606 const roles = document.createElement("div");
21607 roles.className = "desktop-mode-my-wordpress__user-roles";
21608 for (const r of data.profile.roleLabels) {
21609 const badge = document.createElement("span");
21610 badge.className = "desktop-mode-my-wordpress__user-role";
21611 badge.textContent = r;
21612 roles.appendChild(badge);
21613 }
21614 head.appendChild(roles);
21615 }
21616 header.appendChild(head);
21617 wrap.appendChild(header);
21618 if (data.profile.description) {
21619 const bio = document.createElement("div");
21620 bio.className = "desktop-mode-my-wordpress__user-bio";
21621 bio.textContent = data.profile.description;
21622 wrap.appendChild(bio);
21623 }
21624 const cards = document.createElement("div");
21625 cards.className = "desktop-mode-my-wordpress__user-stats";
21626 cards.appendChild(
21627 statCard(
21628 data.counts.posts.total.toLocaleString(),
21629 __("Posts", "desktop-mode")
21630 )
21631 );
21632 cards.appendChild(
21633 statCard(
21634 data.counts.pages.total.toLocaleString(),
21635 __("Pages", "desktop-mode")
21636 )
21637 );
21638 cards.appendChild(
21639 statCard(
21640 data.counts.commentsReceived.toLocaleString(),
21641 __("Comments received", "desktop-mode")
21642 )
21643 );
21644 wrap.appendChild(cards);
21645 return wrap;
21646 }
21647 async function renderTermSummary(file) {
21648 const id = parseInt(file.ref, 10);
21649 const taxonomy = typeof file.taxonomy === "string" && file.taxonomy ? file.taxonomy : "category";
21650 if (!id) {
21651 return renderGenericPreview(file);
21652 }
21653 let data = null;
21654 try {
21655 data = await getJson(
21656 restUrl(`desktop-mode/v1/term-stats/${taxonomy}/${id}`)
21657 );
21658 } catch {
21659 return renderGenericPreview(file);
21660 }
21661 const wrap = articleShell();
21662 const h = document.createElement("h2");
21663 h.className = "desktop-mode-my-wordpress__article-title";
21664 h.textContent = data.profile.name || file.title || `#${id}`;
21665 wrap.appendChild(h);
21666 const meta = document.createElement("p");
21667 meta.className = "desktop-mode-my-wordpress__article-meta";
21668 meta.textContent = data.profile.taxonomyLabel || data.profile.taxonomy;
21669 wrap.appendChild(meta);
21670 if (data.profile.description) {
21671 const desc = document.createElement("div");
21672 desc.className = "desktop-mode-my-wordpress__article-content";
21673 desc.innerHTML = data.profile.description;
21674 wrap.appendChild(desc);
21675 }
21676 const cards = document.createElement("div");
21677 cards.className = "desktop-mode-my-wordpress__user-stats";
21678 cards.appendChild(
21679 statCard(
21680 data.counts.posts.total.toLocaleString(),
21681 __("Posts", "desktop-mode")
21682 )
21683 );
21684 cards.appendChild(
21685 statCard(
21686 data.counts.commentsReceived.toLocaleString(),
21687 __("Comments", "desktop-mode")
21688 )
21689 );
21690 cards.appendChild(
21691 statCard(
21692 data.counts.distinctAuthors.toLocaleString(),
21693 __("Authors", "desktop-mode")
21694 )
21695 );
21696 wrap.appendChild(cards);
21697 return wrap;
21698 }
21699 async function renderCommentSummary(ref, file) {
21700 const id = parseInt(ref, 10);
21701 if (!id) {
21702 return renderGenericPreview(file);
21703 }
21704 let data = null;
21705 try {
21706 data = await getJson(
21707 restUrl(`desktop-mode/v1/comment-stats/${id}`)
21708 );
21709 } catch {
21710 return renderGenericPreview(file);
21711 }
21712 const wrap = articleShell();
21713 const header = document.createElement("header");
21714 header.className = "desktop-mode-my-wordpress__user-header";
21715 if (data.author.avatarUrl) {
21716 const img = document.createElement("img");
21717 img.className = "desktop-mode-my-wordpress__user-avatar";
21718 img.src = data.author.avatarUrl;
21719 img.alt = "";
21720 header.appendChild(img);
21721 }
21722 const head = document.createElement("div");
21723 head.className = "desktop-mode-my-wordpress__user-headline";
21724 const h = document.createElement("h2");
21725 h.className = "desktop-mode-my-wordpress__article-title";
21726 h.textContent = data.author.name;
21727 head.appendChild(h);
21728 const sub = document.createElement("p");
21729 sub.className = "desktop-mode-my-wordpress__article-meta";
21730 sub.textContent = `${formatDate(data.comment.date)} · ${data.comment.status}`;
21731 head.appendChild(sub);
21732 header.appendChild(head);
21733 wrap.appendChild(header);
21734 const body = document.createElement("div");
21735 body.className = "desktop-mode-my-wordpress__article-content";
21736 body.innerHTML = data.comment.rendered;
21737 wrap.appendChild(body);
21738 if (data.post) {
21739 const card = document.createElement("div");
21740 card.className = "desktop-mode-my-wordpress__comment-post";
21741 const link = document.createElement("a");
21742 link.className = "desktop-mode-my-wordpress__comment-post-title";
21743 link.href = data.post.link;
21744 link.target = "_blank";
21745 link.rel = "noopener noreferrer";
21746 link.textContent = data.post.title;
21747 card.appendChild(link);
21748 wrap.appendChild(card);
21749 }
21750 return wrap;
21751 }
21752 async function renderAttachmentPreview(ref, file) {
21753 const id = parseInt(ref, 10);
21754 if (!id) {
21755 return renderGenericPreview(file);
21756 }
21757 let data = null;
21758 try {
21759 data = await getJson(
21760 restUrl(
21761 `wp/v2/media/${id}?_fields=id,title,source_url,mime_type,alt_text,media_details`
21762 )
21763 );
21764 } catch {
21765 return renderGenericPreview(file);
21766 }
21767 const wrap = articleShell();
21768 const h = document.createElement("h2");
21769 h.className = "desktop-mode-my-wordpress__article-title";
21770 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
21771 wrap.appendChild(h);
21772 const meta = document.createElement("p");
21773 meta.className = "desktop-mode-my-wordpress__article-meta";
21774 meta.textContent = data.mime_type;
21775 wrap.appendChild(meta);
21776 if (data.mime_type.startsWith("image/")) {
21777 const img = document.createElement("img");
21778 img.className = "desktop-mode-my-wordpress__article-hero";
21779 const sizes = data.media_details?.sizes;
21780 img.src = sizes?.large?.source_url ?? sizes?.medium?.source_url ?? data.source_url;
21781 img.alt = data.alt_text ?? "";
21782 wrap.appendChild(img);
21783 } else {
21784 const p = document.createElement("p");
21785 const a = document.createElement("a");
21786 a.href = data.source_url;
21787 a.textContent = data.source_url;
21788 a.target = "_blank";
21789 a.rel = "noopener noreferrer";
21790 p.appendChild(a);
21791 wrap.appendChild(p);
21792 }
21793 return wrap;
21794 }
21795 function renderFolderPreview(file) {
21796 const wrap = articleShell();
21797 const h = document.createElement("h2");
21798 h.className = "desktop-mode-my-wordpress__article-title";
21799 h.textContent = file.title || __("(folder)", "desktop-mode");
21800 wrap.appendChild(h);
21801 const meta = document.createElement("p");
21802 meta.className = "desktop-mode-my-wordpress__article-meta";
21803 meta.textContent = __("Double-click to open.", "desktop-mode");
21804 wrap.appendChild(meta);
21805 return wrap;
21806 }
21807 function renderShortcutPreview(file) {
21808 const wrap = articleShell();
21809 const h = document.createElement("h2");
21810 h.className = "desktop-mode-my-wordpress__article-title";
21811 h.textContent = file.title || __("Shortcut", "desktop-mode");
21812 wrap.appendChild(h);
21813 const meta = document.createElement("p");
21814 meta.className = "desktop-mode-my-wordpress__article-meta";
21815 meta.textContent = __("Plugin shortcut. Double-click to open.", "desktop-mode");
21816 wrap.appendChild(meta);
21817 return wrap;
21818 }
21819 function renderBookmarkPreview(file) {
21820 const wrap = articleShell();
21821 const h = document.createElement("h2");
21822 h.className = "desktop-mode-my-wordpress__article-title";
21823 h.textContent = file.title || __("Bookmark", "desktop-mode");
21824 wrap.appendChild(h);
21825 const url = typeof file.url === "string" ? file.url : "";
21826 if (url) {
21827 const a = document.createElement("a");
21828 a.href = url;
21829 a.textContent = url;
21830 a.target = "_blank";
21831 a.rel = "noopener noreferrer";
21832 wrap.appendChild(a);
21833 }
21834 return wrap;
21835 }
21836 function renderGenericPreview(file) {
21837 const wrap = articleShell();
21838 const h = document.createElement("h2");
21839 h.className = "desktop-mode-my-wordpress__article-title";
21840 h.textContent = file.title || file.type;
21841 wrap.appendChild(h);
21842 const meta = document.createElement("p");
21843 meta.className = "desktop-mode-my-wordpress__article-meta";
21844 meta.textContent = sprintf(
21845 // translators: %s is a file-type slug.
21846 __("Type: %s", "desktop-mode"),
21847 file.type
21848 );
21849 wrap.appendChild(meta);
21850 if (!file.exists) {
21851 const warn2 = document.createElement("p");
21852 warn2.className = "desktop-mode-my-wordpress__article-meta";
21853 warn2.textContent = __(
21854 "The underlying entity is no longer available.",
21855 "desktop-mode"
21856 );
21857 wrap.appendChild(warn2);
21858 }
21859 return wrap;
21860 }
21861 function articleShell(extraClass = "") {
21862 const article = document.createElement("article");
21863 article.className = "desktop-mode-my-wordpress__article" + (extraClass ? " " + extraClass : "");
21864 return article;
21865 }
21866 function statCard(value, label) {
21867 const card = document.createElement("div");
21868 card.className = "desktop-mode-my-wordpress__user-stat";
21869 const v = document.createElement("span");
21870 v.className = "desktop-mode-my-wordpress__user-stat-value";
21871 v.textContent = value;
21872 card.appendChild(v);
21873 const l = document.createElement("span");
21874 l.className = "desktop-mode-my-wordpress__user-stat-label";
21875 l.textContent = label;
21876 card.appendChild(l);
21877 return card;
21878 }
21879 function renderLoading() {
21880 const wrap = document.createElement("div");
21881 wrap.className = "desktop-mode-my-wordpress__preview-loading";
21882 const spinner = document.createElement("wpd-spinner");
21883 wrap.appendChild(spinner);
21884 return wrap;
21885 }
21886 function renderError(err) {
21887 const wrap = document.createElement("div");
21888 wrap.className = "desktop-mode-my-wordpress__error";
21889 wrap.textContent = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
21890 return wrap;
21891 }
21892 function stripTags(html2) {
21893 const div = document.createElement("div");
21894 div.innerHTML = html2;
21895 return (div.textContent ?? "").trim();
21896 }
21897 function formatDate(iso) {
21898 if (!iso) {
21899 return "";
21900 }
21901 try {
21902 return new Date(iso).toLocaleString();
21903 } catch {
21904 return iso;
21905 }
21906 }
21907 function renderPreviewEmpty() {
21908 const wrap = document.createElement("div");
21909 wrap.className = "desktop-mode-my-wordpress__preview-empty";
21910 wrap.textContent = __(
21911 "Select an item to preview it here.",
21912 "desktop-mode"
21913 );
21914 return wrap;
21915 }
21916 const ID_PREFIX = "desktop-mode-embed-";
21917 const DEFAULT_W = 800;
21918 const DEFAULT_H = 600;
21919 const MIN_W = 360;
21920 const MIN_H = 240;
21921 const PADDING = 16;
21922 const lastPersisted = /* @__PURE__ */ new Map();
21923 function openEmbedWindow(file, ctx) {
21924 const url = file.ref();
21925 if (!url) {
21926 return;
21927 }
21928 const wm = window.wp?.desktop?.windowManager;
21929 if (!wm) {
21930 return;
21931 }
21932 const placement = ctx?.placement;
21933 const meta = placement?.meta ?? null;
21934 const windowId = placement ? `${ID_PREFIX}${placement.id}` : `${ID_PREFIX}anon-${hash(url)}`;
21935 const customName = meta?.name?.trim() ?? "";
21936 const title = customName !== "" ? customName : file.title();
21937 const cfg = {
21938 id: windowId,
21939 baseId: windowId,
21940 url,
21941 title,
21942 icon: file.icon(),
21943 minWidth: MIN_W,
21944 minHeight: MIN_H
21945 };
21946 const saved = meta?.window;
21947 const area = document.getElementById("desktop-mode-area");
21948 const aw = area?.clientWidth ?? window.innerWidth;
21949 const ah = area?.clientHeight ?? window.innerHeight;
21950 if (saved && Number.isFinite(saved.width) && Number.isFinite(saved.height)) {
21951 const { x, y, width, height } = clampGeometry(saved, aw, ah);
21952 cfg.x = x;
21953 cfg.y = y;
21954 cfg.width = width;
21955 cfg.height = height;
21956 } else {
21957 cfg.width = Math.min(DEFAULT_W, Math.max(MIN_W, aw - PADDING * 2));
21958 cfg.height = Math.min(DEFAULT_H, Math.max(MIN_H, ah - PADDING * 2));
21959 }
21960 if (placement) {
21961 if (saved) {
21962 lastPersisted.set(windowId, { ...saved });
21963 }
21964 }
21965 wm.open(cfg);
21966 }
21967 let installed = false;
21968 function installEmbedPersistence() {
21969 if (installed) {
21970 return;
21971 }
21972 installed = true;
21973 const onChange = (payload) => {
21974 const p = payload;
21975 const id = p?.windowId;
21976 if (!id || !id.startsWith(ID_PREFIX)) {
21977 return;
21978 }
21979 const placementIdStr = id.slice(ID_PREFIX.length);
21980 const placementId = parseInt(placementIdStr, 10);
21981 if (!placementId) {
21982 return;
21983 }
21984 const wm = window.wp?.desktop?.windowManager;
21985 const win = wm?.getById?.(id);
21986 const el = win?.element;
21987 if (!el) {
21988 return;
21989 }
21990 const next = {
21991 x: el.offsetLeft,
21992 y: el.offsetTop,
21993 width: el.offsetWidth,
21994 height: el.offsetHeight
21995 };
21996 const prev = lastPersisted.get(id);
21997 if (prev && prev.x === next.x && prev.y === next.y && prev.width === next.width && prev.height === next.height) {
21998 return;
21999 }
22000 lastPersisted.set(id, next);
22001 void persist(placementId, next);
22002 };
22003 addAction(HOOKS.WINDOW_DRAG_END, "desktop-mode-embed-persist", onChange);
22004 addAction(HOOKS.WINDOW_RESIZE_END, "desktop-mode-embed-persist", onChange);
22005 }
22006 async function persist(placementId, geo) {
22007 try {
22008 const list2 = await listPlacements(0);
22009 const row = list2.placements.find((p) => p.id === placementId);
22010 const prevMeta = row?.meta ?? {};
22011 const nextMeta = {
22012 ...prevMeta,
22013 window: geo
22014 };
22015 await updatePlacement(placementId, { meta: nextMeta });
22016 } catch (err) {
22017 console.warn("[desktop-mode] embed window persist failed:", err);
22018 }
22019 }
22020 function clampGeometry(g, areaW, areaH) {
22021 const width = Math.max(MIN_W, Math.min(g.width, areaW - PADDING));
22022 const height = Math.max(MIN_H, Math.min(g.height, areaH - PADDING));
22023 const x = Math.max(0, Math.min(g.x, Math.max(0, areaW - width)));
22024 const y = Math.max(0, Math.min(g.y, Math.max(0, areaH - height)));
22025 return { x, y, width, height };
22026 }
22027 function hash(s) {
22028 let h = 0;
22029 for (let i = 0; i < s.length; i++) {
22030 h = (Math.imul(h, 31) + s.charCodeAt(i)) % 2147483647;
22031 }
22032 return Math.abs(h).toString(36);
22033 }
22034 function adminBase() {
22035 const cfg = window.wp?.desktop?.config;
22036 const url = cfg?.adminUrl ?? "/wp-admin/";
22037 return url.endsWith("/") ? url : `${url}/`;
22038 }
22039 function registerBuiltInFileOpeners() {
22040 registerOpener({
22041 id: "wp-post-editor",
22042 label: "Block Editor",
22043 types: ["post"],
22044 isDefault: true,
22045 sort: 10,
22046 handler: {
22047 kind: "url",
22048 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
22049 }
22050 });
22051 registerOpener({
22052 id: "wp-media-editor",
22053 label: "Media editor",
22054 types: ["attachment"],
22055 isDefault: true,
22056 sort: 10,
22057 handler: {
22058 kind: "url",
22059 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
22060 }
22061 });
22062 registerOpener({
22063 id: "wp-user-profile",
22064 label: "User profile",
22065 types: ["user"],
22066 isDefault: true,
22067 sort: 10,
22068 handler: {
22069 kind: "url",
22070 url: (file) => `${adminBase()}user-edit.php?user_id=${encodeURIComponent(file.ref())}`
22071 }
22072 });
22073 registerOpener({
22074 id: "wp-term-editor",
22075 label: "Term editor",
22076 types: ["term"],
22077 isDefault: true,
22078 sort: 10,
22079 handler: {
22080 kind: "url",
22081 url: (file) => {
22082 const [taxonomy, termId] = file.ref().split(":");
22083 return `${adminBase()}term.php?taxonomy=${encodeURIComponent(taxonomy ?? "")}&tag_ID=${encodeURIComponent(termId ?? "")}`;
22084 }
22085 }
22086 });
22087 registerOpener({
22088 id: "wp-comment-editor",
22089 label: "Comment editor",
22090 types: ["comment"],
22091 isDefault: true,
22092 sort: 10,
22093 handler: {
22094 kind: "url",
22095 url: (file) => `${adminBase()}comment.php?action=editcomment&c=${encodeURIComponent(file.ref())}`
22096 }
22097 });
22098 registerOpener({
22099 id: "desktop-mode-folder-window",
22100 label: "Open folder",
22101 types: ["folder"],
22102 isDefault: true,
22103 sort: 10,
22104 handler: {
22105 kind: "js",
22106 open: (file) => {
22107 const folderId = parseInt(file.ref(), 10);
22108 if (!folderId) {
22109 return;
22110 }
22111 const wm = window.wp?.desktop?.windowManager;
22112 if (!wm) {
22113 return;
22114 }
22115 const id = `desktop-mode-folder-${folderId}`;
22116 const folderRow = store.getState().folders.get(folderId);
22117 const viewerId2 = Number(window.desktopModeConfig?.currentUserId ?? 0);
22118 const isRecipient = !!folderRow && folderRow.ownerId > 0 && folderRow.ownerId !== viewerId2;
22119 const baseTitle = file.title();
22120 const titleWithCue = isRecipient ? `${baseTitle} · Shared` : baseTitle;
22121 wm.open({
22122 id,
22123 baseId: id,
22124 url: `#folder-${folderId}`,
22125 title: titleWithCue,
22126 icon: file.icon(),
22127 native: true,
22128 render: (body) => {
22129 body.replaceChildren();
22130 body.classList.add("desktop-mode-folder-window");
22131 const routes = [
22132 { folderId, title: file.title() }
22133 ];
22134 let currentDispose = null;
22135 const breadcrumbsHost = document.createElement("header");
22136 body.appendChild(breadcrumbsHost);
22137 const bodyHost = document.createElement("div");
22138 bodyHost.style.cssText = "flex:1 1 auto;min-height:0;display:flex;flex-direction:column;";
22139 body.appendChild(bodyHost);
22140 const paintBreadcrumbs = () => {
22141 const segments = routes.map(
22142 (route, idx) => {
22143 const isCurrent = idx === routes.length - 1;
22144 if (isCurrent) {
22145 return { label: route.title };
22146 }
22147 return {
22148 label: route.title,
22149 onClick: () => {
22150 routes.length = idx + 1;
22151 mountCurrent();
22152 }
22153 };
22154 }
22155 );
22156 renderBreadcrumbs(breadcrumbsHost, segments, {
22157 onBack: () => {
22158 if (routes.length <= 1) {
22159 return;
22160 }
22161 routes.pop();
22162 mountCurrent();
22163 },
22164 backDisabled: routes.length <= 1
22165 });
22166 };
22167 const mountCurrent = () => {
22168 currentDispose?.();
22169 currentDispose = null;
22170 bodyHost.replaceChildren();
22171 const split = document.createElement("div");
22172 split.className = "desktop-mode-folder-window__split";
22173 bodyHost.appendChild(split);
22174 const layerHost = document.createElement("div");
22175 layerHost.className = "desktop-mode-folder-window__layer";
22176 split.appendChild(layerHost);
22177 const previewPane = document.createElement("div");
22178 previewPane.className = "desktop-mode-folder-window__preview";
22179 previewPane.appendChild(renderPreviewEmpty());
22180 split.appendChild(previewPane);
22181 const route = routes[routes.length - 1];
22182 const layer = mountFilesLayer(
22183 layerHost,
22184 route.folderId
22185 );
22186 const offSelection = layer.onSelectionChange(
22187 (placement) => {
22188 if (!placement) {
22189 previewPane.replaceChildren(
22190 renderPreviewEmpty()
22191 );
22192 return;
22193 }
22194 renderPlacementPreview(
22195 placement,
22196 previewPane
22197 );
22198 }
22199 );
22200 const dblClickHandler = (e) => {
22201 if (!(e.target instanceof Element)) {
22202 return;
22203 }
22204 const tile2 = e.target.closest(
22205 ".desktop-mode-file-tile"
22206 );
22207 if (!tile2) {
22208 return;
22209 }
22210 if (tile2.dataset.fileType !== "folder") {
22211 return;
22212 }
22213 const subId = parseInt(
22214 tile2.dataset.fileRef ?? "",
22215 10
22216 );
22217 if (!subId) {
22218 return;
22219 }
22220 e.preventDefault();
22221 e.stopPropagation();
22222 const subTitle = tile2.querySelector(
22223 ".desktop-mode-file-tile__label"
22224 )?.textContent ?? `#${subId}`;
22225 routes.push({
22226 folderId: subId,
22227 title: subTitle
22228 });
22229 mountCurrent();
22230 };
22231 layerHost.addEventListener(
22232 "dblclick",
22233 dblClickHandler,
22234 true
22235 );
22236 const menu = attachIconCanvasMenu(layerHost, {
22237 scope: `desktop-mode-folder:${route.folderId}`,
22238 onSort: (mode) => layer.sort(mode),
22239 extraItems: [
22240 {
22241 id: "new-folder",
22242 label: "New folder",
22243 icon: "dashicons-portfolio",
22244 sort: 5,
22245 onClick: () => {
22246 openCreateFolderDialog({
22247 onSubmit: async (name) => {
22248 const folder = await createFolder({
22249 name
22250 });
22251 const peers = store.getState().placementsByFolder.get(
22252 route.folderId
22253 ) ?? [];
22254 const occupied = buildOccupiedSet(peers);
22255 const cell = snapToEmptyCell(
22256 GRID_PADDING,
22257 GRID_PADDING,
22258 occupied,
22259 layerHost
22260 );
22261 const placement = await createPlacement({
22262 type: "folder",
22263 ref: String(folder.id),
22264 parentId: route.folderId,
22265 x: cell.x,
22266 y: cell.y
22267 });
22268 store.upsertFolder(folder);
22269 store.upsertPlacement(
22270 placement
22271 );
22272 }
22273 });
22274 }
22275 }
22276 ]
22277 });
22278 const status = mountFolderStatusBar(
22279 bodyHost,
22280 route.folderId
22281 );
22282 currentDispose = () => {
22283 offSelection();
22284 menu.dispose();
22285 status.dispose();
22286 layerHost.removeEventListener(
22287 "dblclick",
22288 dblClickHandler,
22289 true
22290 );
22291 layer.dispose();
22292 };
22293 paintBreadcrumbs();
22294 };
22295 mountCurrent();
22296 },
22297 width: 720,
22298 height: 480,
22299 minWidth: 360,
22300 minHeight: 240
22301 });
22302 }
22303 }
22304 });
22305 registerOpener({
22306 id: "desktop-mode-shortcut-opener",
22307 label: "Open shortcut",
22308 types: ["shortcut"],
22309 isDefault: true,
22310 sort: 10,
22311 handler: {
22312 kind: "js",
22313 open: (file) => {
22314 const extras = file.shape;
22315 const wp = window.wp?.desktop;
22316 if (!wp) {
22317 return;
22318 }
22319 if (extras.shortcutWindow && wp.openWindow) {
22320 wp.openWindow(extras.shortcutWindow);
22321 return;
22322 }
22323 if (extras.shortcutUrl && wp.windowManager) {
22324 try {
22325 const u = new URL(extras.shortcutUrl, window.location.origin);
22326 if (u.origin !== window.location.origin) {
22327 window.open(u.toString(), "_blank", "noopener,noreferrer");
22328 return;
22329 }
22330 const id = `desktop-icon-${file.ref()}`;
22331 wp.windowManager.open({
22332 id,
22333 baseId: id,
22334 url: u.toString(),
22335 title: file.title(),
22336 icon: file.icon()
22337 });
22338 } catch {
22339 }
22340 }
22341 }
22342 }
22343 });
22344 registerOpener({
22345 id: "browser-navigate",
22346 label: "Open in browser",
22347 types: ["bookmark"],
22348 isDefault: true,
22349 sort: 10,
22350 handler: {
22351 kind: "js",
22352 open: (file) => {
22353 const url = file.ref();
22354 if (!url) {
22355 return;
22356 }
22357 window.open(url, "_blank", "noopener,noreferrer");
22358 }
22359 }
22360 });
22361 registerOpener({
22362 id: "desktop-mode-link-opener",
22363 label: "Open in browser",
22364 types: ["link"],
22365 isDefault: true,
22366 sort: 10,
22367 handler: {
22368 kind: "js",
22369 open: (file) => {
22370 const url = file.ref();
22371 if (!url) {
22372 return;
22373 }
22374 window.open(url, "_blank", "noopener,noreferrer");
22375 }
22376 }
22377 });
22378 registerOpener({
22379 id: "desktop-mode-embed-opener",
22380 label: "Open as window",
22381 types: ["embed"],
22382 isDefault: true,
22383 sort: 10,
22384 handler: {
22385 kind: "js",
22386 open: (file, ctx) => {
22387 openEmbedWindow(file, ctx);
22388 }
22389 }
22390 });
22391 }
22392 const TAB_ID = "desktop-mode-file-associations";
22393 function registerFileAssociationsTab() {
22394 registerSettingsTab({
22395 id: TAB_ID,
22396 label: "File Associations",
22397 order: 50,
22398 render(body) {
22399 renderTab(body);
22400 }
22401 });
22402 }
22403 function renderTab(body) {
22404 body.replaceChildren();
22405 const types = getTypes();
22406 if (types.length === 0) {
22407 const empty = document.createElement("p");
22408 empty.className = "desktop-mode-file-associations__empty";
22409 empty.textContent = "No file types are registered.";
22410 body.appendChild(empty);
22411 return;
22412 }
22413 const intro = document.createElement("p");
22414 intro.className = "desktop-mode-file-associations__intro";
22415 intro.textContent = "Pick which app opens each kind of file when you double-click it on the desktop.";
22416 body.appendChild(intro);
22417 const associations = getUserAssociations();
22418 const list2 = document.createElement("div");
22419 list2.className = "desktop-mode-file-associations__list";
22420 list2.setAttribute("role", "list");
22421 for (const type of types) {
22422 list2.appendChild(buildRow(type.type, type.label, associations));
22423 }
22424 body.appendChild(list2);
22425 }
22426 function buildRow(typeSlug, typeLabel, associations) {
22427 const row = document.createElement("div");
22428 row.className = "desktop-mode-file-associations__row";
22429 row.setAttribute("role", "listitem");
22430 row.dataset.fileType = typeSlug;
22431 const label = document.createElement("label");
22432 label.className = "desktop-mode-file-associations__label";
22433 label.textContent = typeLabel;
22434 row.appendChild(label);
22435 const candidates = getOpenersForType(typeSlug);
22436 if (candidates.length === 0) {
22437 const empty = document.createElement("span");
22438 empty.className = "desktop-mode-file-associations__none";
22439 empty.textContent = "No app available";
22440 row.appendChild(empty);
22441 return row;
22442 }
22443 const resolved = resolveOpener(typeSlug);
22444 const currentId = associations[typeSlug] ?? resolved?.id ?? "";
22445 const select = document.createElement("wpd-select");
22446 select.setAttribute("value", currentId);
22447 select.setAttribute("aria-label", `Default app for ${typeLabel}`);
22448 select.className = "desktop-mode-file-associations__select";
22449 label.htmlFor = `assoc-${typeSlug}`;
22450 select.id = `assoc-${typeSlug}`;
22451 for (const o of candidates) {
22452 const opt = document.createElement("wpd-option");
22453 opt.setAttribute("value", o.id);
22454 opt.textContent = o.isDefault ? `${o.label} (default)` : o.label;
22455 select.appendChild(opt);
22456 }
22457 select.addEventListener("wpd-pick", (e) => {
22458 const next = e.detail?.value;
22459 if (!next) {
22460 return;
22461 }
22462 const merged = { ...getUserAssociations(), [typeSlug]: next };
22463 setUserAssociations(merged);
22464 void saveAssociations(merged).catch((err) => {
22465 console.error("[desktop-mode] saveAssociations failed:", err);
22466 });
22467 });
22468 row.appendChild(select);
22469 return row;
22470 }
22471 let _store$1 = null;
22472 function sharesStore() {
22473 if (!_store$1) {
22474 _store$1 = createSharedStore("desktop-files/shares", () => ({
22475 byFolder: /* @__PURE__ */ new Map(),
22476 pending: [],
22477 sharesVersion: 0,
22478 deniedFolders: /* @__PURE__ */ new Set()
22479 }));
22480 }
22481 return _store$1;
22482 }
22483 function setSharesForFolder(folderId, shares) {
22484 const s = sharesStore();
22485 s.state.byFolder.set(folderId, shares);
22486 s.notify();
22487 }
22488 function upsertShare(share) {
22489 if (!share || typeof share.folderId !== "number") {
22490 return;
22491 }
22492 const s = sharesStore();
22493 const existing = s.state.byFolder.get(share.folderId) ?? [];
22494 const next = existing.filter((r) => r.id !== share.id);
22495 next.push(share);
22496 s.state.byFolder.set(share.folderId, next);
22497 s.notify();
22498 }
22499 function removeShare(folderId, shareId) {
22500 const s = sharesStore();
22501 const existing = s.state.byFolder.get(folderId) ?? [];
22502 s.state.byFolder.set(
22503 folderId,
22504 existing.filter((r) => r.id !== shareId)
22505 );
22506 s.notify();
22507 }
22508 function inviteEquals(a, b) {
22509 return a.id === b.id && a.folderId === b.folderId && a.capability === b.capability && a.invitedAtMs === b.invitedAtMs && a.folderName === b.folderName && a.ownerName === b.ownerName;
22510 }
22511 function ingestPendingInvites(invites) {
22512 const s = sharesStore();
22513 const existingById = new Map(s.state.pending.map((p) => [p.id, p]));
22514 let mutated = false;
22515 for (const inv of invites) {
22516 if (s.state.deniedFolders.has(inv.folderId)) {
22517 continue;
22518 }
22519 const existing = existingById.get(inv.id);
22520 if (existing) {
22521 if (inviteEquals(existing, inv)) {
22522 continue;
22523 }
22524 s.state.pending = s.state.pending.map((p) => p.id === inv.id ? inv : p);
22525 } else {
22526 s.state.pending.push(inv);
22527 }
22528 if (inv.invitedAtMs > s.state.sharesVersion) {
22529 s.state.sharesVersion = inv.invitedAtMs;
22530 }
22531 mutated = true;
22532 }
22533 if (mutated) {
22534 s.notify();
22535 }
22536 }
22537 function dropPending(shareId, opts = {}) {
22538 const s = sharesStore();
22539 s.state.pending = s.state.pending.filter((p) => p.id !== shareId);
22540 if (opts.denied && typeof opts.folderId === "number") {
22541 s.state.deniedFolders.add(opts.folderId);
22542 }
22543 s.notify();
22544 }
22545 const modalStyles = css`:host{display:none;position:fixed;inset:0;align-items:center;justify-content:center;background:rgba( 0,0,0,0.45 );backdrop-filter:blur( 2px );z-index:10000}:host( [ open ] ){display:flex}.dialog{max-width:92vw;max-height:90vh;background:var( --wpd-modal-bg,var( --desktop-mode-bg,#1d2327 ) );color:var( --wpd-modal-fg,var( --desktop-mode-fg,#fff ) );border:1px solid rgba( 255,255,255,0.08 );border-radius:10px;box-shadow:0 20px 50px rgba( 0,0,0,0.6 );display:flex;flex-direction:column;overflow:hidden}:host( [ size='sm' ] ) .dialog{width:min( 360px,92vw )}:host(:not( [ size ] ) ) .dialog,:host( [ size='md' ] ) .dialog{width:min( 540px,92vw )}:host( [ size='lg' ] ) .dialog{width:min( 760px,94vw )}.header{display:flex;align-items:center;gap:10px;padding:16px 20px 12px;border-bottom:1px solid rgba( 255,255,255,0.06 )}.title{margin:0;flex:1;font-size:15px;font-weight:600}.header-actions{display:flex;gap:6px}.header-actions::slotted( * ){margin-inline-start:6px}.close{background:transparent;border:0;color:inherit;font-size:18px;line-height:1;padding:4px 8px;border-radius:4px;cursor:pointer;opacity:0.7}.close:hover{opacity:1;background:rgba( 255,255,255,0.08 )}.body{padding:16px 20px;overflow:auto;flex:1 1 auto;font-size:13px;line-height:1.5}.footer{padding:12px 20px 16px;border-top:1px solid rgba( 255,255,255,0.06 )}.footer slot{display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap}:host( [ mandatory ] ) .close{display:none}`;
22546 const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
22547 const _WpdModal = class _WpdModal extends Component {
22548 constructor() {
22549 super(...arguments);
22550 this._prevFocus = null;
22551 this._onKey = (e) => {
22552 if (e.key === "Escape" && !this.hasAttribute("mandatory")) {
22553 e.preventDefault();
22554 this._cancel();
22555 return;
22556 }
22557 if (e.key === "Tab") {
22558 const f = this._focusables();
22559 if (f.length === 0) {
22560 return;
22561 }
22562 const first = f[0];
22563 const last = f[f.length - 1];
22564 const doc = this.ownerDocument;
22565 const fallback = doc ? doc.activeElement : null;
22566 const active2 = e.composedPath()[0] || fallback;
22567 if (e.shiftKey && active2 === first) {
22568 e.preventDefault();
22569 last.focus();
22570 } else if (!e.shiftKey && active2 === last) {
22571 e.preventDefault();
22572 first.focus();
22573 }
22574 }
22575 };
22576 this._onBackdrop = (e) => {
22577 if (this.hasAttribute("mandatory")) {
22578 return;
22579 }
22580 const path = e.composedPath();
22581 const original = path.length > 0 ? path[0] : e.target;
22582 if (original === this) {
22583 this._cancel();
22584 }
22585 };
22586 }
22587 connectedCallback() {
22588 super.connectedCallback();
22589 this.setAttribute("role", "dialog");
22590 this.setAttribute("aria-modal", "true");
22591 this.addEventListener("keydown", this._onKey);
22592 this.addEventListener("click", this._onBackdrop);
22593 }
22594 disconnectedCallback() {
22595 this.removeEventListener("keydown", this._onKey);
22596 this.removeEventListener("click", this._onBackdrop);
22597 }
22598 attributeChangedCallback(name, oldValue, newValue) {
22599 super.attributeChangedCallback?.(name, oldValue, newValue);
22600 if (name === "open") {
22601 if (newValue !== null) {
22602 const doc = this.ownerDocument;
22603 this._prevFocus = doc ? doc.activeElement : null;
22604 queueMicrotask(() => this._focusFirst());
22605 } else if (this._prevFocus) {
22606 try {
22607 this._prevFocus.focus();
22608 } catch (e) {
22609 }
22610 this._prevFocus = null;
22611 }
22612 }
22613 }
22614 showModal() {
22615 this.setAttribute("open", "");
22616 }
22617 hideModal() {
22618 this.removeAttribute("open");
22619 }
22620 _focusables() {
22621 const root = this.shadowRoot;
22622 if (!root) {
22623 return [];
22624 }
22625 const slotted = Array.from(this.querySelectorAll(FOCUSABLE));
22626 const inShadow = Array.from(root.querySelectorAll(FOCUSABLE));
22627 return [...slotted, ...inShadow].filter((el) => el.offsetParent !== null || el.tagName === "BUTTON");
22628 }
22629 _focusFirst() {
22630 const f = this._focusables();
22631 if (f.length > 0) {
22632 f[0].focus();
22633 } else {
22634 const inner = this.shadowRoot?.querySelector(".dialog");
22635 inner?.focus?.();
22636 }
22637 }
22638 _cancel() {
22639 const ev = new CustomEvent("wpd-modal-cancel", {
22640 bubbles: true,
22641 cancelable: true,
22642 composed: true
22643 });
22644 const allowed = this.dispatchEvent(ev);
22645 if (allowed) {
22646 this.hideModal();
22647 }
22648 }
22649 render() {
22650 const title = this.getAttribute("title") ?? "";
22651 const mandatory = this.hasAttribute("mandatory");
22652 return html`
22653 <div class="dialog" tabindex="-1">
22654 ${title ? html`
22655 <div class="header">
22656 <h2 class="title">${title}</h2>
22657 <div class="header-actions">
22658 <slot name="header-actions"></slot>
22659 ${mandatory ? html`` : html`<button
22660 type="button"
22661 class="close"
22662 aria-label="Close"
22663 @click=${() => this._cancel()}
22664 >×</button>`}
22665 </div>
22666 </div>
22667 ` : html``}
22668 <div class="body">
22669 <slot></slot>
22670 </div>
22671 <div class="footer">
22672 <slot name="footer"></slot>
22673 </div>
22674 </div>
22675 `;
22676 }
22677 };
22678 _WpdModal.props = ["open", "title", "size", "mandatory"];
22679 _WpdModal.styles = [modalStyles];
22680 _WpdModal.help = {
22681 title: "Modal overlay",
22682 summary: "Overlay container with title, body, and footer slots. Handles ESC, click-outside, focus trap. Use for rich modal flows that go beyond a yes/no confirm.",
22683 status: "experimental",
22684 since: "0.18.0",
22685 props: [
22686 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
22687 { name: "title", type: "string", description: "Heading shown at the top of the dialog." },
22688 { name: "size", type: "'sm' | 'md' | 'lg'", default: "md", description: "Width preset." },
22689 {
22690 name: "mandatory",
22691 type: "boolean attribute",
22692 description: "Disables ESC, click-outside and the close button."
22693 }
22694 ],
22695 slots: [
22696 { name: "(default)", description: "Body content." },
22697 { name: "footer", description: "Footer button row, right-aligned." },
22698 { name: "header-actions", description: "Extra actions next to the close button." }
22699 ],
22700 events: [
22701 {
22702 name: "wpd-modal-cancel",
22703 description: "Fires when the user dismisses the modal (ESC, click-outside, close button). Cancelable; calling `preventDefault()` keeps the modal open."
22704 }
22705 ]
22706 };
22707 let WpdModal = _WpdModal;
22708 defineComponent("wpd-modal", WpdModal);
22709 const userSearchStyles = css`:host{display:block;position:relative;font-size:13px}.input{width:100%;padding:8px 10px;background:var( --wpd-input-bg,rgba( 255,255,255,0.06 ) );color:inherit;border:1px solid rgba( 255,255,255,0.12 );border-radius:6px;font:inherit;box-sizing:border-box}.input:focus{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:-1px}.dropdown{background:var( --desktop-mode-bg,#1d2327 );color:var( --desktop-mode-fg,#fff );border:1px solid rgba( 255,255,255,0.18 );border-radius:6px;overflow:auto;z-index:11000;box-shadow:0 12px 32px rgba( 0,0,0,0.5 )}.empty.error{color:#ff8080}.item{display:flex;align-items:center;gap:10px;padding:8px 10px;cursor:pointer;border:0;background:transparent;color:inherit;width:100%;text-align:start;font:inherit}.item:hover,.item:focus{background:rgba( 255,255,255,0.06 );outline:none}.avatar{width:24px;height:24px;border-radius:50%;flex:0 0 auto;background:rgba( 255,255,255,0.1 )}.name{font-weight:500}.slug{opacity:0.6;font-size:12px}.empty{padding:12px;color:rgba( 255,255,255,0.5 );font-size:12px}`;
22710 const _WpdUserSearch = class _WpdUserSearch extends Component {
22711 constructor() {
22712 super(...arguments);
22713 this._timer = null;
22714 this._abort = null;
22715 this._results = [];
22716 this._query = "";
22717 this._open = false;
22718 this._phase = "idle";
22719 this._error = "";
22720 this._dropdownStyle = "";
22721 this._onScrollOrResize = () => void 0;
22722 this._onInput = (e) => {
22723 const value = e.target.value;
22724 this._query = value;
22725 this._scheduleSearch(value);
22726 };
22727 this._onFocus = () => {
22728 if (this._results.length === 0 && this._phase === "idle") {
22729 this._scheduleSearch(this._query);
22730 return;
22731 }
22732 this._open = true;
22733 this._positionDropdown();
22734 this.requestUpdate();
22735 };
22736 this._onBlur = () => {
22737 setTimeout(() => {
22738 this._open = false;
22739 this.requestUpdate();
22740 }, 150);
22741 };
22742 this._pick = (user) => {
22743 this.emit("wpd-user-pick", { user });
22744 this._results = [];
22745 this._open = false;
22746 this._phase = "idle";
22747 this._query = "";
22748 const input = this.shadowRoot?.querySelector(".input");
22749 if (input) {
22750 input.value = "";
22751 }
22752 this.requestUpdate();
22753 };
22754 }
22755 connectedCallback() {
22756 super.connectedCallback();
22757 this._onScrollOrResize = () => {
22758 if (this._open) {
22759 this._positionDropdown();
22760 this.requestUpdate();
22761 }
22762 };
22763 window.addEventListener("resize", this._onScrollOrResize);
22764 window.addEventListener("scroll", this._onScrollOrResize, true);
22765 }
22766 disconnectedCallback() {
22767 if (this._timer) {
22768 clearTimeout(this._timer);
22769 }
22770 if (this._abort) {
22771 this._abort.abort();
22772 }
22773 window.removeEventListener("resize", this._onScrollOrResize);
22774 window.removeEventListener("scroll", this._onScrollOrResize, true);
22775 }
22776 _endpoint() {
22777 const attr = this.getAttribute("endpoint");
22778 if (attr) {
22779 return attr;
22780 }
22781 return window.desktopModeConfig?.filesUsersSearchUrl || "";
22782 }
22783 _scheduleSearch(q) {
22784 if (this._timer) {
22785 clearTimeout(this._timer);
22786 }
22787 this._phase = "loading";
22788 this._open = true;
22789 this._positionDropdown();
22790 this.requestUpdate();
22791 this._timer = setTimeout(() => this._runSearch(q), 200);
22792 }
22793 async _runSearch(q) {
22794 const url = this._endpoint();
22795 if (!url) {
22796 this._phase = "error";
22797 this._error = "Search endpoint is not configured.";
22798 this._results = [];
22799 this._open = true;
22800 this.requestUpdate();
22801 return;
22802 }
22803 if (this._abort) {
22804 this._abort.abort();
22805 }
22806 const ctrl = new AbortController();
22807 this._abort = ctrl;
22808 const exclude = this.getAttribute("exclude") || "";
22809 const full = url + "?q=" + encodeURIComponent(q) + "&exclude=" + encodeURIComponent(exclude);
22810 try {
22811 const init2 = {
22812 signal: ctrl.signal,
22813 credentials: "same-origin"
22814 };
22815 const res = await trackedFetch$1(full, init2, {
22816 source: "desktop-mode/files-user-search",
22817 silent: true
22818 });
22819 if (!res.ok) {
22820 throw new Error(`HTTP ${res.status}`);
22821 }
22822 const json = await res.json();
22823 this._results = json && Array.isArray(json.users) ? json.users : [];
22824 this._phase = "ready";
22825 this._error = "";
22826 this._open = true;
22827 } catch (e) {
22828 if (e.name === "AbortError") {
22829 return;
22830 }
22831 this._results = [];
22832 this._phase = "error";
22833 this._error = e.message || "Search failed.";
22834 this._open = true;
22835 }
22836 this._positionDropdown();
22837 this.requestUpdate();
22838 }
22839 _positionDropdown() {
22840 const input = this.shadowRoot?.querySelector(".input");
22841 if (!input) {
22842 return;
22843 }
22844 const rect = input.getBoundingClientRect();
22845 const top = rect.bottom + 4;
22846 const left = rect.left;
22847 const width = rect.width;
22848 const viewportH = window.innerHeight;
22849 const spaceBelow = viewportH - rect.bottom;
22850 const spaceAbove = rect.top;
22851 const maxHeight = Math.max(120, Math.min(280, Math.max(spaceBelow, spaceAbove) - 16));
22852 if (spaceBelow < 200 && spaceAbove > spaceBelow) {
22853 this._dropdownStyle = [
22854 "position:fixed",
22855 `left:${left}px`,
22856 `top:${rect.top - 4 - maxHeight}px`,
22857 `width:${width}px`,
22858 `max-height:${maxHeight}px`
22859 ].join(";");
22860 } else {
22861 this._dropdownStyle = [
22862 "position:fixed",
22863 `left:${left}px`,
22864 `top:${top}px`,
22865 `width:${width}px`,
22866 `max-height:${maxHeight}px`
22867 ].join(";");
22868 }
22869 }
22870 _dropdownContent() {
22871 if (this._phase === "loading") {
22872 return html`<div class="empty">Searching…</div>`;
22873 }
22874 if (this._phase === "error") {
22875 return html`<div class="empty error">${this._error}</div>`;
22876 }
22877 if (this._results.length === 0) {
22878 const message = this._query ? "No matches." : "No users available.";
22879 return html`<div class="empty">${message}</div>`;
22880 }
22881 return this._results.map(
22882 (u) => html`
22883 <button
22884 type="button"
22885 class="item"
22886 role="option"
22887 @mousedown=${(e) => e.preventDefault()}
22888 @click=${() => this._pick(u)}
22889 >
22890 <img class="avatar" src=${u.avatarUrl} alt="" />
22891 <div>
22892 <div class="name">${u.name}</div>
22893 <div class="slug">${u.slug}</div>
22894 </div>
22895 </button>
22896 `
22897 );
22898 }
22899 render() {
22900 const placeholder = this.getAttribute("placeholder") || "Search users…";
22901 return html`
22902 <input
22903 class="input"
22904 type="search"
22905 placeholder=${placeholder}
22906 autocomplete="off"
22907 @input=${this._onInput}
22908 @focus=${this._onFocus}
22909 @blur=${this._onBlur}
22910 .value=${this._query}
22911 />
22912 ${this._open ? html`
22913 <div class="dropdown" role="listbox" style=${this._dropdownStyle}>
22914 ${this._dropdownContent()}
22915 </div>
22916 ` : html``}
22917 `;
22918 }
22919 };
22920 _WpdUserSearch.props = ["placeholder", "exclude", "endpoint"];
22921 _WpdUserSearch.styles = [userSearchStyles];
22922 _WpdUserSearch.help = {
22923 title: "User autocomplete",
22924 summary: "Debounced autocomplete over /desktop-mode/v1/files/users/search. Emits wpd-user-pick { user } when a row is chosen. Dropdown anchors as position: fixed so it escapes overflow:auto ancestors.",
22925 status: "experimental",
22926 since: "0.18.0",
22927 props: [
22928 { name: "placeholder", type: "string", description: "Input placeholder text." },
22929 {
22930 name: "exclude",
22931 type: "csv user ids",
22932 description: "Already-picked user ids to suppress in results."
22933 },
22934 {
22935 name: "endpoint",
22936 type: "URL",
22937 description: "Override the search URL (defaults to desktopModeConfig.filesUsersSearchUrl)."
22938 }
22939 ],
22940 events: [
22941 { name: "wpd-user-pick", description: "Emitted on pick. Detail: `{ user: SearchUser }`." }
22942 ]
22943 };
22944 let WpdUserSearch = _WpdUserSearch;
22945 defineComponent("wpd-user-search", WpdUserSearch);
22946 const rolePickerStyles = css`:host{display:flex;flex-wrap:wrap;gap:6px;font-size:13px}.chip{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:rgba( 255,255,255,0.06 );color:inherit;border:1px solid rgba( 255,255,255,0.12 );cursor:pointer;font:inherit}.chip:hover{background:rgba( 255,255,255,0.12 )}.chip[ aria-pressed='true' ]{background:var( --wp-admin-theme-color,#2271b1 );border-color:var( --wp-admin-theme-color,#2271b1 );color:#fff}.empty{color:rgba( 255,255,255,0.5 );font-size:12px}`;
22947 const _WpdRolePicker = class _WpdRolePicker extends Component {
22948 constructor() {
22949 super(...arguments);
22950 this._onToggle = (slug) => {
22951 const selected = !this._selectedSet().has(slug);
22952 this.emit("wpd-role-toggle", { slug, selected });
22953 };
22954 }
22955 _selectedSet() {
22956 const raw = this.getAttribute("selected") || "";
22957 return new Set(
22958 raw.split(",").map((s) => s.trim()).filter((s) => s !== "")
22959 );
22960 }
22961 _roles() {
22962 const attr = this.getAttribute("roles");
22963 if (attr) {
22964 try {
22965 const parsed = JSON.parse(attr);
22966 if (Array.isArray(parsed)) {
22967 return parsed;
22968 }
22969 } catch (e) {
22970 }
22971 }
22972 return window.desktopModeConfig?.shareEligibleRoles || [];
22973 }
22974 render() {
22975 const roles = this._roles();
22976 if (roles.length === 0) {
22977 return html`<span class="empty">No eligible roles.</span>`;
22978 }
22979 const set = this._selectedSet();
22980 return html`
22981 ${roles.map((r) => {
22982 const isSelected = set.has(r.slug);
22983 return html`
22984 <button
22985 type="button"
22986 class="chip"
22987 aria-pressed=${isSelected ? "true" : "false"}
22988 @click=${() => this._onToggle(r.slug)}
22989 >${r.name}</button>
22990 `;
22991 })}
22992 `;
22993 }
22994 };
22995 _WpdRolePicker.props = ["selected", "roles"];
22996 _WpdRolePicker.styles = [rolePickerStyles];
22997 _WpdRolePicker.help = {
22998 title: "Role picker",
22999 summary: "Chip multi-select for WordPress roles. Reads eligible roles from desktopModeConfig.shareEligibleRoles; emits wpd-role-toggle { slug, selected } on every change.",
23000 status: "experimental",
23001 since: "0.18.0",
23002 props: [
23003 {
23004 name: "selected",
23005 type: "csv role slugs",
23006 description: "Comma-separated role slugs that are currently selected."
23007 },
23008 {
23009 name: "roles",
23010 type: "JSON",
23011 description: "Override the source of eligible roles (defaults to the global config)."
23012 }
23013 ],
23014 events: [
23015 {
23016 name: "wpd-role-toggle",
23017 description: "Emitted on every click. Detail: `{ slug, selected }`."
23018 }
23019 ]
23020 };
23021 let WpdRolePicker = _WpdRolePicker;
23022 defineComponent("wpd-role-picker", WpdRolePicker);
23023 const segmentedStyles = css`:host{display:inline-flex;padding:3px;background:var( --wpd-segmented-bg,rgba( 0,0,0,0.05 ) );border-radius:7px;gap:2px}`;
23024 const segmentStyles = css`:host{flex:1 1 auto;min-width:0}button{appearance:none;display:block;width:100%;padding:8px 12px;background:transparent;border:0;font:inherit;font-size:13px;color:var( --desktop-mode-muted,#646970 );cursor:pointer;border-radius:5px;transition:background-color 0.12s ease,color 0.12s ease;white-space:nowrap}:host( [ aria-checked='true' ] ) button{background:var( --desktop-mode-window-bg,#fff );color:var( --desktop-mode-text,#1d2327 );box-shadow:0 1px 3px rgba( 0,0,0,0.12 );font-weight:500}`;
23025 const _WpdSegment = class _WpdSegment extends Component {
23026 render() {
23027 this.setAttribute("role", "radio");
23028 return html`
23029 <button type="button" @click=${() => this._onPick()}>
23030 <slot></slot>
23031 </button>
23032 `;
23033 }
23034 _onPick() {
23035 this.emit("wpd-segment-pick", {
23036 value: this.value
23037 });
23038 }
23039 };
23040 _WpdSegment.props = ["value"];
23041 _WpdSegment.styles = [segmentStyles];
23042 _WpdSegment.help = {
23043 title: "Segment",
23044 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
23045 status: "stable",
23046 since: "0.9.0",
23047 props: [
23048 {
23049 name: "value",
23050 type: "string",
23051 description: "Identifier this segment contributes to the parent group selection."
23052 }
23053 ],
23054 slots: [
23055 { name: "(default)", description: "Visible segment label." }
23056 ],
23057 events: [
23058 {
23059 name: "wpd-segment-pick",
23060 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
23061 detail: "{ value: string }"
23062 }
23063 ]
23064 };
23065 let WpdSegment = _WpdSegment;
23066 defineComponent("wpd-segment", WpdSegment);
23067 const _WpdSegmented = class _WpdSegmented extends Component {
23068 connectedCallback() {
23069 super.connectedCallback();
23070 this.addEventListener("wpd-segment-pick", (e) => {
23071 const detail = e.detail;
23072 e.stopPropagation();
23073 this.value = detail.value;
23074 this.emit("wpd-pick", { value: detail.value });
23075 });
23076 }
23077 /**
23078 * Declarative item-list setter. Replaces the existing
23079 * `<wpd-segment>` children with a fresh set built from a
23080 * `{ value, label }` array; preserves the current selection
23081 * when the value still matches an entry, otherwise falls back
23082 * to the first item.
23083 *
23084 * Collapses the pre-0.11 imperative dance (clear children,
23085 * `createElement`, set `textContent`, `appendChild`, then
23086 * `setAttribute('value', …)` on the group — order matters) to
23087 * a single assignment:
23088 *
23089 * ```js
23090 * segmented.items = [
23091 * { value: 'm', label: 'm' },
23092 * { value: 'km', label: 'km' },
23093 * ];
23094 * ```
23095 *
23096 * @since 0.11.0
23097 */
23098 set items(list2) {
23099 const existing = this.querySelectorAll(":scope > wpd-segment");
23100 for (const el of Array.from(existing)) {
23101 el.remove();
23102 }
23103 for (const item of list2) {
23104 const seg = document.createElement("wpd-segment");
23105 seg.setAttribute("value", item.value);
23106 seg.textContent = item.label;
23107 this.appendChild(seg);
23108 }
23109 const current = this.value;
23110 const stillValid = current !== null && list2.some((i) => i.value === current);
23111 if (!stillValid && list2.length > 0) {
23112 this.value = list2[0].value;
23113 } else {
23114 this.requestUpdate();
23115 }
23116 }
23117 render() {
23118 const label = this.label || "";
23119 if (label) {
23120 this.setAttribute("aria-label", label);
23121 }
23122 this.setAttribute("role", "radiogroup");
23123 const current = this.value;
23124 queueMicrotask(() => {
23125 const segs = this.querySelectorAll("wpd-segment");
23126 for (const seg of Array.from(segs)) {
23127 const v = seg.getAttribute("value");
23128 seg.setAttribute(
23129 "aria-checked",
23130 v === current ? "true" : "false"
23131 );
23132 }
23133 });
23134 return html`<slot></slot>`;
23135 }
23136 };
23137 _WpdSegmented.props = ["value", "label"];
23138 _WpdSegmented.styles = [segmentedStyles];
23139 _WpdSegmented.help = {
23140 title: "Segmented",
23141 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
23142 status: "stable",
23143 since: "0.9.0",
23144 props: [
23145 {
23146 name: "value",
23147 type: "string",
23148 description: "Currently selected segment value. Mirrored onto child aria-checked."
23149 },
23150 {
23151 name: "label",
23152 type: "string",
23153 description: "aria-label for the radiogroup."
23154 }
23155 ],
23156 slots: [
23157 { name: "(default)", description: '<wpd-segment value="…"> children.' }
23158 ],
23159 events: [
23160 {
23161 name: "wpd-pick",
23162 description: "Fires when the selected segment changes.",
23163 detail: "{ value: string }"
23164 }
23165 ],
23166 cssProps: [
23167 { name: "--desktop-mode-window-bg", description: "Pill background." },
23168 { name: "--desktop-mode-text", description: "Active label colour." },
23169 { name: "--desktop-mode-muted", description: "Inactive label colour." }
23170 ],
23171 example: html`
23172 <wpd-segmented value="md" label="Dock size">
23173 <wpd-segment value="sm">Small</wpd-segment>
23174 <wpd-segment value="md">Medium</wpd-segment>
23175 <wpd-segment value="lg">Large</wpd-segment>
23176 </wpd-segmented>
23177 `
23178 };
23179 let WpdSegmented = _WpdSegmented;
23180 defineComponent("wpd-segmented", WpdSegmented);
23181 const styles$1 = css`:host{display:inline-flex}:host( [ fill-cell ] ){display:flex;width:100%}button{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:var( --wpd-button-padding,6px 12px );border-radius:var( --wpd-button-border-radius,6px );font:inherit;font-weight:500;cursor:pointer;transition:background-color 0.12s ease,color 0.12s ease,border-color 0.12s ease;background:var( --wpd-button-bg,transparent );color:var( --wpd-button-fg,var( --desktop-mode-text,#1d2327 ) );border:var( --wpd-button-border,1px solid var( --desktop-mode-border,#c3c4c7 ) )}:host( [ fill-cell ] ) button{width:100%;min-height:var( --wpd-button-min-height,44px )}button:disabled{opacity:0.5;cursor:not-allowed}button:hover:not(:disabled ){background:rgba( 0,0,0,0.04 )}:host( [ variant='primary' ] ) button{background:var( --wpd-button-bg,var( --wp-admin-theme-color,#2271b1 ) );color:var( --wpd-button-fg,#fff );border:var( --wpd-button-border,1px solid transparent )}:host( [ variant='primary' ] ) button:hover:not(:disabled ){filter:brightness( 1.06 );background:var( --wpd-button-bg,var( --wp-admin-theme-color,#2271b1 ) )}:host( [ variant='secondary' ] ) button{background:var( --wpd-button-bg,rgba( 0,0,0,0.06 ) );color:var( --wpd-button-fg,var( --desktop-mode-text,#1d2327 ) );border:var( --wpd-button-border,1px solid transparent )}:host( [ variant='secondary' ] ) button:hover:not(:disabled ){background:var( --wpd-button-bg-hover,rgba( 0,0,0,0.1 ) )}:host( [ variant='danger' ] ) button{background:var( --wpd-button-bg,transparent );color:var( --wpd-button-fg,#d63638 );border:var( --wpd-button-border,1px solid currentColor )}:host( [ variant='danger' ] ) button:hover:not(:disabled ){background:#d63638;color:#fff}:host( [ variant='link' ] ) button{background:transparent;color:var( --wpd-button-fg,var( --wp-admin-theme-color,#2271b1 ) );border:0;padding:0;text-decoration:underline}:host( [ busy ] ) button{pointer-events:none;opacity:0.75}`;
23182 const _WpdButton = class _WpdButton extends Component {
23183 render() {
23184 const disabled = this.disabled !== null;
23185 const type = this.type || "button";
23186 return html`
23187 <button part="button" type=${type} ?disabled=${disabled}>
23188 <slot></slot>
23189 </button>
23190 `;
23191 }
23192 };
23193 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
23194 _WpdButton.styles = [styles$1];
23195 _WpdButton.help = {
23196 title: "Button",
23197 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
23198 status: "stable",
23199 since: "0.9.0",
23200 props: [
23201 {
23202 name: "variant",
23203 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
23204 default: "ghost",
23205 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
23206 },
23207 {
23208 name: "disabled",
23209 type: "boolean attribute",
23210 description: "Disable pointer + keyboard interaction and dim the chrome."
23211 },
23212 {
23213 name: "type",
23214 type: "'button' | 'submit' | 'reset'",
23215 default: "button",
23216 description: "Forwarded to the underlying native <button>."
23217 },
23218 {
23219 name: "busy",
23220 type: "boolean attribute",
23221 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
23222 },
23223 {
23224 name: "fill-cell",
23225 type: "boolean attribute",
23226 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
23227 }
23228 ],
23229 slots: [{ name: "(default)", description: "Button label." }],
23230 parts: [{ name: "button", description: "Underlying <button> element." }],
23231 cssProps: [
23232 { name: "--wpd-button-bg", description: "Background color." },
23233 { name: "--wpd-button-fg", description: "Text color." },
23234 { name: "--wpd-button-border", description: "Border shorthand." },
23235 { name: "--wpd-button-border-radius", default: "6px" },
23236 { name: "--wpd-button-padding", default: "6px 12px" },
23237 {
23238 name: "--wpd-button-min-height",
23239 description: "Minimum height when fill-cell is set."
23240 }
23241 ],
23242 example: html`
23243 <wpd-cluster gap="8">
23244 <wpd-button variant="primary">Primary</wpd-button>
23245 <wpd-button variant="secondary">Secondary</wpd-button>
23246 <wpd-button variant="ghost">Ghost</wpd-button>
23247 <wpd-button variant="danger">Danger</wpd-button>
23248 <wpd-button variant="link">Link</wpd-button>
23249 </wpd-cluster>
23250 `
23251 };
23252 let WpdButton = _WpdButton;
23253 defineComponent("wpd-button", WpdButton);
23254 const containerStyles = css`:host{position:fixed;top:calc( var( --wp-admin--admin-bar--height,32px ) + 16px );inset-inline-end:16px;display:flex;flex-direction:column;gap:8px;z-index:calc( var( --desktop-mode-z-fullscreen,99999 ) + 10 );pointer-events:none}`;
23255 const toastStyles = css`:host{display:flex;align-items:center;gap:12px;min-width:280px;max-width:420px;padding:10px 14px;background:#1d2327;color:#fff;border-radius:8px;box-shadow:0 8px 24px rgba( 0,0,0,0.2 ),0 2px 6px rgba( 0,0,0,0.1 );font-size:13px;line-height:1.4;opacity:0;transform:translateY( -8px );transition:opacity 0.18s ease,transform 0.18s ease;pointer-events:auto}:host( [ state='in' ] ){opacity:1;transform:translateY( 0 )}:host( [ state='out' ] ){opacity:0;transform:translateY( -8px )}.wpd-toast__label{flex:1}button{flex-shrink:0;padding:4px 10px;border:none;border-radius:4px;background:rgba( 255,255,255,0.12 );color:#fff;font:inherit;font-size:12px;font-weight:500;cursor:pointer;transition:background-color 0.12s ease}button:hover{background:rgba( 255,255,255,0.22 )}button:focus-visible{outline:2px solid rgba( 255,255,255,0.6 );outline-offset:2px}@media ( prefers-reduced-motion:reduce ){:host{transition-duration:0.01ms}}`;
23256 const _WpdToastContainer = class _WpdToastContainer extends Component {
23257 connectedCallback() {
23258 super.connectedCallback();
23259 this.setAttribute("aria-live", "polite");
23260 }
23261 render() {
23262 return html`<slot></slot>`;
23263 }
23264 };
23265 _WpdToastContainer.styles = [containerStyles];
23266 _WpdToastContainer.help = {
23267 title: "Toast container",
23268 summary: "Singleton stack beneath <body> that hosts transient <wpd-toast> notifications in the top-right. Created lazily by showToast(); authors rarely place one themselves.",
23269 status: "stable",
23270 since: "0.9.0",
23271 slots: [
23272 { name: "(default)", description: "<wpd-toast> children, stacked vertically." }
23273 ],
23274 cssProps: [
23275 { name: "--desktop-mode-z-fullscreen", description: "z-index base — toasts sit above fullscreen windows." }
23276 ],
23277 example: html`
23278 <wpd-toast-container>
23279 <wpd-toast state="in">Settings saved.</wpd-toast>
23280 <wpd-toast state="in" action="Undo">Theme changed.</wpd-toast>
23281 </wpd-toast-container>
23282 `
23283 };
23284 let WpdToastContainer = _WpdToastContainer;
23285 defineComponent("wpd-toast-container", WpdToastContainer);
23286 const _WpdToast = class _WpdToast extends Component {
23287 connectedCallback() {
23288 super.connectedCallback();
23289 if (!this.hasAttribute("role")) {
23290 this.setAttribute("role", "status");
23291 }
23292 }
23293 render() {
23294 const action = this.action || "";
23295 return html`
23296 <span class="wpd-toast__label"><slot></slot></span>
23297 <button
23298 type="button"
23299 ?hidden=${!action}
23300 @click=${(e) => this._onAction(e)}
23301 >
23302 ${action}
23303 </button>
23304 `;
23305 }
23306 _onAction(e) {
23307 e.preventDefault();
23308 e.stopPropagation();
23309 this.emit("wpd-toast-action", {});
23310 }
23311 };
23312 _WpdToast.props = ["action", "state"];
23313 _WpdToast.styles = [toastStyles];
23314 _WpdToast.help = {
23315 title: "Toast",
23316 summary: 'Single transient notification. Message is slotted; fade-in / fade-out is CSS-driven by flipping the state attribute between "in" and "out". Usually created via the showToast() helper rather than authored by hand.',
23317 status: "stable",
23318 since: "0.9.0",
23319 props: [
23320 {
23321 name: "action",
23322 type: "string",
23323 description: "Optional action button label. When set, a button renders on the right and emits wpd-toast-action on click."
23324 },
23325 {
23326 name: "state",
23327 type: "'in' | 'out'",
23328 description: 'Drives the CSS fade transition. Set to "in" when rendered, flip to "out" before removal.'
23329 }
23330 ],
23331 slots: [
23332 { name: "(default)", description: "Message text." }
23333 ],
23334 events: [
23335 {
23336 name: "wpd-toast-action",
23337 description: "Fires when the action button is clicked.",
23338 detail: "{}"
23339 }
23340 ],
23341 example: html`
23342 <wpd-toast state="in" action="Undo">Post moved to trash.</wpd-toast>
23343 `
23344 };
23345 let WpdToast = _WpdToast;
23346 defineComponent("wpd-toast", WpdToast);
23347 function buildCapSegmented(initial, onChange) {
23348 const segmented = document.createElement("wpd-segmented");
23349 segmented.setAttribute("value", initial);
23350 segmented.setAttribute("label", "Capability");
23351 segmented.style.setProperty("--wpd-segmented-bg", "rgba(255,255,255,0.06)");
23352 segmented.style.setProperty(
23353 "--desktop-mode-window-bg",
23354 "var(--wp-admin-theme-color, #2271b1)"
23355 );
23356 segmented.style.setProperty("--desktop-mode-text", "#fff");
23357 segmented.style.setProperty("--desktop-mode-muted", "rgba(255,255,255,0.65)");
23358 const segRead = document.createElement("wpd-segment");
23359 segRead.setAttribute("value", "read");
23360 segRead.textContent = "Read";
23361 segmented.appendChild(segRead);
23362 const segWrite = document.createElement("wpd-segment");
23363 segWrite.setAttribute("value", "write");
23364 segWrite.textContent = "Read + Write";
23365 segmented.appendChild(segWrite);
23366 segmented.addEventListener("wpd-pick", (e) => {
23367 const detail = e.detail;
23368 onChange(detail.value);
23369 });
23370 return segmented;
23371 }
23372 function buildIconButton(label, onClick, opts = {}) {
23373 const btn = document.createElement("wpd-button");
23374 btn.setAttribute("variant", "ghost");
23375 btn.setAttribute("aria-label", opts.danger ? "Remove" : "Dismiss");
23376 btn.textContent = label;
23377 const fg = opts.danger ? "#ff8080" : "rgba(255,255,255,0.75)";
23378 const border = opts.danger ? "1px solid rgba(255,128,128,0.45)" : "1px solid rgba(255,255,255,0.18)";
23379 btn.style.setProperty("--wpd-button-fg", fg);
23380 btn.style.setProperty("--wpd-button-border", border);
23381 btn.style.setProperty("--wpd-button-padding", "6px 12px");
23382 btn.style.setProperty("--wpd-button-border-radius", "7px");
23383 btn.style.setProperty("--wpd-button-min-height", "34px");
23384 btn.style.minWidth = "34px";
23385 btn.style.fontSize = "18px";
23386 btn.style.lineHeight = "1";
23387 btn.addEventListener("click", onClick);
23388 return btn;
23389 }
23390 async function openShareSettingsModal(opts) {
23391 const modal = document.createElement("wpd-modal");
23392 modal.setAttribute("open", "");
23393 modal.setAttribute("size", "lg");
23394 modal.setAttribute("title", `Share "${opts.folderName}"`);
23395 document.body.appendChild(modal);
23396 let shares = [];
23397 let pendingPicks = [];
23398 const renderBody = () => {
23399 modal.innerHTML = "";
23400 const owner = document.createElement("div");
23401 owner.style.cssText = "opacity:0.7;margin-bottom:14px;font-size:12px;";
23402 owner.textContent = opts.ownerName ? `Owner: ${opts.ownerName} — cannot be changed` : "Owner cannot be changed";
23403 modal.appendChild(owner);
23404 const addPeople = document.createElement("div");
23405 addPeople.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
23406 const addPeopleLabel = document.createElement("div");
23407 addPeopleLabel.textContent = "Add people";
23408 addPeopleLabel.style.cssText = "font-weight:600;";
23409 addPeople.appendChild(addPeopleLabel);
23410 const userSearch = document.createElement("wpd-user-search");
23411 const excludedUserIds = shares.filter((s) => s.principalType === "user").map((s) => s.principalRef).concat(pendingPicks.filter((p) => p.kind === "user").map((p) => p.ref));
23412 userSearch.setAttribute("exclude", excludedUserIds.join(","));
23413 userSearch.setAttribute("placeholder", "Search users…");
23414 userSearch.addEventListener("wpd-user-pick", (e) => {
23415 const detail = e.detail;
23416 pendingPicks.push({
23417 kind: "user",
23418 ref: String(detail.user.id),
23419 label: detail.user.name,
23420 cap: "read"
23421 });
23422 renderBody();
23423 });
23424 addPeople.appendChild(userSearch);
23425 modal.appendChild(addPeople);
23426 const addRoles = document.createElement("div");
23427 addRoles.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
23428 const addRolesLabel = document.createElement("div");
23429 addRolesLabel.textContent = "Add roles";
23430 addRolesLabel.style.cssText = "font-weight:600;";
23431 addRoles.appendChild(addRolesLabel);
23432 const rolePicker = document.createElement("wpd-role-picker");
23433 const grantedRoles = shares.filter((s) => s.principalType === "role").map((s) => s.principalRef);
23434 const pickedRoles = pendingPicks.filter((p) => p.kind === "role").map((p) => p.ref);
23435 rolePicker.setAttribute("selected", [...grantedRoles, ...pickedRoles].join(","));
23436 rolePicker.addEventListener("wpd-role-toggle", (e) => {
23437 const detail = e.detail;
23438 const existing = shares.find(
23439 (s) => s.principalType === "role" && s.principalRef === detail.slug
23440 );
23441 if (existing) {
23442 if (!detail.selected) {
23443 void revoke(existing);
23444 }
23445 return;
23446 }
23447 if (detail.selected) {
23448 const eligible = (window.desktopModeConfig?.shareEligibleRoles ?? []).find(
23449 (r) => r.slug === detail.slug
23450 );
23451 pendingPicks.push({
23452 kind: "role",
23453 ref: detail.slug,
23454 label: eligible ? eligible.name : detail.slug,
23455 cap: "read"
23456 });
23457 } else {
23458 pendingPicks = pendingPicks.filter(
23459 (p) => !(p.kind === "role" && p.ref === detail.slug)
23460 );
23461 }
23462 renderBody();
23463 });
23464 addRoles.appendChild(rolePicker);
23465 modal.appendChild(addRoles);
23466 if (pendingPicks.length > 0) {
23467 const pendingBlock = document.createElement("div");
23468 pendingBlock.style.cssText = "border:1px dashed rgba(255,255,255,0.18);border-radius:8px;padding:10px;margin-bottom:14px;";
23469 const pendingTitle = document.createElement("div");
23470 pendingTitle.textContent = "New invites (not sent yet)";
23471 pendingTitle.style.cssText = "font-weight:600;margin-bottom:6px;font-size:12px;";
23472 pendingBlock.appendChild(pendingTitle);
23473 for (const pick of pendingPicks) {
23474 const row = document.createElement("div");
23475 row.style.cssText = "display:flex;align-items:center;gap:8px;padding:4px 0;font-size:13px;";
23476 const tag = document.createElement("span");
23477 tag.textContent = pick.kind === "role" ? `Role: ${pick.label}` : pick.label;
23478 tag.style.flex = "1";
23479 row.appendChild(tag);
23480 const capSeg = buildCapSegmented(pick.cap, (next) => {
23481 pick.cap = next;
23482 });
23483 row.appendChild(capSeg);
23484 const removeBtn = buildIconButton("×", () => {
23485 pendingPicks = pendingPicks.filter(
23486 (p) => !(p.kind === pick.kind && p.ref === pick.ref)
23487 );
23488 renderBody();
23489 });
23490 row.appendChild(removeBtn);
23491 pendingBlock.appendChild(row);
23492 }
23493 const sendBtn = document.createElement("wpd-button");
23494 sendBtn.setAttribute("variant", "primary");
23495 sendBtn.textContent = `Send ${pendingPicks.length} invite${pendingPicks.length === 1 ? "" : "s"}`;
23496 sendBtn.style.marginTop = "8px";
23497 sendBtn.addEventListener("click", async () => {
23498 if (pendingPicks.length === 0) {
23499 return;
23500 }
23501 sendBtn.setAttribute("busy", "");
23502 sendBtn.setAttribute("disabled", "");
23503 const snapshot = pendingPicks.slice();
23504 let succeeded = 0;
23505 let firstError = null;
23506 for (const pick of snapshot) {
23507 try {
23508 await inviteShare(opts.folderId, {
23509 principalType: pick.kind,
23510 principalRef: pick.ref,
23511 capability: pick.cap
23512 });
23513 succeeded++;
23514 } catch (err) {
23515 firstError = err;
23516 break;
23517 }
23518 }
23519 if (succeeded > 0) {
23520 pendingPicks = pendingPicks.slice(succeeded);
23521 }
23522 try {
23523 await refresh();
23524 } catch (_e) {
23525 }
23526 if (firstError) {
23527 showToast({
23528 message: `Could not send invites: ${firstError.message}`
23529 });
23530 } else {
23531 showToast({
23532 message: 1 === succeeded ? "Invite sent." : `${succeeded} invites sent.`
23533 });
23534 }
23535 sendBtn.removeAttribute("busy");
23536 sendBtn.removeAttribute("disabled");
23537 renderBody();
23538 });
23539 pendingBlock.appendChild(sendBtn);
23540 modal.appendChild(pendingBlock);
23541 }
23542 const listTitle = document.createElement("div");
23543 listTitle.textContent = "Who has access";
23544 listTitle.style.cssText = "font-weight:600;margin:8px 0 6px;";
23545 modal.appendChild(listTitle);
23546 if (shares.length === 0) {
23547 const empty = document.createElement("div");
23548 empty.textContent = "Only you can see this folder.";
23549 empty.style.cssText = "opacity:0.6;font-size:12px;";
23550 modal.appendChild(empty);
23551 } else {
23552 for (const s of shares) {
23553 const row = document.createElement("div");
23554 row.style.cssText = "display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.04);";
23555 const label = document.createElement("div");
23556 label.style.flex = "1";
23557 label.textContent = s.principalType === "role" ? `Role: ${s.displayName}` : s.displayName;
23558 if (s.state === "pending") {
23559 const tag = document.createElement("span");
23560 tag.textContent = " · pending";
23561 tag.style.cssText = "opacity:0.6;font-size:12px;";
23562 label.appendChild(tag);
23563 } else if (s.state === "denied") {
23564 const tag = document.createElement("span");
23565 tag.textContent = " · denied";
23566 tag.style.cssText = "color:#d63638;font-size:12px;";
23567 label.appendChild(tag);
23568 }
23569 row.appendChild(label);
23570 const cap = s.capability === "write" ? "write" : "read";
23571 const capSeg = buildCapSegmented(cap, (next) => {
23572 void changeCap(s, next);
23573 });
23574 row.appendChild(capSeg);
23575 const removeBtn = buildIconButton(
23576 "×",
23577 () => {
23578 void revoke(s);
23579 },
23580 { danger: true }
23581 );
23582 row.appendChild(removeBtn);
23583 modal.appendChild(row);
23584 }
23585 }
23586 const footer = document.createElement("div");
23587 footer.setAttribute("slot", "footer");
23588 footer.style.display = "flex";
23589 footer.style.justifyContent = "flex-end";
23590 footer.style.gap = "10px";
23591 footer.style.flexWrap = "wrap";
23592 const doneBtn = document.createElement("wpd-button");
23593 doneBtn.setAttribute("variant", "secondary");
23594 doneBtn.textContent = "Done";
23595 doneBtn.addEventListener("click", () => modal.remove());
23596 footer.appendChild(doneBtn);
23597 modal.appendChild(footer);
23598 };
23599 const refresh = async () => {
23600 try {
23601 const res = await listShares(opts.folderId);
23602 shares = res.shares;
23603 setSharesForFolder(opts.folderId, shares);
23604 } catch (err) {
23605 showToast({
23606 message: `Could not load shares: ${err.message}`
23607 });
23608 }
23609 renderBody();
23610 };
23611 const revoke = async (s) => {
23612 try {
23613 await revokeShare(opts.folderId, s.id);
23614 removeShare(opts.folderId, s.id);
23615 await refresh();
23616 showToast({ message: "Access revoked." });
23617 } catch (err) {
23618 showToast({
23619 message: `Could not revoke: ${err.message}`
23620 });
23621 }
23622 };
23623 const changeCap = async (s, cap) => {
23624 try {
23625 const next = await updateShareCapability(opts.folderId, s.id, cap);
23626 upsertShare(next);
23627 await refresh();
23628 } catch (err) {
23629 showToast({
23630 message: `Could not update capability: ${err.message}`
23631 });
23632 }
23633 };
23634 modal.addEventListener("wpd-modal-cancel", () => modal.remove());
23635 renderBody();
23636 await refresh();
23637 }
23638 function openPendingInviteModal(invite) {
23639 return new Promise((resolve2) => {
23640 const modal = document.createElement("wpd-modal");
23641 modal.setAttribute("open", "");
23642 modal.setAttribute("title", invite.folderName ? `${invite.ownerName ?? "Someone"} shared "${invite.folderName}" with you` : "Folder shared with you");
23643 const body = document.createElement("div");
23644 const capLabel = invite.capability === "write" ? "Read + Write" : "Read";
23645 body.innerHTML = `
23646 <p style="margin: 0 0 12px;">Accept the invite to add this folder to your desktop.</p>
23647 <p style="margin: 0; opacity: 0.75;">Access level: <strong>${capLabel}</strong></p>
23648 `;
23649 modal.appendChild(body);
23650 const footer = document.createElement("div");
23651 footer.setAttribute("slot", "footer");
23652 footer.style.display = "flex";
23653 footer.style.justifyContent = "flex-end";
23654 footer.style.gap = "10px";
23655 footer.style.flexWrap = "wrap";
23656 const laterBtn = document.createElement("wpd-button");
23657 laterBtn.setAttribute("variant", "secondary");
23658 laterBtn.textContent = "Decide later";
23659 laterBtn.addEventListener("click", () => {
23660 modal.remove();
23661 resolve2("dismissed");
23662 });
23663 const denyBtn = document.createElement("wpd-button");
23664 denyBtn.setAttribute("variant", "danger");
23665 denyBtn.textContent = "Deny";
23666 denyBtn.addEventListener("click", async () => {
23667 denyBtn.setAttribute("busy", "");
23668 denyBtn.setAttribute("disabled", "");
23669 try {
23670 await denyShare(invite.folderId, invite.id);
23671 sharesStore().state.deniedFolders.add(invite.folderId);
23672 sharesStore().notify();
23673 modal.remove();
23674 resolve2("denied");
23675 } catch (err) {
23676 showToast({
23677 message: `Could not deny: ${err.message}`
23678 });
23679 denyBtn.removeAttribute("busy");
23680 denyBtn.removeAttribute("disabled");
23681 }
23682 });
23683 const acceptBtn = document.createElement("wpd-button");
23684 acceptBtn.setAttribute("variant", "primary");
23685 acceptBtn.textContent = "Accept";
23686 acceptBtn.addEventListener("click", async () => {
23687 acceptBtn.setAttribute("busy", "");
23688 acceptBtn.setAttribute("disabled", "");
23689 try {
23690 await acceptShare(invite.folderId, invite.id);
23691 try {
23692 const res = await listPlacements(0);
23693 setFolderPlacements(0, res.placements);
23694 } catch (_e) {
23695 }
23696 modal.remove();
23697 resolve2("accepted");
23698 } catch (err) {
23699 showToast({
23700 message: `Could not accept: ${err.message}`
23701 });
23702 acceptBtn.removeAttribute("busy");
23703 acceptBtn.removeAttribute("disabled");
23704 }
23705 });
23706 footer.appendChild(laterBtn);
23707 footer.appendChild(denyBtn);
23708 footer.appendChild(acceptBtn);
23709 modal.appendChild(footer);
23710 modal.addEventListener("wpd-modal-cancel", () => {
23711 modal.remove();
23712 resolve2("dismissed");
23713 });
23714 document.body.appendChild(modal);
23715 });
23716 }
23717 function viewerId() {
23718 return Number(window.desktopModeConfig?.currentUserId ?? 0);
23719 }
23720 function sharingEnabled$1() {
23721 const settings = window.wp?.desktop?.getOsSettings?.();
23722 if (!settings) {
23723 return true;
23724 }
23725 return settings.foldersSharingEnabled !== false;
23726 }
23727 function folderOwnerId(folderId) {
23728 const folder = getFilesState().folders.get(folderId);
23729 return folder ? Number(folder.ownerId) : 0;
23730 }
23731 function folderIdFromBaseId(baseId) {
23732 if (typeof baseId !== "string") {
23733 return null;
23734 }
23735 const m = /^desktop-mode-folder-(\d+)$/.exec(baseId);
23736 return m ? Number(m[1]) : null;
23737 }
23738 function placementFolderId(placement) {
23739 if (placement.file.type !== "folder") {
23740 return null;
23741 }
23742 const ref = Number(placement.file.ref);
23743 if (!Number.isFinite(ref) || ref <= 0) {
23744 return null;
23745 }
23746 return ref;
23747 }
23748 function placementOwnerId(placement) {
23749 return Number(placement.file.ownerId ?? 0);
23750 }
23751 function installShareMenuItems() {
23752 addFilter(
23753 "desktop-mode.files.tile-menu",
23754 "desktop-mode/folder-share",
23755 (items, placement) => {
23756 if (!sharingEnabled$1()) {
23757 return items;
23758 }
23759 const folderId = placementFolderId(placement);
23760 if (folderId === null) {
23761 return items;
23762 }
23763 const ownerId = folderOwnerId(folderId) || placementOwnerId(placement);
23764 const viewer = viewerId();
23765 if (ownerId === viewer) {
23766 const shared = !!placement.file.shareSummary?.shared;
23767 const label = shared ? "Manage sharing…" : "Share folder…";
23768 items.push({
23769 id: "desktop-mode/folder-share",
23770 label,
23771 icon: "dashicons-share",
23772 sort: 30,
23773 onClick: () => {
23774 void openShareSettingsModal({
23775 folderId,
23776 folderName: placement.file.title || `Folder ${folderId}`
23777 });
23778 }
23779 });
23780 } else if (ownerId > 0) {
23781 items.push({
23782 id: "desktop-mode/folder-leave",
23783 label: "Leave shared folder",
23784 icon: "dashicons-exit",
23785 sort: 80,
23786 danger: true,
23787 onClick: async () => {
23788 const ok = await wpdConfirm$1({
23789 title: "Leave this folder?",
23790 message: "The folder will be removed from your desktop. The original and its contents are not deleted; the owner keeps them.",
23791 confirmLabel: "Leave",
23792 danger: true
23793 });
23794 if (!ok) {
23795 return;
23796 }
23797 try {
23798 await leaveShare(folderId);
23799 removePlacement(placement.id);
23800 try {
23801 const res = await listPlacements(0);
23802 setFolderPlacements(0, res.placements);
23803 } catch (_e) {
23804 }
23805 const winId = `desktop-mode-folder-${folderId}`;
23806 const mgr = window.desktopMode?.windowManager;
23807 mgr?.close?.(winId);
23808 showToast({ message: "You left the shared folder." });
23809 } catch (err) {
23810 showToast({
23811 message: `Could not leave: ${err.message}`
23812 });
23813 }
23814 }
23815 });
23816 }
23817 return items;
23818 }
23819 );
23820 registerTitleBarButton({
23821 id: "desktop-mode/folder-share",
23822 label: "Share folder",
23823 icon: "dashicons-share",
23824 placement: "right",
23825 order: 50,
23826 match: (w) => {
23827 if (!sharingEnabled$1()) {
23828 return false;
23829 }
23830 const base = w.config.baseId ?? w.id;
23831 const folderId = folderIdFromBaseId(base);
23832 if (folderId === null) {
23833 return false;
23834 }
23835 return folderOwnerId(folderId) === viewerId();
23836 },
23837 onClick: (w) => {
23838 const base = w.config.baseId ?? w.id;
23839 const folderId = folderIdFromBaseId(base);
23840 if (folderId === null) {
23841 return;
23842 }
23843 void openShareSettingsModal({
23844 folderId,
23845 folderName: w.config.title || `Folder ${folderId}`
23846 });
23847 }
23848 });
23849 addAction(
23850 "desktop-mode.files.tile-rendered",
23851 "desktop-mode/folder-share",
23852 (payload) => {
23853 const { tile: tile2, placement } = payload;
23854 if (placement.file.type !== "folder") {
23855 return;
23856 }
23857 const summary = placement.file.shareSummary;
23858 if (!summary?.shared) {
23859 return;
23860 }
23861 if (tile2.querySelector(".desktop-mode-file-tile__share-badge")) {
23862 return;
23863 }
23864 const badge = document.createElement("span");
23865 badge.className = "desktop-mode-file-tile__share-badge dashicons dashicons-share";
23866 badge.setAttribute("aria-label", "Shared folder");
23867 badge.title = "Shared folder";
23868 badge.style.cssText = [
23869 "position:absolute",
23870 "top:6px",
23871 "inset-inline-end:6px",
23872 "background:rgba(0,0,0,0.55)",
23873 "color:#fff",
23874 "border-radius:50%",
23875 "width:18px",
23876 "height:18px",
23877 "font-size:12px",
23878 "line-height:18px",
23879 "text-align:center",
23880 "pointer-events:none"
23881 ].join(";");
23882 tile2.appendChild(badge);
23883 }
23884 );
23885 }
23886 const prompted = /* @__PURE__ */ new Set();
23887 function sharingEnabled() {
23888 const settings = window.wp?.desktop?.getOsSettings?.();
23889 if (!settings) {
23890 return true;
23891 }
23892 return settings.foldersSharingEnabled !== false;
23893 }
23894 function installShareInviteBanner() {
23895 const store2 = sharesStore();
23896 const handle = (state2) => {
23897 if (!sharingEnabled()) {
23898 return;
23899 }
23900 for (const invite of state2.pending) {
23901 if (prompted.has(invite.id)) {
23902 continue;
23903 }
23904 prompted.add(invite.id);
23905 void openPendingInviteModal({
23906 id: invite.id,
23907 folderId: invite.folderId,
23908 folderName: invite.folderName,
23909 ownerName: invite.ownerName,
23910 capability: invite.capability
23911 }).then((decision) => {
23912 if (decision === "accepted") {
23913 dropPending(invite.id);
23914 } else if (decision === "denied") {
23915 dropPending(invite.id, { denied: true, folderId: invite.folderId });
23916 }
23917 });
23918 }
23919 };
23920 store2.subscribe(handle);
23921 handle(store2.state);
23922 }
23923 registerBuiltInFileTypes();
23924 registerBuiltInFileOpeners();
23925 installEmbedPersistence();
23926 registerFileAssociationsTab();
23927 installShareMenuItems();
23928 const seededPending = window.desktopModeConfig?.serverPendingShares;
23929 if (Array.isArray(seededPending) && seededPending.length > 0) {
23930 ingestPendingInvites(seededPending);
23931 }
23932 installShareInviteBanner();
23933 const filesApi = {
23934 DesktopFile,
23935 registerType,
23936 unregisterType,
23937 getType,
23938 getTypes,
23939 resolve,
23940 subscribe,
23941 registerOpener,
23942 unregisterOpener,
23943 getOpener,
23944 getOpeners,
23945 getOpenersForType,
23946 resolveOpener,
23947 subscribeOpeners,
23948 getUserAssociations,
23949 open: openFile,
23950 rest: filesRest,
23951 store: {
23952 get: getFilesStore,
23953 getState: getFilesState,
23954 subscribe: subscribeFilesStore,
23955 setFolderPlacements,
23956 upsertPlacement,
23957 removePlacement,
23958 setFolders,
23959 upsertFolder,
23960 removeFolder
23961 }
23962 };
23963 const SYNTH_META_KEY = "__synthFromDockItem";
23964 function hashToNegativeId(s) {
23965 let h = 0;
23966 for (let i = 0; i < s.length; i++) {
23967 h = (h * 31 + s.charCodeAt(i)) % 2147483647;
23968 }
23969 return -(h + 1);
23970 }
23971 function buildSyntheticPlacement(item, persistedPositions) {
23972 const saved = persistedPositions[item.id];
23973 return {
23974 id: hashToNegativeId(item.id),
23975 parentId: 0,
23976 x: saved ? saved.x : 0,
23977 y: saved ? saved.y : 0,
23978 sortOrder: 9999,
23979 updatedAtMs: Date.now(),
23980 meta: { [SYNTH_META_KEY]: item.id },
23981 file: {
23982 type: "shortcut",
23983 ref: `dock-promoted:${item.id}`,
23984 title: item.title,
23985 icon: item.icon,
23986 previewUrl: "",
23987 exists: true,
23988 // The shortcut opener (built-in-openers.ts) reads these
23989 // off the file shape — `shortcutUrl` is what a dock-item
23990 // promotion naturally has.
23991 shortcutUrl: item.url
23992 }
23993 };
23994 }
23995 function readDockItems() {
23996 const api = window.wp?.desktop;
23997 if (api?.getMenuItems) {
23998 const items = api.getMenuItems();
23999 return items.map((i) => ({
24000 id: i.id,
24001 title: i.title,
24002 icon: i.icon,
24003 url: i.url,
24004 badge: i.badge ?? 0,
24005 submenu: i.submenu ?? []
24006 }));
24007 }
24008 const cfg = window.desktopModeConfig;
24009 return cfg?.dockItems ?? [];
24010 }
24011 function readServerIcons() {
24012 const cfg = window.desktopModeConfig;
24013 return cfg?.desktopIcons ?? [];
24014 }
24015 let reentrant = false;
24016 const removedServerPlacementsByRef = /* @__PURE__ */ new Map();
24017 function syncShortcutsWithVisibility(visibility, positions = {}) {
24018 if (reentrant) {
24019 return;
24020 }
24021 reentrant = true;
24022 try {
24023 const dockItems = readDockItems();
24024 const serverIcons = readServerIcons();
24025 const state2 = filesApi.store.getState();
24026 const root = state2.placementsByFolder.get(0) ?? [];
24027 const currentSynth = /* @__PURE__ */ new Map();
24028 for (const p of root) {
24029 const sourceId = (p.meta ?? null) && typeof p.meta === "object" ? p.meta[SYNTH_META_KEY] : null;
24030 if (typeof sourceId === "string") {
24031 currentSynth.set(sourceId, p);
24032 }
24033 }
24034 const realByRef = /* @__PURE__ */ new Map();
24035 const registeredIconIds = new Set(
24036 serverIcons.map((i) => i.id)
24037 );
24038 for (const p of root) {
24039 const ref = p?.file?.ref;
24040 if (typeof ref === "string" && registeredIconIds.has(ref)) {
24041 realByRef.set(ref, p);
24042 }
24043 }
24044 const desiredSynth = /* @__PURE__ */ new Set();
24045 for (const item of dockItems) {
24046 const placement = visibility[item.id];
24047 if (placement === "desktop" || placement === "both") {
24048 desiredSynth.add(item.id);
24049 if (!currentSynth.has(item.id)) {
24050 filesApi.store.upsertPlacement(
24051 buildSyntheticPlacement(item, positions)
24052 );
24053 }
24054 }
24055 }
24056 for (const [sourceId, p] of currentSynth) {
24057 if (!desiredSynth.has(sourceId)) {
24058 filesApi.store.removePlacement(p.id);
24059 }
24060 }
24061 for (const icon of serverIcons) {
24062 const placement = visibility[icon.id];
24063 const inStore = realByRef.get(icon.id);
24064 if (placement === "dock" || placement === "hidden") {
24065 if (inStore) {
24066 removedServerPlacementsByRef.set(icon.id, inStore);
24067 filesApi.store.removePlacement(inStore.id);
24068 }
24069 continue;
24070 }
24071 if (!inStore) {
24072 const cached = removedServerPlacementsByRef.get(icon.id);
24073 if (cached) {
24074 filesApi.store.upsertPlacement(cached);
24075 removedServerPlacementsByRef.delete(icon.id);
24076 }
24077 }
24078 }
24079 } finally {
24080 reentrant = false;
24081 }
24082 }
24083 function installShortcutsSync(getVisibility, getPositions = () => ({})) {
24084 queueMicrotask(
24085 () => syncShortcutsWithVisibility(getVisibility(), getPositions())
24086 );
24087 const off = filesApi.store.subscribe(() => {
24088 syncShortcutsWithVisibility(getVisibility(), getPositions());
24089 });
24090 return off;
24091 }
24092 const clock = {
24093 id: "clock",
24094 // Labels/descriptions on built-in defs stay string-literal at
24095 // module-eval time so the extract-pot pass picks them up. The
24096 // values are wrapped in `__()` so they translate at runtime.
24097 get label() {
24098 return __("Clock");
24099 },
24100 get description() {
24101 return __("Local time and date, refreshed every second.");
24102 },
24103 icon: "dashicons-clock",
24104 mount: (container) => {
24105 container.classList.add("desktop-mode-widget-clock");
24106 const time = document.createElement("div");
24107 time.className = "desktop-mode-widget-clock__time";
24108 container.appendChild(time);
24109 const date = document.createElement("div");
24110 date.className = "desktop-mode-widget-clock__date";
24111 container.appendChild(date);
24112 const render2 = () => {
24113 const now = /* @__PURE__ */ new Date();
24114 time.textContent = now.toLocaleTimeString(void 0, {
24115 hour: "2-digit",
24116 minute: "2-digit"
24117 });
24118 date.textContent = now.toLocaleDateString(void 0, {
24119 weekday: "long",
24120 month: "short",
24121 day: "numeric"
24122 });
24123 };
24124 render2();
24125 const msUntilNextSecond = 1e3 - Date.now() % 1e3;
24126 let interval = null;
24127 const kickoff = window.setTimeout(() => {
24128 render2();
24129 interval = window.setInterval(render2, 1e3);
24130 }, msUntilNextSecond);
24131 return () => {
24132 window.clearTimeout(kickoff);
24133 if (interval !== null) {
24134 window.clearInterval(interval);
24135 }
24136 };
24137 }
24138 };
24139 function registerBuiltInWidgets() {
24140 register(clock);
24141 }
24142 function createWidgetRegistrySync(deps2) {
24143 const { layer } = deps2;
24144 const registered = /* @__PURE__ */ new Set();
24145 const loadedScripts = /* @__PURE__ */ new Set();
24146 const ensureScript = async (entry) => {
24147 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
24148 return;
24149 }
24150 try {
24151 await loadVendorScript(entry.scriptUrl, {
24152 translations: entry.scriptTranslations,
24153 l10n: entry.scriptL10n,
24154 before: entry.scriptBefore,
24155 after: entry.scriptAfter
24156 });
24157 } catch (err) {
24158 doAction(HOOKS.SHELL_ERROR, {
24159 scope: "widget-script-load",
24160 id: entry.id,
24161 error: err
24162 });
24163 }
24164 loadedScripts.add(entry.scriptUrl);
24165 };
24166 const buildDefFromEntry = (entry) => {
24167 const globals = window.desktopModeWidgets || {};
24168 const mount = globals[entry.id];
24169 if (!mount) {
24170 doAction(HOOKS.SHELL_ERROR, {
24171 scope: "widget-missing-mount",
24172 id: entry.id,
24173 error: new Error(
24174 `[desktop-mode] No mount callback on window.desktopModeWidgets["${entry.id}"]. Plugin script loaded but didn't register. Check the plugin's enqueue + global assignment.`
24175 )
24176 });
24177 return null;
24178 }
24179 return {
24180 id: entry.id,
24181 label: entry.label,
24182 description: entry.description,
24183 icon: entry.icon,
24184 movable: entry.movable,
24185 resizable: entry.resizable,
24186 minWidth: entry.minWidth || void 0,
24187 minHeight: entry.minHeight || void 0,
24188 maxWidth: entry.maxWidth || void 0,
24189 maxHeight: entry.maxHeight || void 0,
24190 defaultWidth: entry.defaultWidth || void 0,
24191 defaultHeight: entry.defaultHeight || void 0,
24192 mount
24193 };
24194 };
24195 const registerEntry = async (entry) => {
24196 if (registered.has(entry.id)) {
24197 return;
24198 }
24199 await ensureScript(entry);
24200 const def = buildDefFromEntry(entry);
24201 if (!def) {
24202 return;
24203 }
24204 try {
24205 register(def);
24206 } catch (err) {
24207 doAction(HOOKS.SHELL_ERROR, {
24208 scope: "widget-register",
24209 id: entry.id,
24210 error: err
24211 });
24212 return;
24213 }
24214 registered.add(entry.id);
24215 refreshWidgetPicker();
24216 if (layer) {
24217 layer.mountIfEnabled(entry.id);
24218 }
24219 };
24220 const unregisterEntry = (id) => {
24221 if (!registered.has(id)) {
24222 return;
24223 }
24224 layer?.unmount(id);
24225 unregister(id);
24226 registered.delete(id);
24227 refreshWidgetPicker();
24228 };
24229 return async (list2) => {
24230 const incoming = /* @__PURE__ */ new Set();
24231 for (const entry of list2) {
24232 incoming.add(entry.id);
24233 }
24234 for (const id of Array.from(registered)) {
24235 if (!incoming.has(id)) {
24236 unregisterEntry(id);
24237 }
24238 }
24239 for (const entry of list2) {
24240 if (!registered.has(entry.id)) {
24241 await registerEntry(entry);
24242 }
24243 }
24244 };
24245 }
24246 const WPD_COMPONENT_TAGS = [
24247 "wpd-section",
24248 "wpd-button",
24249 "wpd-swatch",
24250 "wpd-swatch-grid",
24251 "wpd-segmented",
24252 "wpd-segment",
24253 "wpd-select",
24254 "wpd-option",
24255 "wpd-multiselect",
24256 "wpd-color-field",
24257 "wpd-range-field",
24258 "wpd-text-field",
24259 "wpd-number-field",
24260 "wpd-checkbox",
24261 "wpd-checkbox-label",
24262 "wpd-toast",
24263 "wpd-toast-container",
24264 "wpd-tabs",
24265 "wpd-tab",
24266 "wpd-tabpanel",
24267 "wpd-window-button",
24268 "wpd-menu",
24269 "wpd-menu-item",
24270 "wpd-context-menu",
24271 "wpd-context-menu-option",
24272 "wpd-confirm-dialog",
24273 "wpd-modal",
24274 "wpd-user-search",
24275 "wpd-role-picker",
24276 "wpd-flyout",
24277 "wpd-tab-chip",
24278 "wpd-stack",
24279 "wpd-cluster",
24280 "wpd-icon",
24281 "wpd-body",
24282 "wpd-panel",
24283 "wpd-row",
24284 "wpd-grid",
24285 "wpd-display",
24286 "wpd-empty-state",
24287 "wpd-key",
24288 "wpd-code",
24289 "wpd-badge",
24290 "wpd-log",
24291 "wpd-steps",
24292 "wpd-step",
24293 "wpd-table",
24294 "wpd-spinner",
24295 "wpd-relative-time",
24296 "wpd-avatar",
24297 "wpd-textarea",
24298 "wpd-chip",
24299 "wpd-tag-input",
24300 "wpd-form",
24301 "wpd-save-status",
24302 "wpd-category-picker",
24303 "wpd-crumb-chain",
24304 "wpd-card",
24305 "wpd-notice"
24306 ];
24307 const KNOWN = new Set(WPD_COMPONENT_TAGS);
24308 const WARN_GRACE_MS = 2e3;
24309 const warnedTags = /* @__PURE__ */ new Set();
24310 const observedRoots = /* @__PURE__ */ new WeakSet();
24311 let started$2 = false;
24312 function distance(a, b) {
24313 const m = a.length;
24314 const n = b.length;
24315 if (m === 0) {
24316 return n;
24317 }
24318 if (n === 0) {
24319 return m;
24320 }
24321 const dp = new Array(n + 1);
24322 for (let j = 0; j <= n; j++) {
24323 dp[j] = j;
24324 }
24325 for (let i = 1; i <= m; i++) {
24326 let prev = dp[0];
24327 dp[0] = i;
24328 for (let j = 1; j <= n; j++) {
24329 const tmp = dp[j];
24330 dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
24331 prev = tmp;
24332 }
24333 }
24334 return dp[n];
24335 }
24336 function suggest(tag) {
24337 let best = null;
24338 let bestD = Infinity;
24339 for (const known of KNOWN) {
24340 const d = distance(tag, known);
24341 if (d < bestD) {
24342 bestD = d;
24343 best = known;
24344 }
24345 }
24346 return bestD > 0 && bestD <= 3 ? best : null;
24347 }
24348 function folderFor(tag) {
24349 return tag.startsWith("wpd-") ? tag.slice(4) : tag;
24350 }
24351 function warnFor(tag, sample) {
24352 if (warnedTags.has(tag)) {
24353 return;
24354 }
24355 warnedTags.add(tag);
24356 const isKnown = KNOWN.has(tag);
24357 if (isKnown) {
24358 const folder = folderFor(tag);
24359 console.error(
24360 `[wp.desktop] <${tag}> is in the DOM but its module was never imported, so the tag will not upgrade and the component will render as inert HTML.
24361
24362 Fix — side-effect-import the component module from wherever you render it:
24363
24364 import '<rel>/ui/components/${folder}/${folder}';
24365
24366 Or pull every wpd-* component in one go (heavier — only do this from an entry bundle):
24367
24368 import '<rel>/ui/components';
24369
24370 See docs/components-reference.md for the full list.`,
24371 "\nFirst offending element:",
24372 sample
24373 );
24374 return;
24375 }
24376 const guess = suggest(tag);
24377 if (guess) {
24378 console.error(
24379 `[wp.desktop] <${tag}> is not a registered wpd-* component. Did you mean <${guess}>?
24380
24381 If the typo is in your template, update it. If you meant to ship a new component, register it via 'src/ui/components/<name>/<name>.ts' and add it to 'src/ui/components/tags.ts' + 'src/ui/components/index.ts'.`,
24382 "\nFirst offending element:",
24383 sample
24384 );
24385 return;
24386 }
24387 console.error(
24388 `[wp.desktop] <${tag}> looks like a wpd-* tag but no component by that name exists.
24389
24390 See 'src/ui/components/index.ts' (or docs/components-reference.md) for the canonical list. If you intended to register a new component, add it to 'tags.ts' and side-effect-import its module.`,
24391 "\nFirst offending element:",
24392 sample
24393 );
24394 }
24395 function checkElement(el) {
24396 const tag = el.tagName.toLowerCase();
24397 if (!tag.startsWith("wpd-")) {
24398 return;
24399 }
24400 if (warnedTags.has(tag)) {
24401 return;
24402 }
24403 if (customElements.get(tag)) {
24404 return;
24405 }
24406 let settled = false;
24407 customElements.whenDefined(tag).then(() => {
24408 settled = true;
24409 });
24410 setTimeout(() => {
24411 if (settled) {
24412 return;
24413 }
24414 if (customElements.get(tag)) {
24415 return;
24416 }
24417 warnFor(tag, el);
24418 }, WARN_GRACE_MS);
24419 }
24420 function walk(root) {
24421 if (root instanceof Element) {
24422 checkElement(root);
24423 if (root.shadowRoot) {
24424 observeRoot(root.shadowRoot);
24425 }
24426 }
24427 const all2 = root.querySelectorAll("*");
24428 for (let i = 0; i < all2.length; i++) {
24429 const el = all2[i];
24430 checkElement(el);
24431 if (el.shadowRoot) {
24432 observeRoot(el.shadowRoot);
24433 }
24434 }
24435 }
24436 function observeRoot(root) {
24437 if (observedRoots.has(root)) {
24438 return;
24439 }
24440 observedRoots.add(root);
24441 walk(root);
24442 const mo = new MutationObserver((records) => {
24443 for (let i = 0; i < records.length; i++) {
24444 const added = records[i].addedNodes;
24445 for (let j = 0; j < added.length; j++) {
24446 const node = added[j];
24447 if (node.nodeType === 1) {
24448 walk(node);
24449 }
24450 }
24451 }
24452 });
24453 mo.observe(root, { childList: true, subtree: true });
24454 }
24455 function patchAttachShadow() {
24456 const proto = Element.prototype;
24457 const original = proto.attachShadow;
24458 if (original.__wpdPatched) {
24459 return;
24460 }
24461 const patched = function(init2) {
24462 const root = original.call(this, init2);
24463 if (root.mode === "open") {
24464 observeRoot(root);
24465 }
24466 return root;
24467 };
24468 patched.__wpdPatched = true;
24469 proto.attachShadow = patched;
24470 }
24471 function startMissingImportWarner() {
24472 if (started$2) {
24473 return;
24474 }
24475 if (typeof document === "undefined") {
24476 return;
24477 }
24478 started$2 = true;
24479 patchAttachShadow();
24480 observeRoot(document);
24481 }
24482 const TRASH_DROP_ACTIVE_ATTR = "data-desktop-mode-trash-drop-active";
24483 const RECYCLE_BIN_WINDOW_ID = "desktop-mode-recycle-bin";
24484 const BIN_TILE_SELECTORS = [
24485 `.desktop-mode-file-tile[data-file-ref="${RECYCLE_BIN_WINDOW_ID}"]`,
24486 `[data-icon-id="${RECYCLE_BIN_WINDOW_ID}"]`,
24487 `[data-system-id="${RECYCLE_BIN_WINDOW_ID}"]`
24488 ];
24489 function findBinTile() {
24490 for (const sel of BIN_TILE_SELECTORS) {
24491 const el = document.querySelector(sel);
24492 if (el instanceof HTMLElement) {
24493 return el;
24494 }
24495 }
24496 return null;
24497 }
24498 let _installed = false;
24499 let _dockDeregister = null;
24500 let _windowDeregister = null;
24501 let _binMutationObserver = null;
24502 function isDesktopFilePayload(session) {
24503 return session.payload.type === "desktop-file";
24504 }
24505 function registerOn(dragManager, id, el) {
24506 return dragManager.registerDropTarget({
24507 id,
24508 element: el,
24509 // Reject the drop UP FRONT when the viewer can't trash the
24510 // payload's placement (e.g. an item inside a read-only
24511 // shared folder, or someone else's tile in a shared
24512 // namespace). `accept` flipping to `false` means the
24513 // drop-active highlight never lights up + onDrop never
24514 // fires + the drag manager surfaces a `rejected` outcome.
24515 // The user sees the icon snap back instead of attempting a
24516 // REST call that would 403 and only log to the console.
24517 accept: (payload) => {
24518 if (payload.type !== "desktop-file") {
24519 return false;
24520 }
24521 const data = payload.data;
24522 const placement = data?.placement;
24523 if (!placement) {
24524 return false;
24525 }
24526 if (placement.file?.ref === RECYCLE_BIN_WINDOW_ID) {
24527 return false;
24528 }
24529 return placement.canTrash !== false;
24530 },
24531 onEnter: () => {
24532 el.setAttribute(TRASH_DROP_ACTIVE_ATTR, "");
24533 },
24534 onLeave: () => {
24535 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
24536 },
24537 onDrop: (session) => {
24538 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
24539 if (!isDesktopFilePayload(session)) {
24540 return;
24541 }
24542 const placement = session.payload.data.placement;
24543 void trashByFileType(placement);
24544 }
24545 });
24546 }
24547 function installRecycleBinDropTargets(dragManager) {
24548 if (_installed) {
24549 return;
24550 }
24551 _installed = true;
24552 const reprobeTile = () => {
24553 const el = findBinTile();
24554 if (!el) {
24555 _dockDeregister?.();
24556 _dockDeregister = null;
24557 return;
24558 }
24559 if (_dockDeregister && getRegisteredElementId(dragManager) === el) {
24560 return;
24561 }
24562 _dockDeregister?.();
24563 _dockDeregister = registerOn(dragManager, "recycle-bin-dock", el);
24564 };
24565 reprobeTile();
24566 document.addEventListener("desktop-mode-files-changed", reprobeTile);
24567 document.addEventListener("desktop-mode-desktop-icons-rendered", reprobeTile);
24568 addAction(
24569 HOOKS.DOCK_AFTER_RENDER,
24570 "desktop-mode/files/recycle-bin-dock-target",
24571 reprobeTile
24572 );
24573 if (typeof MutationObserver !== "undefined") {
24574 _binMutationObserver = new MutationObserver(() => {
24575 reprobeTile();
24576 });
24577 const desktopArea = document.getElementById("desktop-mode-area") ?? document.body;
24578 _binMutationObserver.observe(desktopArea, {
24579 childList: true,
24580 subtree: true
24581 });
24582 }
24583 addAction(
24584 HOOKS.WINDOW_OPENED,
24585 "desktop-mode/files/recycle-bin-window-target",
24586 (detail) => {
24587 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
24588 return;
24589 }
24590 _windowDeregister?.();
24591 _windowDeregister = null;
24592 const el = document.querySelector(
24593 "[data-desktop-mode-recycle-bin-root]"
24594 );
24595 if (el instanceof HTMLElement) {
24596 _windowDeregister = registerOn(
24597 dragManager,
24598 "recycle-bin-window",
24599 el
24600 );
24601 }
24602 }
24603 );
24604 addAction(
24605 HOOKS.WINDOW_CLOSED,
24606 "desktop-mode/files/recycle-bin-window-cleanup",
24607 (detail) => {
24608 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
24609 return;
24610 }
24611 _windowDeregister?.();
24612 _windowDeregister = null;
24613 }
24614 );
24615 }
24616 function getRegisteredElementId(dragManager) {
24617 const t = dragManager.debug().listTargets().find((target) => target.id === "recycle-bin-dock");
24618 return t ? t.element : null;
24619 }
24620 let started$1 = false;
24621 let highWaterMs = 0;
24622 function startFilesHeartbeat() {
24623 if (started$1) {
24624 return;
24625 }
24626 started$1 = true;
24627 heartbeat.contribute("desktop_mode_files_subscribe", () => {
24628 const state2 = getFilesState();
24629 const folderVersions = {};
24630 for (const [id, folder] of state2.folders) {
24631 folderVersions[String(id)] = folder.updatedAtMs;
24632 }
24633 return {
24634 folderVersions,
24635 placementsVersion: highWaterMs,
24636 sharesVersion: sharesStore().state.sharesVersion
24637 };
24638 });
24639 heartbeat.subscribe("desktop_mode_files", (payload) => {
24640 applyDelta(payload);
24641 });
24642 }
24643 function applyDelta(payload) {
24644 const folders = payload.folders ?? [];
24645 for (const folder of folders) {
24646 upsertFolder(folder, "remote");
24647 if (folder.updatedAtMs > highWaterMs) {
24648 highWaterMs = folder.updatedAtMs;
24649 }
24650 }
24651 const placements = payload.placements ?? [];
24652 for (const placement of placements) {
24653 upsertPlacement(placement, "remote");
24654 if (placement.updatedAtMs > highWaterMs) {
24655 highWaterMs = placement.updatedAtMs;
24656 }
24657 }
24658 const removed = payload.removed ?? {};
24659 for (const id of removed.folders ?? []) {
24660 removeFolder(id, "remote");
24661 }
24662 for (const id of removed.placements ?? []) {
24663 removePlacement(id, "remote");
24664 }
24665 if (typeof payload.serverTimeMs === "number" && payload.serverTimeMs > highWaterMs) {
24666 highWaterMs = payload.serverTimeMs;
24667 }
24668 const pending2 = payload.shares?.pending;
24669 if (Array.isArray(pending2) && pending2.length > 0) {
24670 ingestPendingInvites(pending2);
24671 }
24672 if (payload.truncated) {
24673 const hydrated = Array.from(getFilesState().hydratedFolders);
24674 for (const folderId of hydrated) {
24675 void listPlacements(folderId).then((res) => {
24676 setFolderPlacements(folderId, res.placements);
24677 }).catch(() => {
24678 });
24679 }
24680 }
24681 }
24682 let started = false;
24683 const unsubscribers = [];
24684 function startFilesRestoreSync() {
24685 if (started) {
24686 return;
24687 }
24688 started = true;
24689 const onChange = (payload) => {
24690 const detail = payload;
24691 if (!detail || detail.action !== "untrashed") {
24692 return;
24693 }
24694 resyncFromServer();
24695 };
24696 unsubscribers.push(
24697 subscribe$2("desktop-mode.placement.changed", onChange),
24698 subscribe$2("desktop-mode.shortcut.changed", onChange),
24699 subscribe$2("desktop-mode.folder.changed", onChange)
24700 );
24701 }
24702 function resyncFromServer() {
24703 void listFolders().then((res) => {
24704 setFolders(res.folders);
24705 }).catch((err) => {
24706 console.error(
24707 "[desktop-mode] files restore-sync: listFolders failed",
24708 err
24709 );
24710 });
24711 const hydrated = Array.from(getFilesState().hydratedFolders);
24712 for (const folderId of hydrated) {
24713 void listPlacements(folderId).then((res) => {
24714 setFolderPlacements(folderId, res.placements);
24715 }).catch((err) => {
24716 console.error(
24717 "[desktop-mode] files restore-sync: listPlacements failed for",
24718 folderId,
24719 err
24720 );
24721 });
24722 }
24723 }
24724 const MENU_CLASS = "desktop-mode-wallpaper-menu";
24725 let activeMenu = null;
24726 function isWallpaperMenuOpen() {
24727 return activeMenu !== null;
24728 }
24729 let openGeneration = 0;
24730 function openWallpaperMenu(host, pos, items, options = {}) {
24731 closeWallpaperMenu();
24732 const myGen = ++openGeneration;
24733 openWithShellOverlays(
24734 () => myGen === openGeneration,
24735 () => openWallpaperMenuImmediate(host, pos, items, options)
24736 );
24737 }
24738 function openWallpaperMenuImmediate(host, pos, items, options = {}) {
24739 if (items.length === 0) {
24740 return;
24741 }
24742 items = items.slice().sort((a, b) => {
24743 const sa = typeof a.sort === "number" ? a.sort : 100;
24744 const sb = typeof b.sort === "number" ? b.sort : 100;
24745 if (sa !== sb) {
24746 return sa - sb;
24747 }
24748 return a.label.localeCompare(b.label);
24749 });
24750 const menu = document.createElement("wpd-context-menu");
24751 menu.setAttribute("open", "");
24752 menu.classList.add(MENU_CLASS);
24753 menu.style.left = `${pos.x}px`;
24754 menu.style.top = `${pos.y}px`;
24755 const itemById = /* @__PURE__ */ new Map();
24756 let activeFlyout2 = null;
24757 let activeFlyoutParent = null;
24758 const closeActiveFlyout = () => {
24759 if (activeFlyout2) {
24760 activeFlyout2.remove();
24761 activeFlyout2 = null;
24762 activeFlyoutParent = null;
24763 }
24764 };
24765 for (const item of items) {
24766 itemById.set(item.id, item);
24767 const opt = document.createElement("wpd-context-menu-option");
24768 opt.dataset.menuItemId = item.id;
24769 opt.setAttribute("value", item.id);
24770 if (item.heading) {
24771 opt.setAttribute("heading", "");
24772 }
24773 if (item.disabled) {
24774 opt.setAttribute("disabled", "");
24775 }
24776 if (item.icon) {
24777 opt.setAttribute("icon", sanitizeClass(item.icon));
24778 }
24779 const hasChildren2 = Array.isArray(item.children) && item.children.length > 0;
24780 if (hasChildren2) {
24781 opt.setAttribute("has-children", "");
24782 }
24783 opt.textContent = item.label;
24784 opt.addEventListener("mouseenter", () => {
24785 if (hasChildren2) {
24786 openFlyout2(item, opt);
24787 return;
24788 }
24789 closeActiveFlyout();
24790 });
24791 menu.appendChild(opt);
24792 }
24793 menu.addEventListener("wpd-context-menu-pick", (e) => {
24794 const detail = e.detail;
24795 const item = itemById.get(detail.id) ?? null;
24796 if (!item) {
24797 return;
24798 }
24799 if (Array.isArray(item.children) && item.children.length > 0) {
24800 e.stopPropagation();
24801 if (activeFlyoutParent && activeFlyoutParent.id === item.id) {
24802 closeActiveFlyout();
24803 return;
24804 }
24805 const anchor = menu.querySelector(
24806 `[data-menu-item-id="${item.id}"]`
24807 );
24808 if (anchor) {
24809 openFlyout2(item, anchor);
24810 }
24811 return;
24812 }
24813 closeWallpaperMenu();
24814 void item.onClick(new MouseEvent("click"));
24815 });
24816 function openFlyout2(parent, anchor) {
24817 closeActiveFlyout();
24818 const fly = document.createElement("wpd-context-menu");
24819 fly.setAttribute("open", "");
24820 fly.classList.add(MENU_CLASS, `${MENU_CLASS}--flyout`);
24821 fly.dataset.parentId = parent.id;
24822 const sortedKids = (parent.children ?? []).slice().sort((a, b) => {
24823 const sa = typeof a.sort === "number" ? a.sort : 100;
24824 const sb = typeof b.sort === "number" ? b.sort : 100;
24825 if (sa !== sb) {
24826 return sa - sb;
24827 }
24828 return a.label.localeCompare(b.label);
24829 });
24830 for (const child of sortedKids) {
24831 const kopt = document.createElement("wpd-context-menu-option");
24832 kopt.dataset.menuItemId = child.id;
24833 kopt.setAttribute("value", child.id);
24834 if (child.icon) {
24835 kopt.setAttribute("icon", sanitizeClass(child.icon));
24836 }
24837 if (child.disabled) {
24838 kopt.setAttribute("disabled", "");
24839 }
24840 if (child.checked) {
24841 kopt.setAttribute("checked", "");
24842 }
24843 kopt.textContent = child.label;
24844 kopt.addEventListener("wpd-context-menu-pick", (e) => {
24845 e.stopPropagation();
24846 closeWallpaperMenu();
24847 void child.onClick(new MouseEvent("click"));
24848 });
24849 fly.appendChild(kopt);
24850 }
24851 document.body.appendChild(fly);
24852 activeFlyout2 = fly;
24853 activeFlyoutParent = parent;
24854 positionFlyout2(fly, anchor);
24855 }
24856 function positionFlyout2(fly, anchor) {
24857 const ar = anchor.getBoundingClientRect();
24858 fly.style.position = "fixed";
24859 fly.style.left = `${ar.right}px`;
24860 fly.style.top = `${ar.top}px`;
24861 const fr = fly.getBoundingClientRect();
24862 if (fr.right > window.innerWidth) {
24863 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
24864 }
24865 if (fr.bottom > window.innerHeight) {
24866 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
24867 }
24868 }
24869 host.appendChild(menu);
24870 activeMenu = menu;
24871 const rect = menu.getBoundingClientRect();
24872 if (rect.right > window.innerWidth) {
24873 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
24874 }
24875 if (rect.bottom > window.innerHeight) {
24876 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
24877 }
24878 const detach = attachDismissable(menu, {
24879 close: () => closeWallpaperMenu(),
24880 siblingSelectors: [`.${MENU_CLASS}--flyout`],
24881 excludeOutsideTarget: options.excludeOutsideTarget
24882 });
24883 menu.addEventListener("wallpaper-menu-closed", detach);
24884 doAction("desktop-mode.wallpaper-menu.opened", { items: items.map((i) => i.id) });
24885 }
24886 function closeWallpaperMenu() {
24887 if (!activeMenu) {
24888 return;
24889 }
24890 document.querySelectorAll(`.${MENU_CLASS}--flyout`).forEach((el) => el.remove());
24891 activeMenu.dispatchEvent(new CustomEvent("wallpaper-menu-closed"));
24892 activeMenu.remove();
24893 activeMenu = null;
24894 doAction("desktop-mode.wallpaper-menu.closed", {});
24895 }
24896 function buildMenuItems(deps2) {
24897 const builtIn = [
24898 {
24899 id: "create-folder",
24900 label: deps2.labels.createFolder,
24901 icon: "dashicons-portfolio",
24902 sort: 10,
24903 onClick: () => deps2.createFolder()
24904 },
24905 {
24906 id: "new-url",
24907 label: deps2.labels.newUrl,
24908 icon: "dashicons-admin-links",
24909 sort: 12,
24910 onClick: () => deps2.createUrl()
24911 },
24912 {
24913 id: "sort-by",
24914 label: deps2.labels.sortHeading,
24915 icon: "dashicons-sort",
24916 sort: 16,
24917 onClick: () => void 0,
24918 children: [
24919 {
24920 id: "sort-name-asc",
24921 label: deps2.labels.sortNameAsc,
24922 sort: 10,
24923 checked: deps2.currentSortMode === "name-asc",
24924 onClick: () => deps2.sortIcons("name-asc")
24925 },
24926 {
24927 id: "sort-name-desc",
24928 label: deps2.labels.sortNameDesc,
24929 sort: 20,
24930 checked: deps2.currentSortMode === "name-desc",
24931 onClick: () => deps2.sortIcons("name-desc")
24932 },
24933 {
24934 id: "sort-date-desc",
24935 label: deps2.labels.sortDateDesc,
24936 sort: 30,
24937 checked: deps2.currentSortMode === "date-desc",
24938 onClick: () => deps2.sortIcons("date-desc")
24939 },
24940 {
24941 id: "sort-date-asc",
24942 label: deps2.labels.sortDateAsc,
24943 sort: 40,
24944 checked: deps2.currentSortMode === "date-asc",
24945 onClick: () => deps2.sortIcons("date-asc")
24946 }
24947 ]
24948 },
24949 ...deps2.includeShowDesktop === false ? [] : [
24950 {
24951 id: "show-desktop",
24952 label: deps2.labels.showDesktop,
24953 icon: "dashicons-desktop",
24954 sort: 20,
24955 onClick: () => deps2.toggleShowDesktop()
24956 }
24957 ],
24958 {
24959 id: "os-settings",
24960 label: deps2.labels.osSettings,
24961 icon: "dashicons-admin-generic",
24962 sort: 30,
24963 onClick: () => deps2.openOsSettings()
24964 }
24965 ];
24966 const serverItems = (deps2.serverItems ?? []).map(
24967 (s) => serverItemToMenuItem(s, deps2)
24968 );
24969 const merged = [...builtIn, ...serverItems];
24970 const filtered = applyFilters(
24971 "desktop-mode.wallpaper-context-menu",
24972 merged
24973 );
24974 return Array.isArray(filtered) ? filtered : merged;
24975 }
24976 function serverItemToMenuItem(server, deps2) {
24977 return {
24978 id: server.id,
24979 label: server.label,
24980 icon: server.icon,
24981 sort: server.sort,
24982 disabled: server.disabled,
24983 onClick: () => {
24984 if (server.callbackId) {
24985 const cb = deps2.serverCallbacks?.[server.callbackId];
24986 if (typeof cb === "function") {
24987 return cb();
24988 }
24989 }
24990 doAction("desktop-mode.wallpaper-context-menu.activated", {
24991 id: server.id,
24992 callbackId: server.callbackId ?? ""
24993 });
24994 }
24995 };
24996 }
24997 function sanitizeClass(raw) {
24998 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
24999 }
25000 const ROOT_CLASS = "desktop-mode-url-dialog";
25001 let active = null;
25002 function closeUrlDialog() {
25003 if (!active) {
25004 return;
25005 }
25006 active.dispatchEvent(new CustomEvent("url-dialog-closed"));
25007 active.remove();
25008 active = null;
25009 doAction("desktop-mode.files.url-dialog.closed", {});
25010 }
25011 function openUrlDialog(options) {
25012 closeUrlDialog();
25013 const decision = applyFilters(
25014 "desktop-mode.files.url-dialog",
25015 null,
25016 options
25017 );
25018 if (decision === false) {
25019 return;
25020 }
25021 const overlay = document.createElement("div");
25022 overlay.className = `${ROOT_CLASS}__overlay desktop-mode-create-folder-dialog__overlay`;
25023 overlay.setAttribute("role", "presentation");
25024 const dialog2 = document.createElement("div");
25025 dialog2.className = `${ROOT_CLASS} desktop-mode-create-folder-dialog`;
25026 dialog2.setAttribute("role", "dialog");
25027 dialog2.setAttribute("aria-modal", "true");
25028 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS}-title`);
25029 const title = document.createElement("h2");
25030 title.id = `${ROOT_CLASS}-title`;
25031 title.className = "desktop-mode-create-folder-dialog__title";
25032 title.textContent = options.title;
25033 dialog2.appendChild(title);
25034 if (options.description) {
25035 const desc = document.createElement("p");
25036 desc.className = `${ROOT_CLASS}__description`;
25037 desc.textContent = options.description;
25038 dialog2.appendChild(desc);
25039 }
25040 const nameField = document.createElement("wpd-text-field");
25041 nameField.setAttribute("label", options.nameLabel ?? "Name");
25042 nameField.setAttribute("value", options.initialName ?? "");
25043 nameField.setAttribute("placeholder", "My web app");
25044 nameField.setAttribute("autocomplete", "off");
25045 dialog2.appendChild(nameField);
25046 const urlField = document.createElement("wpd-text-field");
25047 urlField.setAttribute("label", options.urlLabel ?? "URL");
25048 urlField.setAttribute("value", options.initialUrl ?? "https://");
25049 urlField.setAttribute("placeholder", "https://example.com");
25050 urlField.setAttribute("type", "url");
25051 urlField.setAttribute("autocomplete", "off");
25052 dialog2.appendChild(urlField);
25053 const error = document.createElement("p");
25054 error.className = "desktop-mode-create-folder-dialog__error";
25055 error.hidden = true;
25056 error.setAttribute("role", "alert");
25057 dialog2.appendChild(error);
25058 const actions = document.createElement("div");
25059 actions.className = "desktop-mode-create-folder-dialog__actions";
25060 const cancel = document.createElement("button");
25061 cancel.type = "button";
25062 cancel.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--secondary";
25063 cancel.textContent = "Cancel";
25064 const submit = document.createElement("button");
25065 submit.type = "button";
25066 submit.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--primary";
25067 submit.textContent = options.submitLabel ?? "Create";
25068 actions.appendChild(cancel);
25069 actions.appendChild(submit);
25070 dialog2.appendChild(actions);
25071 overlay.appendChild(dialog2);
25072 document.body.appendChild(overlay);
25073 active = overlay;
25074 queueMicrotask(() => {
25075 const input = nameField.shadowRoot?.querySelector("input");
25076 input?.focus();
25077 input?.select();
25078 });
25079 doAction("desktop-mode.files.url-dialog.opened", {});
25080 const readValue = (field) => {
25081 const v = field.value;
25082 if (typeof v === "string") {
25083 return v;
25084 }
25085 return field.shadowRoot?.querySelector("input")?.value ?? "";
25086 };
25087 const setBusy = (busy) => {
25088 nameField.disabled = busy;
25089 urlField.disabled = busy;
25090 cancel.disabled = busy;
25091 submit.disabled = busy;
25092 dialog2.classList.toggle("desktop-mode-create-folder-dialog--busy", busy);
25093 };
25094 const showError = (msg) => {
25095 error.textContent = msg;
25096 error.hidden = false;
25097 };
25098 const doCancel = () => {
25099 closeUrlDialog();
25100 options.onCancel?.();
25101 };
25102 const doSubmit = async () => {
25103 const url = readValue(urlField).trim();
25104 if (!url) {
25105 showError("Please enter a URL.");
25106 return;
25107 }
25108 const finalUrl = /^[a-z][a-z0-9+\-.]*:/i.test(url) ? url : `https://${url}`;
25109 try {
25110 new URL(finalUrl);
25111 } catch {
25112 showError("That doesn't look like a valid URL.");
25113 return;
25114 }
25115 const name = readValue(nameField).trim();
25116 error.hidden = true;
25117 setBusy(true);
25118 try {
25119 await options.onSubmit({ name, url: finalUrl });
25120 closeUrlDialog();
25121 } catch (err) {
25122 setBusy(false);
25123 showError(err instanceof Error ? err.message : "Could not save.");
25124 }
25125 };
25126 cancel.addEventListener("click", () => doCancel());
25127 submit.addEventListener("click", () => void doSubmit());
25128 overlay.addEventListener("click", (e) => {
25129 if (e.target === overlay) {
25130 doCancel();
25131 }
25132 });
25133 const onKey = (e) => {
25134 if (e.key === "Escape") {
25135 e.preventDefault();
25136 doCancel();
25137 } else if (e.key === "Enter" && !e.isComposing) {
25138 e.preventDefault();
25139 void doSubmit();
25140 }
25141 };
25142 dialog2.addEventListener("keydown", onKey);
25143 overlay.addEventListener("url-dialog-closed", () => {
25144 dialog2.removeEventListener("keydown", onKey);
25145 });
25146 }
25147 const _earlyReadyQueue = [];
25148 let _earlyReady = false;
25149 (function installEarlyDesktopShim() {
25150 const w = window;
25151 if (!w.wp) {
25152 w.wp = {};
25153 }
25154 if (w.wp.desktop) {
25155 return;
25156 }
25157 const shim = {
25158 whenReady(cb) {
25159 if (typeof cb !== "function") {
25160 return;
25161 }
25162 if (_earlyReady) {
25163 Promise.resolve().then(cb);
25164 return;
25165 }
25166 _earlyReadyQueue.push(cb);
25167 },
25168 ready(cb) {
25169 shim.whenReady(cb);
25170 },
25171 isReady() {
25172 return _earlyReady;
25173 }
25174 };
25175 w.wp.desktop = shim;
25176 })();
25177 const OS_SETTINGS_WINDOW_ID = "desktop-mode-os-settings";
25178 function init() {
25179 const config = window.desktopModeConfig;
25180 if (!config) {
25181 return;
25182 }
25183 const desktopArea = document.getElementById("desktop-mode-area");
25184 if (!desktopArea) {
25185 return;
25186 }
25187 const manager = new WindowManager(desktopArea);
25188 const wallpaperEl = document.getElementById("desktop-mode-wallpaper");
25189 const pluginUrl = config.pluginUrl || "";
25190 let wallpaperLayer = null;
25191 if (wallpaperEl) {
25192 wallpaperLayer = new WallpaperLayer(wallpaperEl, pluginUrl);
25193 }
25194 const widgetsEl = document.getElementById("desktop-mode-widgets");
25195 let widgetLayer = null;
25196 registerBuiltInWidgets();
25197 installDefaultDockRailRenderer();
25198 if (widgetsEl) {
25199 widgetLayer = new WidgetLayer(widgetsEl, pluginUrl);
25200 }
25201 registerModule({
25202 id: "pixijs",
25203 url: `${pluginUrl}/assets/vendor/pixi.min.js`,
25204 isReady: () => typeof window.PIXI !== "undefined"
25205 });
25206 const osSettings = new OsSettings(
25207 {
25208 mediaUrl: config.mediaUrl,
25209 restNonce: config.restNonce,
25210 canUpload: !!config.canUpload,
25211 isAdmin: !!config.currentUserIsAdmin,
25212 aiPlatformSettings: config.aiPlatformSettings ?? null,
25213 aiPlatformSettingsUrl: config.aiPlatformSettingsUrl ?? "",
25214 extendedOptions: config.extendedOptions ?? null,
25215 extendedOptionsUrl: config.extendedOptionsUrl ?? "",
25216 osSettingsPanelBundleUrl: config.osSettingsPanelBundleUrl ?? ""
25217 },
25218 wallpaperLayer ?? new WallpaperLayer(document.createElement("div"), pluginUrl)
25219 );
25220 osSettings.apply();
25221 const aiAssistant = new AiAssistantStub(
25222 {
25223 aiSearchUrl: config.aiSearchUrl ?? "",
25224 aiSearchStreamUrl: config.aiSearchStreamUrl ?? "",
25225 restNonce: config.restNonce,
25226 // Transport picker lives in OS Settings → AI Settings. Read
25227 // live (not captured at construction) so a change applies on
25228 // the next search without a page reload.
25229 getTransport: () => osSettings.getOsSettingsSnapshot().ai.transport
25230 },
25231 config.aiAssistantBundleUrl ?? ""
25232 );
25233 aiAssistant.attachAsk(
25234 createAsk({
25235 config: () => config,
25236 fallbackContext: () => ({
25237 close: () => aiAssistant.close(),
25238 openInWindow: (url, title, icon) => {
25239 manager.open({
25240 url,
25241 title,
25242 icon: icon ?? "dashicons-admin-generic"
25243 });
25244 },
25245 confirm: (msg) => wpdConfirm({ message: msg })
25246 })
25247 })
25248 );
25249 const dragBridge = new DragBridge();
25250 const dragManager = new DragManager();
25251 document.addEventListener(DRAG_EVENTS.START, (e) => {
25252 const detail = e.detail;
25253 const payload = detail?.payload;
25254 if (!payload) {
25255 return;
25256 }
25257 if (payload.type !== "shortcut" && payload.type !== "desktop-file") {
25258 return;
25259 }
25260 const bridgePayload = payload.data?.bridgePayload;
25261 if (bridgePayload) {
25262 dragBridge.start(bridgePayload);
25263 }
25264 });
25265 document.addEventListener(DRAG_EVENTS.END, () => {
25266 dragBridge.end();
25267 });
25268 installIframeDropTargets(dragManager);
25269 window.addEventListener("message", (e) => {
25270 if (e.origin !== window.location.origin) {
25271 return;
25272 }
25273 const data = e.data;
25274 if (!data || data.type !== "desktop-mode-drop-failed") {
25275 return;
25276 }
25277 showToast({
25278 message: "Could not insert into the editor."
25279 });
25280 });
25281 registerPalette({
25282 id: "desktop-mode-ai-assistant",
25283 label: "AI Assistant",
25284 open: () => aiAssistant.open(),
25285 close: () => aiAssistant.close(),
25286 isOpen: () => aiAssistant.isOpen
25287 });
25288 installPaletteShortcut();
25289 installWindowSwitcherShortcut(manager);
25290 installDesktopArrowShortcuts(manager);
25291 new IframeCommandBridge({
25292 manager,
25293 adminUrl: config.adminUrl
25294 }).install();
25295 new ShellCommandHarvester({
25296 manager,
25297 adminUrl: config.adminUrl
25298 }).install();
25299 document.addEventListener("desktop-mode-open-ai", () => {
25300 openPaletteOnly("desktop-mode-ai-assistant");
25301 });
25302 const bottomDockEl = document.getElementById("desktop-mode-dock");
25303 const shellEl = document.getElementById("desktop-mode-shell");
25304 const shellBody = shellEl?.querySelector(
25305 ".desktop-mode-shell__body"
25306 );
25307 let layoutDispatcher = null;
25308 const nativeWindows = createNativeWindowSync({
25309 manager,
25310 appendSystemTile: (item) => layoutDispatcher?.appendSystemTile(item),
25311 removeSystemTile: (id) => layoutDispatcher?.removeSystemTile(id)
25312 });
25313 const syncNativeWindows = nativeWindows.sync;
25314 bindNativeUrlRemap({
25315 getSnapshot: () => osSettings.getOsSettingsSnapshot(),
25316 openById: (id) => nativeWindows.openById(id),
25317 adminUrl: config.adminUrl
25318 });
25319 const findDockEntryForUrl2 = (url) => {
25320 const targetSlug = deriveWindowId(url, config.adminUrl);
25321 const items = layoutDispatcher ? layoutDispatcher.getMenuItems() : config.dockItems ?? [];
25322 for (const item of items) {
25323 if (deriveWindowId(item.url, config.adminUrl) === targetSlug) {
25324 return {
25325 title: item.title,
25326 icon: item.icon,
25327 url: item.url,
25328 submenu: item.submenu,
25329 multi: item.multi
25330 };
25331 }
25332 for (const sub of item.submenu ?? []) {
25333 if (deriveWindowId(sub.url, config.adminUrl) === targetSlug) {
25334 return {
25335 title: sub.title,
25336 // Sub-menu entries inherit the parent tile's
25337 // icon — that's the dock's own convention and
25338 // avoids painting a generic glyph on a window
25339 // the user knows by its parent's identity.
25340 icon: item.icon,
25341 // `url` holds the PARENT tile's landing page, so
25342 // the new window's synthetic "back to parent"
25343 // tab links to the dock URL (themes.php) rather
25344 // than to the sub-page itself.
25345 url: item.url,
25346 multi: item.multi
25347 };
25348 }
25349 }
25350 }
25351 return null;
25352 };
25353 bindAdminLinkDispatch({
25354 adminUrl: config.adminUrl,
25355 deriveSlug: (url) => deriveWindowId(url, config.adminUrl),
25356 openWindow: (windowConfig) => {
25357 void manager.open(windowConfig);
25358 },
25359 findDockEntry: findDockEntryForUrl2
25360 });
25361 registerNativeUrlRemap({
25362 id: "desktop-mode-posts",
25363 nativeWindowId: "desktop-mode-posts",
25364 matches: (_url, parsed) => {
25365 if (!parsed.pathname.endsWith("/edit.php")) {
25366 return false;
25367 }
25368 const postType = parsed.searchParams.get("post_type");
25369 return !postType || postType === "post";
25370 },
25371 enabled: (snapshot) => snapshot.nativePostsEnabled === true
25372 });
25373 registerNativeUrlRemap({
25374 id: "desktop-mode-pages",
25375 nativeWindowId: "desktop-mode-pages",
25376 matches: (_url, parsed) => {
25377 if (!parsed.pathname.endsWith("/edit.php")) {
25378 return false;
25379 }
25380 return parsed.searchParams.get("post_type") === "page";
25381 },
25382 enabled: (snapshot) => snapshot.nativePagesEnabled === true
25383 });
25384 registerNativeUrlRemap({
25385 id: "desktop-mode-users",
25386 nativeWindowId: "desktop-mode-users",
25387 matches: (_url, parsed) => parsed.pathname.endsWith("/users.php"),
25388 enabled: (snapshot) => snapshot.nativeUsersEnabled === true
25389 });
25390 registerNativeUrlRemap({
25391 id: "desktop-mode-user-edit",
25392 nativeWindowId: "desktop-mode-user-edit",
25393 matches: (_url, parsed) => {
25394 const path = parsed.pathname;
25395 if (path.endsWith("/profile.php")) {
25396 return true;
25397 }
25398 if (path.endsWith("/user-edit.php")) {
25399 return parsed.searchParams.has("user_id");
25400 }
25401 return false;
25402 },
25403 enabled: (snapshot) => snapshot.nativeUsersEnabled === true,
25404 onMatch: (_url, parsed) => {
25405 const userId = parseInt(
25406 parsed.searchParams.get("user_id") ?? "0",
25407 10
25408 );
25409 if (userId > 0) {
25410 setUserEditTarget(userId);
25411 }
25412 }
25413 });
25414 registerNativeUrlRemap({
25415 id: "desktop-mode-comments",
25416 nativeWindowId: "desktop-mode-comments",
25417 matches: (_url, parsed) => parsed.pathname.endsWith("/edit-comments.php"),
25418 enabled: (snapshot) => snapshot.nativeCommentsEnabled === true
25419 });
25420 registerNativeUrlRemap({
25421 id: "desktop-mode-plugins",
25422 nativeWindowId: "desktop-mode-plugins",
25423 matches: (_url, parsed) => {
25424 const path = parsed.pathname;
25425 return path.endsWith("/plugins.php") || path.endsWith("/plugin-install.php");
25426 },
25427 enabled: (snapshot) => snapshot.nativePluginsEnabled === true,
25428 onMatch: (_url, parsed) => {
25429 const tab = parsed.pathname.endsWith("/plugin-install.php") ? "browse" : "installed";
25430 void Promise.resolve().then(() => tabTarget).then((m) => {
25431 m.setPluginsWindowTab(tab);
25432 });
25433 }
25434 });
25435 if (bottomDockEl && shellEl && shellBody && config.dockItems) {
25436 desktopArea.classList.add("desktop-mode-area--with-dock");
25437 const initialLayout = osSettings.getOsSettingsSnapshot().desktopLayout;
25438 const renderIcons2 = (icons) => {
25439 renderDesktopIcons(desktopArea, icons, {
25440 openWindow: nativeWindows.openById,
25441 manager
25442 });
25443 };
25444 layoutDispatcher = createLayoutDispatcher(
25445 {
25446 shellRoot: shellEl,
25447 shellBody,
25448 bottomDockEl,
25449 desktopArea,
25450 windowManager: manager,
25451 adminUrl: config.adminUrl,
25452 renderIcons: renderIcons2,
25453 getSettings: () => {
25454 const snap = osSettings.getOsSettingsSnapshot();
25455 return {
25456 itemVisibility: snap.itemVisibility,
25457 dockOrder: snap.dockOrder
25458 };
25459 }
25460 },
25461 initialLayout,
25462 config.dockItems,
25463 config.desktopIcons
25464 );
25465 layoutDispatcher.appendSystemTile(
25466 {
25467 id: OS_SETTINGS_WINDOW_ID,
25468 title: "OS Settings",
25469 icon: "dashicons-desktop",
25470 // "Open" for the dock dot means "open on the currently
25471 // active desktop." OS Settings on another desktop
25472 // shouldn't paint the dot on the active view.
25473 isOpen: () => {
25474 const win = manager.getById(OS_SETTINGS_WINDOW_ID);
25475 if (!win) {
25476 return false;
25477 }
25478 return (win.config.desktopId || manager.getActiveDesktopId()) === manager.getActiveDesktopId();
25479 },
25480 onOpen: openOsSettings
25481 },
25482 "core"
25483 );
25484 if (!isStandaloneDisplay()) {
25485 layoutDispatcher.appendSystemTile(
25486 getInstallTileDef(
25487 config.pwa?.appName || "WordPress",
25488 showToast
25489 ),
25490 "core"
25491 );
25492 }
25493 window.matchMedia("(display-mode: standalone)").addEventListener("change", (e) => {
25494 if (e.matches) {
25495 layoutDispatcher?.removeSystemTile(
25496 "desktop-mode-pwa-install"
25497 );
25498 }
25499 });
25500 void isLikelyInstalled().then((installed2) => {
25501 if (installed2) {
25502 layoutDispatcher?.removeSystemTile(
25503 "desktop-mode-pwa-install"
25504 );
25505 }
25506 });
25507 }
25508 function openOsSettings() {
25509 void manager.open({
25510 id: OS_SETTINGS_WINDOW_ID,
25511 baseId: OS_SETTINGS_WINDOW_ID,
25512 url: "#os-settings",
25513 title: "OS Settings",
25514 icon: "dashicons-desktop",
25515 native: true,
25516 render: (body) => osSettings.renderPanel(body),
25517 width: 820,
25518 height: 720,
25519 minWidth: 560,
25520 minHeight: 480
25521 });
25522 }
25523 function openBugReport() {
25524 void manager.open({
25525 id: BUG_REPORT_WINDOW_ID,
25526 baseId: BUG_REPORT_WINDOW_ID,
25527 url: `#${BUG_REPORT_WINDOW_ID}`,
25528 title: "Report a bug",
25529 icon: "dashicons-buddicons-replies",
25530 native: true,
25531 render: (body) => renderBugReport(body),
25532 width: 560,
25533 height: 620,
25534 minWidth: 420,
25535 minHeight: 480
25536 });
25537 }
25538 document.addEventListener("desktop-mode-open-bug-report", () => {
25539 openBugReport();
25540 });
25541 if (layoutDispatcher) {
25542 layoutDispatcher.appendSystemTile(
25543 {
25544 id: BUG_REPORT_WINDOW_ID,
25545 title: "Report a bug",
25546 icon: "dashicons-buddicons-replies",
25547 isOpen: () => {
25548 const win = manager.getById(BUG_REPORT_WINDOW_ID);
25549 if (!win) {
25550 return false;
25551 }
25552 return (win.config.desktopId || manager.getActiveDesktopId()) === manager.getActiveDesktopId();
25553 },
25554 onOpen: openBugReport
25555 },
25556 "core"
25557 );
25558 layoutDispatcher.appendSystemTile(
25559 getExitDesktopModeTileDef(),
25560 "core"
25561 );
25562 }
25563 const dock = layoutDispatcher?.getPrimary() ?? null;
25564 void syncNativeWindows(
25565 Array.isArray(config.nativeWindows) ? config.nativeWindows : []
25566 );
25567 const hasSession = !!(config.session && config.session.windows && config.session.windows.length > 0);
25568 const sessionRestore = hasSession ? restoreSession(manager, config, desktopArea).catch((err) => {
25569 if (typeof console !== "undefined") {
25570 console.error("[desktop-mode] session restore failed:", err);
25571 }
25572 }) : Promise.resolve();
25573 const defaultEnabled = config.defaultWindow?.enabled !== false;
25574 const defaultUrlEarly = config.defaultWindow?.url ?? "";
25575 const isNativeDefault = typeof defaultUrlEarly === "string" && defaultUrlEarly.startsWith("native:");
25576 if (shouldAutoOpenCurrentPage({
25577 fromPortal: config.fromPortal,
25578 fromPortalIntent: config.fromPortalIntent,
25579 hasSession,
25580 defaultEnabled,
25581 isNativeDefault
25582 })) {
25583 void sessionRestore.then(
25584 () => openCurrentPage(manager, config).catch((err) => {
25585 if (typeof console !== "undefined") {
25586 console.error("[desktop-mode] openCurrentPage failed:", err);
25587 }
25588 })
25589 );
25590 }
25591 const saveSession = createSessionSaver(manager, config);
25592 wireSessionEvents(saveSession);
25593 const setDefaultWindow = async (url) => {
25594 try {
25595 const response = await trackedFetch(
25596 manager,
25597 config.defaultWindowUrl,
25598 {
25599 method: "POST",
25600 credentials: "same-origin",
25601 headers: {
25602 "Content-Type": "application/json",
25603 "X-WP-Nonce": config.restNonce
25604 },
25605 body: JSON.stringify({ url })
25606 },
25607 { source: "desktop-mode/default-window" }
25608 );
25609 if (!response.ok) {
25610 throw new Error(`HTTP ${response.status}`);
25611 }
25612 const data = await response.json();
25613 config.defaultWindow = data;
25614 document.dispatchEvent(
25615 new CustomEvent("desktop-mode-default-window-changed", {
25616 detail: data
25617 })
25618 );
25619 } catch (err) {
25620 doAction(HOOKS.SHELL_ERROR, { scope: "default-window-save", error: err });
25621 if (typeof console !== "undefined") {
25622 console.error(
25623 "[desktop-mode] Failed to save default window:",
25624 err
25625 );
25626 }
25627 }
25628 };
25629 manager.onToggleStartupRequested = (win) => {
25630 const currentPref = config.defaultWindow;
25631 const isNative = !!win.config.native;
25632 const winValue = isNative ? `native:${win.id}` : win.getCurrentUrl();
25633 const matchesCurrent = isNative ? currentPref?.url === winValue : urlMatchKey(currentPref?.url ?? "") === urlMatchKey(winValue);
25634 const alreadyDefault = !!currentPref?.enabled && matchesCurrent;
25635 void setDefaultWindow(alreadyDefault ? null : winValue);
25636 };
25637 if (config.defaultWindow?.enabled && config.fromPortal && !config.fromPortalIntent && !hasSession && isNativeDefault) {
25638 const nativeId = defaultUrlEarly.slice("native:".length);
25639 queueMicrotask(() => {
25640 if (nativeId === OS_SETTINGS_WINDOW_ID) {
25641 openOsSettings();
25642 return;
25643 }
25644 void nativeWindows.openById(nativeId);
25645 });
25646 }
25647 const placeSystemTile = (item) => {
25648 layoutDispatcher?.appendSystemTile(item);
25649 };
25650 const syncServerWidgets = createWidgetRegistrySync({
25651 layer: widgetLayer
25652 });
25653 void syncServerWidgets(
25654 Array.isArray(config.serverWidgets) ? config.serverWidgets : []
25655 );
25656 const syncServerWallpapers = createWallpaperRegistrySync({
25657 osSettings
25658 });
25659 void syncServerWallpapers(
25660 Array.isArray(config.serverWallpapers) ? config.serverWallpapers : []
25661 );
25662 const syncServerCommands = createCommandRegistrySync();
25663 void syncServerCommands(
25664 Array.isArray(config.serverCommandScripts) ? config.serverCommandScripts : [],
25665 Array.isArray(config.serverCommands) ? config.serverCommands : []
25666 );
25667 const syncServerSettingsTabs = createSettingsTabRegistrySync();
25668 void syncServerSettingsTabs(
25669 Array.isArray(config.serverSettingsTabScripts) ? config.serverSettingsTabScripts : [],
25670 Array.isArray(config.serverSettingsTabs) ? config.serverSettingsTabs : []
25671 );
25672 const syncServerTitleBarButtons = createTitleBarButtonRegistrySync();
25673 void syncServerTitleBarButtons(
25674 Array.isArray(config.serverTitleBarButtonScripts) ? config.serverTitleBarButtonScripts : []
25675 );
25676 const syncServerDockRailRenderers = createDockRailRendererSync();
25677 void syncServerDockRailRenderers(
25678 Array.isArray(config.serverDockRailRendererScripts) ? config.serverDockRailRendererScripts : []
25679 );
25680 const syncServerWindowThemes = createWindowThemeRegistrySync();
25681 void syncServerWindowThemes(
25682 Array.isArray(config.serverWindowThemeScripts) ? config.serverWindowThemeScripts : [],
25683 Array.isArray(config.serverWindowThemes) ? config.serverWindowThemes : []
25684 );
25685 registerBuiltInControls();
25686 const syncServerWindowControls = createWindowControlRegistrySync();
25687 void syncServerWindowControls(
25688 Array.isArray(config.serverWindowControlScripts) ? config.serverWindowControlScripts : [],
25689 Array.isArray(config.serverWindowControls) ? config.serverWindowControls : []
25690 );
25691 const syncServerWindowSlots = createWindowSlotRegistrySync();
25692 void syncServerWindowSlots(
25693 Array.isArray(config.serverWindowSlotScripts) ? config.serverWindowSlotScripts : [],
25694 Array.isArray(config.serverWindowSlots) ? config.serverWindowSlots : []
25695 );
25696 applyServerWindowNotices(
25697 Array.isArray(config.serverWindowNotices) ? config.serverWindowNotices : []
25698 );
25699 const syncServerWindowChromes = createWindowChromeRegistrySync();
25700 void syncServerWindowChromes(
25701 Array.isArray(config.serverWindowChromeScripts) ? config.serverWindowChromeScripts : [],
25702 Array.isArray(config.serverWindowChromes) ? config.serverWindowChromes : []
25703 );
25704 const connectionBridge = createConnectionBridge(manager);
25705 attachBroadcastBus(manager);
25706 installBroadcastReceiver();
25707 installWindowLoadingTransitions();
25708 addAction(
25709 "desktop-mode.shell.toast",
25710 "desktop-mode/shell-toast",
25711 (payload) => {
25712 if (!payload || typeof payload.message !== "string") {
25713 return;
25714 }
25715 showToast({
25716 message: payload.message,
25717 action: payload.action,
25718 duration: payload.duration
25719 });
25720 }
25721 );
25722 const cfgWithBin = config;
25723 const cfgCountRaw = cfgWithBin.recycleBinCount;
25724 startRecycleBinBadge(
25725 Number(cfgCountRaw) || 0,
25726 typeof cfgWithBin.recycleBinCountUrl === "string" ? cfgWithBin.recycleBinCountUrl : ""
25727 );
25728 registerBuiltInPeekRenderers({
25729 getRecycleBinCount: _currentRecycleBinBadge
25730 });
25731 window.__desktopModeConnectionBridge = connectionBridge;
25732 addAction(HOOKS.WINDOW_CLOSED, "desktop-mode/connection-cleanup", (e) => {
25733 if (e?.windowId) {
25734 connectionBridge.onWindowClosed(e.windowId);
25735 }
25736 });
25737 addAction(HOOKS.IFRAME_READY, "desktop-mode/connection-rearm", (e) => {
25738 if (e?.windowId) {
25739 connectionBridge.onIframeReady(e.windowId);
25740 }
25741 });
25742 const registerWindow = createRegisterWindow(manager);
25743 const renderIcons = (icons) => {
25744 if (layoutDispatcher) {
25745 layoutDispatcher.applyDesktopIcons(icons);
25746 return;
25747 }
25748 renderDesktopIcons(desktopArea, icons, {
25749 openWindow: nativeWindows.openById,
25750 manager
25751 });
25752 };
25753 const refreshMenu = bindMenuRefresh({
25754 layoutDispatcher,
25755 config,
25756 syncNativeWindows,
25757 syncServerWidgets,
25758 syncServerWallpapers,
25759 syncServerCommands,
25760 syncServerSettingsTabs,
25761 syncServerTitleBarButtons,
25762 syncServerDockRailRenderers,
25763 renderIcons
25764 });
25765 osSettings.subscribeOsSettings((snapshot) => {
25766 if (!layoutDispatcher) {
25767 return;
25768 }
25769 const prevLayout = layoutDispatcher.getLayout();
25770 layoutDispatcher.setLayout(snapshot.desktopLayout);
25771 desktopApi.dock = layoutDispatcher.getPrimary();
25772 desktopApi.sideDock = layoutDispatcher.getSide();
25773 desktopApi.desktopLayout = snapshot.desktopLayout;
25774 if (prevLayout === snapshot.desktopLayout) {
25775 layoutDispatcher.refresh();
25776 }
25777 syncShortcutsWithVisibility(
25778 snapshot.itemVisibility,
25779 snapshot.dockPromotedPositions
25780 );
25781 setCurrentLayout(snapshot.desktopLayout);
25782 });
25783 installShortcutsSync(
25784 () => osSettings.getOsSettingsSnapshot().itemVisibility,
25785 () => osSettings.getOsSettingsSnapshot().dockPromotedPositions
25786 );
25787 setCurrentLayout(osSettings.getOsSettingsSnapshot().desktopLayout);
25788 const desktopApi = buildPublicApi({
25789 manager,
25790 dock,
25791 layoutDispatcher,
25792 osSettings,
25793 iconsApi,
25794 filesApi,
25795 saveSession,
25796 widgetLayer,
25797 registerWindow,
25798 openWindowById: nativeWindows.openById,
25799 openNewWindowById: nativeWindows.openNewById,
25800 placeSystemTile,
25801 setDefaultWindow,
25802 refreshMenu,
25803 openOsSettings,
25804 aiAssistant,
25805 dragBridge,
25806 dragManager,
25807 connect: connectionBridge.connect,
25808 config
25809 });
25810 installPublicApi(desktopApi);
25811 installRecycleBinDropTargets(dragManager);
25812 bootHeartbeatBus();
25813 bootNonceRefresh();
25814 installOpenDeps({
25815 openUrl: ({ id, url, title, icon }) => {
25816 if (tryNativeUrlRemap(url)) {
25817 return true;
25818 }
25819 void manager.open({ id, baseId: id, url, title, icon });
25820 return true;
25821 },
25822 openNativeWindow: (id) => nativeWindows.openById(id),
25823 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
25824 });
25825 setUserAssociations(
25826 config.userFileAssociations ?? {}
25827 );
25828 if (typeof config.filesUrl === "string" && config.filesUrl) {
25829 installRestDeps({
25830 baseUrl: config.filesUrl,
25831 nonce: config.restNonce
25832 });
25833 const rootHost = document.getElementById("desktop-mode-area");
25834 if (rootHost) {
25835 const layerHandle = mountFilesLayer(rootHost, 0);
25836 const reveal = () => {
25837 if (!desktopArea.classList.contains("desktop-mode-area--booting")) {
25838 return;
25839 }
25840 requestAnimationFrame(() => {
25841 desktopArea.classList.remove("desktop-mode-area--booting");
25842 });
25843 };
25844 const safetyTimer = setTimeout(reveal, 2e3);
25845 void layerHandle.hydrated.then(() => {
25846 clearTimeout(safetyTimer);
25847 reveal();
25848 });
25849 }
25850 }
25851 startFilesHeartbeat();
25852 startFilesRestoreSync();
25853 bootPresenceProbe();
25854 doAction(HOOKS.COMPONENTS_REGISTERED, { tags: [...WPD_COMPONENT_TAGS] });
25855 registerBuiltInCommands();
25856 bootstrapPwa(config, showToast);
25857 const overlayPreload = () => {
25858 preloadShellOverlays(config.shellOverlaysBundleUrl ?? "");
25859 preloadWindowSystem(config.windowSystemBundleUrl ?? "");
25860 };
25861 if (typeof window.requestIdleCallback === "function") {
25862 window.requestIdleCallback(overlayPreload, { timeout: 1500 });
25863 } else {
25864 window.setTimeout(overlayPreload, 0);
25865 }
25866 doAction(HOOKS.INIT, { config });
25867 _earlyReady = true;
25868 const queued = _earlyReadyQueue.splice(0);
25869 for (const cb of queued) {
25870 try {
25871 cb();
25872 } catch (err) {
25873 doAction(HOOKS.SHELL_ERROR, {
25874 scope: "when-ready-cb",
25875 error: err
25876 });
25877 if (typeof console !== "undefined") {
25878 console.error("[desktop-mode] whenReady cb threw:", err);
25879 }
25880 }
25881 }
25882 osSettings.apply();
25883 widgetLayer?.hydrate();
25884 window.addEventListener("pagehide", () => {
25885 wallpaperLayer?.teardownActive();
25886 widgetLayer?.disposeAll();
25887 });
25888 bindShellLifecycle();
25889 bindTopWindowLinkInterceptor(manager, config);
25890 const relayoutRoot = (transform, persist2 = true) => {
25891 const root = filesApi.store.getState().placementsByFolder.get(0) ?? [];
25892 const ordered = transform(root);
25893 const rowsPerCol = Math.max(
25894 1,
25895 Math.floor((desktopArea.clientHeight - 16) / 110)
25896 );
25897 const occupied = /* @__PURE__ */ new Set();
25898 let i = 0;
25899 for (const p of ordered) {
25900 const cell = snapToEmptyCell(
25901 16 + Math.floor(i / rowsPerCol) * 96,
25902 16 + i % rowsPerCol * 110,
25903 occupied,
25904 desktopArea
25905 );
25906 occupied.add(`${cell.col},${cell.row}`);
25907 i++;
25908 if (p.x === cell.x && p.y === cell.y) {
25909 continue;
25910 }
25911 filesApi.store.upsertPlacement({
25912 ...p,
25913 x: cell.x,
25914 y: cell.y,
25915 sortOrder: i
25916 });
25917 if (!persist2) {
25918 continue;
25919 }
25920 void updatePlacement(p.id, {
25921 x: cell.x,
25922 y: cell.y,
25923 sortOrder: i
25924 }).catch((err) => {
25925 console.error("[desktop-mode] relayout persist failed", err);
25926 });
25927 }
25928 };
25929 const rootSortTransform = (mode) => (arr) => {
25930 const sorted = arr.slice();
25931 switch (mode) {
25932 case "name-asc":
25933 sorted.sort(
25934 (a, b) => a.file.title.localeCompare(b.file.title)
25935 );
25936 break;
25937 case "name-desc":
25938 sorted.sort(
25939 (a, b) => b.file.title.localeCompare(a.file.title)
25940 );
25941 break;
25942 case "date-asc":
25943 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
25944 break;
25945 case "date-desc":
25946 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
25947 break;
25948 }
25949 return sorted;
25950 };
25951 const ROOT_SORT_MODE_KEY = "desktop-mode:root-sort-mode";
25952 const isRootSortMode = (v) => v === "name-asc" || v === "name-desc" || v === "date-asc" || v === "date-desc";
25953 let rootSortMode = (() => {
25954 try {
25955 const raw = window.localStorage.getItem(ROOT_SORT_MODE_KEY);
25956 return isRootSortMode(raw) ? raw : null;
25957 } catch {
25958 return null;
25959 }
25960 })();
25961 const setRootSortMode = (mode) => {
25962 rootSortMode = mode;
25963 try {
25964 if (mode) {
25965 window.localStorage.setItem(ROOT_SORT_MODE_KEY, mode);
25966 } else {
25967 window.localStorage.removeItem(ROOT_SORT_MODE_KEY);
25968 }
25969 } catch {
25970 }
25971 };
25972 addAction(
25973 "desktop-mode.files.tile-manually-placed",
25974 "desktop-mode/root-sort-clear",
25975 (payload) => {
25976 const folderId = payload?.folderId;
25977 if (folderId === 0) {
25978 setRootSortMode(null);
25979 }
25980 }
25981 );
25982 if (typeof ResizeObserver !== "undefined") {
25983 let lastW = desktopArea.clientWidth;
25984 let lastH = desktopArea.clientHeight;
25985 const ro = new ResizeObserver(() => {
25986 if (!rootSortMode) {
25987 return;
25988 }
25989 const w = desktopArea.clientWidth;
25990 const h = desktopArea.clientHeight;
25991 if (w === lastW && h === lastH) {
25992 return;
25993 }
25994 lastW = w;
25995 lastH = h;
25996 relayoutRoot(rootSortTransform(rootSortMode), false);
25997 });
25998 ro.observe(desktopArea);
25999 }
26000 desktopArea.addEventListener("click", (e) => {
26001 if (!osSettings.state.showDesktopOnWallpaperClick) {
26002 return;
26003 }
26004 if (e.target !== desktopArea) {
26005 return;
26006 }
26007 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
26008 return;
26009 }
26010 if (isWallpaperMenuOpen()) {
26011 return;
26012 }
26013 if (dragManager.recentlyEndedDrag()) {
26014 return;
26015 }
26016 manager.toggleShowDesktop();
26017 });
26018 desktopArea.addEventListener("contextmenu", (e) => {
26019 if (e.target !== desktopArea) {
26020 return;
26021 }
26022 e.preventDefault();
26023 const clientX = e.clientX;
26024 const clientY = e.clientY;
26025 (() => {
26026 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
26027 return;
26028 }
26029 if (isWallpaperMenuOpen()) {
26030 closeWallpaperMenu();
26031 return;
26032 }
26033 const dropClient = { x: clientX, y: clientY };
26034 const cellAtClick = () => {
26035 const rect = desktopArea.getBoundingClientRect();
26036 const rawX = Math.max(0, dropClient.x - rect.left);
26037 const rawY = Math.max(0, dropClient.y - rect.top);
26038 const occupied = buildOccupiedSet(
26039 filesApi.store.getState().placementsByFolder.get(0) ?? []
26040 );
26041 return snapToEmptyCell(rawX, rawY, occupied, desktopArea);
26042 };
26043 const createUrlPlacement = (dialogTitle, description) => {
26044 openUrlDialog({
26045 title: dialogTitle,
26046 description,
26047 nameLabel: "Name",
26048 urlLabel: "URL",
26049 submitLabel: "Create",
26050 onSubmit: async ({ name, url }) => {
26051 const cell = cellAtClick();
26052 const placement = await createPlacement({
26053 type: "link",
26054 ref: url,
26055 parentId: 0,
26056 x: cell.x,
26057 y: cell.y,
26058 meta: name ? { name } : void 0
26059 });
26060 filesApi.store.upsertPlacement(placement);
26061 }
26062 });
26063 };
26064 const items = buildMenuItems({
26065 createFolder: () => {
26066 openCreateFolderDialog({
26067 onSubmit: async (name) => {
26068 const folder = await createFolder({ name });
26069 const cell = cellAtClick();
26070 const placement = await createPlacement({
26071 type: "folder",
26072 ref: String(folder.id),
26073 parentId: 0,
26074 x: cell.x,
26075 y: cell.y
26076 });
26077 filesApi.store.upsertFolder(folder);
26078 filesApi.store.upsertPlacement(placement);
26079 }
26080 });
26081 },
26082 createUrl: () => createUrlPlacement(
26083 "New URL",
26084 "Opens the URL in a new browser tab."
26085 ),
26086 toggleShowDesktop: () => manager.toggleShowDesktop(),
26087 openOsSettings: () => openOsSettings(),
26088 sortIcons: (mode) => {
26089 setRootSortMode(mode);
26090 relayoutRoot(rootSortTransform(mode));
26091 },
26092 currentSortMode: rootSortMode,
26093 includeShowDesktop: !osSettings.state.showDesktopOnWallpaperClick,
26094 labels: {
26095 createFolder: "New folder",
26096 showDesktop: "Show desktop",
26097 osSettings: "OS Settings",
26098 sortHeading: "Sort by",
26099 sortNameAsc: "Name (A → Z)",
26100 sortNameDesc: "Name (Z → A)",
26101 sortDateAsc: "Date (oldest first)",
26102 sortDateDesc: "Date (newest first)",
26103 newUrl: "New URL"
26104 },
26105 serverItems: config.serverWallpaperMenuItems ?? []
26106 });
26107 openWallpaperMenu(
26108 document.body,
26109 { x: clientX, y: clientY },
26110 items
26111 );
26112 })();
26113 });
26114 void Promise.resolve().then(() => index).then((mod) => {
26115 mod.bootOsFileDrop({
26116 config: config.dropConfig,
26117 mediaUrl: config.mediaUrl,
26118 restNonce: config.restNonce
26119 });
26120 });
26121 document.dispatchEvent(
26122 new CustomEvent("desktop-mode-init", {
26123 detail: { config, restored: hasSession }
26124 })
26125 );
26126 }
26127 startMissingImportWarner();
26128 if (document.readyState === "loading") {
26129 document.addEventListener("DOMContentLoaded", init);
26130 } else {
26131 init();
26132 }
26133 const _initial = {
26134 tab: null,
26135 requestedAt: 0
26136 };
26137 let _store = null;
26138 function getStore() {
26139 if (_store) {
26140 return _store;
26141 }
26142 const w = window;
26143 const factory = w.wp?.desktop?.createSharedStore;
26144 if (typeof factory !== "function") {
26145 return null;
26146 }
26147 _store = factory(
26148 "desktop-mode/plugins-window/tab-target",
26149 () => ({ ..._initial })
26150 );
26151 return _store;
26152 }
26153 function setPluginsWindowTab(tab) {
26154 const store2 = getStore();
26155 if (store2) {
26156 store2.state.tab = tab;
26157 store2.state.requestedAt = Date.now();
26158 store2.notify();
26159 return;
26160 }
26161 const w = window;
26162 w._wpdPluginsWindowTab = { tab, requestedAt: Date.now() };
26163 }
26164 function consumePluginsWindowTab() {
26165 const store2 = getStore();
26166 if (store2) {
26167 const tab = store2.state.tab;
26168 if (tab !== null) {
26169 store2.state.tab = null;
26170 store2.state.requestedAt = 0;
26171 store2.notify();
26172 }
26173 return tab;
26174 }
26175 const w = window;
26176 const prev = w._wpdPluginsWindowTab;
26177 if (prev) {
26178 w._wpdPluginsWindowTab = { tab: null, requestedAt: 0 };
26179 return prev.tab;
26180 }
26181 return null;
26182 }
26183 function subscribePluginsWindowTab(cb) {
26184 const store2 = getStore();
26185 if (!store2) {
26186 return () => {
26187 };
26188 }
26189 return store2.subscribe((state2) => cb({ ...state2 }));
26190 }
26191 const tabTarget = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
26192 __proto__: null,
26193 consumePluginsWindowTab,
26194 setPluginsWindowTab,
26195 subscribePluginsWindowTab
26196 }, Symbol.toStringTag, { value: "Module" }));
26197 const FILE_DROP_HOOKS = {
26198 /**
26199 * Filter — fires once per drop, after the manager has parsed
26200 * the OS `DataTransfer` into `File[]` and BEFORE the mime /
26201 * size filter runs.
26202 *
26203 * Signature: `(files: File[], ctx: DropContext) => File[]`.
26204 * Return an empty array to abort the drop silently.
26205 */
26206 FILES_DETECTED: "desktop-mode.drop.files-detected",
26207 /**
26208 * Action — fires after the mime / size filter has rejected
26209 * one or more files. Payload: `{ rejections: DropRejection[],
26210 * context: DropContext }`. The shell toasts a default message;
26211 * subscribers can surface a custom UX (a side panel with the
26212 * list, an analytics call).
26213 */
26214 FILES_REJECTED: "desktop-mode.drop.files-rejected",
26215 /**
26216 * Filter — fires per file before the upload dialog renders.
26217 * Receives `DropFileEntry` (the underlying file + the
26218 * manager's default `fields`). Mutate `fields` (or return a
26219 * new object) to change what the user sees in the form.
26220 *
26221 * Signature: `(entry: DropFileEntry, ctx: DropContext)
26222 * => DropFileEntry`.
26223 */
26224 DIALOG_FIELDS: "desktop-mode.drop.dialog-fields",
26225 /**
26226 * Filter — last call before the manager `POST`s to
26227 * `wp/v2/media`. Receives `{ file: File, fields:
26228 * DropDialogFields, mime: string }`. Return `null` to cancel
26229 * the upload entirely (e.g. a plugin handled it via a
26230 * different endpoint).
26231 *
26232 * Signature: `(payload, ctx: DropContext) => payload | null`.
26233 */
26234 BEFORE_UPLOAD: "desktop-mode.drop.before-upload",
26235 /**
26236 * Action — fires once `BEFORE_UPLOAD` has cleared and the XHR
26237 * is `open()`ed, immediately before `send()`. Payload:
26238 * `{ file: File, fields: DropDialogFields, context: DropContext,
26239 * abort: () => void }`. The `abort` handle aborts the in-flight
26240 * request; the manager rejects with `UploadAbortedError` and
26241 * fires `UPLOAD_FAILED` with that error.
26242 *
26243 * Pair with `UPLOAD_PROGRESS` to drive a progress UI; pair with
26244 * `AFTER_UPLOAD` / `UPLOAD_FAILED` to know when the upload ends.
26245 *
26246 * @since 0.31.0
26247 */
26248 UPLOAD_STARTED: "desktop-mode.drop.upload-started",
26249 /**
26250 * Action — fires for every `XMLHttpRequestUpload.progress` event.
26251 * Payload: `{ file: File, fields: DropDialogFields, context:
26252 * DropContext, loaded: number, total: number, indeterminate:
26253 * boolean }`. `total` is `0` and `indeterminate` is `true` when
26254 * the request body length isn't known (rare for multipart, but
26255 * possible on transcoding proxies); subscribers should treat
26256 * that as an indeterminate state.
26257 *
26258 * A synthetic 100%-loaded event is dispatched once the `upload`
26259 * stream emits `load` so a HUD can show a definite "wrapping up"
26260 * state while the server finishes the response.
26261 *
26262 * @since 0.31.0
26263 */
26264 UPLOAD_PROGRESS: "desktop-mode.drop.upload-progress",
26265 /**
26266 * Action — fires after a successful upload. Payload:
26267 * `{ file: File, result: DropUploadResult, fields:
26268 * DropDialogFields, context: DropContext }`.
26269 *
26270 * The `file` field carries the same `File` reference that
26271 * `UPLOAD_STARTED` / `UPLOAD_PROGRESS` exposed (i.e. the
26272 * payload returned by the `BEFORE_UPLOAD` filter, in case a
26273 * plugin swapped the file). Subscribers tracking per-file
26274 * state — progress HUDs, sequence counters — should match on
26275 * this identity rather than the filename: two drops of
26276 * `photo.jpg` from different folders would otherwise route
26277 * each other's success event to the wrong row.
26278 *
26279 * @since 0.31.0 the `file` field was added; pre-0.31.0 code
26280 * that destructured `{ result, fields, context }` keeps working.
26281 */
26282 AFTER_UPLOAD: "desktop-mode.drop.after-upload",
26283 /**
26284 * Action — fires after an upload fails. Payload:
26285 * `{ file: File, error: Error, context: DropContext }`.
26286 * `error` is an `UploadAbortedError` when the failure came
26287 * from the caller invoking the `abort()` handle on
26288 * `UPLOAD_STARTED`.
26289 *
26290 * `file` carries the same identity as `UPLOAD_STARTED` /
26291 * `UPLOAD_PROGRESS` / `AFTER_UPLOAD` — the post-`BEFORE_UPLOAD`
26292 * `File`, in case a plugin swapped it. Match by reference, not
26293 * filename: a HUD that keys its row map on the started-File
26294 * needs the same key here, otherwise the row stays stuck in
26295 * "running" after a failure when a `BEFORE_UPLOAD` filter
26296 * replaced the file.
26297 */
26298 UPLOAD_FAILED: "desktop-mode.drop.upload-failed"
26299 };
26300 const IFRAME_PASSTHROUGH_SELECTORS = [
26301 ".components-drop-zone",
26302 "[data-drop-zone]",
26303 ".uploader-window",
26304 ".media-frame-content"
26305 ];
26306 function dragHasFiles(ev) {
26307 const types = ev.dataTransfer?.types;
26308 if (!types) {
26309 return false;
26310 }
26311 const list2 = types;
26312 if (typeof list2.includes === "function") {
26313 return list2.includes("Files");
26314 }
26315 if (typeof list2.contains === "function") {
26316 return list2.contains("Files");
26317 }
26318 for (let i = 0; i < list2.length; i++) {
26319 if (list2[i] === "Files") {
26320 return true;
26321 }
26322 }
26323 return false;
26324 }
26325 function resolveWindowIdFromSource(source) {
26326 if (!source) {
26327 return void 0;
26328 }
26329 const iframes = document.querySelectorAll("iframe");
26330 for (const f of Array.from(iframes)) {
26331 if (f.contentWindow === source) {
26332 const host = f.closest("[data-window-id]");
26333 return host?.getAttribute("data-window-id") || void 0;
26334 }
26335 }
26336 return void 0;
26337 }
26338 function mountOsFileDropManager(opts) {
26339 const host = window;
26340 if (host.__desktopModeOsFileDropMounted) {
26341 return host.__desktopModeOsFileDropMounted;
26342 }
26343 if (!opts.config.enabled) {
26344 return mountNoOp();
26345 }
26346 const overlayEl = ensureDropOverlay();
26347 let dragDepth = 0;
26348 let dragWatchdog = null;
26349 const resetOverlay = () => {
26350 dragDepth = 0;
26351 overlayEl.classList.remove("is-active");
26352 if (dragWatchdog !== null) {
26353 clearTimeout(dragWatchdog);
26354 dragWatchdog = null;
26355 }
26356 };
26357 const bumpWatchdog = () => {
26358 if (dragWatchdog !== null) {
26359 clearTimeout(dragWatchdog);
26360 }
26361 dragWatchdog = setTimeout(resetOverlay, 250);
26362 };
26363 const onDragEnter = (ev) => {
26364 if (!dragHasFiles(ev)) {
26365 return;
26366 }
26367 ev.preventDefault();
26368 dragDepth++;
26369 overlayEl.classList.add("is-active");
26370 bumpWatchdog();
26371 };
26372 const onDragOver = (ev) => {
26373 if (!dragHasFiles(ev)) {
26374 return;
26375 }
26376 if (ev.defaultPrevented) {
26377 resetOverlay();
26378 return;
26379 }
26380 ev.preventDefault();
26381 if (ev.dataTransfer) {
26382 ev.dataTransfer.dropEffect = "copy";
26383 }
26384 bumpWatchdog();
26385 };
26386 const onDragLeave = () => {
26387 dragDepth = Math.max(0, dragDepth - 1);
26388 if (dragDepth === 0) {
26389 overlayEl.classList.remove("is-active");
26390 }
26391 };
26392 const onDrop = (ev) => {
26393 if (!dragHasFiles(ev)) {
26394 return;
26395 }
26396 if (ev.defaultPrevented) {
26397 resetOverlay();
26398 return;
26399 }
26400 ev.preventDefault();
26401 resetOverlay();
26402 const files = ev.dataTransfer?.files ? Array.from(ev.dataTransfer.files) : [];
26403 if (files.length === 0) {
26404 return;
26405 }
26406 const ctx = classifyDropTarget(ev);
26407 void handleFiles(files, ctx, opts);
26408 };
26409 const onDragEnd2 = () => resetOverlay();
26410 const onVisibilityChange = () => {
26411 if (document.visibilityState === "hidden") {
26412 resetOverlay();
26413 }
26414 };
26415 const onIframeMessage = (ev) => {
26416 if (ev.origin !== window.location.origin) {
26417 return;
26418 }
26419 const data = ev.data;
26420 if (!data || data.type !== "desktop-mode-os-file-drop") {
26421 return;
26422 }
26423 if (!Array.isArray(data.files) || data.files.length === 0) {
26424 return;
26425 }
26426 const files = data.files.filter((f) => f instanceof File);
26427 if (files.length === 0) {
26428 return;
26429 }
26430 const windowId = resolveWindowIdFromSource(ev.source);
26431 if (!windowId) {
26432 return;
26433 }
26434 const ctx = {
26435 surface: "iframe",
26436 windowId,
26437 x: typeof data.x === "number" ? data.x : 0,
26438 y: typeof data.y === "number" ? data.y : 0
26439 };
26440 dragDepth = 0;
26441 overlayEl.classList.remove("is-active");
26442 void handleFiles(files, ctx, opts);
26443 };
26444 window.addEventListener("dragenter", onDragEnter);
26445 window.addEventListener("dragover", onDragOver);
26446 window.addEventListener("dragleave", onDragLeave);
26447 window.addEventListener("drop", onDrop);
26448 window.addEventListener("dragend", onDragEnd2);
26449 document.addEventListener("visibilitychange", onVisibilityChange);
26450 window.addEventListener("blur", onDragEnd2);
26451 window.addEventListener("message", onIframeMessage);
26452 const manager = {
26453 dispose: () => {
26454 window.removeEventListener("dragenter", onDragEnter);
26455 window.removeEventListener("dragover", onDragOver);
26456 window.removeEventListener("dragleave", onDragLeave);
26457 window.removeEventListener("drop", onDrop);
26458 window.removeEventListener("dragend", onDragEnd2);
26459 document.removeEventListener(
26460 "visibilitychange",
26461 onVisibilityChange
26462 );
26463 window.removeEventListener("blur", onDragEnd2);
26464 window.removeEventListener("message", onIframeMessage);
26465 overlayEl.remove();
26466 delete window.__desktopModeOsFileDropMounted;
26467 }
26468 };
26469 host.__desktopModeOsFileDropMounted = manager;
26470 return manager;
26471 }
26472 function ensureDropOverlay() {
26473 const existing = document.querySelector(".desktop-mode-os-drop-overlay");
26474 if (existing) {
26475 return existing;
26476 }
26477 const el = document.createElement("div");
26478 el.className = "desktop-mode-os-drop-overlay";
26479 el.setAttribute("aria-hidden", "true");
26480 el.style.cssText = [
26481 "position:fixed",
26482 "inset:0",
26483 "pointer-events:none",
26484 "z-index:200",
26485 "opacity:0",
26486 "transition:opacity 120ms ease",
26487 "background:radial-gradient(circle at center, rgba(34,113,177,0.18) 0%, rgba(34,113,177,0.06) 60%, transparent 100%)",
26488 "box-shadow:inset 0 0 0 3px rgba(34,113,177,0.55)"
26489 ].join(";");
26490 const label = document.createElement("div");
26491 label.style.cssText = [
26492 "position:absolute",
26493 "top:50%",
26494 "left:50%",
26495 "transform:translate(-50%,-50%)",
26496 "padding:14px 22px",
26497 "border-radius:12px",
26498 "background:rgba(20,20,24,0.78)",
26499 "color:#fff",
26500 "font:600 14px/1.2 -apple-system,BlinkMacSystemFont,sans-serif",
26501 "letter-spacing:0.02em"
26502 ].join(";");
26503 label.textContent = "Drop to upload";
26504 el.appendChild(label);
26505 document.body.appendChild(el);
26506 const style = document.createElement("style");
26507 style.textContent = ".desktop-mode-os-drop-overlay.is-active{opacity:1!important;}";
26508 document.head.appendChild(style);
26509 return el;
26510 }
26511 function mountNoOp() {
26512 const cancel = (ev) => {
26513 if (!dragHasFiles(ev)) {
26514 return;
26515 }
26516 const target = ev.target;
26517 if (target?.closest && IFRAME_PASSTHROUGH_SELECTORS.some((s) => target.closest(s))) {
26518 return;
26519 }
26520 ev.preventDefault();
26521 };
26522 window.addEventListener("dragover", cancel);
26523 window.addEventListener("drop", cancel);
26524 const host = window;
26525 const manager = {
26526 dispose: () => {
26527 window.removeEventListener("dragover", cancel);
26528 window.removeEventListener("drop", cancel);
26529 delete host.__desktopModeOsFileDropMounted;
26530 }
26531 };
26532 host.__desktopModeOsFileDropMounted = manager;
26533 return manager;
26534 }
26535 function classifyDropTarget(ev) {
26536 const x = ev.clientX;
26537 const y = ev.clientY;
26538 let node = ev.target;
26539 while (node && node !== document.body) {
26540 if (node.tagName === "IFRAME") {
26541 const id = node.closest(
26542 "[data-window-id]"
26543 );
26544 return {
26545 surface: "iframe",
26546 windowId: id?.getAttribute("data-window-id") || void 0,
26547 x,
26548 y
26549 };
26550 }
26551 if (node.hasAttribute("data-window-id")) {
26552 return {
26553 surface: "window",
26554 windowId: node.getAttribute("data-window-id") || void 0,
26555 x,
26556 y
26557 };
26558 }
26559 if (node.classList.contains("desktop-mode-folder-grid")) {
26560 return { surface: "folder", x, y };
26561 }
26562 if (node.id === "desktop-mode-wallpaper" || node.classList.contains("desktop-mode-wallpaper") || node.classList.contains("desktop-mode-desktop")) {
26563 return { surface: "wallpaper", x, y };
26564 }
26565 node = node.parentElement;
26566 }
26567 return { surface: "unknown", x, y };
26568 }
26569 async function handleFiles(rawFiles, ctx, opts) {
26570 const detected = applyFilters(
26571 FILE_DROP_HOOKS.FILES_DETECTED,
26572 rawFiles,
26573 ctx
26574 );
26575 if (!Array.isArray(detected) || detected.length === 0) {
26576 return;
26577 }
26578 const { accepted, rejected } = partitionByPolicy(
26579 detected,
26580 opts.config
26581 );
26582 if (rejected.length > 0) {
26583 doAction(FILE_DROP_HOOKS.FILES_REJECTED, {
26584 rejections: rejected,
26585 context: ctx
26586 });
26587 showToast({
26588 message: rejected.length === 1 ? rejected[0].message : `${rejected.length} files couldn't be uploaded.`
26589 });
26590 }
26591 if (accepted.length === 0) {
26592 return;
26593 }
26594 const entries = accepted.map(({ file, mime }) => {
26595 const base = {
26596 file,
26597 mime,
26598 fields: defaultFields(file, mime)
26599 };
26600 const filtered = applyFilters(
26601 FILE_DROP_HOOKS.DIALOG_FIELDS,
26602 base,
26603 ctx
26604 );
26605 if (!filtered || typeof filtered !== "object" || !("fields" in filtered) || typeof filtered.fields !== "object") {
26606 return base;
26607 }
26608 return filtered;
26609 });
26610 await opts.openDialog(entries, ctx);
26611 }
26612 function partitionByPolicy(files, config) {
26613 const accepted = [];
26614 const rejected = [];
26615 for (const file of files) {
26616 if (file.size === 0) {
26617 rejected.push({
26618 file,
26619 reason: "empty",
26620 message: `“${file.name}” is empty.`
26621 });
26622 continue;
26623 }
26624 if (config.maxSize > 0 && file.size > config.maxSize) {
26625 rejected.push({
26626 file,
26627 reason: "size",
26628 message: `“${file.name}” exceeds the ${formatBytes$1(
26629 config.maxSize
26630 )} upload limit.`
26631 });
26632 continue;
26633 }
26634 const mime = resolveAllowedMime(
26635 file,
26636 config.allowedMimes,
26637 config.extToMime
26638 );
26639 if (!mime) {
26640 rejected.push({
26641 file,
26642 reason: "mime",
26643 message: `“${file.name}” is not an allowed file type.`
26644 });
26645 continue;
26646 }
26647 accepted.push({ file, mime });
26648 }
26649 return { accepted, rejected };
26650 }
26651 function resolveAllowedMime(file, allowedMimes, extToMime) {
26652 if (allowedMimes.length === 0) {
26653 return null;
26654 }
26655 const lower = file.type.toLowerCase();
26656 if (lower && allowedMimes.includes(lower)) {
26657 return lower;
26658 }
26659 const ext = extensionOf(file.name);
26660 if (!ext) {
26661 return null;
26662 }
26663 if (extToMime) {
26664 for (const [key, mime] of Object.entries(extToMime)) {
26665 if (key.split("|").includes(ext) && allowedMimes.includes(mime)) {
26666 return mime;
26667 }
26668 }
26669 return null;
26670 }
26671 const guess = EXTENSION_GUESSES[ext];
26672 if (guess && allowedMimes.includes(guess)) {
26673 return guess;
26674 }
26675 return null;
26676 }
26677 const EXTENSION_GUESSES = {
26678 jpg: "image/jpeg",
26679 jpeg: "image/jpeg",
26680 png: "image/png",
26681 gif: "image/gif",
26682 webp: "image/webp",
26683 avif: "image/avif",
26684 heic: "image/heic",
26685 heif: "image/heif",
26686 svg: "image/svg+xml",
26687 mp4: "video/mp4",
26688 mov: "video/quicktime",
26689 webm: "video/webm",
26690 mp3: "audio/mpeg",
26691 wav: "audio/wav",
26692 pdf: "application/pdf"
26693 };
26694 function extensionOf(name) {
26695 const dot = name.lastIndexOf(".");
26696 if (dot < 0) {
26697 return "";
26698 }
26699 return name.slice(dot + 1).toLowerCase();
26700 }
26701 function defaultFields(file, mime) {
26702 const safeName = sanitizeFilename(file.name);
26703 const ext = extensionOf(safeName);
26704 const stem = ext ? safeName.slice(0, safeName.length - ext.length - 1) : safeName;
26705 const title = humanize(stem);
26706 return {
26707 title,
26708 altText: mime.startsWith("image/") ? title : "",
26709 caption: "",
26710 description: "",
26711 filename: safeName
26712 };
26713 }
26714 function sanitizeFilename(name) {
26715 const cleaned = name.replace(/[\\/]/g, "-").replace(/[\x00-\x1f\x7f]/g, "").replace(/\s+/g, " ").replace(/ *- */g, "-").replace(/-+/g, "-").trim().replace(/^[-.]+|[-.]+$/g, "");
26716 return cleaned || "upload";
26717 }
26718 function humanize(stem) {
26719 const spaced = stem.replace(/[-_]+/g, " ").trim();
26720 if (!spaced) {
26721 return "Upload";
26722 }
26723 return spaced.charAt(0).toUpperCase() + spaced.slice(1);
26724 }
26725 function formatBytes$1(bytes) {
26726 if (bytes >= 1024 * 1024) {
26727 return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
26728 }
26729 if (bytes >= 1024) {
26730 return `${(bytes / 1024).toFixed(0)} KB`;
26731 }
26732 return `${bytes} B`;
26733 }
26734 function formatBytes(bytes) {
26735 if (!Number.isFinite(bytes) || bytes <= 0) {
26736 return "0 B";
26737 }
26738 const units = ["B", "KB", "MB", "GB", "TB"];
26739 let v = bytes;
26740 let i = 0;
26741 while (v >= 1024 && i < units.length - 1) {
26742 v /= 1024;
26743 i++;
26744 }
26745 const decimals = v >= 100 || i === 0 ? 0 : 1;
26746 return `${v.toFixed(decimals)} ${units[i]}`;
26747 }
26748 const styles = css`:host{display:block;--wpd-progress-track-bg:var( --desktop-mode-control-bg,rgba( 0,0,0,0.08 ) );--wpd-progress-fill:var( --wp-admin-theme-color,#2271b1 );--wpd-progress-height:6px;--wpd-progress-radius:999px;--wpd-progress-label-color:inherit;--wpd-progress-label-size:12px;--wpd-progress-label-gap:4px;width:100%;font:inherit;color:var( --wpd-progress-label-color )}:host( [ hidden ] ){display:none}.header{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin-bottom:var( --wpd-progress-label-gap );font-size:var( --wpd-progress-label-size );line-height:1.3}.label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.percent{font-variant-numeric:tabular-nums;opacity:0.75;flex-shrink:0}.track{position:relative;width:100%;height:var( --wpd-progress-height );background:var( --wpd-progress-track-bg );border-radius:var( --wpd-progress-radius );overflow:hidden}.fill{position:absolute;inset-block:0;inset-inline-start:0;width:0;background:var( --wpd-progress-fill );border-radius:inherit;transition:width 0.18s ease-out}:host( [ tone='success' ] ){--wpd-progress-fill:var( --desktop-mode-status-success,#3a8a3a )}:host( [ tone='warning' ] ){--wpd-progress-fill:var( --desktop-mode-status-warning,#dba617 )}:host( [ tone='danger' ] ){--wpd-progress-fill:var( --desktop-mode-status-danger,#d63638 )}:host( [ indeterminate ] ) .fill{width:33%;animation:wpd-progress-sweep 1.1s linear infinite;transition:none}@keyframes wpd-progress-sweep{0%{transform:translateX( -120% )}100%{transform:translateX( 320% )}}@media ( prefers-reduced-motion:reduce ){.fill{transition:none}:host( [ indeterminate ] ) .fill{animation:none;width:100%;opacity:0.6}}`;
26749 const _WpdProgressBar = class _WpdProgressBar extends Component {
26750 constructor() {
26751 super(...arguments);
26752 this._ownedAriaLabel = null;
26753 }
26754 render() {
26755 return html`<div class="root" part="root">
26756 <div class="header" part="header" hidden>
26757 <span class="label" part="label"></span>
26758 <span class="percent" part="percent"></span>
26759 </div>
26760 <div class="track" part="track">
26761 <div class="fill" part="fill"></div>
26762 </div>
26763 </div>`;
26764 }
26765 requestUpdate() {
26766 super.requestUpdate();
26767 queueMicrotask(() => this._paint());
26768 }
26769 connectedCallback() {
26770 super.connectedCallback();
26771 queueMicrotask(() => this._paint());
26772 }
26773 _paint() {
26774 const root = this.shadowRoot;
26775 if (!root) {
26776 return;
26777 }
26778 const max = this._readMax();
26779 const indeterminate = this.hasAttribute("indeterminate") || max <= 0;
26780 const value = indeterminate ? 0 : this._readValue(max);
26781 const ratio = indeterminate ? 0 : value / max;
26782 const percent = Math.round(ratio * 100);
26783 const label = this.getAttribute("label") ?? "";
26784 const showPercent = this.hasAttribute("show-percent");
26785 const fill = root.querySelector(".fill");
26786 if (fill && !indeterminate) {
26787 fill.style.width = `${(ratio * 100).toFixed(2)}%`;
26788 } else if (fill && indeterminate) {
26789 fill.style.removeProperty("width");
26790 }
26791 const header = root.querySelector(".header");
26792 const labelEl = root.querySelector(".label");
26793 const percentEl = root.querySelector(".percent");
26794 if (header && labelEl && percentEl) {
26795 const visible = label || showPercent && !indeterminate;
26796 header.hidden = !visible;
26797 labelEl.textContent = label;
26798 percentEl.hidden = !(showPercent && !indeterminate);
26799 percentEl.textContent = `${percent}%`;
26800 }
26801 this._syncAria(max, value, indeterminate, label);
26802 const track = root.querySelector(".track");
26803 if (track) {
26804 track.setAttribute("role", "progressbar");
26805 track.setAttribute("aria-valuemin", "0");
26806 if (indeterminate) {
26807 track.removeAttribute("aria-valuenow");
26808 track.removeAttribute("aria-valuemax");
26809 } else {
26810 track.setAttribute("aria-valuemax", String(max));
26811 track.setAttribute("aria-valuenow", String(value));
26812 }
26813 if (label) {
26814 track.setAttribute("aria-label", label);
26815 } else {
26816 track.removeAttribute("aria-label");
26817 }
26818 }
26819 }
26820 _syncAria(max, value, indeterminate, label) {
26821 this.setAttribute("role", "progressbar");
26822 this.setAttribute("aria-valuemin", "0");
26823 if (indeterminate) {
26824 this.removeAttribute("aria-valuenow");
26825 this.removeAttribute("aria-valuemax");
26826 } else {
26827 this.setAttribute("aria-valuemax", String(max));
26828 this.setAttribute("aria-valuenow", String(value));
26829 }
26830 const existing = this.getAttribute("aria-label");
26831 if (label) {
26832 if (existing === null || existing === this._ownedAriaLabel) {
26833 this.setAttribute("aria-label", label);
26834 this._ownedAriaLabel = label;
26835 }
26836 } else if (existing !== null && existing === this._ownedAriaLabel) {
26837 this.removeAttribute("aria-label");
26838 this._ownedAriaLabel = null;
26839 }
26840 }
26841 _readMax() {
26842 const attr = this.getAttribute("max");
26843 if (attr === null) {
26844 return 100;
26845 }
26846 const raw = parseFloat(attr);
26847 return Number.isFinite(raw) ? raw : 100;
26848 }
26849 _readValue(max) {
26850 const raw = parseFloat(this.getAttribute("value") ?? "0");
26851 if (!Number.isFinite(raw)) {
26852 return 0;
26853 }
26854 if (raw < 0) {
26855 return 0;
26856 }
26857 if (raw > max) {
26858 return max;
26859 }
26860 return raw;
26861 }
26862 };
26863 _WpdProgressBar.props = [
26864 "value",
26865 "max",
26866 "indeterminate",
26867 "tone",
26868 "label",
26869 "showPercent"
26870 ];
26871 _WpdProgressBar.styles = [styles];
26872 _WpdProgressBar.help = {
26873 title: "Progress bar",
26874 summary: "Linear progress indicator. Determinate mode shows `value/max` as a fill width; indeterminate mode sweeps across the track. Supports tone tinting, an optional inline label + percent header, and full CSS-variable theming.",
26875 status: "experimental",
26876 since: "0.31.0",
26877 props: [
26878 {
26879 name: "value",
26880 type: "number",
26881 default: "0",
26882 description: "Current progress. Clamped to `[0, max]`."
26883 },
26884 {
26885 name: "max",
26886 type: "number",
26887 default: "100",
26888 description: "Maximum value. Setting `max <= 0` forces indeterminate."
26889 },
26890 {
26891 name: "indeterminate",
26892 type: "boolean",
26893 description: "Show the sweeping indeterminate animation instead of a value-driven fill. The `value` attribute is ignored while this is set."
26894 },
26895 {
26896 name: "tone",
26897 type: '"default" | "success" | "warning" | "danger"',
26898 default: "default",
26899 description: "Tints the fill from the shared status palette."
26900 },
26901 {
26902 name: "label",
26903 type: "string",
26904 description: "Optional inline label rendered above the track. Also wired into `aria-label` when set."
26905 },
26906 {
26907 name: "show-percent",
26908 type: "boolean",
26909 description: "Render a right-aligned percent readout next to the label. Only meaningful in determinate mode."
26910 }
26911 ],
26912 cssProps: [
26913 {
26914 name: "--wpd-progress-track-bg",
26915 default: "var(--desktop-mode-control-bg, rgba(0,0,0,0.08))"
26916 },
26917 {
26918 name: "--wpd-progress-fill",
26919 default: "var(--wp-admin-theme-color, #2271b1)"
26920 },
26921 { name: "--wpd-progress-height", default: "6px" },
26922 { name: "--wpd-progress-radius", default: "999px" },
26923 { name: "--wpd-progress-label-color", default: "inherit" },
26924 { name: "--wpd-progress-label-size", default: "12px" },
26925 { name: "--wpd-progress-label-gap", default: "4px" }
26926 ],
26927 example: html`<wpd-progress-bar
26928 value="42"
26929 label="Uploading hero.jpg"
26930 show-percent
26931 ></wpd-progress-bar>`
26932 };
26933 let WpdProgressBar = _WpdProgressBar;
26934 defineComponent("wpd-progress-bar", WpdProgressBar);
26935 const ROWS = /* @__PURE__ */ new Map();
26936 let panel = null;
26937 function mountUploadProgressHud() {
26938 if (document.body.hasAttribute("data-desktop-mode-suppress-upload-hud")) {
26939 return;
26940 }
26941 if (window.__wpdUploadHud) {
26942 return;
26943 }
26944 window.__wpdUploadHud = true;
26945 const ns = "desktop-mode/os-file-drop-hud";
26946 addAction(
26947 FILE_DROP_HOOKS.UPLOAD_STARTED,
26948 ns,
26949 (payload) => onStarted(payload.file, payload.fields, payload.abort)
26950 );
26951 addAction(
26952 FILE_DROP_HOOKS.UPLOAD_PROGRESS,
26953 ns,
26954 (payload) => onProgress(
26955 payload.file,
26956 payload.loaded,
26957 payload.total,
26958 payload.indeterminate
26959 )
26960 );
26961 addAction(
26962 FILE_DROP_HOOKS.AFTER_UPLOAD,
26963 ns,
26964 (payload) => onComplete(payload.file, payload.fields, payload.result)
26965 );
26966 addAction(
26967 FILE_DROP_HOOKS.UPLOAD_FAILED,
26968 ns,
26969 (payload) => onFailed(payload.file, payload.error)
26970 );
26971 }
26972 function onStarted(file, fields, abort) {
26973 const p = ensurePanel();
26974 const row = document.createElement("div");
26975 row.className = "desktop-mode-upload-hud__row";
26976 const meta = document.createElement("div");
26977 meta.className = "desktop-mode-upload-hud__meta";
26978 const name = document.createElement("div");
26979 name.className = "desktop-mode-upload-hud__name";
26980 name.textContent = fields.filename || file.name;
26981 name.title = fields.filename || file.name;
26982 const statusEl = document.createElement("div");
26983 statusEl.className = "desktop-mode-upload-hud__status";
26984 statusEl.textContent = "Uploading…";
26985 meta.append(name, statusEl);
26986 const bar = document.createElement("wpd-progress-bar");
26987 bar.setAttribute("indeterminate", "");
26988 bar.setAttribute("show-percent", "");
26989 const actions = document.createElement("div");
26990 actions.className = "desktop-mode-upload-hud__actions";
26991 const cancelBtn = document.createElement("wpd-button");
26992 cancelBtn.setAttribute("variant", "tertiary");
26993 cancelBtn.setAttribute("size", "small");
26994 cancelBtn.textContent = "Cancel";
26995 cancelBtn.addEventListener("click", () => {
26996 const r = ROWS.get(file);
26997 if (!r) {
26998 return;
26999 }
27000 if (r.state === "running") {
27001 r.statusEl.textContent = "Cancelling…";
27002 r.cancelBtn.disabled = true;
27003 r.abort();
27004 } else {
27005 dismissRow(r);
27006 }
27007 });
27008 actions.appendChild(cancelBtn);
27009 row.append(meta, bar, actions);
27010 p.querySelector(".desktop-mode-upload-hud__list").appendChild(row);
27011 ROWS.set(file, {
27012 file,
27013 abort,
27014 root: row,
27015 bar,
27016 statusEl,
27017 cancelBtn,
27018 state: "running",
27019 lingerTimer: null
27020 });
27021 updateHeader();
27022 }
27023 function onProgress(file, loaded, total, indeterminate) {
27024 const r = ROWS.get(file);
27025 if (!r || r.state !== "running") {
27026 return;
27027 }
27028 if (indeterminate || total <= 0) {
27029 r.bar.setAttribute("indeterminate", "");
27030 r.statusEl.textContent = `${formatBytes(loaded)} sent`;
27031 } else {
27032 r.bar.removeAttribute("indeterminate");
27033 r.bar.setAttribute("max", String(total));
27034 r.bar.setAttribute("value", String(loaded));
27035 r.statusEl.textContent = `${formatBytes(loaded)} / ${formatBytes(total)}`;
27036 }
27037 }
27038 function onComplete(file, fields, result) {
27039 const r = ROWS.get(file);
27040 if (!r) {
27041 return;
27042 }
27043 r.state = "success";
27044 r.bar.removeAttribute("indeterminate");
27045 r.bar.setAttribute("value", "100");
27046 r.bar.setAttribute("max", "100");
27047 r.bar.setAttribute("tone", "success");
27048 r.statusEl.textContent = "Uploaded";
27049 r.cancelBtn.textContent = "Dismiss";
27050 r.lingerTimer = setTimeout(() => dismissRow(r), 2500);
27051 updateHeader();
27052 activity.publish("desktop-mode/upload-hud-complete", {
27053 filename: fields.filename || result.filename,
27054 attachmentId: result.id
27055 });
27056 }
27057 function onFailed(file, error) {
27058 const r = ROWS.get(file);
27059 if (!r) {
27060 return;
27061 }
27062 r.bar.removeAttribute("indeterminate");
27063 r.bar.setAttribute("tone", "danger");
27064 r.cancelBtn.textContent = "Dismiss";
27065 r.cancelBtn.disabled = false;
27066 if (error.name === "UploadAbortedError") {
27067 r.state = "aborted";
27068 r.statusEl.textContent = "Cancelled";
27069 } else {
27070 r.state = "failed";
27071 r.statusEl.textContent = error.message || "Upload failed";
27072 }
27073 updateHeader();
27074 }
27075 function dismissRow(r) {
27076 if (r.lingerTimer) {
27077 clearTimeout(r.lingerTimer);
27078 }
27079 ROWS.delete(r.file);
27080 r.root.remove();
27081 updateHeader();
27082 if (ROWS.size === 0 && panel) {
27083 panel.hidden = true;
27084 }
27085 }
27086 function ensurePanel() {
27087 if (panel && panel.isConnected) {
27088 panel.hidden = false;
27089 return panel;
27090 }
27091 const p = document.createElement("div");
27092 p.className = "desktop-mode-upload-hud";
27093 p.setAttribute("role", "region");
27094 p.setAttribute("aria-label", "Uploads");
27095 const header = document.createElement("div");
27096 header.className = "desktop-mode-upload-hud__header";
27097 const title = document.createElement("div");
27098 title.className = "desktop-mode-upload-hud__title";
27099 title.textContent = "Uploads";
27100 const closeBtn = document.createElement("button");
27101 closeBtn.type = "button";
27102 closeBtn.className = "desktop-mode-upload-hud__close";
27103 closeBtn.setAttribute("aria-label", "Hide upload panel");
27104 closeBtn.textContent = "×";
27105 closeBtn.addEventListener("click", () => {
27106 for (const r of [...ROWS.values()]) {
27107 if (r.state !== "running") {
27108 dismissRow(r);
27109 }
27110 }
27111 if (ROWS.size === 0) {
27112 p.hidden = true;
27113 }
27114 });
27115 header.append(title, closeBtn);
27116 const list2 = document.createElement("div");
27117 list2.className = "desktop-mode-upload-hud__list";
27118 p.append(header, list2);
27119 document.body.appendChild(p);
27120 panel = p;
27121 return p;
27122 }
27123 function updateHeader() {
27124 if (!panel) {
27125 return;
27126 }
27127 const title = panel.querySelector(
27128 ".desktop-mode-upload-hud__title"
27129 );
27130 if (!title) {
27131 return;
27132 }
27133 const total = ROWS.size;
27134 const running = [...ROWS.values()].filter((r) => r.state === "running").length;
27135 if (running > 0) {
27136 title.textContent = running === total ? `Uploading ${running} file${running === 1 ? "" : "s"}…` : `${running} of ${total} uploading…`;
27137 } else if (total > 0) {
27138 title.textContent = `Uploads (${total})`;
27139 } else {
27140 title.textContent = "Uploads";
27141 }
27142 }
27143 function mountMediaLibraryRefresher() {
27144 if (document.body.hasAttribute(
27145 "data-desktop-mode-suppress-media-library-refresh"
27146 )) {
27147 return;
27148 }
27149 const sentinel = window;
27150 if (sentinel.__wpdMediaLibraryRefresher) {
27151 return;
27152 }
27153 sentinel.__wpdMediaLibraryRefresher = true;
27154 addAction(
27155 FILE_DROP_HOOKS.AFTER_UPLOAD,
27156 "desktop-mode/os-file-drop-library-refresh",
27157 () => refreshOpenLibraries()
27158 );
27159 }
27160 function refreshOpenLibraries() {
27161 const iframes = document.querySelectorAll("iframe");
27162 for (const frame of Array.from(iframes)) {
27163 if (!isMediaLibraryUrl(resolveIframeUrl(frame))) {
27164 continue;
27165 }
27166 try {
27167 frame.contentWindow?.location.reload();
27168 } catch {
27169 const reloadHref = resolveIframeUrl(frame);
27170 if (reloadHref) {
27171 frame.setAttribute("src", reloadHref);
27172 }
27173 }
27174 }
27175 }
27176 function resolveIframeUrl(frame) {
27177 try {
27178 return frame.contentWindow?.location.href ?? frame.src ?? "";
27179 } catch {
27180 return frame.src ?? "";
27181 }
27182 }
27183 function isMediaLibraryUrl(url) {
27184 if (!url) {
27185 return false;
27186 }
27187 return /\/wp-admin\/upload\.php(?:[?#]|$)/.test(url);
27188 }
27189 function bootOsFileDrop(args) {
27190 const config = args.config || {
27191 enabled: false,
27192 allowedMimes: [],
27193 maxSize: 0
27194 };
27195 mountUploadProgressHud();
27196 mountMediaLibraryRefresher();
27197 mountOsFileDropManager({
27198 config,
27199 mediaUrl: args.mediaUrl,
27200 restNonce: args.restNonce,
27201 openDialog: async (entries, ctx) => {
27202 const { openUploadDialog: openUploadDialog2 } = await Promise.resolve().then(() => dialog);
27203 await openUploadDialog2({
27204 entries,
27205 context: ctx,
27206 mediaUrl: args.mediaUrl,
27207 restNonce: args.restNonce
27208 });
27209 }
27210 });
27211 }
27212 const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
27213 __proto__: null,
27214 FILE_DROP_HOOKS,
27215 bootOsFileDrop
27216 }, Symbol.toStringTag, { value: "Module" }));
27217 const textFieldStyles = css`:host{display:flex;flex-direction:column;gap:4px;font-size:13px;color:var( --desktop-mode-text,#1d2327 );min-width:0}:host( [ hidden ] ){display:none}.wpd-text-field__label{font-size:12px;color:var( --desktop-mode-muted,#646970 )}.wpd-text-field__row{position:relative;display:flex;align-items:center;width:100%}input{appearance:none;-webkit-appearance:none;display:block;width:100%;min-width:0;box-sizing:border-box;padding:7px 10px;background:var( --desktop-mode-window-bg,#fff );border:1px solid var( --desktop-mode-border,#dcdcde );border-radius:6px;font:inherit;font-size:13px;color:var( --desktop-mode-text,#1d2327 );transition:border-color 0.12s ease,box-shadow 0.12s ease}.wpd-text-field__suffix{position:absolute;inset-inline-end:10px;top:50%;transform:translateY( -50% );pointer-events:none;font-size:12px;color:var( --desktop-mode-muted,#646970 )}.wpd-text-field__row--has-reveal input{padding-inline-end:36px}.wpd-text-field__reveal{position:absolute;inset-inline-end:0;top:0;bottom:0;width:34px;display:flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var( --desktop-mode-muted,#646970 );cursor:pointer;border-radius:0 6px 6px 0;transition:color 0.12s ease}.wpd-text-field__reveal:hover{color:var( --wp-admin-theme-color,#2271b1 )}.wpd-text-field__reveal:focus-visible{outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:-2px;border-radius:0 6px 6px 0}.wpd-text-field__reveal:disabled{opacity:0.45;cursor:not-allowed}.wpd-text-field__input--masked{-webkit-text-security:disc;text-security:disc}@supports not ( ( -webkit-text-security:disc ) or ( text-security:disc ) ){.wpd-text-field__input--masked{font-family:text-security-disc,"password",monospace;letter-spacing:0.2em}}input:hover{border-color:var( --desktop-mode-muted,#8c8f94 )}input:focus-visible{outline:none;border-color:var( --wp-admin-theme-color,#2271b1 );box-shadow:0 0 0 1px var( --wp-admin-theme-color,#2271b1 )}input:disabled{opacity:0.55;cursor:not-allowed;background:rgba( 0,0,0,0.03 )}input[ aria-invalid='true' ]{border-color:#d63638}input[ aria-invalid='true' ]:focus-visible{box-shadow:0 0 0 1px #d63638}input[ type='number' ]::-webkit-inner-spin-button,input[ type='number' ]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}input[ type='number' ]{-moz-appearance:textfield}`;
27218 const _WpdTextField = class _WpdTextField extends Component {
27219 constructor() {
27220 super(...arguments);
27221 this._revealed = false;
27222 }
27223 connectedCallback() {
27224 super.connectedCallback();
27225 ensureAutoId(this);
27226 }
27227 render() {
27228 const label = this.label || "";
27229 const value = this.value ?? "";
27230 const placeholder = this.placeholder || "";
27231 const disabled = this.disabled !== null;
27232 const readonly = this.readonly !== null;
27233 const declaredAutocomplete = this.autocomplete;
27234 const declaredType = this.type || "text";
27235 const isPassword = declaredType === "password";
27236 let autocomplete = declaredAutocomplete || "off";
27237 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
27238 autocomplete = "new-password";
27239 }
27240 const maxLength = this.maxlength;
27241 const minLength = this.minlength;
27242 const pattern = this.pattern || "";
27243 const name = this.name || "";
27244 const suffix = this.suffix || "";
27245 const invalid = this.invalid !== null;
27246 const reveal = this.reveal !== null;
27247 const isPasswordIntent = declaredType === "password";
27248 const isMasked = isPasswordIntent && !(reveal && this._revealed);
27249 let effectiveType;
27250 if (isPasswordIntent) {
27251 effectiveType = "text";
27252 } else if (reveal && this._revealed) {
27253 effectiveType = "text";
27254 } else {
27255 effectiveType = declaredType;
27256 }
27257 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
27258 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
27259 const hostId = this.id || "wpd-unnamed";
27260 const inputId = `${hostId}__input`;
27261 return html`
27262 ${label ? html`<label
27263 class="wpd-text-field__label"
27264 for=${inputId}
27265 >${label}</label>` : html``}
27266 <span class=${rowClass}>
27267 <input
27268 id=${inputId}
27269 class=${inputClass}
27270 type=${effectiveType}
27271 .value=${value}
27272 placeholder=${placeholder}
27273 ?disabled=${disabled}
27274 ?readonly=${readonly}
27275 autocomplete=${autocomplete}
27276 maxlength=${maxLength ?? ""}
27277 minlength=${minLength ?? ""}
27278 pattern=${pattern}
27279 name=${name}
27280 aria-invalid=${invalid ? "true" : "false"}
27281 aria-label=${label || ""}
27282 @input=${(e) => this._onInput(e)}
27283 @change=${(e) => this._onChange(e)}
27284 @keydown=${(e) => this._onKeyDown(e)}
27285 />
27286 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
27287 ${reveal ? this._renderRevealButton(disabled) : html``}
27288 </span>
27289 `;
27290 }
27291 _renderRevealButton(disabled) {
27292 const label = this._revealed ? "Hide" : "Show";
27293 return html`
27294 <button
27295 type="button"
27296 class="wpd-text-field__reveal"
27297 aria-label=${label}
27298 aria-pressed=${this._revealed ? "true" : "false"}
27299 ?disabled=${disabled}
27300 tabindex="0"
27301 @click=${() => this._onToggleReveal()}
27302 >
27303 ${this._revealed ? _iconEyeOff() : _iconEye()}
27304 </button>
27305 `;
27306 }
27307 _onToggleReveal() {
27308 this._revealed = !this._revealed;
27309 this.requestUpdate();
27310 }
27311 _onInput(e) {
27312 const input = e.target;
27313 this.value = input.value;
27314 this.emit("wpd-input-change", { value: input.value });
27315 }
27316 _onChange(e) {
27317 const input = e.target;
27318 this.emit("wpd-input-commit", { value: input.value });
27319 }
27320 _onKeyDown(e) {
27321 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
27322 const input = e.target;
27323 this.emit("wpd-submit", { value: input.value });
27324 }
27325 }
27326 };
27327 _WpdTextField.props = [
27328 "label",
27329 "value",
27330 "placeholder",
27331 "disabled",
27332 "readonly",
27333 "autocomplete",
27334 "type",
27335 "maxlength",
27336 "minlength",
27337 "pattern",
27338 "name",
27339 "suffix",
27340 "invalid",
27341 "reveal"
27342 ];
27343 _WpdTextField.styles = [textFieldStyles];
27344 _WpdTextField.help = {
27345 title: "Text field",
27346 summary: "Labelled text input primitive. Two-way reflects `value`, emits wpd-input-change per keystroke, wpd-input-commit on blur/change, and wpd-submit on Enter. Optional password reveal toggle.",
27347 status: "stable",
27348 since: "0.11.0",
27349 props: [
27350 { name: "label", type: "string", description: "Visible label above the input." },
27351 { name: "value", type: "string", description: "Current input value; reflected two-way." },
27352 { name: "placeholder", type: "string", description: "Native placeholder string." },
27353 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
27354 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
27355 {
27356 name: "autocomplete",
27357 type: "string",
27358 default: "off",
27359 description: "Forwarded to the native input autocomplete attribute."
27360 },
27361 {
27362 name: "type",
27363 type: "string",
27364 default: "text",
27365 description: "Native input type (text, password, email, search, tel, url)."
27366 },
27367 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
27368 { name: "minlength", type: "integer (string)", description: "Native minlength." },
27369 { name: "pattern", type: "regex string", description: "Native validation pattern." },
27370 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
27371 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
27372 {
27373 name: "invalid",
27374 type: "boolean attribute",
27375 description: "Marks the field aria-invalid and applies the error style."
27376 },
27377 {
27378 name: "reveal",
27379 type: "boolean attribute",
27380 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
27381 }
27382 ],
27383 events: [
27384 {
27385 name: "wpd-input-change",
27386 description: "Fires on every input keystroke.",
27387 detail: "{ value: string }"
27388 },
27389 {
27390 name: "wpd-input-commit",
27391 description: "Fires on the native change event (blur / Enter).",
27392 detail: "{ value: string }"
27393 },
27394 {
27395 name: "wpd-submit",
27396 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
27397 detail: "{ value: string }"
27398 }
27399 ],
27400 cssProps: [
27401 { name: "--desktop-mode-text", description: "Text colour." },
27402 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
27403 { name: "--desktop-mode-border", description: "Input outline." },
27404 { name: "--desktop-mode-window-bg", description: "Input background." }
27405 ],
27406 example: html`
27407 <wpd-stack gap="8">
27408 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
27409 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
27410 </wpd-stack>
27411 `
27412 };
27413 let WpdTextField = _WpdTextField;
27414 defineComponent("wpd-text-field", WpdTextField);
27415 function _iconEye() {
27416 return html`
27417 <svg
27418 viewBox="0 0 16 16"
27419 width="14"
27420 height="14"
27421 fill="none"
27422 stroke="currentColor"
27423 stroke-width="1.5"
27424 stroke-linecap="round"
27425 stroke-linejoin="round"
27426 aria-hidden="true"
27427 focusable="false"
27428 >
27429 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
27430 <circle cx="8" cy="8" r="2" />
27431 </svg>
27432 `;
27433 }
27434 function _iconEyeOff() {
27435 return html`
27436 <svg
27437 viewBox="0 0 16 16"
27438 width="14"
27439 height="14"
27440 fill="none"
27441 stroke="currentColor"
27442 stroke-width="1.5"
27443 stroke-linecap="round"
27444 stroke-linejoin="round"
27445 aria-hidden="true"
27446 focusable="false"
27447 >
27448 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
27449 <circle cx="8" cy="8" r="2" />
27450 <line x1="2" y1="2" x2="14" y2="14" />
27451 </svg>
27452 `;
27453 }
27454 const textareaStyles = css`:host{display:flex;flex-direction:column;gap:4px;font-size:13px;color:var( --desktop-mode-text,#1d2327 );min-width:0}:host( [ hidden ] ){display:none}.wpd-textarea__label{font-size:12px;color:var( --desktop-mode-muted,#646970 )}textarea{appearance:none;-webkit-appearance:none;display:block;width:100%;min-width:0;box-sizing:border-box;padding:8px 10px;background:var( --desktop-mode-window-bg,#fff );border:1px solid var( --desktop-mode-border,#dcdcde );border-radius:6px;font:inherit;font-size:13px;line-height:1.45;color:var( --desktop-mode-text,#1d2327 );resize:vertical;transition:border-color 0.12s ease,box-shadow 0.12s ease}textarea:hover{border-color:var( --desktop-mode-muted,#8c8f94 )}textarea:focus-visible{outline:none;border-color:var( --wp-admin-theme-color,#2271b1 );box-shadow:0 0 0 1px var( --wp-admin-theme-color,#2271b1 )}textarea:disabled{opacity:0.55;cursor:not-allowed;background:rgba( 0,0,0,0.03 )}textarea[ aria-invalid='true' ]{border-color:#d63638}textarea[ aria-invalid='true' ]:focus-visible{box-shadow:0 0 0 1px #d63638}:host( [ auto-grow ] ) textarea{resize:none;overflow:hidden}`;
27455 const _WpdTextarea = class _WpdTextarea extends Component {
27456 constructor() {
27457 super(...arguments);
27458 this._textareaEl = null;
27459 }
27460 connectedCallback() {
27461 super.connectedCallback();
27462 ensureAutoId(this);
27463 }
27464 render() {
27465 const label = this._attr("label") || "";
27466 const value = this._attr("value") ?? "";
27467 const placeholder = this._attr("placeholder") || "";
27468 const disabled = this._boolAttr("disabled");
27469 const readonly = this._boolAttr("readonly");
27470 const name = this._attr("name") || "";
27471 const rows = Number(this._attr("rows")) || 3;
27472 const maxLength = this._attr("maxlength");
27473 const minLength = this._attr("minlength");
27474 const invalid = this._boolAttr("invalid");
27475 const hostId = this.id || "wpd-unnamed";
27476 const fieldId = `${hostId}__field`;
27477 return html`
27478 ${label ? html`<label class="wpd-textarea__label" for=${fieldId}>${label}</label>` : html``}
27479 <textarea
27480 id=${fieldId}
27481 part="textarea"
27482 .value=${value}
27483 placeholder=${placeholder}
27484 ?disabled=${disabled}
27485 ?readonly=${readonly}
27486 rows=${rows}
27487 maxlength=${maxLength ?? ""}
27488 minlength=${minLength ?? ""}
27489 name=${name}
27490 aria-invalid=${invalid ? "true" : "false"}
27491 aria-label=${label || ""}
27492 @input=${(e) => this._onInput(e)}
27493 @change=${(e) => this._onChange(e)}
27494 @keydown=${(e) => this._onKeyDown(e)}
27495 ></textarea>
27496 `;
27497 }
27498 _attr(name) {
27499 return this.getAttribute(name);
27500 }
27501 _boolAttr(name) {
27502 return this.getAttribute(name) !== null;
27503 }
27504 _onInput(e) {
27505 const ta = e.target;
27506 this._textareaEl = ta;
27507 this.setAttribute("value", ta.value);
27508 this.emit("wpd-input-change", { value: ta.value });
27509 if (this._boolAttr("auto-grow")) {
27510 this._autosize(ta);
27511 }
27512 }
27513 _onChange(e) {
27514 const ta = e.target;
27515 this.emit("wpd-input-commit", { value: ta.value });
27516 }
27517 _onKeyDown(e) {
27518 if (!this._boolAttr("submit-on-enter")) {
27519 return;
27520 }
27521 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
27522 e.preventDefault();
27523 const ta = e.target;
27524 this.emit("wpd-submit", { value: ta.value });
27525 }
27526 }
27527 /**
27528 * Grow the textarea height to fit content, capped at `max-rows`.
27529 * Resets to scroll-height each input then clamps; cheap because
27530 * the browser caches layout.
27531 */
27532 _autosize(ta) {
27533 const maxRows = Number(this._attr("max-rows")) || 8;
27534 const cs = window.getComputedStyle(ta);
27535 const fontSize = parseFloat(cs.fontSize) || 13;
27536 const lineHeightRaw = cs.lineHeight;
27537 const lineHeight = lineHeightRaw === "normal" ? fontSize * 1.45 : parseFloat(lineHeightRaw) || fontSize * 1.45;
27538 const paddingTop = parseFloat(cs.paddingTop) || 0;
27539 const paddingBottom = parseFloat(cs.paddingBottom) || 0;
27540 const max = lineHeight * maxRows + paddingTop + paddingBottom;
27541 ta.style.height = "auto";
27542 const next = Math.min(ta.scrollHeight, max);
27543 ta.style.height = `${next}px`;
27544 }
27545 /** Public helper for callers that programmatically set `.value` and want autosize to re-run. */
27546 refreshAutosize() {
27547 if (this._textareaEl && this._boolAttr("auto-grow")) {
27548 this._autosize(this._textareaEl);
27549 }
27550 }
27551 /** Imperatively focus the underlying textarea. */
27552 focusInput() {
27553 const root = this.shadowRoot ?? this;
27554 const ta = root.querySelector("textarea");
27555 ta?.focus();
27556 }
27557 /** Imperatively clear the value. */
27558 clear() {
27559 this.setAttribute("value", "");
27560 const root = this.shadowRoot ?? this;
27561 const ta = root.querySelector("textarea");
27562 if (ta) {
27563 ta.value = "";
27564 if (this._boolAttr("auto-grow")) {
27565 this._autosize(ta);
27566 }
27567 }
27568 }
27569 };
27570 _WpdTextarea.props = [
27571 "label",
27572 "value",
27573 "placeholder",
27574 "disabled",
27575 "readonly",
27576 "name",
27577 "rows",
27578 "maxlength",
27579 "minlength",
27580 "invalid",
27581 "autoGrow",
27582 "maxRows",
27583 "submitOnEnter"
27584 ];
27585 _WpdTextarea.styles = [textareaStyles];
27586 _WpdTextarea.help = {
27587 title: "Textarea",
27588 summary: "Multi-line text input. Same event shape as wpd-text-field. Optional auto-grow up to max-rows; optional submit-on-enter (Enter sends, Shift+Enter newlines).",
27589 status: "stable",
27590 since: "0.22.0",
27591 props: [
27592 { name: "label", type: "string", description: "Visible label above the textarea." },
27593 { name: "value", type: "string", description: "Current value; reflected two-way." },
27594 { name: "placeholder", type: "string", description: "Native placeholder." },
27595 { name: "disabled", type: "boolean attribute" },
27596 { name: "readonly", type: "boolean attribute" },
27597 { name: "name", type: "string", description: "Forwarded to native textarea for form submission." },
27598 { name: "rows", type: "integer (string)", default: "3", description: "Initial visible row count." },
27599 { name: "maxlength", type: "integer (string)" },
27600 { name: "minlength", type: "integer (string)" },
27601 { name: "invalid", type: "boolean attribute", description: "Sets aria-invalid + error styling." },
27602 { name: "auto-grow", type: "boolean attribute", description: "Grows up to max-rows as the user types." },
27603 { name: "max-rows", type: "integer (string)", default: "8" },
27604 {
27605 name: "submit-on-enter",
27606 type: "boolean attribute",
27607 description: "Enter fires wpd-submit; Shift+Enter inserts a newline."
27608 }
27609 ],
27610 events: [
27611 { name: "wpd-input-change", description: "Fires on every keystroke.", detail: "{ value: string }" },
27612 { name: "wpd-input-commit", description: "Fires on blur / native change.", detail: "{ value: string }" },
27613 {
27614 name: "wpd-submit",
27615 description: "Fires on Enter (without Shift) when submit-on-enter is set.",
27616 detail: "{ value: string }"
27617 }
27618 ],
27619 example: html`
27620 <wpd-textarea label="Message" rows="3" auto-grow max-rows="8" submit-on-enter></wpd-textarea>
27621 `
27622 };
27623 let WpdTextarea = _WpdTextarea;
27624 defineComponent("wpd-textarea", WpdTextarea);
27625 async function uploadFile(args) {
27626 const initial = {
27627 file: args.file,
27628 mime: args.mime,
27629 fields: args.fields
27630 };
27631 const filtered = applyFilters(
27632 FILE_DROP_HOOKS.BEFORE_UPLOAD,
27633 initial,
27634 args.context
27635 );
27636 if (!filtered) {
27637 throw new UploadCancelledError();
27638 }
27639 const body = new FormData();
27640 const renamed = filtered.fields.filename !== filtered.file.name ? new File([filtered.file], filtered.fields.filename, {
27641 type: filtered.mime || filtered.file.type
27642 }) : filtered.file;
27643 body.append("file", renamed);
27644 body.append("title", filtered.fields.title);
27645 body.append("alt_text", filtered.fields.altText);
27646 body.append("caption", filtered.fields.caption);
27647 body.append("description", filtered.fields.description);
27648 return new Promise((resolve2, reject) => {
27649 const xhr = new XMLHttpRequest();
27650 xhr.open("POST", args.mediaUrl, true);
27651 xhr.withCredentials = true;
27652 xhr.setRequestHeader("X-WP-Nonce", args.restNonce);
27653 xhr.responseType = "text";
27654 let aborted = false;
27655 let bodyFullySent = false;
27656 let cancelRequested = false;
27657 const abort = () => {
27658 cancelRequested = true;
27659 if (bodyFullySent) {
27660 return;
27661 }
27662 aborted = true;
27663 try {
27664 xhr.abort();
27665 } catch {
27666 }
27667 };
27668 doAction(FILE_DROP_HOOKS.UPLOAD_STARTED, {
27669 file: filtered.file,
27670 fields: filtered.fields,
27671 context: args.context,
27672 abort
27673 });
27674 xhr.upload.addEventListener("progress", (e) => {
27675 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
27676 file: filtered.file,
27677 fields: filtered.fields,
27678 context: args.context,
27679 loaded: e.loaded,
27680 total: e.lengthComputable ? e.total : 0,
27681 indeterminate: !e.lengthComputable
27682 });
27683 });
27684 xhr.upload.addEventListener("load", () => {
27685 bodyFullySent = true;
27686 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
27687 file: filtered.file,
27688 fields: filtered.fields,
27689 context: args.context,
27690 loaded: filtered.file.size,
27691 total: filtered.file.size,
27692 indeterminate: false
27693 });
27694 });
27695 xhr.addEventListener("error", () => {
27696 if (aborted) {
27697 return;
27698 }
27699 const error = new Error("Network error during upload.");
27700 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
27701 // `filtered.file` — same identity as UPLOAD_STARTED /
27702 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
27703 // that swapped the File would otherwise route this
27704 // failure to a row keyed by the original (pre-swap)
27705 // File, leaving the HUD row stuck in "running".
27706 file: filtered.file,
27707 error,
27708 context: args.context
27709 });
27710 reject(error);
27711 });
27712 xhr.addEventListener("abort", () => {
27713 const error = new UploadAbortedError();
27714 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
27715 // `filtered.file` — same identity as UPLOAD_STARTED /
27716 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
27717 // that swapped the File would otherwise route this
27718 // failure to a row keyed by the original (pre-swap)
27719 // File, leaving the HUD row stuck in "running".
27720 file: filtered.file,
27721 error,
27722 context: args.context
27723 });
27724 reject(error);
27725 });
27726 xhr.addEventListener("load", () => {
27727 if (aborted) {
27728 return;
27729 }
27730 if (xhr.status < 200 || xhr.status >= 300) {
27731 const message = extractXhrMessage(xhr);
27732 const error = new Error(message);
27733 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
27734 file: filtered.file,
27735 error,
27736 context: args.context
27737 });
27738 reject(error);
27739 return;
27740 }
27741 let data;
27742 try {
27743 data = JSON.parse(xhr.responseText);
27744 } catch (err) {
27745 const error = err instanceof Error ? err : new Error("Could not parse server response.");
27746 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
27747 file: filtered.file,
27748 error,
27749 context: args.context
27750 });
27751 reject(error);
27752 return;
27753 }
27754 if (cancelRequested && data.id) {
27755 void deleteAttachment(
27756 args.mediaUrl,
27757 args.restNonce,
27758 data.id
27759 );
27760 const error = new UploadAbortedError();
27761 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
27762 file: filtered.file,
27763 error,
27764 context: args.context
27765 });
27766 reject(error);
27767 return;
27768 }
27769 const result = {
27770 id: data.id,
27771 url: data.source_url,
27772 mime: data.mime_type || filtered.mime,
27773 title: data.title?.rendered || filtered.fields.title,
27774 filename: data.media_details?.file || filtered.fields.filename
27775 };
27776 doAction(FILE_DROP_HOOKS.AFTER_UPLOAD, {
27777 file: filtered.file,
27778 result,
27779 fields: filtered.fields,
27780 context: args.context
27781 });
27782 resolve2(result);
27783 });
27784 xhr.send(body);
27785 });
27786 }
27787 class UploadCancelledError extends Error {
27788 constructor() {
27789 super("Upload cancelled by desktop-mode.drop.before-upload filter.");
27790 this.name = "UploadCancelledError";
27791 }
27792 }
27793 class UploadAbortedError extends Error {
27794 constructor() {
27795 super("Upload aborted by the caller.");
27796 this.name = "UploadAbortedError";
27797 }
27798 }
27799 function deleteAttachment(mediaUrl, restNonce, id) {
27800 const url = `${mediaUrl.replace(/\/$/, "")}/${id}?force=true`;
27801 const cleanup = new XMLHttpRequest();
27802 cleanup.open("DELETE", url, true);
27803 cleanup.withCredentials = true;
27804 cleanup.setRequestHeader("X-WP-Nonce", restNonce);
27805 return new Promise((resolve2) => {
27806 cleanup.addEventListener("loadend", () => {
27807 if (cleanup.status < 200 || cleanup.status >= 300) {
27808 console.warn(
27809 `[os-file-drop] late-cancel cleanup failed for attachment ${id} (HTTP ${cleanup.status}). The attachment remains in the Media Library; delete it manually.`
27810 );
27811 }
27812 resolve2();
27813 });
27814 cleanup.addEventListener("error", () => {
27815 console.warn(
27816 `[os-file-drop] late-cancel cleanup network error for attachment ${id}. The attachment remains in the Media Library; delete it manually.`
27817 );
27818 resolve2();
27819 });
27820 try {
27821 cleanup.send();
27822 } catch (err) {
27823 console.warn(
27824 `[os-file-drop] late-cancel cleanup could not be dispatched for attachment ${id}:`,
27825 err
27826 );
27827 resolve2();
27828 }
27829 });
27830 }
27831 function extractXhrMessage(xhr) {
27832 const fallback = `Upload failed (HTTP ${xhr.status}).`;
27833 const text = xhr.responseText;
27834 if (!text) {
27835 return fallback;
27836 }
27837 try {
27838 const data = JSON.parse(text);
27839 if (data && typeof data.message === "string") {
27840 return data.message;
27841 }
27842 } catch {
27843 }
27844 return fallback;
27845 }
27846 async function openUploadDialog(args) {
27847 if (args.entries.length === 0) {
27848 return;
27849 }
27850 const modal = document.createElement("wpd-modal");
27851 modal.setAttribute("open", "");
27852 modal.setAttribute("size", "md");
27853 modal.setAttribute(
27854 "title",
27855 args.entries.length === 1 ? "Upload to Media Library" : `Upload ${args.entries.length} files to Media Library`
27856 );
27857 document.body.appendChild(modal);
27858 const draft = args.entries.map((entry) => ({
27859 ...entry.fields
27860 }));
27861 const renderBody = () => {
27862 modal.innerHTML = "";
27863 const list2 = document.createElement("div");
27864 list2.style.cssText = "display:flex;flex-direction:column;gap:18px;max-height:60vh;overflow:auto;padding-right:6px;";
27865 args.entries.forEach((entry, i) => {
27866 list2.appendChild(renderEntry(entry, draft[i], i + 1));
27867 });
27868 modal.appendChild(list2);
27869 const footer = document.createElement("div");
27870 footer.setAttribute("slot", "footer");
27871 footer.style.cssText = "display:flex;gap:8px;justify-content:flex-end;";
27872 const cancel = document.createElement("wpd-button");
27873 cancel.setAttribute("variant", "secondary");
27874 cancel.textContent = "Cancel";
27875 cancel.addEventListener("click", () => {
27876 modal.remove();
27877 });
27878 const upload = document.createElement("wpd-button");
27879 upload.setAttribute("variant", "primary");
27880 upload.textContent = args.entries.length === 1 ? "Upload" : `Upload ${args.entries.length} files`;
27881 upload.addEventListener("click", () => {
27882 void runUploads(upload, cancel);
27883 });
27884 footer.appendChild(cancel);
27885 footer.appendChild(upload);
27886 modal.appendChild(footer);
27887 };
27888 const renderEntry = (entry, fields, index2) => {
27889 const wrap = document.createElement("div");
27890 wrap.style.cssText = "display:flex;flex-direction:column;gap:8px;border:1px solid rgba(255,255,255,0.08);border-radius:10px;padding:14px;";
27891 const heading = document.createElement("div");
27892 heading.style.cssText = "display:flex;gap:10px;align-items:center;font-weight:600;";
27893 const tag = document.createElement("span");
27894 tag.textContent = args.entries.length === 1 ? "" : `#${index2} · `;
27895 tag.style.opacity = "0.6";
27896 const fname = document.createElement("span");
27897 fname.textContent = entry.file.name;
27898 fname.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
27899 const size = document.createElement("span");
27900 size.textContent = `${entry.mime || "unknown"} · ${formatBytes(
27901 entry.file.size
27902 )}`;
27903 size.style.cssText = "opacity:0.6;font-size:12px;";
27904 heading.appendChild(tag);
27905 heading.appendChild(fname);
27906 heading.appendChild(size);
27907 wrap.appendChild(heading);
27908 wrap.appendChild(textField("Title", fields.title, (v) => fields.title = v));
27909 wrap.appendChild(textField("Filename", fields.filename, (v) => fields.filename = v));
27910 if (entry.mime.startsWith("image/")) {
27911 wrap.appendChild(
27912 textField("Alt text", fields.altText, (v) => fields.altText = v)
27913 );
27914 }
27915 wrap.appendChild(textField("Caption", fields.caption, (v) => fields.caption = v));
27916 wrap.appendChild(
27917 textareaField("Description", fields.description, (v) => fields.description = v)
27918 );
27919 return wrap;
27920 };
27921 const runUploads = async (uploadBtn, cancelBtn) => {
27922 uploadBtn.disabled = true;
27923 cancelBtn.disabled = true;
27924 uploadBtn.textContent = "Uploading…";
27925 const total = args.entries.length;
27926 let successes = 0;
27927 let failures = 0;
27928 let cancelled = 0;
27929 const failureDetails = [];
27930 for (let i = 0; i < total; i++) {
27931 const entry = args.entries[i];
27932 try {
27933 await uploadFile({
27934 file: entry.file,
27935 mime: entry.mime,
27936 fields: draft[i],
27937 context: args.context,
27938 mediaUrl: args.mediaUrl,
27939 restNonce: args.restNonce
27940 });
27941 successes++;
27942 } catch (err) {
27943 if (err instanceof UploadCancelledError) {
27944 cancelled++;
27945 continue;
27946 }
27947 if (err instanceof UploadAbortedError) {
27948 cancelled++;
27949 continue;
27950 }
27951 failures++;
27952 const message = err instanceof Error ? err.message : "Upload failed.";
27953 failureDetails.push(`“${entry.file.name}” — ${message}`);
27954 }
27955 }
27956 modal.remove();
27957 showBatchSummaryToast({
27958 total,
27959 successes,
27960 failures,
27961 cancelled,
27962 failureDetails
27963 });
27964 };
27965 renderBody();
27966 await new Promise((resolve2) => {
27967 modal.addEventListener("wpd-modal-cancel", () => {
27968 modal.remove();
27969 resolve2();
27970 });
27971 const observer = new MutationObserver(() => {
27972 if (!modal.isConnected) {
27973 observer.disconnect();
27974 resolve2();
27975 }
27976 });
27977 observer.observe(document.body, { childList: true, subtree: true });
27978 });
27979 }
27980 function textField(label, value, onChange) {
27981 const el = document.createElement("wpd-text-field");
27982 el.setAttribute("label", label);
27983 el.setAttribute("value", value);
27984 el.addEventListener("input", () => {
27985 const v = el.value;
27986 if (typeof v === "string") {
27987 onChange(v);
27988 }
27989 });
27990 return el;
27991 }
27992 function textareaField(label, value, onChange) {
27993 const el = document.createElement("wpd-textarea");
27994 el.setAttribute("label", label);
27995 el.setAttribute("value", value);
27996 el.setAttribute("rows", "3");
27997 el.addEventListener("input", () => {
27998 const v = el.value;
27999 if (typeof v === "string") {
28000 onChange(v);
28001 }
28002 });
28003 return el;
28004 }
28005 function showBatchSummaryToast(args) {
28006 const { total, successes, failures, cancelled, failureDetails } = args;
28007 if (total === 0) {
28008 return;
28009 }
28010 if (total === 1) {
28011 if (successes === 1) {
28012 showToast({ message: "Uploaded to Media Library." });
28013 } else if (failures === 1 && failureDetails[0]) {
28014 showToast({ message: failureDetails[0] });
28015 } else if (cancelled === 1) {
28016 showToast({ message: "Upload cancelled." });
28017 }
28018 return;
28019 }
28020 if (successes === total) {
28021 showToast({
28022 message: `Uploaded ${successes} files to Media Library.`
28023 });
28024 return;
28025 }
28026 if (cancelled === total) {
28027 showToast({ message: "All uploads cancelled." });
28028 return;
28029 }
28030 if (failures === total) {
28031 showToast({
28032 message: failures === 1 && failureDetails[0] ? failureDetails[0] : `${failures} uploads failed.`
28033 });
28034 return;
28035 }
28036 const parts = [];
28037 if (successes > 0) {
28038 parts.push(
28039 `Uploaded ${successes} file${successes === 1 ? "" : "s"}.`
28040 );
28041 }
28042 if (cancelled > 0) {
28043 parts.push(`Cancelled ${cancelled}.`);
28044 }
28045 if (failures > 0) {
28046 parts.push(`Failed ${failures}.`);
28047 }
28048 showToast({ message: parts.join(" ") });
28049 }
28050 const dialog = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
28051 __proto__: null,
28052 openUploadDialog
28053 }, Symbol.toStringTag, { value: "Module" }));
28054 exports.clampGeometryToViewport = clampGeometryToViewport;
28055 Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
28056 return exports;
28057 }({});
28058