PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.8.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.8.8
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.8, at assets/js/desktop.js

29,957 lines 977.3 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 target2 = e.target;
1407 const winEl = target2?.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 (target2 === 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 target2 = e.target;
1482 if (target2?.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 target2 = e.target;
1497 const winEl = target2?.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 target2 = e.target;
2106 const winEl = target2?.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 (target2) {
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 target2 = list2[nextIdx];
3492 if (target2.state === "minimized") {
3493 target2.restore();
3494 } else {
3495 mgr.focus(target2);
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 target2 = cur;
4145 for (const k of Object.keys(target2)) {
4146 delete target2[k];
4147 }
4148 Object.assign(target2, 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(restRoot2, path) {
6380 const base = typeof window !== "undefined" && window.location ? window.location.href : FALLBACK_BASE;
6381 const url = new URL(restRoot2, 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 restRoot2 = 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(restRoot2, `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 const scroll = document.createElement("div");
6726 scroll.className = "desktop-mode-dock__scroll";
6727 const pinned = document.createElement("div");
6728 pinned.className = "desktop-mode-dock__pinned";
6729 container.appendChild(scroll);
6730 container.appendChild(pinned);
6731 this.itemHost = scroll;
6732 this.systemHost = pinned;
6733 this.tooltip = document.createElement("div");
6734 this.tooltip.className = "desktop-mode-dock__tooltip";
6735 this.tooltip.setAttribute("role", "tooltip");
6736 if (orientation === "bottom") {
6737 this.tooltip.classList.add("desktop-mode-dock__tooltip--above");
6738 } else if (orientation === "right") {
6739 this.tooltip.classList.add("desktop-mode-dock__tooltip--before");
6740 } else {
6741 this.tooltip.classList.add("desktop-mode-dock__tooltip--after");
6742 }
6743 document.body.appendChild(this.tooltip);
6744 this.render();
6745 this.bindWindowEvents();
6746 }
6747 /**
6748 * Build the base context object every dock decoration hook
6749 * receives. Read from `this` so a single subscriber can
6750 * disambiguate two coexisting rails by `dockId`.
6751 */
6752 buildHookContextBase() {
6753 return {
6754 rail: this.rail,
6755 orientation: this.orientation,
6756 dockId: this.container.id,
6757 container: this.container
6758 };
6759 }
6760 /**
6761 * Replace the menu-derived tile list with a fresh one, preserving
6762 * any JS-registered system tiles. Used by the live menu-refresh
6763 * path: after a plugin is activated or deactivated, the chromeless
6764 * bridge postMessages a fresh payload built from real admin
6765 * context, and the shell calls this so the dock repaints without
6766 * a tab reload.
6767 *
6768 * Old menu tiles are removed from both the DOM and the lookup
6769 * map; new tiles are inserted before the system separator (or
6770 * appended at the end if none exists yet), so the menu-items →
6771 * hairline → system-items ordering stays intact. Active-state
6772 * classes are re-computed once the new tiles are in place so
6773 * window indicators survive the swap.
6774 *
6775 * @param items New DockItem list. Pass `[]` to clear everything
6776 * menu-derived.
6777 */
6778 /**
6779 * Update the dock's orientation. Writes the new value to the
6780 * dock element's `data-desktop-mode-dock-placement` attribute (CSS
6781 * keys off it for layout) and keeps the tooltip anchor in sync.
6782 *
6783 * In practice, the layout dispatcher in `desktop.ts` rebuilds the
6784 * dock(s) from scratch on a layout change rather than re-orienting
6785 * a live instance — but this stays correct in case any caller
6786 * wants to flip orientation without the rebuild.
6787 */
6788 setOrientation(orientation) {
6789 if (this.orientation === orientation) {
6790 return;
6791 }
6792 this.orientation = orientation;
6793 this.container.setAttribute(
6794 "data-desktop-mode-dock-placement",
6795 orientation
6796 );
6797 this.tooltip.classList.remove(
6798 "desktop-mode-dock__tooltip--above",
6799 "desktop-mode-dock__tooltip--before",
6800 "desktop-mode-dock__tooltip--after"
6801 );
6802 if (orientation === "bottom") {
6803 this.tooltip.classList.add("desktop-mode-dock__tooltip--above");
6804 } else if (orientation === "right") {
6805 this.tooltip.classList.add("desktop-mode-dock__tooltip--before");
6806 } else {
6807 this.tooltip.classList.add("desktop-mode-dock__tooltip--after");
6808 }
6809 }
6810 replaceItems(items) {
6811 for (const itemId of this.itemElements.keys()) {
6812 const teardown = this.peekTeardowns.get(itemId);
6813 if (teardown) {
6814 teardown();
6815 this.peekTeardowns.delete(itemId);
6816 }
6817 }
6818 for (const el of this.itemElements.values()) {
6819 el.remove();
6820 }
6821 this.itemHost.querySelectorAll(
6822 ".desktop-mode-dock__separator--group"
6823 ).forEach((el) => el.remove());
6824 this.itemElements.clear();
6825 this.items = items;
6826 const base = this.buildHookContextBase();
6827 doAction(HOOKS.DOCK_BEFORE_RENDER, {
6828 ...base,
6829 items,
6830 tileElements: this.itemElements
6831 });
6832 let insertedGroupSeparator = false;
6833 let tilesInsertedThisPass = 0;
6834 for (const item of items) {
6835 if (!insertedGroupSeparator && item.isCore === false) {
6836 if (tilesInsertedThisPass > 0) {
6837 const sep = document.createElement("div");
6838 sep.className = "desktop-mode-dock__separator desktop-mode-dock__separator--group";
6839 sep.setAttribute("aria-hidden", "true");
6840 this.itemHost.appendChild(sep);
6841 }
6842 insertedGroupSeparator = true;
6843 }
6844 const btn = this.createItemButton(item);
6845 this.itemElements.set(item.id, btn);
6846 this.itemHost.appendChild(btn);
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.systemHost.appendChild(this.systemSeparator);
7031 }
7032 const tile2 = this.createSystemItemButton(item);
7033 this.systemItemElements.set(item.id, tile2);
7034 this.systemHost.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.itemHost.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.itemHost.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.itemHost.appendChild(sep);
7079 }
7080 insertedGroupSeparator = true;
7081 }
7082 const btn = this.createItemButton(item);
7083 this.itemElements.set(item.id, btn);
7084 this.itemHost.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.itemHost.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.itemHost.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.itemHost.insertBefore(tile2, targetTile);
7411 reordered = true;
7412 }
7413 } else if (targetTile.nextSibling !== tile2) {
7414 this.itemHost.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.itemHost.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 target2;
7646 try {
7647 const u = new URL(url, window.location.href);
7648 const filename = u.pathname.split("/").pop() || "";
7649 target2 = filename + u.search;
7650 } catch {
7651 return null;
7652 }
7653 if (!target2) {
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(target2)) {
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 target2 = 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 target2 = json.data.redirect;
8368 }
8369 }
8370 } catch {
8371 }
8372 navigateTop(target2);
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$6 = 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$6];
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 // Ship the live Connection alongside the id so
10561 // iframe-initiated connections can be subscribed
10562 // to directly from the hook handler — without
10563 // `wp.desktop.getConnection(id)` plumbing the
10564 // payload would carry the id but no way to call
10565 // `.subscribe()` against it.
10566 connection: conn
10567 });
10568 try {
10569 opts.onOpen?.();
10570 } catch (err) {
10571 if (typeof console !== "undefined") {
10572 console.error(
10573 "[desktop-mode] connection.onOpen threw:",
10574 err
10575 );
10576 }
10577 }
10578 flushQueue();
10579 return;
10580 }
10581 if (msg.type === "desktop-mode-bridge-publish") {
10582 const m = data;
10583 const topic = typeof m.topic === "string" ? m.topic : "";
10584 if (!topic) {
10585 return;
10586 }
10587 doAction(HOOKS.CONNECTION_MESSAGE, {
10588 connectionId: id,
10589 topic,
10590 direction: "in"
10591 });
10592 const exact = subs.get(topic);
10593 if (exact) {
10594 for (const cb of Array.from(exact)) {
10595 try {
10596 cb(m.payload, { topic });
10597 } catch (err) {
10598 if (typeof console !== "undefined") {
10599 console.error(
10600 "[desktop-mode] connection subscriber threw:",
10601 err
10602 );
10603 }
10604 }
10605 }
10606 }
10607 const wildcard = subs.get("*");
10608 if (wildcard) {
10609 for (const cb of Array.from(wildcard)) {
10610 try {
10611 cb(m.payload, { topic });
10612 } catch (err) {
10613 if (typeof console !== "undefined") {
10614 console.error(
10615 "[desktop-mode] connection wildcard subscriber threw:",
10616 err
10617 );
10618 }
10619 }
10620 }
10621 }
10622 return;
10623 }
10624 if (msg.type === "desktop-mode-bridge-disconnect") {
10625 conn._destroy("disconnect");
10626 }
10627 },
10628 _destroy(reason) {
10629 if (destroyed) {
10630 return;
10631 }
10632 destroyed = true;
10633 const wasOpen = isOpen;
10634 isOpen = false;
10635 _connections.delete(id);
10636 const targetSet = _connectionsByTarget.get(targetWindowId);
10637 if (targetSet) {
10638 targetSet.delete(id);
10639 if (targetSet.size === 0) {
10640 _connectionsByTarget.delete(targetWindowId);
10641 }
10642 }
10643 for (const off of nativeSubUnsubs.splice(0)) {
10644 try {
10645 off();
10646 } catch {
10647 }
10648 }
10649 if (wasOpen) {
10650 const iframe2 = targetIframe();
10651 if (iframe2) {
10652 sendToIframe(iframe2, {
10653 type: "desktop-mode-bridge-disconnect",
10654 connectionId: id
10655 });
10656 }
10657 }
10658 doAction(HOOKS.CONNECTION_CLOSED, {
10659 connectionId: id,
10660 reason
10661 });
10662 try {
10663 opts.onClose?.(reason);
10664 } catch (err) {
10665 if (typeof console !== "undefined") {
10666 console.error(
10667 "[desktop-mode] connection.onClose threw:",
10668 err
10669 );
10670 }
10671 }
10672 }
10673 };
10674 _connections.set(id, conn);
10675 let bucket2 = _connectionsByTarget.get(targetWindowId);
10676 if (!bucket2) {
10677 bucket2 = /* @__PURE__ */ new Set();
10678 _connectionsByTarget.set(targetWindowId, bucket2);
10679 }
10680 bucket2.add(id);
10681 if (isNativeTarget()) {
10682 Promise.resolve().then(() => {
10683 if (destroyed || isOpen) {
10684 return;
10685 }
10686 isOpen = true;
10687 doAction(HOOKS.CONNECTION_OPENED, {
10688 connectionId: id,
10689 targetWindowId,
10690 topics
10691 });
10692 try {
10693 opts.onOpen?.();
10694 } catch (err) {
10695 if (typeof console !== "undefined") {
10696 console.error(
10697 "[desktop-mode] connection.onOpen threw:",
10698 err
10699 );
10700 }
10701 }
10702 });
10703 return conn;
10704 }
10705 const iframe = targetIframe();
10706 if (iframe) {
10707 sendToIframe(iframe, {
10708 type: "desktop-mode-bridge-handshake",
10709 connectionId: id,
10710 targetWindowId,
10711 topics
10712 });
10713 }
10714 return conn;
10715 };
10716 const routeIncomingFromIframe = (data, windowId) => {
10717 if (!data || typeof data !== "object") {
10718 return;
10719 }
10720 const msg = data;
10721 if (typeof msg.type !== "string" || !msg.type.startsWith("desktop-mode-bridge-")) {
10722 return;
10723 }
10724 if (msg.type === "desktop-mode-bridge-connection-request" && typeof msg.requestId === "string" && typeof windowId === "string" && windowId !== "") {
10725 handleConnectionRequest(windowId, msg.requestId, Array.isArray(msg.topics) ? msg.topics : []);
10726 return;
10727 }
10728 if (typeof msg.connectionId !== "string") {
10729 return;
10730 }
10731 const conn = _connections.get(msg.connectionId);
10732 conn?._handleIframeMessage(data);
10733 };
10734 const handleConnectionRequest = (windowId, requestId, topics) => {
10735 const synth = _syntheticIframes.get(windowId);
10736 const iframe = synth ?? manager.getById(windowId)?.iframe ?? null;
10737 if (!iframe) {
10738 return;
10739 }
10740 const decision = applyFilters(
10741 HOOKS.IFRAME_CONNECTION_REQUEST,
10742 true,
10743 { windowId, requestId, topics: topics.slice() }
10744 );
10745 if (decision === false) {
10746 try {
10747 iframe.contentWindow?.postMessage({
10748 type: "desktop-mode-bridge-connection-ack",
10749 requestId,
10750 accepted: false,
10751 reason: "rejected"
10752 }, INITIAL_ORIGIN$2);
10753 } catch {
10754 }
10755 return;
10756 }
10757 const finalTopics = decision && typeof decision === "object" && Array.isArray(decision.topics) ? decision.topics : topics;
10758 const conn = connect(windowId, { topics: finalTopics });
10759 try {
10760 iframe.contentWindow?.postMessage({
10761 type: "desktop-mode-bridge-connection-ack",
10762 requestId,
10763 accepted: true,
10764 connectionId: conn.id
10765 }, INITIAL_ORIGIN$2);
10766 } catch {
10767 }
10768 };
10769 const onIframeReady = (windowId) => {
10770 const bucket2 = _connectionsByTarget.get(windowId);
10771 if (!bucket2) {
10772 return;
10773 }
10774 for (const connId of Array.from(bucket2)) {
10775 const conn = _connections.get(connId);
10776 if (!conn || conn.isOpen()) {
10777 continue;
10778 }
10779 const iframe = conn._targetWindow();
10780 if (!iframe) {
10781 continue;
10782 }
10783 sendToIframe(iframe, {
10784 type: "desktop-mode-bridge-handshake",
10785 connectionId: conn.id,
10786 targetWindowId: conn.target,
10787 topics: []
10788 // already negotiated client-side; iframe re-uses
10789 });
10790 }
10791 };
10792 const onWindowClosed = (windowId) => {
10793 const bucket2 = _connectionsByTarget.get(windowId);
10794 if (!bucket2) {
10795 return;
10796 }
10797 for (const connId of Array.from(bucket2)) {
10798 const conn = _connections.get(connId);
10799 conn?._destroy("window-closed");
10800 }
10801 };
10802 const getConnection = (connectionId) => {
10803 const conn = _connections.get(connectionId);
10804 return conn ?? null;
10805 };
10806 return {
10807 connect,
10808 getConnection,
10809 routeIncomingFromIframe,
10810 onIframeReady,
10811 onWindowClosed
10812 };
10813 }
10814 const __vite_import_meta_env__ = {};
10815 function devLog(...args) {
10816 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;
10817 if (mode !== "production") {
10818 console.log(...args);
10819 }
10820 }
10821 const OWNER_PREFIX = "iframe:";
10822 function ownerFor(windowId) {
10823 return OWNER_PREFIX + windowId;
10824 }
10825 function iconFor(harvested) {
10826 if (harvested.icon && typeof harvested.icon === "string" && harvested.icon.startsWith("dashicons-")) {
10827 return harvested.icon;
10828 }
10829 return harvested.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
10830 }
10831 function slugFor(windowId, name) {
10832 const safeName = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
10833 const safeWin = windowId.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
10834 return `win-${safeWin}-${safeName}`;
10835 }
10836 class IframeCommandBridge {
10837 constructor(opts) {
10838 this.subscribedWindowId = null;
10839 this.manager = opts.manager;
10840 this.adminUrl = opts.adminUrl;
10841 }
10842 /** Wire up the focus / close / message listeners. Idempotent. */
10843 install() {
10844 document.addEventListener("desktop-mode-window-focused", (e) => {
10845 const detail = e.detail;
10846 if (detail && typeof detail.windowId === "string") {
10847 this.onFocused(detail.windowId);
10848 }
10849 });
10850 document.addEventListener("desktop-mode-window-closed", (e) => {
10851 const detail = e.detail;
10852 if (detail && typeof detail.windowId === "string") {
10853 unregisterByOwner(ownerFor(detail.windowId));
10854 if (this.subscribedWindowId === detail.windowId) {
10855 this.subscribedWindowId = null;
10856 }
10857 }
10858 });
10859 document.addEventListener("desktop-mode-window-changed", (e) => {
10860 const detail = e.detail;
10861 if (!detail || typeof detail.windowId !== "string") {
10862 return;
10863 }
10864 if (detail.reason !== "state") {
10865 return;
10866 }
10867 if (detail.state !== "minimized") {
10868 return;
10869 }
10870 if (this.subscribedWindowId === detail.windowId) {
10871 this.subscribedWindowId = null;
10872 }
10873 });
10874 window.addEventListener("message", (e) => {
10875 if (e.origin !== window.location.origin) {
10876 return;
10877 }
10878 const data = e.data;
10879 if (!data || typeof data.type !== "string") {
10880 return;
10881 }
10882 if (data.type === "desktop-mode-bridge-ready") {
10883 const win2 = this.manager.findByIframeSource(e.source);
10884 if (win2 && win2.id === this.subscribedWindowId) {
10885 this.sendSubscribe(win2.id);
10886 }
10887 return;
10888 }
10889 if (data.type !== "desktop-mode-commands-list") {
10890 return;
10891 }
10892 if (!Array.isArray(data.commands)) {
10893 return;
10894 }
10895 const win = this.manager.findByIframeSource(e.source);
10896 if (!win) {
10897 return;
10898 }
10899 if (win.id !== this.subscribedWindowId) {
10900 return;
10901 }
10902 this.applyList(win.id, data.commands);
10903 });
10904 const focused = this.manager.getFocused();
10905 if (focused) {
10906 this.onFocused(focused.id);
10907 }
10908 }
10909 onFocused(windowId) {
10910 if (this.subscribedWindowId === windowId) {
10911 return;
10912 }
10913 if (this.subscribedWindowId) {
10914 const prev = this.manager.getById(this.subscribedWindowId);
10915 if (prev && prev.iframe && prev.iframe.contentWindow) {
10916 try {
10917 prev.iframe.contentWindow.postMessage(
10918 { type: "desktop-mode-commands-unsubscribe" },
10919 window.location.origin
10920 );
10921 } catch {
10922 }
10923 }
10924 unregisterByOwner(ownerFor(this.subscribedWindowId));
10925 }
10926 this.subscribedWindowId = windowId;
10927 this.sendSubscribe(windowId);
10928 }
10929 sendSubscribe(windowId) {
10930 const win = this.manager.getById(windowId);
10931 if (!win) {
10932 return;
10933 }
10934 if (!win.iframe) {
10935 return;
10936 }
10937 if (!win.iframe.contentWindow) {
10938 return;
10939 }
10940 try {
10941 win.iframe.contentWindow.postMessage(
10942 { type: "desktop-mode-commands-subscribe" },
10943 window.location.origin
10944 );
10945 } catch (err) {
10946 devLog("[wpd-cmd:parent] sendSubscribe: postMessage threw", err);
10947 }
10948 }
10949 applyList(windowId, commands) {
10950 const owner = ownerFor(windowId);
10951 unregisterByOwner(owner);
10952 for (const cmd of commands) {
10953 if (!cmd || !cmd.name || !cmd.label) {
10954 continue;
10955 }
10956 const slug = slugFor(windowId, cmd.name);
10957 const safeSvg = typeof cmd.iconSvg === "string" && cmd.iconSvg !== "" ? sanitizeIconSvg(cmd.iconSvg) : "";
10958 const def = {
10959 slug,
10960 label: cmd.label,
10961 icon: iconFor(cmd),
10962 iconSvg: safeSvg !== "" ? safeSvg : void 0,
10963 owner,
10964 // Harvested commands are contextual by construction —
10965 // they come from whichever window has focus. Surface
10966 // them eagerly so the user sees "Duplicate block" /
10967 // "Toggle distraction free" without having to type `/`
10968 // first.
10969 eager: true,
10970 run: cmd.kind === "navigate" && cmd.url ? this.runNavigate(cmd.url, cmd.label, iconFor(cmd)) : this.runProxy(windowId, cmd.name)
10971 };
10972 try {
10973 registerCommand(def);
10974 } catch (err) {
10975 console.error(
10976 "[desktop-mode] iframe-bridge: dropping bad command",
10977 def,
10978 err
10979 );
10980 }
10981 }
10982 }
10983 runNavigate(url, title, icon) {
10984 return (_args, ctx) => {
10985 ctx.close();
10986 if (tryNativeUrlRemap(url)) {
10987 return;
10988 }
10989 const id = deriveWindowId(url, this.adminUrl);
10990 this.manager.open({ id, baseId: id, url, title, icon });
10991 };
10992 }
10993 runProxy(windowId, name) {
10994 return (_args, ctx) => {
10995 ctx.close();
10996 const win = this.manager.getById(windowId);
10997 if (!win || !win.iframe || !win.iframe.contentWindow) {
10998 return;
10999 }
11000 try {
11001 win.iframe.contentWindow.postMessage(
11002 { type: "desktop-mode-commands-invoke", name },
11003 window.location.origin
11004 );
11005 } catch {
11006 }
11007 this.manager.focus(win);
11008 };
11009 }
11010 }
11011 const OWNER = "global";
11012 const NAV_HREF_LITERAL_RE = /(?:document\.location\.href|window\.location\.href|location\.href)\s*=\s*['"]([^'"$]+?)['"]/;
11013 const NAV_ASSIGN_LITERAL_RE = /(?:document\.location|window\.location|location)\s*=\s*['"]([^'"$]+?)['"]/;
11014 const NAV_CALL_LITERAL_RE = /location\.(?:assign|replace)\s*\(\s*['"]([^'"$]+?)['"]\s*\)/;
11015 const NAV_INTENT_RE = /(?:document\.location|window\.location|location)\s*(?:\.href\s*)?=|location\.(?:assign|replace)\s*\(/;
11016 const SITE_EDITOR_INTENT_RE = /getSiteEditorPage\s*\(|site-editor\.php/;
11017 const SITE_EDITOR_NAME_RE = /^(wp_template_part|wp_template|wp_navigation|wp_block)-(.+)$/;
11018 function lookupMenuCommand(name) {
11019 const list2 = window.__desktopModeMenuCommands;
11020 if (!Array.isArray(list2)) {
11021 return null;
11022 }
11023 for (const entry of list2) {
11024 if (entry && typeof entry === "object" && entry.name === name && typeof entry.url === "string" && entry.url !== "") {
11025 return {
11026 label: typeof entry.label === "string" ? entry.label : "",
11027 url: entry.url
11028 };
11029 }
11030 }
11031 return null;
11032 }
11033 class ShellCommandHarvester {
11034 constructor(opts) {
11035 this.mounted = false;
11036 this.host = null;
11037 this.root = null;
11038 this.kindCache = /* @__PURE__ */ Object.create(null);
11039 this.callbackCache = /* @__PURE__ */ Object.create(null);
11040 this.lastFingerprint = "";
11041 this.manager = opts.manager;
11042 this.adminUrl = opts.adminUrl;
11043 }
11044 /** Mount the harvester. Idempotent. Safe to call before `wp.data` loads. */
11045 install() {
11046 this.tryMount(0);
11047 }
11048 tryMount(attempt) {
11049 if (this.mounted) {
11050 return;
11051 }
11052 const wp = window.wp;
11053 if (!wp || !wp.data || !wp.element || typeof wp.data.subscribe !== "function") {
11054 if (attempt < 40) {
11055 window.setTimeout(() => this.tryMount(attempt + 1), 150);
11056 }
11057 return;
11058 }
11059 this.mount();
11060 }
11061 mount() {
11062 const wp = window.wp;
11063 const el = wp.element;
11064 const data = wp.data;
11065 const createEl = el.createElement;
11066 const useEffect = el.useEffect;
11067 const useRef = el.useRef;
11068 const useMemo = el.useMemo;
11069 const useSelect = data.useSelect;
11070 if (typeof createEl !== "function" || typeof useEffect !== "function" || typeof useRef !== "function" || typeof useMemo !== "function" || typeof useSelect !== "function" || typeof el.createRoot !== "function") {
11071 return;
11072 }
11073 this.mounted = true;
11074 const host = document.createElement("div");
11075 host.setAttribute("aria-hidden", "true");
11076 host.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;left:-9999px;top:-9999px;";
11077 (document.body || document.documentElement).appendChild(host);
11078 this.host = host;
11079 const bucket2 = {
11080 perLoader: {},
11081 statics: [],
11082 loadersList: []
11083 };
11084 const fingerprint2 = (cmds) => {
11085 if (!Array.isArray(cmds) || cmds.length === 0) {
11086 return "";
11087 }
11088 const keys = new Array(cmds.length);
11089 for (let i = 0; i < cmds.length; i++) {
11090 const c = cmds[i];
11091 keys[i] = c && c.name ? c.name : "";
11092 }
11093 return keys.join("|");
11094 };
11095 const mergeAndPublish = () => {
11096 let merged = [];
11097 for (const name of bucket2.loadersList) {
11098 const slice = bucket2.perLoader[name];
11099 if (Array.isArray(slice)) {
11100 merged = merged.concat(slice);
11101 }
11102 }
11103 if (Array.isArray(bucket2.statics)) {
11104 merged = merged.concat(bucket2.statics);
11105 }
11106 this.callbackCache = /* @__PURE__ */ Object.create(null);
11107 for (const cc of merged) {
11108 if (cc && cc.name && typeof cc.callback === "function") {
11109 this.callbackCache[cc.name] = cc.callback;
11110 }
11111 }
11112 this.publish(merged);
11113 };
11114 const LoaderSlot = (props) => {
11115 const loader = props.loader;
11116 let result = null;
11117 try {
11118 result = loader.hook({ search: "" });
11119 } catch {
11120 }
11121 const cmds = result && Array.isArray(result.commands) ? result.commands : [];
11122 const key = useMemo(() => fingerprint2(cmds), [cmds]);
11123 useEffect(() => {
11124 bucket2.perLoader[loader.name] = cmds;
11125 mergeAndPublish();
11126 }, [key]);
11127 useEffect(() => {
11128 return () => {
11129 delete bucket2.perLoader[loader.name];
11130 mergeAndPublish();
11131 };
11132 }, []);
11133 return null;
11134 };
11135 const Harvester = () => {
11136 const loaders = useSelect((s) => {
11137 const ss = s("core/commands");
11138 if (!ss || typeof ss.getCommandLoaders !== "function") {
11139 return [];
11140 }
11141 return [
11142 ...ss.getCommandLoaders(false) || [],
11143 ...ss.getCommandLoaders(true) || []
11144 ];
11145 }, []);
11146 const staticCmds = useSelect((s) => {
11147 const ss = s("core/commands");
11148 if (!ss || typeof ss.getCommands !== "function") {
11149 return [];
11150 }
11151 return [
11152 ...ss.getCommands(false) || [],
11153 ...ss.getCommands(true) || []
11154 ];
11155 }, []);
11156 const loadersNames = useMemo(() => {
11157 return Array.isArray(loaders) ? loaders.map((l) => l ? l.name || "" : "") : [];
11158 }, [loaders]);
11159 const loadersKey = loadersNames.join("|");
11160 useEffect(() => {
11161 bucket2.loadersList = loadersNames;
11162 mergeAndPublish();
11163 }, [loadersKey]);
11164 const staticKey = useMemo(
11165 () => fingerprint2(Array.isArray(staticCmds) ? staticCmds : []),
11166 [staticCmds]
11167 );
11168 useEffect(() => {
11169 bucket2.statics = Array.isArray(staticCmds) ? staticCmds : [];
11170 mergeAndPublish();
11171 }, [staticKey]);
11172 if (!Array.isArray(loaders) || loaders.length === 0) {
11173 return null;
11174 }
11175 const children = [];
11176 for (const loader of loaders) {
11177 if (!loader || typeof loader.hook !== "function") {
11178 continue;
11179 }
11180 children.push(
11181 createEl(LoaderSlot, { key: loader.name, loader })
11182 );
11183 }
11184 return createEl(el.Fragment || "div", null, children);
11185 };
11186 try {
11187 this.root = el.createRoot(host);
11188 this.root.render(createEl(Harvester));
11189 } catch {
11190 this.mounted = false;
11191 this.root = null;
11192 if (this.host && this.host.parentNode) {
11193 this.host.parentNode.removeChild(this.host);
11194 }
11195 this.host = null;
11196 }
11197 }
11198 publish(raw) {
11199 const seen = /* @__PURE__ */ Object.create(null);
11200 const classified = [];
11201 for (const cmd of raw) {
11202 if (!cmd || !cmd.name || !cmd.label) {
11203 continue;
11204 }
11205 if (cmd.disabled) {
11206 continue;
11207 }
11208 if (seen[cmd.name]) {
11209 continue;
11210 }
11211 seen[cmd.name] = true;
11212 classified.push(this.classify(cmd));
11213 }
11214 let key = "";
11215 for (const c of classified) {
11216 key += `${c.name}|${c.kind}|${c.url || ""}
11217 `;
11218 }
11219 if (key === this.lastFingerprint) {
11220 return;
11221 }
11222 this.lastFingerprint = key;
11223 unregisterByOwner(OWNER);
11224 for (const c of classified) {
11225 if (c.kind === "skip") {
11226 continue;
11227 }
11228 const slug = `global-${c.name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-")}`;
11229 const icon = this.iconFor(c);
11230 const def = {
11231 slug,
11232 label: c.label,
11233 icon,
11234 iconSvg: c.iconSvg && c.iconSvg !== "" ? sanitizeIconSvg(c.iconSvg) : void 0,
11235 owner: OWNER,
11236 // NOT eager. The palette splits the registry into two
11237 // disjoint surfaces: `eager` commands show on empty
11238 // input (and are excluded from slash search at
11239 // `src/ai-assistant/impl.ts:494`); non-eager commands
11240 // show when the user types `/<query>`. The WP baseline
11241 // is large (~150 entries) and meant to be searched —
11242 // surfacing it eagerly would drown the iframe-harvested
11243 // contextual shortcuts on every open. Slash-search is
11244 // the right surface for it, matching the native WP
11245 // palette UX (open, type, find).
11246 run: c.kind === "navigate" && c.url ? this.runNavigate(c.url, c.windowTitle || c.label, icon) : this.runInvoke(c.name, c.label, icon)
11247 };
11248 try {
11249 registerCommand(def);
11250 } catch (err) {
11251 console.error(
11252 "[desktop-mode] shell-harvester: dropping bad command",
11253 def,
11254 err
11255 );
11256 }
11257 }
11258 }
11259 classify(cmd) {
11260 const out = {
11261 name: String(cmd.name),
11262 label: String(cmd.label),
11263 icon: typeof cmd.icon === "string" ? cmd.icon : void 0,
11264 iconSvg: void 0,
11265 kind: "action",
11266 url: void 0,
11267 callback: typeof cmd.callback === "function" ? cmd.callback : void 0
11268 };
11269 const cached = this.kindCache[out.name];
11270 if (cached) {
11271 out.kind = cached.kind;
11272 out.url = cached.url;
11273 out.iconSvg = cached.iconSvg;
11274 return out;
11275 }
11276 if (cmd.icon && typeof cmd.icon !== "string") {
11277 out.iconSvg = this.renderIcon(cmd.icon);
11278 }
11279 const menuEntry = lookupMenuCommand(out.name);
11280 if (menuEntry) {
11281 try {
11282 out.url = new URL(menuEntry.url, this.adminUrl).toString();
11283 out.kind = "navigate";
11284 if (menuEntry.label !== "") {
11285 out.windowTitle = menuEntry.label;
11286 }
11287 } catch {
11288 out.kind = "skip";
11289 }
11290 this.kindCache[out.name] = {
11291 kind: out.kind,
11292 url: out.url,
11293 iconSvg: out.iconSvg
11294 };
11295 return out;
11296 }
11297 if (typeof cmd.callback === "function") {
11298 let src = "";
11299 try {
11300 src = Function.prototype.toString.call(cmd.callback);
11301 } catch {
11302 src = "";
11303 }
11304 const literal = src.match(NAV_HREF_LITERAL_RE) || src.match(NAV_ASSIGN_LITERAL_RE) || src.match(NAV_CALL_LITERAL_RE);
11305 if (literal && literal[1]) {
11306 try {
11307 out.url = new URL(literal[1], window.location.href).toString();
11308 out.kind = "navigate";
11309 } catch {
11310 out.kind = "action";
11311 }
11312 } else if (NAV_INTENT_RE.test(src)) {
11313 const isSiteEditorIntent = SITE_EDITOR_INTENT_RE.test(src);
11314 const nameMatch = isSiteEditorIntent ? out.name.match(SITE_EDITOR_NAME_RE) : null;
11315 if (nameMatch) {
11316 const entityType = nameMatch[1];
11317 const entityId = nameMatch[2];
11318 const p = `/${entityType}/${entityId}`;
11319 try {
11320 const siteEditor = new URL("site-editor.php", this.adminUrl);
11321 siteEditor.searchParams.set("p", p);
11322 siteEditor.searchParams.set("canvas", "edit");
11323 out.url = siteEditor.toString();
11324 out.kind = "navigate";
11325 } catch {
11326 out.kind = "skip";
11327 }
11328 } else {
11329 out.kind = "skip";
11330 }
11331 }
11332 }
11333 this.kindCache[out.name] = {
11334 kind: out.kind,
11335 url: out.url,
11336 iconSvg: out.iconSvg
11337 };
11338 return out;
11339 }
11340 renderIcon(icon) {
11341 const wp = window.wp;
11342 if (!wp || !wp.element || typeof wp.element.renderToString !== "function") {
11343 return "";
11344 }
11345 try {
11346 const rendered = wp.element.renderToString(icon);
11347 if (typeof rendered === "string" && rendered.toLowerCase().startsWith("<svg")) {
11348 return rendered;
11349 }
11350 } catch {
11351 }
11352 return "";
11353 }
11354 iconFor(c) {
11355 if (c.icon && c.icon.startsWith("dashicons-")) {
11356 return c.icon;
11357 }
11358 return c.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
11359 }
11360 runNavigate(url, title, icon) {
11361 return (_args, ctx) => {
11362 ctx.close();
11363 if (tryNativeUrlRemap(url)) {
11364 return;
11365 }
11366 const id = deriveWindowId(url, this.adminUrl);
11367 this.manager.open({ id, baseId: id, url, title, icon });
11368 };
11369 }
11370 runInvoke(name, title, icon) {
11371 return (_args, ctx) => {
11372 ctx.close();
11373 const cb = this.callbackCache[name];
11374 if (typeof cb !== "function") {
11375 return;
11376 }
11377 const captured = this.runWithNavCapture(cb);
11378 if (captured) {
11379 const id = deriveWindowId(captured, this.adminUrl);
11380 this.manager.open({ id, baseId: id, url: captured, title, icon });
11381 }
11382 };
11383 }
11384 /**
11385 * Invoke `cb` with navigation sinks (`document.location`,
11386 * `window.location`, `location.assign`, `location.replace`)
11387 * shadowed so any assignment is captured instead of navigating
11388 * the shell. Returns the captured URL or `null` if the callback
11389 * was a pure JS action.
11390 *
11391 * The shadow uses `Object.defineProperty` on the document /
11392 * window instance to override the prototype's accessor for the
11393 * duration of the call. `delete` afterwards unshadows so the
11394 * native setter is restored.
11395 */
11396 runWithNavCapture(cb) {
11397 let captured = null;
11398 const setCaptured = (v) => {
11399 if (captured === null && typeof v === "string" && v !== "") {
11400 captured = v;
11401 }
11402 };
11403 const realLocation = window.location;
11404 const locationProxy = new Proxy(realLocation, {
11405 get(target2, prop) {
11406 const value = target2[prop];
11407 if (prop === "assign" || prop === "replace") {
11408 return (url) => setCaptured(url);
11409 }
11410 if (typeof value === "function") {
11411 return value.bind(target2);
11412 }
11413 return value;
11414 },
11415 set(_target, prop, value) {
11416 if (prop === "href") {
11417 setCaptured(value);
11418 return true;
11419 }
11420 return true;
11421 }
11422 });
11423 const shadowed = [];
11424 const installShadow = (obj) => {
11425 try {
11426 Object.defineProperty(obj, "location", {
11427 configurable: true,
11428 get: () => locationProxy,
11429 set: (v) => setCaptured(v)
11430 });
11431 shadowed.push({ obj, key: "location" });
11432 } catch {
11433 }
11434 };
11435 installShadow(document);
11436 installShadow(window);
11437 try {
11438 cb({ close: () => {
11439 } });
11440 } catch {
11441 } finally {
11442 for (const s of shadowed) {
11443 try {
11444 delete s.obj[s.key];
11445 } catch {
11446 }
11447 }
11448 }
11449 return captured;
11450 }
11451 }
11452 const seed$2 = [];
11453 function register(def) {
11454 throwOnRegistrationErrors(
11455 "Widget",
11456 collectRegistrationErrors(def, WIDGET_CHECKS),
11457 def
11458 );
11459 const idx = seed$2.findIndex((w) => w.id === def.id);
11460 if (idx >= 0) {
11461 seed$2[idx] = def;
11462 } else {
11463 seed$2.push(def);
11464 }
11465 }
11466 function unregister(id) {
11467 const idx = seed$2.findIndex((w) => w.id === id);
11468 if (idx >= 0) {
11469 seed$2.splice(idx, 1);
11470 }
11471 }
11472 function all() {
11473 const copy = seed$2.slice();
11474 const filtered = applyFilters(HOOKS.WIDGETS, copy);
11475 if (!Array.isArray(filtered)) {
11476 if (typeof console !== "undefined") {
11477 console.warn(
11478 "[desktop-mode] `desktop-mode.widgets` filter returned a non-array; falling back to seed list."
11479 );
11480 }
11481 return copy;
11482 }
11483 return filtered.filter(isValidDef);
11484 }
11485 function get(id) {
11486 return all().find((w) => w.id === id);
11487 }
11488 const WIDGET_CHECKS = [
11489 {
11490 field: "id",
11491 message: "missing or not a non-empty string",
11492 valid: (d) => typeof d.id === "string" && d.id !== ""
11493 },
11494 {
11495 field: "label",
11496 message: "missing or not a non-empty string",
11497 valid: (d) => typeof d.label === "string" && d.label !== ""
11498 },
11499 {
11500 field: "description",
11501 message: "not a string",
11502 valid: (d) => typeof d.description === "string"
11503 },
11504 {
11505 field: "icon",
11506 message: "missing or not a non-empty string",
11507 valid: (d) => typeof d.icon === "string" && d.icon !== ""
11508 },
11509 {
11510 field: "mount",
11511 message: "not a function",
11512 valid: (d) => typeof d.mount === "function"
11513 }
11514 ];
11515 function isValidDef(def) {
11516 return collectRegistrationErrors(def, WIDGET_CHECKS).length === 0;
11517 }
11518 let active$2 = null;
11519 function openWidgetPicker(options) {
11520 if (active$2) {
11521 return;
11522 }
11523 const panel2 = document.createElement("div");
11524 panel2.className = "desktop-mode-widget-picker";
11525 panel2.setAttribute("role", "menu");
11526 panel2.setAttribute("aria-label", __("Add widget"));
11527 const title = document.createElement("div");
11528 title.className = "desktop-mode-widget-picker__title";
11529 title.textContent = __("Add widget");
11530 panel2.appendChild(title);
11531 const list2 = document.createElement("div");
11532 list2.className = "desktop-mode-widget-picker__list";
11533 panel2.appendChild(list2);
11534 paintList(list2, options);
11535 document.body.appendChild(panel2);
11536 positionPanel(panel2, options.anchor);
11537 const onOutsidePointerDown = (e) => {
11538 const target2 = e.target;
11539 if (!target2) {
11540 return;
11541 }
11542 if (panel2.contains(target2) || options.anchor.contains(target2)) {
11543 return;
11544 }
11545 closeWidgetPicker();
11546 };
11547 window.setTimeout(() => {
11548 document.addEventListener("pointerdown", onOutsidePointerDown, true);
11549 }, 0);
11550 const onKeyDown = (e) => {
11551 if (e.key === "Escape") {
11552 closeWidgetPicker();
11553 }
11554 };
11555 document.addEventListener("keydown", onKeyDown);
11556 active$2 = { panel: panel2, options, onOutsidePointerDown, onKeyDown };
11557 const first = list2.querySelector(
11558 "button:not([disabled])"
11559 );
11560 first?.focus();
11561 }
11562 function refreshWidgetPicker() {
11563 if (!active$2) {
11564 return;
11565 }
11566 const list2 = active$2.panel.querySelector(
11567 ".desktop-mode-widget-picker__list"
11568 );
11569 if (list2) {
11570 paintList(list2, active$2.options);
11571 }
11572 }
11573 function closeWidgetPicker() {
11574 if (!active$2) {
11575 return;
11576 }
11577 document.removeEventListener(
11578 "pointerdown",
11579 active$2.onOutsidePointerDown,
11580 true
11581 );
11582 document.removeEventListener("keydown", active$2.onKeyDown);
11583 active$2.panel.remove();
11584 active$2 = null;
11585 }
11586 function paintList(list2, options) {
11587 list2.innerHTML = "";
11588 const enabled = new Set(options.enabledIds());
11589 const defs = options.registry();
11590 if (defs.length === 0) {
11591 const empty = document.createElement("div");
11592 empty.className = "desktop-mode-widget-picker__empty";
11593 empty.textContent = __(
11594 "No widgets available. Activate a plugin that registers one, or see the docs for the registerWidget API."
11595 );
11596 list2.appendChild(empty);
11597 return;
11598 }
11599 for (const def of defs) {
11600 const entry = document.createElement("button");
11601 entry.type = "button";
11602 entry.className = "desktop-mode-widget-picker__entry";
11603 const isAdded = enabled.has(def.id);
11604 if (isAdded) {
11605 entry.classList.add(
11606 "desktop-mode-widget-picker__entry--added"
11607 );
11608 entry.disabled = true;
11609 entry.setAttribute("aria-disabled", "true");
11610 }
11611 entry.setAttribute("role", "menuitem");
11612 let ariaLabel;
11613 if (isAdded) {
11614 ariaLabel = sprintf(__("%s (already added)"), def.label);
11615 } else {
11616 ariaLabel = sprintf(__("Add %s"), def.label);
11617 }
11618 entry.setAttribute("aria-label", ariaLabel);
11619 const icon = document.createElement("span");
11620 icon.className = `desktop-mode-widget-picker__entry-icon dashicons ${def.icon}`;
11621 icon.setAttribute("aria-hidden", "true");
11622 entry.appendChild(icon);
11623 const textWrap = document.createElement("span");
11624 textWrap.className = "desktop-mode-widget-picker__entry-text";
11625 const label = document.createElement("span");
11626 label.className = "desktop-mode-widget-picker__entry-label";
11627 label.textContent = def.label;
11628 textWrap.appendChild(label);
11629 if (def.description) {
11630 const desc = document.createElement("span");
11631 desc.className = "desktop-mode-widget-picker__entry-description";
11632 desc.textContent = def.description;
11633 textWrap.appendChild(desc);
11634 }
11635 entry.appendChild(textWrap);
11636 if (isAdded) {
11637 const status = document.createElement("span");
11638 status.className = "desktop-mode-widget-picker__entry-status";
11639 status.textContent = __("Added");
11640 entry.appendChild(status);
11641 }
11642 if (!isAdded) {
11643 entry.addEventListener("click", (e) => {
11644 e.preventDefault();
11645 e.stopPropagation();
11646 options.onAdd(def.id);
11647 });
11648 }
11649 list2.appendChild(entry);
11650 }
11651 }
11652 function positionPanel(panel2, anchor) {
11653 const rect = anchor.getBoundingClientRect();
11654 panel2.style.position = "fixed";
11655 panel2.style.left = "0px";
11656 panel2.style.top = "0px";
11657 panel2.style.visibility = "hidden";
11658 const panelRect = panel2.getBoundingClientRect();
11659 const width = panelRect.width || 320;
11660 const height = panelRect.height || 200;
11661 const gap = 6;
11662 let left = rect.right - width;
11663 let top = rect.top - height - gap;
11664 if (left < 8) {
11665 left = 8;
11666 }
11667 if (top < 8) {
11668 top = rect.bottom + gap;
11669 }
11670 panel2.style.left = `${Math.round(left)}px`;
11671 panel2.style.top = `${Math.round(top)}px`;
11672 panel2.style.visibility = "";
11673 }
11674 const FLOATING_CLASS = "desktop-mode-widgets__card--floating";
11675 const MOVABLE_CLASS = "desktop-mode-widgets__card--movable";
11676 const RESIZABLE_CLASS = "desktop-mode-widgets__card--resizable";
11677 const DRAGGING_CLASS = "desktop-mode-widgets__card--dragging";
11678 const RESIZING_CLASS = "desktop-mode-widgets__card--resizing";
11679 const DEFAULT_MIN_WIDTH = 160;
11680 const DEFAULT_MIN_HEIGHT = 80;
11681 const DEFAULT_WIDTH$1 = 280;
11682 const DEFAULT_HEIGHT$1 = 180;
11683 const VIEWPORT_MARGIN = 20;
11684 const DRAG_THRESHOLD_PX$1 = 5;
11685 const DRAG_THRESHOLD_SQUARED = DRAG_THRESHOLD_PX$1 * DRAG_THRESHOLD_PX$1;
11686 const DRAG_EXCLUDED_SELECTORS = 'input, textarea, select, button, a, [contenteditable="true"]';
11687 function buildFrame(def, ctx, handlers) {
11688 const card = document.createElement("div");
11689 card.className = "desktop-mode-widgets__card";
11690 card.dataset.widgetId = def.id;
11691 const movable = def.movable === true;
11692 const resizable = def.resizable === true;
11693 if (movable) {
11694 card.classList.add(MOVABLE_CLASS);
11695 }
11696 if (resizable) {
11697 card.classList.add(RESIZABLE_CLASS);
11698 }
11699 if (movable) {
11700 card.appendChild(buildChrome(def, handlers.onRemove, handlers.onRedock));
11701 } else {
11702 card.appendChild(buildCornerClose(def, handlers.onRemove));
11703 }
11704 const body = document.createElement("div");
11705 body.className = "desktop-mode-widgets__card-body";
11706 card.appendChild(body);
11707 let isFloating = false;
11708 if (ctx.geometry) {
11709 applyGeometry(card, ctx.geometry);
11710 card.classList.add(FLOATING_CLASS);
11711 isFloating = true;
11712 }
11713 const resizeCleanups = [];
11714 if (resizable) {
11715 for (const dir of allHandleDirs()) {
11716 const handle = document.createElement("div");
11717 handle.className = `desktop-mode-widgets__resize desktop-mode-widgets__resize--${dir}`;
11718 handle.setAttribute("aria-hidden", "true");
11719 handle.dataset.dir = dir;
11720 card.appendChild(handle);
11721 resizeCleanups.push(
11722 attachResize(card, handle, dir, def, ctx, handlers, () => isFloating)
11723 );
11724 }
11725 }
11726 let dragCleanup = null;
11727 if (movable) {
11728 const chrome = card.querySelector(
11729 ".desktop-mode-widgets__chrome"
11730 );
11731 if (chrome) {
11732 dragCleanup = attachDrag(card, chrome, def, ctx, handlers, (next) => {
11733 isFloating = next;
11734 });
11735 }
11736 }
11737 return {
11738 card,
11739 body,
11740 dispose: () => {
11741 for (const fn of resizeCleanups) {
11742 try {
11743 fn();
11744 } catch {
11745 }
11746 }
11747 if (dragCleanup) {
11748 try {
11749 dragCleanup();
11750 } catch {
11751 }
11752 }
11753 card.remove();
11754 }
11755 };
11756 }
11757 function buildChrome(def, onRemove, onRedock) {
11758 const chrome = document.createElement("header");
11759 chrome.className = "desktop-mode-widgets__chrome";
11760 const grip = document.createElement("span");
11761 grip.className = "desktop-mode-widgets__grip";
11762 grip.setAttribute("aria-hidden", "true");
11763 chrome.appendChild(grip);
11764 const title = document.createElement("span");
11765 title.className = "desktop-mode-widgets__title";
11766 title.textContent = def.label;
11767 chrome.appendChild(title);
11768 chrome.appendChild(buildRedockButton(def, onRedock));
11769 const close = buildCloseButton(def, onRemove);
11770 chrome.appendChild(close);
11771 return chrome;
11772 }
11773 function buildRedockButton(def, onRedock) {
11774 const btn = document.createElement("button");
11775 btn.type = "button";
11776 btn.className = "desktop-mode-widgets__card-redock";
11777 btn.setAttribute(
11778 "aria-label",
11779 // translators: %s is the widget label (e.g., "Clock")
11780 sprintf(__("Dock %s back to widget column"), def.label)
11781 );
11782 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>';
11783 btn.addEventListener("click", (e) => {
11784 e.preventDefault();
11785 e.stopPropagation();
11786 onRedock();
11787 });
11788 btn.dataset.noDrag = "true";
11789 return btn;
11790 }
11791 function buildCornerClose(def, onRemove) {
11792 const close = buildCloseButton(def, onRemove);
11793 close.classList.add("desktop-mode-widgets__card-close--corner");
11794 return close;
11795 }
11796 function buildCloseButton(def, onRemove) {
11797 const close = document.createElement("button");
11798 close.type = "button";
11799 close.className = "desktop-mode-widgets__card-close";
11800 close.setAttribute("aria-label", sprintf(__("Remove %s"), def.label));
11801 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>';
11802 close.addEventListener("click", (e) => {
11803 e.preventDefault();
11804 e.stopPropagation();
11805 onRemove();
11806 });
11807 return close;
11808 }
11809 function attachDrag(card, chrome, def, ctx, handlers, setFloating) {
11810 let pointerId = null;
11811 let startX = 0;
11812 let startY = 0;
11813 let initialLeft = 0;
11814 let initialTop = 0;
11815 let committed = false;
11816 const onDown = (e) => {
11817 if (e.button !== 0) {
11818 return;
11819 }
11820 const target2 = e.target;
11821 if (target2 && target2.closest(DRAG_EXCLUDED_SELECTORS)) {
11822 return;
11823 }
11824 e.preventDefault();
11825 pointerId = e.pointerId;
11826 startX = e.clientX;
11827 startY = e.clientY;
11828 committed = false;
11829 initialLeft = parseFloat(card.style.left) || 0;
11830 initialTop = parseFloat(card.style.top) || 0;
11831 chrome.setPointerCapture(pointerId);
11832 };
11833 const commitDrag = () => {
11834 if (!card.classList.contains(FLOATING_CLASS)) {
11835 const parentRect = ctx.floatingParent.getBoundingClientRect();
11836 const rect = card.getBoundingClientRect();
11837 const initial = {
11838 x: rect.left - parentRect.left,
11839 y: rect.top - parentRect.top,
11840 width: rect.width || def.defaultWidth || DEFAULT_WIDTH$1,
11841 height: rect.height || def.defaultHeight || DEFAULT_HEIGHT$1
11842 };
11843 applyGeometry(card, initial);
11844 card.classList.add(FLOATING_CLASS);
11845 setFloating(true);
11846 handlers.onLiberate(initial);
11847 initialLeft = parseFloat(card.style.left) || 0;
11848 initialTop = parseFloat(card.style.top) || 0;
11849 }
11850 card.classList.add(DRAGGING_CLASS);
11851 };
11852 const onMove = (e) => {
11853 if (pointerId === null || e.pointerId !== pointerId) {
11854 return;
11855 }
11856 const dx = e.clientX - startX;
11857 const dy = e.clientY - startY;
11858 if (!committed) {
11859 if (dx * dx + dy * dy < DRAG_THRESHOLD_SQUARED) {
11860 return;
11861 }
11862 committed = true;
11863 commitDrag();
11864 }
11865 const clamped = clampToParent(
11866 initialLeft + dx,
11867 initialTop + dy,
11868 card.offsetWidth,
11869 card.offsetHeight,
11870 ctx.floatingParent
11871 );
11872 card.style.left = `${clamped.x}px`;
11873 card.style.top = `${clamped.y}px`;
11874 };
11875 const onUp = (e) => {
11876 if (pointerId === null || e.pointerId !== pointerId) {
11877 return;
11878 }
11879 try {
11880 chrome.releasePointerCapture(pointerId);
11881 } catch {
11882 }
11883 pointerId = null;
11884 if (!committed) {
11885 return;
11886 }
11887 committed = false;
11888 card.classList.remove(DRAGGING_CLASS);
11889 handlers.onGeometryChanged(currentGeometry(card));
11890 };
11891 chrome.addEventListener("pointerdown", onDown);
11892 chrome.addEventListener("pointermove", onMove);
11893 chrome.addEventListener("pointerup", onUp);
11894 chrome.addEventListener("pointercancel", onUp);
11895 return () => {
11896 chrome.removeEventListener("pointerdown", onDown);
11897 chrome.removeEventListener("pointermove", onMove);
11898 chrome.removeEventListener("pointerup", onUp);
11899 chrome.removeEventListener("pointercancel", onUp);
11900 };
11901 }
11902 function attachResize(card, handle, dir, def, ctx, handlers, isFloating) {
11903 let pointerId = null;
11904 let startX = 0;
11905 let startY = 0;
11906 let startLeft = 0;
11907 let startTop = 0;
11908 let startW = 0;
11909 let startH = 0;
11910 const onDown = (e) => {
11911 if (e.button !== 0) {
11912 return;
11913 }
11914 if (!isFloating() && !isHeightOnlyDir(dir)) {
11915 return;
11916 }
11917 e.preventDefault();
11918 e.stopPropagation();
11919 pointerId = e.pointerId;
11920 startX = e.clientX;
11921 startY = e.clientY;
11922 const rect = card.getBoundingClientRect();
11923 const parentRect = ctx.floatingParent.getBoundingClientRect();
11924 startLeft = rect.left - parentRect.left;
11925 startTop = rect.top - parentRect.top;
11926 startW = rect.width;
11927 startH = rect.height;
11928 handle.setPointerCapture(pointerId);
11929 card.classList.add(RESIZING_CLASS);
11930 };
11931 const onMove = (e) => {
11932 if (pointerId === null || e.pointerId !== pointerId) {
11933 return;
11934 }
11935 const dx = e.clientX - startX;
11936 const dy = e.clientY - startY;
11937 const next = computeResize(
11938 dir,
11939 dx,
11940 dy,
11941 startLeft,
11942 startTop,
11943 startW,
11944 startH,
11945 def,
11946 ctx.floatingParent,
11947 isFloating()
11948 );
11949 if (isFloating()) {
11950 card.style.left = `${next.x}px`;
11951 card.style.top = `${next.y}px`;
11952 card.style.width = `${next.width}px`;
11953 }
11954 card.style.height = `${next.height}px`;
11955 };
11956 const onUp = (e) => {
11957 if (pointerId === null || e.pointerId !== pointerId) {
11958 return;
11959 }
11960 try {
11961 handle.releasePointerCapture(pointerId);
11962 } catch {
11963 }
11964 pointerId = null;
11965 card.classList.remove(RESIZING_CLASS);
11966 handlers.onGeometryChanged(currentGeometry(card));
11967 };
11968 handle.addEventListener("pointerdown", onDown);
11969 handle.addEventListener("pointermove", onMove);
11970 handle.addEventListener("pointerup", onUp);
11971 handle.addEventListener("pointercancel", onUp);
11972 return () => {
11973 handle.removeEventListener("pointerdown", onDown);
11974 handle.removeEventListener("pointermove", onMove);
11975 handle.removeEventListener("pointerup", onUp);
11976 handle.removeEventListener("pointercancel", onUp);
11977 };
11978 }
11979 function allHandleDirs() {
11980 return ["n", "e", "s", "w", "ne", "nw", "se", "sw"];
11981 }
11982 function isHeightOnlyDir(dir) {
11983 return dir === "s";
11984 }
11985 function applyGeometry(card, geometry) {
11986 card.style.left = `${geometry.x}px`;
11987 card.style.top = `${geometry.y}px`;
11988 card.style.width = `${geometry.width}px`;
11989 card.style.height = `${geometry.height}px`;
11990 }
11991 function currentGeometry(card) {
11992 return {
11993 x: parseFloat(card.style.left) || 0,
11994 y: parseFloat(card.style.top) || 0,
11995 width: card.offsetWidth,
11996 height: card.offsetHeight
11997 };
11998 }
11999 function clampToParent(x, y, width, height, parent) {
12000 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
12001 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
12002 const maxX = Math.max(0, parentWidth - width - VIEWPORT_MARGIN);
12003 const maxY = Math.max(0, parentHeight - height - VIEWPORT_MARGIN);
12004 return {
12005 x: Math.min(Math.max(VIEWPORT_MARGIN, x), maxX),
12006 y: Math.min(Math.max(VIEWPORT_MARGIN, y), maxY)
12007 };
12008 }
12009 function computeResize(dir, dx, dy, startLeft, startTop, startW, startH, def, parent, floating) {
12010 const minW = def.minWidth ?? DEFAULT_MIN_WIDTH;
12011 const minH = def.minHeight ?? DEFAULT_MIN_HEIGHT;
12012 const maxW = def.maxWidth ?? Infinity;
12013 const maxH = def.maxHeight ?? Infinity;
12014 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
12015 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
12016 let x = startLeft;
12017 let y = startTop;
12018 let width = startW;
12019 let height = startH;
12020 if (dir === "e" || dir === "ne" || dir === "se") {
12021 width = clamp$1(startW + dx, minW, Math.min(maxW, parentWidth - startLeft));
12022 }
12023 if (dir === "w" || dir === "nw" || dir === "sw") {
12024 const nextWidth = clamp$1(startW - dx, minW, Math.min(maxW, startLeft + startW));
12025 x = startLeft + (startW - nextWidth);
12026 width = nextWidth;
12027 }
12028 if (dir === "s" || dir === "se" || dir === "sw") {
12029 height = clamp$1(
12030 startH + dy,
12031 minH,
12032 Math.min(maxH, parentHeight - startTop)
12033 );
12034 }
12035 if (dir === "n" || dir === "ne" || dir === "nw") {
12036 const nextHeight = clamp$1(startH - dy, minH, Math.min(maxH, startTop + startH));
12037 y = startTop + (startH - nextHeight);
12038 height = nextHeight;
12039 }
12040 if (!floating) {
12041 width = startW;
12042 x = startLeft;
12043 }
12044 return { x, y, width, height };
12045 }
12046 function clamp$1(value, min, max) {
12047 if (max < min) {
12048 return min;
12049 }
12050 return Math.min(Math.max(value, min), max);
12051 }
12052 const IDS_KEY = "desktop-mode-widgets";
12053 const GEOMETRY_KEY$1 = "desktop-mode-widgets-geometry";
12054 function readRawEnabled() {
12055 try {
12056 return window.localStorage.getItem(IDS_KEY);
12057 } catch {
12058 return null;
12059 }
12060 }
12061 function loadEnabledIds() {
12062 const raw = readRawEnabled();
12063 if (raw === null) {
12064 return [];
12065 }
12066 try {
12067 const parsed = JSON.parse(raw);
12068 if (!Array.isArray(parsed)) {
12069 return [];
12070 }
12071 return parsed.filter((x) => typeof x === "string");
12072 } catch {
12073 return [];
12074 }
12075 }
12076 function saveEnabledIds(ids) {
12077 try {
12078 window.localStorage.setItem(IDS_KEY, JSON.stringify(ids));
12079 } catch {
12080 }
12081 }
12082 function loadGeometry$1() {
12083 try {
12084 const raw = window.localStorage.getItem(GEOMETRY_KEY$1);
12085 if (!raw) {
12086 return {};
12087 }
12088 const parsed = JSON.parse(raw);
12089 if (!parsed || typeof parsed !== "object") {
12090 return {};
12091 }
12092 const out = {};
12093 for (const [id, rawEntry] of Object.entries(parsed)) {
12094 const entry = sanitizeGeometry(rawEntry);
12095 if (entry) {
12096 out[id] = entry;
12097 }
12098 }
12099 return out;
12100 } catch {
12101 return {};
12102 }
12103 }
12104 function saveGeometry$1(geometry) {
12105 try {
12106 window.localStorage.setItem(GEOMETRY_KEY$1, JSON.stringify(geometry));
12107 } catch {
12108 }
12109 }
12110 function sanitizeGeometry(raw) {
12111 if (!raw || typeof raw !== "object") {
12112 return null;
12113 }
12114 const { x, y, width, height } = raw;
12115 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) {
12116 return null;
12117 }
12118 return { x, y, width, height };
12119 }
12120 function createWidgetStorage(widgetId) {
12121 const prefix = `desktop-mode.widget.${widgetId}.`;
12122 const safeGet = (key) => {
12123 try {
12124 return localStorage.getItem(prefix + key);
12125 } catch {
12126 return null;
12127 }
12128 };
12129 return {
12130 get(key) {
12131 const raw = safeGet(key);
12132 if (raw === null) {
12133 return null;
12134 }
12135 try {
12136 return JSON.parse(raw);
12137 } catch {
12138 return null;
12139 }
12140 },
12141 set(key, value) {
12142 try {
12143 localStorage.setItem(prefix + key, JSON.stringify(value));
12144 } catch {
12145 }
12146 },
12147 remove(key) {
12148 try {
12149 localStorage.removeItem(prefix + key);
12150 } catch {
12151 }
12152 },
12153 clear() {
12154 try {
12155 for (let i = localStorage.length - 1; i >= 0; i--) {
12156 const key = localStorage.key(i);
12157 if (key && key.startsWith(prefix)) {
12158 localStorage.removeItem(key);
12159 }
12160 }
12161 } catch {
12162 }
12163 }
12164 };
12165 }
12166 const DEFAULT_ENABLED_IDS = ["clock"];
12167 class WidgetLayer {
12168 /**
12169 * @param root The column element (`#desktop-mode-widgets`).
12170 * @param pluginUrl Absolute plugin URL — passed to widget ctx.
12171 * @param floatingHost Parent for liberated (floating) widgets.
12172 * Defaults to the column's parent (the desktop
12173 * area) so floats are bounded by the visible
12174 * desktop, not the 320 px-wide column.
12175 */
12176 constructor(root, pluginUrl, floatingHost) {
12177 this.mounted = /* @__PURE__ */ new Map();
12178 this.generation = 0;
12179 this.root = root;
12180 this.pluginUrl = pluginUrl;
12181 this.enabledIds = loadEnabledIds();
12182 this.geometry = loadGeometry$1();
12183 this.floatingHost = floatingHost ?? root.parentElement ?? root;
12184 this.listEl = document.createElement("div");
12185 this.listEl.className = "desktop-mode-widgets__list";
12186 this.root.appendChild(this.listEl);
12187 this.addTile = this.buildAddTile();
12188 this.root.appendChild(this.addTile);
12189 this.paintEmptyState();
12190 }
12191 /**
12192 * Mount every widget the user has enabled (per localStorage).
12193 * Called once during shell boot, AFTER the registry seed has run
12194 * so built-ins are available. Safe to call multiple times — the
12195 * `mounted` map dedupes.
12196 */
12197 hydrate() {
12198 if (readRawEnabled() === null) {
12199 this.enabledIds = DEFAULT_ENABLED_IDS.filter(
12200 (id) => !!get(id)
12201 );
12202 saveEnabledIds(this.enabledIds);
12203 }
12204 for (const id of this.enabledIds) {
12205 if (this.mounted.has(id)) {
12206 continue;
12207 }
12208 this.mountById(id);
12209 }
12210 this.paintEmptyState();
12211 }
12212 /**
12213 * Add a widget by id — called by the picker after the user
12214 * selects an available entry. Idempotent.
12215 */
12216 add(id) {
12217 if (this.enabledIds.includes(id)) {
12218 return;
12219 }
12220 if (!get(id)) {
12221 return;
12222 }
12223 this.enabledIds.push(id);
12224 saveEnabledIds(this.enabledIds);
12225 this.mountById(id);
12226 this.paintEmptyState();
12227 doAction(HOOKS.WIDGET_ADDED, { id });
12228 refreshWidgetPicker();
12229 }
12230 /**
12231 * Remove a widget by id — called from the card's × button and
12232 * from the picker. Idempotent.
12233 */
12234 remove(id) {
12235 const before = this.enabledIds.length;
12236 this.enabledIds = this.enabledIds.filter((e) => e !== id);
12237 if (this.enabledIds.length === before) {
12238 return;
12239 }
12240 saveEnabledIds(this.enabledIds);
12241 if (this.geometry[id]) {
12242 delete this.geometry[id];
12243 saveGeometry$1(this.geometry);
12244 }
12245 this.unmountById(id);
12246 this.paintEmptyState();
12247 doAction(HOOKS.WIDGET_REMOVED, { id });
12248 refreshWidgetPicker();
12249 }
12250 /** Public read for the picker / external callers. */
12251 getEnabledIds() {
12252 return [...this.enabledIds];
12253 }
12254 /**
12255 * Mount a widget ONLY if it's already in the user's enabled
12256 * list AND not currently mounted. No-op when the widget isn't
12257 * enabled (user never opted in) and no-op when it's already on
12258 * screen. Used by the server-driven sync: when a plugin
12259 * activates mid-session, its widget def registers via the
12260 * sync's path; if the user had previously enabled that widget
12261 * (in a prior session or before the plugin was deactivated),
12262 * we want to bring it back on screen without toggling the
12263 * "enabled" state or firing a `WIDGET_ADDED` action.
12264 *
12265 * The net behaviour is "rehydrate this one widget now that
12266 * its def is finally registered," which is subtly different
12267 * from `ensureMounted` (which OPT-INs the user into enabling
12268 * the widget for the first time).
12269 */
12270 mountIfEnabled(id) {
12271 if (!get(id)) {
12272 return;
12273 }
12274 if (!this.enabledIds.includes(id)) {
12275 return;
12276 }
12277 if (this.mounted.has(id)) {
12278 return;
12279 }
12280 this.mountById(id);
12281 this.paintEmptyState();
12282 }
12283 /**
12284 * Unmount a widget without touching the persisted enablement.
12285 * Used by the server-driven widget-registry sync: when a plugin
12286 * deactivates mid-session, its widget defs disappear from the
12287 * registry and we need to pull any mounted instance off the
12288 * screen — but we deliberately KEEP the id in the user's
12289 * enabled list so re-activating the plugin re-mounts it
12290 * automatically through `hydrate()`.
12291 *
12292 * Idempotent; a no-op when the widget isn't currently mounted.
12293 */
12294 unmount(id) {
12295 if (!this.mounted.has(id)) {
12296 return;
12297 }
12298 this.unmountById(id);
12299 this.paintEmptyState();
12300 }
12301 /**
12302 * Guarantee the widget identified by `id` is currently mounted,
12303 * adding it to the enabled list if it isn't. No-op when the
12304 * widget is already on screen. Intended for companion plugins
12305 * that want to pin their widget programmatically — a monitor
12306 * plugin that auto-pins itself on the first error burst, a
12307 * first-run onboarding flow that ensures the quick-start widget
12308 * is present, etc.
12309 *
12310 * Returns `true` when the widget is mounted (either newly added
12311 * or already present), `false` when the id isn't registered —
12312 * callers can branch on the failure without having to maintain
12313 * their own registry snapshot.
12314 */
12315 ensureMounted(id) {
12316 if (!get(id)) {
12317 return false;
12318 }
12319 if (this.enabledIds.includes(id)) {
12320 return true;
12321 }
12322 this.add(id);
12323 return true;
12324 }
12325 /**
12326 * Tear down every widget. Called on shell unload via `pagehide`
12327 * so intervals / RAF loops stop before the beacon flush.
12328 */
12329 disposeAll() {
12330 for (const id of Array.from(this.mounted.keys())) {
12331 this.unmountById(id);
12332 }
12333 }
12334 // --- Internal ---------------------------------------------------
12335 mountById(id) {
12336 const def = get(id);
12337 if (!def) {
12338 return;
12339 }
12340 const gen = ++this.generation;
12341 const initialGeometry = def.movable === true ? this.geometry[id] : void 0;
12342 const frame = buildFrame(
12343 def,
12344 { floatingParent: this.floatingHost, geometry: initialGeometry },
12345 {
12346 onRemove: () => this.remove(id),
12347 onGeometryChanged: (geom) => this.persistGeometry(id, geom),
12348 onLiberate: (geom) => this.liberate(id, geom),
12349 onRedock: () => this.redock(id)
12350 }
12351 );
12352 const floating = !!initialGeometry;
12353 const record = {
12354 id,
12355 frame,
12356 generation: gen,
12357 teardown: null,
12358 floating
12359 };
12360 this.mounted.set(id, record);
12361 this.placeCard(frame.card, floating);
12362 const ctx = {
12363 id,
12364 pluginUrl: this.pluginUrl,
12365 storage: createWidgetStorage(id)
12366 };
12367 doAction(HOOKS.WIDGET_MOUNTING, { id, container: frame.body, ctx });
12368 const onResolve = (teardown) => {
12369 const current = this.mounted.get(id);
12370 if (!current || current.generation !== gen) {
12371 try {
12372 teardown();
12373 } catch {
12374 }
12375 return;
12376 }
12377 current.teardown = teardown;
12378 doAction(HOOKS.WIDGET_MOUNTED, { id, container: frame.body, ctx });
12379 };
12380 let result;
12381 try {
12382 result = def.mount(frame.body, ctx);
12383 } catch (err) {
12384 this.handleMountFailure(id, err);
12385 return;
12386 }
12387 if (isThenable(result)) {
12388 result.then(onResolve, (err) => {
12389 if (this.mounted.get(id)?.generation === gen) {
12390 this.handleMountFailure(id, err);
12391 }
12392 });
12393 return;
12394 }
12395 onResolve(result);
12396 }
12397 unmountById(id) {
12398 const record = this.mounted.get(id);
12399 if (!record) {
12400 return;
12401 }
12402 doAction(HOOKS.WIDGET_UNMOUNTING, { id });
12403 try {
12404 record.teardown?.();
12405 } catch (err) {
12406 doAction(HOOKS.SHELL_ERROR, { scope: "widget-teardown", id, error: err });
12407 if (typeof console !== "undefined") {
12408 console.error(
12409 `[desktop-mode] Widget "${id}" teardown threw:`,
12410 err
12411 );
12412 }
12413 }
12414 this.generation++;
12415 record.frame.dispose();
12416 this.mounted.delete(id);
12417 }
12418 handleMountFailure(id, err) {
12419 const record = this.mounted.get(id);
12420 if (record) {
12421 record.frame.dispose();
12422 this.mounted.delete(id);
12423 }
12424 doAction(HOOKS.WIDGET_MOUNT_FAILED, { id, error: err });
12425 doAction(HOOKS.SHELL_ERROR, { scope: "widget-mount", id, error: err });
12426 if (typeof console !== "undefined") {
12427 console.error(
12428 `[desktop-mode] Widget "${id}" failed to mount:`,
12429 err
12430 );
12431 }
12432 }
12433 buildAddTile() {
12434 const tile2 = document.createElement("button");
12435 tile2.type = "button";
12436 tile2.className = "desktop-mode-widgets__add";
12437 tile2.setAttribute("aria-label", __("Add widget"));
12438 const plus = document.createElement("span");
12439 plus.className = "desktop-mode-widgets__add-plus";
12440 plus.setAttribute("aria-hidden", "true");
12441 plus.textContent = "+";
12442 const label = document.createElement("span");
12443 label.className = "desktop-mode-widgets__add-label";
12444 label.textContent = __("Add widget");
12445 tile2.appendChild(plus);
12446 tile2.appendChild(label);
12447 tile2.addEventListener("click", (e) => {
12448 e.preventDefault();
12449 e.stopPropagation();
12450 openWidgetPicker({
12451 anchor: tile2,
12452 registry: () => all(),
12453 enabledIds: () => [...this.enabledIds],
12454 onAdd: (id) => this.add(id)
12455 });
12456 });
12457 return tile2;
12458 }
12459 /**
12460 * Drop a card into the right parent based on its floating state.
12461 * Docked cards append to the column list above the `+` tile;
12462 * floating cards append to the desktop-area-level host so they
12463 * sit above the wallpaper and can range across the viewport.
12464 */
12465 placeCard(card, floating) {
12466 if (floating) {
12467 this.floatingHost.appendChild(card);
12468 } else {
12469 this.listEl.appendChild(card);
12470 }
12471 }
12472 /**
12473 * Move a widget from the column into the floating host. Called by
12474 * the frame on the user's first drag of a movable widget.
12475 */
12476 liberate(id, geometry) {
12477 const record = this.mounted.get(id);
12478 if (!record || record.floating) {
12479 return;
12480 }
12481 record.floating = true;
12482 this.floatingHost.appendChild(record.frame.card);
12483 applyGeometry(record.frame.card, geometry);
12484 this.persistGeometry(id, geometry);
12485 this.paintEmptyState();
12486 }
12487 /**
12488 * Inverse of {@link liberate}: move a floating card back into
12489 * the column and drop its persisted geometry so a subsequent
12490 * shell boot brings it up docked. Called when the user clicks
12491 * the re-dock button in the card's chrome header, or
12492 * programmatically by companion plugins via
12493 * `wp.desktop.widgets.redock( id )` /
12494 * `wp.desktop.widgetLayer.redock( id )`.
12495 *
12496 * Idempotent — a docked widget silently no-ops, an unknown id
12497 * silently no-ops. The `--floating` class on the card is
12498 * removed as part of the same write so CSS rules that depend
12499 * on it (re-dock button visibility, absolute positioning) flip
12500 * back in one paint.
12501 *
12502 * @since 0.7.0 (private)
12503 * @since 0.25.0 (public)
12504 */
12505 redock(id) {
12506 const record = this.mounted.get(id);
12507 if (!record || !record.floating) {
12508 return;
12509 }
12510 record.floating = false;
12511 if (this.geometry[id]) {
12512 delete this.geometry[id];
12513 saveGeometry$1(this.geometry);
12514 }
12515 const card = record.frame.card;
12516 card.classList.remove("desktop-mode-widgets__card--floating");
12517 card.style.left = "";
12518 card.style.top = "";
12519 card.style.width = "";
12520 card.style.height = "";
12521 this.listEl.appendChild(card);
12522 this.paintEmptyState();
12523 }
12524 persistGeometry(id, geometry) {
12525 this.geometry[id] = geometry;
12526 saveGeometry$1(this.geometry);
12527 }
12528 /**
12529 * Toggle a `--has-widgets` modifier so CSS can hide the column's
12530 * decorative backdrop when nothing's mounted (keeps the empty
12531 * state clean — just the `+` tile floating in the corner).
12532 *
12533 * Floating widgets don't count toward "has widgets" in the column
12534 * sense — if every enabled widget is floating, the column itself
12535 * shows only the empty state + add tile.
12536 */
12537 paintEmptyState() {
12538 let docked = 0;
12539 for (const record of this.mounted.values()) {
12540 if (!record.floating) {
12541 docked++;
12542 }
12543 }
12544 this.root.classList.toggle(
12545 "desktop-mode-widgets--has-widgets",
12546 docked > 0
12547 );
12548 }
12549 }
12550 function isThenable(x) {
12551 return !!x && (typeof x === "object" || typeof x === "function") && typeof x.then === "function";
12552 }
12553 const DEFAULT_NATIVE_MIN_WIDTH = 280;
12554 const DEFAULT_NATIVE_MIN_HEIGHT = 220;
12555 const DEFAULT_NATIVE_WIDTH = 520;
12556 const DEFAULT_NATIVE_HEIGHT = 400;
12557 function buildIframeContentRender(cfg, cleanups, windowId) {
12558 return (body) => {
12559 const iframe = document.createElement("iframe");
12560 iframe.style.width = "100%";
12561 iframe.style.height = "100%";
12562 iframe.style.border = "0";
12563 iframe.setAttribute("src", cfg.url);
12564 if (typeof cfg.sandbox === "string" && cfg.sandbox !== "") {
12565 iframe.setAttribute("sandbox", cfg.sandbox);
12566 }
12567 body.style.padding = "0";
12568 body.appendChild(iframe);
12569 const unregisterSynth = registerSyntheticIframe(windowId, iframe);
12570 cleanups.push(unregisterSynth);
12571 let targetOrigin;
12572 try {
12573 targetOrigin = new URL(cfg.url, window.location.origin).origin;
12574 } catch {
12575 targetOrigin = window.location.origin;
12576 }
12577 let resolveReady = null;
12578 const readyPromise = new Promise((resolve2) => {
12579 resolveReady = resolve2;
12580 });
12581 const onLoad = () => {
12582 if (cfg.bridge) {
12583 try {
12584 const doc = iframe.contentDocument;
12585 if (doc && !doc.querySelector("script[data-desktop-mode-iframe-bridge]")) {
12586 const bridgeUrl = window.desktopModeConfig?.iframeBridgeUrl;
12587 if (bridgeUrl) {
12588 const s = doc.createElement("script");
12589 s.src = bridgeUrl;
12590 s.setAttribute("data-desktop-mode-iframe-bridge", "1");
12591 doc.head?.appendChild(s);
12592 }
12593 }
12594 } catch {
12595 }
12596 }
12597 markWindowContentReady(windowId);
12598 resolveReady?.();
12599 };
12600 iframe.addEventListener("load", onLoad);
12601 const onMessage = (e) => {
12602 if (!iframe.contentWindow || e.source !== iframe.contentWindow) {
12603 return;
12604 }
12605 if (e.origin !== targetOrigin && e.origin !== window.location.origin) {
12606 return;
12607 }
12608 const data = e.data;
12609 if (data && typeof data === "object" && typeof data.type === "string" && data.type.startsWith("desktop-mode-bridge-")) {
12610 const bridgeRouter = window.__desktopModeConnectionBridge;
12611 bridgeRouter?.routeIncomingFromIframe(data, windowId);
12612 }
12613 if (data && typeof data === "object" && data.type === "desktop-mode-window-publish" && typeof data.channel === "string" && data.channel !== "") {
12614 dispatchFromWindow(
12615 windowId,
12616 data.channel,
12617 data.payload
12618 );
12619 }
12620 try {
12621 cfg.onMessage?.(e.data);
12622 } catch (err) {
12623 if (typeof console !== "undefined") {
12624 console.error(
12625 "[desktop-mode] iframeContent.onMessage threw:",
12626 err
12627 );
12628 }
12629 }
12630 };
12631 window.addEventListener("message", onMessage);
12632 cleanups.push(() => {
12633 window.removeEventListener("message", onMessage);
12634 iframe.removeEventListener("load", onLoad);
12635 });
12636 return readyPromise;
12637 };
12638 }
12639 function createRegisterWindow(manager) {
12640 return async (def) => {
12641 const userRender = def.render;
12642 let render2 = userRender;
12643 const cleanups = [];
12644 if (def.iframeContent) {
12645 if (userRender && typeof console !== "undefined") {
12646 console.warn(
12647 "[desktop-mode] registerWindow: both `render` and `iframeContent` provided — ignoring `render` and using the iframe shorthand. Drop one."
12648 );
12649 }
12650 render2 = buildIframeContentRender(
12651 def.iframeContent,
12652 cleanups,
12653 def.id
12654 );
12655 }
12656 const userOnClose = def.onClose;
12657 const onClose = cleanups.length ? () => {
12658 for (const fn of cleanups) {
12659 try {
12660 fn();
12661 } catch {
12662 }
12663 }
12664 userOnClose?.();
12665 } : userOnClose;
12666 const win = await manager.open({
12667 id: def.id,
12668 baseId: def.baseId || def.id,
12669 native: true,
12670 url: def.url || `#${def.id}`,
12671 title: def.title,
12672 icon: def.icon,
12673 x: def.x ?? 0,
12674 y: def.y ?? 0,
12675 width: def.width ?? DEFAULT_NATIVE_WIDTH,
12676 height: def.height ?? DEFAULT_NATIVE_HEIGHT,
12677 minWidth: def.minWidth ?? DEFAULT_NATIVE_MIN_WIDTH,
12678 minHeight: def.minHeight ?? DEFAULT_NATIVE_MIN_HEIGHT,
12679 render: render2,
12680 onClose,
12681 onResize: def.onResize,
12682 autofocus: def.autofocus,
12683 initialState: def.initialState,
12684 ownerHandle: def.ownerHandle,
12685 multi: def.multi,
12686 desktopId: def.desktopId
12687 });
12688 return win;
12689 };
12690 }
12691 let onWindowInstanceCounter = 0;
12692 function onWindow(id, handlers, options = {}) {
12693 const namespace = `desktop-mode/on-window/${id}/${++onWindowInstanceCounter}`;
12694 const persistent = options.persistent === true;
12695 const bindings = [
12696 ["opened", HOOKS.WINDOW_OPENED],
12697 ["reopened", HOOKS.WINDOW_REOPENED],
12698 ["focused", HOOKS.WINDOW_FOCUSED],
12699 ["blurred", HOOKS.WINDOW_BLURRED],
12700 ["closing", HOOKS.WINDOW_CLOSING],
12701 ["closed", HOOKS.WINDOW_CLOSED],
12702 ["minimized", HOOKS.WINDOW_MINIMIZED],
12703 ["restored", HOOKS.WINDOW_RESTORED],
12704 ["maximized", HOOKS.WINDOW_MAXIMIZED],
12705 ["unmaximized", HOOKS.WINDOW_UNMAXIMIZED],
12706 ["fullscreenEntered", HOOKS.WINDOW_FULLSCREEN_ENTERED],
12707 ["fullscreenExited", HOOKS.WINDOW_FULLSCREEN_EXITED],
12708 ["resized", HOOKS.WINDOW_RESIZED],
12709 ["bodyResized", HOOKS.WINDOW_BODY_RESIZED],
12710 ["boundsChanged", HOOKS.WINDOW_BOUNDS_CHANGED]
12711 ];
12712 const registered = [];
12713 let disposed = false;
12714 const unsubscribe = () => {
12715 if (disposed) {
12716 return;
12717 }
12718 disposed = true;
12719 for (const hookName2 of registered) {
12720 removeAction(hookName2, namespace);
12721 }
12722 };
12723 for (const [key, hookName2] of bindings) {
12724 const handler = handlers[key];
12725 if (!handler) {
12726 continue;
12727 }
12728 registered.push(hookName2);
12729 addAction(hookName2, namespace, (payload) => {
12730 const p = payload;
12731 if (p.windowId !== id) {
12732 return;
12733 }
12734 const { windowId: _w, ...rest } = p;
12735 handler(rest);
12736 if (key === "closed" && !persistent) {
12737 unsubscribe();
12738 }
12739 });
12740 }
12741 return unsubscribe;
12742 }
12743 function createNativeWindowSync(deps2) {
12744 const { manager, appendSystemTile, removeSystemTile } = deps2;
12745 const registered = /* @__PURE__ */ new Set();
12746 const injectedTemplates = /* @__PURE__ */ new Set();
12747 const loadedScripts = /* @__PURE__ */ new Set();
12748 const loadedStyles = /* @__PURE__ */ new Set();
12749 const entriesById = /* @__PURE__ */ new Map();
12750 const resolveSizeForEntry = (entry) => {
12751 const saved = loadNativeWindowGeometry(entry.id);
12752 if (!saved) {
12753 return { width: entry.width, height: entry.height };
12754 }
12755 return {
12756 width: Math.max(saved.width, entry.minWidth),
12757 height: Math.max(saved.height, entry.minHeight)
12758 };
12759 };
12760 const ensureTemplate = (entry) => {
12761 if (injectedTemplates.has(entry.templateId)) {
12762 return;
12763 }
12764 if (document.getElementById(entry.templateId)) {
12765 injectedTemplates.add(entry.templateId);
12766 return;
12767 }
12768 if (!entry.templateHtml) {
12769 return;
12770 }
12771 const tpl = document.createElement("template");
12772 tpl.id = entry.templateId;
12773 tpl.innerHTML = entry.templateHtml;
12774 document.body.appendChild(tpl);
12775 injectedTemplates.add(entry.templateId);
12776 };
12777 const ensureStyle = (entry) => {
12778 const url = entry.styleUrl;
12779 if (!url || loadedStyles.has(url)) {
12780 return;
12781 }
12782 const safeUrl = url.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
12783 const existing = document.head.querySelector(
12784 `link[rel="stylesheet"][href="${safeUrl}"]`
12785 );
12786 if (!existing) {
12787 const link = document.createElement("link");
12788 link.rel = "stylesheet";
12789 link.href = url;
12790 if (entry.styleHandle) {
12791 link.dataset.desktopModeStyleHandle = entry.styleHandle;
12792 }
12793 document.head.appendChild(link);
12794 }
12795 if (Array.isArray(entry.styleInline)) {
12796 for (const css2 of entry.styleInline) {
12797 if (typeof css2 !== "string" || css2 === "") {
12798 continue;
12799 }
12800 const style = document.createElement("style");
12801 if (entry.styleHandle) {
12802 style.dataset.desktopModeStyleHandle = entry.styleHandle;
12803 }
12804 style.textContent = css2;
12805 document.head.appendChild(style);
12806 }
12807 }
12808 loadedStyles.add(url);
12809 };
12810 const ensureScript = async (entry) => {
12811 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
12812 return;
12813 }
12814 try {
12815 await loadVendorScript(entry.scriptUrl, {
12816 translations: entry.scriptTranslations,
12817 l10n: entry.scriptL10n,
12818 before: entry.scriptBefore,
12819 after: entry.scriptAfter
12820 });
12821 } catch (err) {
12822 doAction(HOOKS.SHELL_ERROR, {
12823 scope: "native-window-script-load",
12824 id: entry.id,
12825 error: err
12826 });
12827 }
12828 loadedScripts.add(entry.scriptUrl);
12829 };
12830 const openFromEntry = (entry) => {
12831 const globalRegistry = window.desktopModeNativeWindows || {};
12832 const render2 = globalRegistry[entry.id];
12833 const finalRender = (body, ctx) => {
12834 body.appendChild(cloneTemplate(entry.templateId));
12835 return render2?.(body, ctx);
12836 };
12837 const size = resolveSizeForEntry(entry);
12838 void manager.open({
12839 id: entry.id,
12840 baseId: entry.id,
12841 native: true,
12842 url: `#${entry.id}`,
12843 title: entry.title,
12844 icon: entry.icon,
12845 width: size.width,
12846 height: size.height,
12847 minWidth: entry.minWidth,
12848 minHeight: entry.minHeight,
12849 render: finalRender,
12850 autofocus: entry.autofocus,
12851 ownerHandle: entry.ownerHandle || entry.scriptHandle
12852 });
12853 };
12854 const openNewFromEntry = (entry) => {
12855 const globalRegistry = window.desktopModeNativeWindows || {};
12856 const render2 = globalRegistry[entry.id];
12857 const finalRender = (body, ctx) => {
12858 body.appendChild(cloneTemplate(entry.templateId));
12859 return render2?.(body, ctx);
12860 };
12861 const size = resolveSizeForEntry(entry);
12862 void manager.openNew({
12863 id: entry.id,
12864 baseId: entry.id,
12865 native: true,
12866 url: `#${entry.id}`,
12867 title: entry.title,
12868 icon: entry.icon,
12869 width: size.width,
12870 height: size.height,
12871 minWidth: entry.minWidth,
12872 minHeight: entry.minHeight,
12873 initialState: "normal",
12874 render: finalRender,
12875 autofocus: entry.autofocus,
12876 ownerHandle: entry.ownerHandle || entry.scriptHandle
12877 });
12878 };
12879 const registerTile = async (entry) => {
12880 if (registered.has(entry.id)) {
12881 return;
12882 }
12883 if ("none" === entry.placement) {
12884 ensureTemplate(entry);
12885 ensureStyle(entry);
12886 await ensureScript(entry);
12887 registered.add(entry.id);
12888 return;
12889 }
12890 ensureTemplate(entry);
12891 ensureStyle(entry);
12892 await ensureScript(entry);
12893 appendSystemTile({
12894 id: entry.id,
12895 title: entry.title,
12896 icon: entry.icon,
12897 isOpen: () => !!manager.getById(entry.id),
12898 onOpen: () => openFromEntry(entry)
12899 });
12900 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: entry.id });
12901 registered.add(entry.id);
12902 };
12903 const unregisterTile = (id) => {
12904 if (!registered.has(id)) {
12905 return;
12906 }
12907 removeSystemTile(id);
12908 registered.delete(id);
12909 entriesById.delete(id);
12910 };
12911 const sync = async (list2) => {
12912 const incoming = /* @__PURE__ */ new Set();
12913 for (const entry of list2) {
12914 incoming.add(entry.id);
12915 entriesById.set(entry.id, entry);
12916 }
12917 for (const id of Array.from(registered)) {
12918 if (!incoming.has(id)) {
12919 unregisterTile(id);
12920 }
12921 }
12922 for (const entry of list2) {
12923 if (!registered.has(entry.id)) {
12924 await registerTile(entry);
12925 }
12926 }
12927 };
12928 const openById = (id, opts = {}) => {
12929 const entry = entriesById.get(id);
12930 if (!entry) {
12931 return false;
12932 }
12933 activity.publish("desktop-mode/open-requested", {
12934 windowId: id,
12935 source: opts.source ?? "api"
12936 });
12937 openFromEntry(entry);
12938 return true;
12939 };
12940 const openNewById = (id, opts = {}) => {
12941 const entry = entriesById.get(id);
12942 if (!entry) {
12943 return false;
12944 }
12945 activity.publish("desktop-mode/open-requested", {
12946 windowId: id,
12947 source: opts.source ?? "api"
12948 });
12949 openNewFromEntry(entry);
12950 return true;
12951 };
12952 addAction(
12953 HOOKS.WINDOW_RESIZE_END,
12954 "desktop-mode-native-window-geometry",
12955 (payload) => {
12956 const p = payload;
12957 const windowId = p?.windowId;
12958 const width = p?.width;
12959 const height = p?.height;
12960 if (!windowId || typeof width !== "number" || typeof height !== "number") {
12961 return;
12962 }
12963 const win = manager.getById(windowId);
12964 if (!win) {
12965 return;
12966 }
12967 if (win.state !== "normal") {
12968 return;
12969 }
12970 const baseId = win.config.baseId || win.id;
12971 saveNativeWindowGeometry(baseId, { width, height });
12972 if (win.element) {
12973 saveNativeWindowPosition(baseId, {
12974 x: win.element.offsetLeft,
12975 y: win.element.offsetTop
12976 });
12977 }
12978 }
12979 );
12980 addAction(
12981 HOOKS.WINDOW_DRAG_END,
12982 "desktop-mode-native-window-geometry",
12983 (payload) => {
12984 const windowId = payload?.windowId;
12985 if (!windowId) {
12986 return;
12987 }
12988 const win = manager.getById(windowId);
12989 if (!win) {
12990 return;
12991 }
12992 if (win.state !== "normal") {
12993 return;
12994 }
12995 if (!win.element) {
12996 return;
12997 }
12998 const baseId = win.config.baseId || win.id;
12999 saveNativeWindowGeometry(baseId, {
13000 width: win.element.offsetWidth,
13001 height: win.element.offsetHeight
13002 });
13003 saveNativeWindowPosition(baseId, {
13004 x: win.element.offsetLeft,
13005 y: win.element.offsetTop
13006 });
13007 }
13008 );
13009 addAction(
13010 HOOKS.WINDOW_MAXIMIZED,
13011 "desktop-mode-native-window-geometry",
13012 (payload) => {
13013 const windowId = payload?.windowId;
13014 if (!windowId) {
13015 return;
13016 }
13017 const win = manager.getById(windowId);
13018 if (!win) {
13019 return;
13020 }
13021 const baseId = win.config.baseId || win.id;
13022 const entry = entriesById.get(baseId);
13023 const defaults = entry ? { width: entry.width, height: entry.height } : { width: win.config.width, height: win.config.height };
13024 setNativeWindowSavedState(baseId, "maximized", defaults);
13025 }
13026 );
13027 addAction(
13028 HOOKS.WINDOW_UNMAXIMIZED,
13029 "desktop-mode-native-window-geometry",
13030 (payload) => {
13031 const windowId = payload?.windowId;
13032 if (!windowId) {
13033 return;
13034 }
13035 const win = manager.getById(windowId);
13036 if (!win) {
13037 return;
13038 }
13039 const baseId = win.config.baseId || win.id;
13040 setNativeWindowSavedState(baseId, null);
13041 }
13042 );
13043 return { sync, openById, openNewById };
13044 }
13045 function cloneTemplate(template) {
13046 let tpl = null;
13047 if (typeof template === "string") {
13048 const found = document.getElementById(template);
13049 if (found instanceof HTMLTemplateElement) {
13050 tpl = found;
13051 }
13052 } else {
13053 tpl = template;
13054 }
13055 if (!tpl) {
13056 throw new Error(
13057 `[desktop-mode] cloneTemplate: no <template> found for ${typeof template === "string" ? `#${template}` : "<reference>"}`
13058 );
13059 }
13060 return tpl.content.cloneNode(true);
13061 }
13062 function renderIcon(icon, opts) {
13063 const className = opts.className ?? "";
13064 const title = opts.title ?? "";
13065 if (typeof icon === "string" && icon.startsWith("dashicons-")) {
13066 const el = document.createElement("span");
13067 el.className = `dashicons ${icon} ${className}`.trim();
13068 el.setAttribute("aria-hidden", "true");
13069 return el;
13070 }
13071 if (typeof icon === "string" && icon.startsWith("data:image/svg+xml;base64,")) {
13072 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
13073 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
13074 const el = document.createElement("span");
13075 el.className = className;
13076 el.setAttribute("aria-hidden", "true");
13077 el.style.backgroundImage = `url("${icon}")`;
13078 el.style.backgroundRepeat = "no-repeat";
13079 el.style.backgroundPosition = "center";
13080 el.style.backgroundSize = "contain";
13081 el.style.display = "inline-block";
13082 return el;
13083 }
13084 }
13085 if (typeof icon === "string" && /^data:image\/(png|jpeg|jpg|gif|webp|x-icon|vnd\.microsoft\.icon);base64,/i.test(icon)) {
13086 const commaIdx = icon.indexOf(",");
13087 const payload = commaIdx >= 0 ? icon.slice(commaIdx + 1) : "";
13088 if (/^[A-Za-z0-9+/=]+$/.test(payload)) {
13089 return makeImgIcon(icon, className);
13090 }
13091 }
13092 if (typeof icon === "string" && (icon.startsWith("http://") || icon.startsWith("https://"))) {
13093 return makeImgIcon(icon, className);
13094 }
13095 const span = document.createElement("span");
13096 span.className = `${className} desktop-mode-icon-letter`.trim();
13097 span.setAttribute("aria-hidden", "true");
13098 const letters = letterFromTitle(title);
13099 span.textContent = letters;
13100 const hue = hashTitleToHue(title);
13101 span.style.backgroundColor = `hsl( ${hue}, 60%, 45% )`;
13102 span.style.color = "#fff";
13103 span.style.display = "inline-flex";
13104 span.style.alignItems = "center";
13105 span.style.justifyContent = "center";
13106 span.style.fontWeight = "600";
13107 span.style.borderRadius = "4px";
13108 return span;
13109 }
13110 function makeImgIcon(src, className) {
13111 const img = document.createElement("img");
13112 img.className = className;
13113 img.src = src;
13114 img.alt = "";
13115 img.setAttribute("aria-hidden", "true");
13116 img.draggable = false;
13117 return img;
13118 }
13119 function letterFromTitle(title) {
13120 const trimmed = (title ?? "").trim();
13121 if (trimmed === "") {
13122 return "?";
13123 }
13124 const words = trimmed.split(/\s+/);
13125 if (words.length >= 2) {
13126 return (words[0][0] + words[1][0]).toUpperCase();
13127 }
13128 const first = words[0];
13129 if (first.length >= 2) {
13130 return first.slice(0, 2).toUpperCase();
13131 }
13132 return first.toUpperCase();
13133 }
13134 const BADGE_CLASS = "desktop-mode-icon__badge";
13135 const _badges = /* @__PURE__ */ new Map();
13136 function _safeBadge(count) {
13137 return Math.max(0, Math.floor(Number(count) || 0));
13138 }
13139 function setIconBadge(iconId, count) {
13140 if (!iconId) {
13141 return;
13142 }
13143 const tile2 = _findIconTile(iconId);
13144 if (!tile2) {
13145 return;
13146 }
13147 const safe = _safeBadge(count);
13148 const previous = _badges.get(iconId) ?? 0;
13149 if (safe === previous) {
13150 return;
13151 }
13152 if (safe === 0) {
13153 _badges.delete(iconId);
13154 } else {
13155 _badges.set(iconId, safe);
13156 }
13157 _paintBadgeNode(tile2, safe);
13158 activity.publish("desktop-mode/badge-changed", {
13159 itemId: iconId,
13160 count: safe,
13161 rail: "icon"
13162 });
13163 doAction(HOOKS.ICON_BADGE_CHANGED, {
13164 iconId,
13165 count: safe,
13166 previousCount: previous
13167 });
13168 }
13169 function clearIconBadge(iconId) {
13170 setIconBadge(iconId, 0);
13171 }
13172 function getIconBadge(iconId) {
13173 return _badges.get(iconId) ?? 0;
13174 }
13175 const iconsApi = {
13176 setBadge: setIconBadge,
13177 clearBadge: clearIconBadge,
13178 getBadge: getIconBadge
13179 };
13180 function fingerprintIcons(icons) {
13181 if (!icons || icons.length === 0) {
13182 return "";
13183 }
13184 return icons.map(
13185 (i) => `${i.id}|${i.title}|${i.icon}|${i.window ?? ""}|${i.url ?? ""}|${i.position ?? 0}|${i.pinned ? 1 : 0}`
13186 ).join(";");
13187 }
13188 let _lastFingerprint = "";
13189 function renderDesktopIcons(host, icons, deps2) {
13190 const fp = fingerprintIcons(icons);
13191 if (fp === _lastFingerprint && host.querySelector(":scope > .desktop-mode-icons")) {
13192 return;
13193 }
13194 _lastFingerprint = fp;
13195 const existing = host.querySelector(":scope > .desktop-mode-icons");
13196 if (existing) {
13197 existing.remove();
13198 }
13199 if (!icons || icons.length === 0) {
13200 return;
13201 }
13202 const container = document.createElement("div");
13203 container.className = "desktop-mode-icons";
13204 container.setAttribute("role", "list");
13205 container.setAttribute("aria-label", __("Desktop icons"));
13206 const ordered = [...icons].sort((a, b) => {
13207 const ap = a.pinned ? 0 : 1;
13208 const bp = b.pinned ? 0 : 1;
13209 return ap - bp;
13210 });
13211 const tiles = /* @__PURE__ */ new Map();
13212 for (const entry of ordered) {
13213 const tile2 = buildIcon(entry, deps2);
13214 const stored = _badges.get(entry.id) ?? 0;
13215 if (stored > 0) {
13216 _paintBadgeNode(tile2, stored);
13217 }
13218 container.appendChild(tile2);
13219 tiles.set(entry.id, tile2);
13220 }
13221 host.appendChild(container);
13222 doAction(HOOKS.DESKTOP_ICONS_RENDERED, {
13223 ids: (icons ?? []).map((i) => i.id),
13224 container,
13225 tiles
13226 });
13227 }
13228 function _findIconTile(iconId) {
13229 if (!iconId) {
13230 return null;
13231 }
13232 const container = document.querySelector(
13233 ".desktop-mode-icons"
13234 );
13235 if (!container) {
13236 return null;
13237 }
13238 return container.querySelector(
13239 `[data-icon-id="${_cssEscape(iconId)}"]`
13240 );
13241 }
13242 function _paintBadgeNode(host, count) {
13243 const existing = host.querySelector(
13244 `:scope > .${BADGE_CLASS}`
13245 );
13246 if (count <= 0) {
13247 existing?.remove();
13248 return;
13249 }
13250 const display = count > 99 ? "99+" : String(count);
13251 const ariaLabel = sprintf(
13252 // translators: %d is the number of pending items in a desktop-icon badge.
13253 _n("%d notification", "%d notifications", count),
13254 count
13255 );
13256 if (existing) {
13257 if (existing.textContent !== display) {
13258 existing.textContent = display;
13259 }
13260 existing.setAttribute("aria-label", ariaLabel);
13261 return;
13262 }
13263 const badge = document.createElement("span");
13264 badge.className = BADGE_CLASS;
13265 badge.textContent = display;
13266 badge.setAttribute("aria-label", ariaLabel);
13267 host.appendChild(badge);
13268 }
13269 function _cssEscape(value) {
13270 const c = window.CSS;
13271 return c?.escape ? c.escape(value) : value;
13272 }
13273 function buildIcon(entry, deps2) {
13274 const tile2 = document.createElement("button");
13275 tile2.type = "button";
13276 tile2.className = entry.pinned ? "desktop-mode-icon desktop-mode-icon--pinned" : "desktop-mode-icon";
13277 tile2.dataset.iconId = entry.id;
13278 if (entry.pinned) {
13279 tile2.dataset.pinned = "1";
13280 }
13281 tile2.setAttribute("role", "listitem");
13282 tile2.setAttribute("aria-label", entry.title);
13283 const icon = renderIcon(entry.icon, {
13284 title: entry.title,
13285 className: "desktop-mode-icon__image"
13286 });
13287 tile2.appendChild(icon);
13288 const label = document.createElement("span");
13289 label.className = "desktop-mode-icon__label";
13290 label.textContent = entry.title;
13291 tile2.appendChild(label);
13292 tile2.addEventListener("click", (e) => {
13293 e.stopPropagation();
13294 doAction(HOOKS.DESKTOP_ICON_CLICKED, {
13295 id: entry.id,
13296 target: entry.window ? "window" : "url"
13297 });
13298 openTarget(entry, deps2);
13299 });
13300 tile2.addEventListener("contextmenu", (e) => {
13301 if (entry.pinned) {
13302 return;
13303 }
13304 e.preventDefault();
13305 e.stopPropagation();
13306 openItemVisibilityMenu({
13307 x: e.clientX,
13308 y: e.clientY,
13309 id: entry.id,
13310 title: entry.title,
13311 surface: "desktop"
13312 });
13313 });
13314 return tile2;
13315 }
13316 function openTarget(entry, deps2) {
13317 if (entry.window) {
13318 const opened = deps2.openWindow(entry.window);
13319 if (!opened) {
13320 return;
13321 }
13322 return;
13323 }
13324 if (entry.url) {
13325 if (tryOpenExternalUrl(entry.url)) {
13326 return;
13327 }
13328 try {
13329 const parsed = new URL(entry.url, window.location.origin);
13330 void deps2.manager.open({
13331 id: `desktop-icon-${entry.id}`,
13332 url: parsed.toString(),
13333 title: entry.title,
13334 icon: entry.icon
13335 });
13336 } catch {
13337 }
13338 }
13339 }
13340 const SIDE_DOCK_ID = "desktop-mode-side-dock";
13341 function coreItemToIconEntry(item, index2) {
13342 return {
13343 id: `dock-core:${item.id}`,
13344 title: item.title,
13345 icon: item.icon,
13346 window: "",
13347 url: item.url,
13348 // Synthesized icons render after server-registered ones; the
13349 // large offset leaves headroom for plugin authors who set
13350 // explicit `position` values.
13351 position: 1e3 + index2
13352 };
13353 }
13354 function createLayoutDispatcher(deps2, initialLayout, initialDockItems, initialServerIcons) {
13355 let layout = initialLayout;
13356 let items = initialDockItems;
13357 let serverIcons = initialServerIcons ?? [];
13358 let primary = null;
13359 let side = null;
13360 let primaryDock = null;
13361 let sideDock = null;
13362 let sideDockEl = null;
13363 const systemTiles = /* @__PURE__ */ new Map();
13364 const railFor = (affinity) => {
13365 if (affinity === "core" && side) {
13366 return side;
13367 }
13368 return primary;
13369 };
13370 const ensureSideDockEl = () => {
13371 const existing = document.getElementById(
13372 SIDE_DOCK_ID
13373 );
13374 if (existing) {
13375 return existing;
13376 }
13377 const el = document.createElement("nav");
13378 el.id = SIDE_DOCK_ID;
13379 el.className = "desktop-mode-dock";
13380 el.setAttribute("role", "toolbar");
13381 el.setAttribute("aria-label", "Core admin navigation");
13382 deps2.shellBody.insertBefore(el, deps2.shellBody.firstChild);
13383 return el;
13384 };
13385 const removeSideDockEl = () => {
13386 if (sideDockEl && sideDockEl.parentNode) {
13387 sideDockEl.parentNode.removeChild(sideDockEl);
13388 }
13389 sideDockEl = null;
13390 };
13391 const readSettings = () => deps2.getSettings?.() ?? { itemVisibility: {}, dockOrder: [] };
13392 const effectiveDockItems = () => {
13393 const dockedNativeWindows = /* @__PURE__ */ new Set();
13394 for (const entry of systemTiles.values()) {
13395 dockedNativeWindows.add(entry.item.id);
13396 }
13397 return applyDockPlacement(
13398 items,
13399 serverIcons,
13400 readSettings(),
13401 dockedNativeWindows
13402 );
13403 };
13404 const partition = () => {
13405 const effective = effectiveDockItems();
13406 const core = [];
13407 const plugin = [];
13408 for (const item of effective) {
13409 if (item.isCore) {
13410 core.push(item);
13411 } else {
13412 plugin.push(item);
13413 }
13414 }
13415 return { core, plugin };
13416 };
13417 const repaintIcons = () => {
13418 const settings = readSettings();
13419 if (layout !== "spatial") {
13420 deps2.renderIcons(
13421 applyDesktopPlacement(serverIcons, items, settings.itemVisibility)
13422 );
13423 return;
13424 }
13425 const { core } = partition();
13426 const synthesized = core.map(coreItemToIconEntry);
13427 const explicitlyPromoted = [];
13428 let synthIndex = 0;
13429 for (const item of items) {
13430 const placement = settings.itemVisibility[item.id];
13431 if (placement === "desktop" || placement === "both") {
13432 explicitlyPromoted.push({
13433 id: `dock:${item.id}`,
13434 title: item.title,
13435 icon: item.icon,
13436 window: "",
13437 url: item.url || "",
13438 position: 2e3 + synthIndex++
13439 });
13440 }
13441 }
13442 deps2.renderIcons([...synthesized, ...explicitlyPromoted]);
13443 };
13444 const tearDownDocks = () => {
13445 if (primary) {
13446 try {
13447 primary.destroy();
13448 } catch (err) {
13449 doAction(HOOKS.SHELL_ERROR, {
13450 scope: "dock-rail-renderer/destroy",
13451 error: err
13452 });
13453 }
13454 primary = null;
13455 primaryDock = null;
13456 }
13457 if (side) {
13458 try {
13459 side.destroy();
13460 } catch (err) {
13461 doAction(HOOKS.SHELL_ERROR, {
13462 scope: "dock-rail-renderer/destroy",
13463 error: err
13464 });
13465 }
13466 side = null;
13467 sideDock = null;
13468 }
13469 };
13470 const mountRail = (mountDeps) => {
13471 const renderer = resolveActive();
13472 if (!renderer) {
13473 doAction(HOOKS.SHELL_ERROR, {
13474 scope: "dock-rail-renderer",
13475 message: "No dock rail renderer is registered."
13476 });
13477 return null;
13478 }
13479 try {
13480 return renderer.mount(mountDeps);
13481 } catch (err) {
13482 doAction(HOOKS.SHELL_ERROR, {
13483 scope: "dock-rail-renderer/mount",
13484 rendererId: renderer.id,
13485 error: err
13486 });
13487 if (renderer === defaultDockRailRenderer) {
13488 return null;
13489 }
13490 try {
13491 return defaultDockRailRenderer.mount(mountDeps);
13492 } catch {
13493 return null;
13494 }
13495 }
13496 };
13497 const buildMountDeps = (container, railItems, orientation) => ({
13498 container,
13499 items: railItems,
13500 // `fullMenu` is the complete admin-menu list. Renderers that
13501 // want to ignore the layout's partitioning (e.g., paint
13502 // every menu item in one ring regardless of `isCore`) read
13503 // this instead of `items`. Snapshot per-mount so a renderer
13504 // holding the array sees a stable list; live updates flow
13505 // through `replaceItems`.
13506 fullMenu: items.slice(),
13507 // Same idea for system tiles — OS Settings, plugin-owned
13508 // native-window launchers, etc. Lets a renderer apply
13509 // uniform treatment across menu + system cohorts in one
13510 // pass. Live updates flow through `appendSystemItem` /
13511 // `removeSystemItem`.
13512 fullSystemTiles: Array.from(systemTiles.values()).map(
13513 (entry) => entry.item
13514 ),
13515 orientation,
13516 windowManager: deps2.windowManager,
13517 adminUrl: deps2.adminUrl,
13518 // `openItem` / `openSubmenuPick` / `openSystemItem` /
13519 // `requestSubmenu` are routing callbacks for custom
13520 // renderers. They mirror exactly what the default renderer
13521 // (`Dock.openPage` / `Dock.openSubmenuPick`) does internally
13522 // — same `deriveWindowId(url, adminUrl)` call, same window-
13523 // config shape — so a custom renderer addresses the same
13524 // window with the same id at runtime. Switching renderer
13525 // mid-session doesn't lose the user's open windows.
13526 openItem: (item) => {
13527 const baseId = deriveWindowId(item.url, deps2.adminUrl);
13528 deps2.windowManager.open({
13529 id: baseId,
13530 baseId,
13531 url: item.url,
13532 parentUrl: item.url,
13533 title: item.title,
13534 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
13535 submenu: item.submenu,
13536 multi: !!item.multi
13537 });
13538 },
13539 openSubmenuPick: (item, sub) => {
13540 deps2.windowManager.open({
13541 id: deriveWindowId(sub.url, deps2.adminUrl),
13542 baseId: deriveWindowId(item.url, deps2.adminUrl),
13543 url: sub.url,
13544 // Pin the synthetic parent tab to the dock landing
13545 // page, not to the sub-page the user picked. Without
13546 // this, a submenu-pick (e.g. clicking "Editor" inside
13547 // Appearance's submenu popover) would open at
13548 // site-editor.php with no way back to themes.php.
13549 parentUrl: item.url,
13550 title: item.title,
13551 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
13552 submenu: item.submenu,
13553 multi: !!item.multi
13554 });
13555 },
13556 openSystemItem: (item) => item.onOpen()
13557 });
13558 const buildDocksForCurrentLayout = () => {
13559 tearDownDocks();
13560 const { core, plugin } = partition();
13561 if (layout === "classic") {
13562 sideDockEl = ensureSideDockEl();
13563 side = mountRail(
13564 buildMountDeps(sideDockEl, core, "left")
13565 );
13566 sideDock = unwrapDefaultDock(side);
13567 primary = mountRail(
13568 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
13569 );
13570 primaryDock = unwrapDefaultDock(primary);
13571 } else if (layout === "unified") {
13572 removeSideDockEl();
13573 primary = mountRail(
13574 buildMountDeps(deps2.bottomDockEl, effectiveDockItems(), "bottom")
13575 );
13576 primaryDock = unwrapDefaultDock(primary);
13577 } else {
13578 removeSideDockEl();
13579 primary = mountRail(
13580 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
13581 );
13582 primaryDock = unwrapDefaultDock(primary);
13583 }
13584 for (const entry of systemTiles.values()) {
13585 railFor(entry.affinity)?.appendSystemItem(entry.item);
13586 }
13587 };
13588 const dispatcher = {
13589 getLayout: () => layout,
13590 getPrimary: () => primaryDock,
13591 getSide: () => sideDock,
13592 setLayout: (next) => {
13593 if (next === layout) {
13594 return;
13595 }
13596 layout = next;
13597 deps2.shellRoot.setAttribute("data-desktop-mode-layout", next);
13598 buildDocksForCurrentLayout();
13599 repaintIcons();
13600 document.dispatchEvent(
13601 new CustomEvent("desktop-mode-layout-changed", {
13602 detail: {
13603 layout: next,
13604 primary: primaryDock,
13605 side: sideDock
13606 }
13607 })
13608 );
13609 },
13610 applyDockItems: (nextItems) => {
13611 items = nextItems;
13612 const { core, plugin } = partition();
13613 if (layout === "classic") {
13614 side?.replaceItems(core);
13615 primary?.replaceItems(plugin);
13616 } else if (layout === "unified") {
13617 primary?.replaceItems(effectiveDockItems());
13618 } else {
13619 primary?.replaceItems(plugin);
13620 }
13621 repaintIcons();
13622 },
13623 applyDesktopIcons: (next) => {
13624 serverIcons = next ?? [];
13625 repaintIcons();
13626 },
13627 appendSystemTile: (item, affinity = "plugin") => {
13628 systemTiles.set(item.id, { item, affinity });
13629 railFor(affinity)?.appendSystemItem(item);
13630 },
13631 removeSystemTile: (id) => {
13632 const entry = systemTiles.get(id);
13633 if (!entry) {
13634 return;
13635 }
13636 systemTiles.delete(id);
13637 railFor(entry.affinity)?.removeSystemItem(id);
13638 },
13639 listSystemTiles: () => Array.from(systemTiles.values()).map((entry) => ({
13640 id: entry.item.id,
13641 title: entry.item.title,
13642 icon: entry.item.icon,
13643 affinity: entry.affinity
13644 })),
13645 getSystemTile: (id) => systemTiles.get(id)?.item ?? null,
13646 getMenuItems: () => items.slice(),
13647 refresh: () => {
13648 const { core, plugin } = partition();
13649 if (layout === "classic") {
13650 side?.replaceItems(core);
13651 primary?.replaceItems(plugin);
13652 } else if (layout === "unified") {
13653 primary?.replaceItems(effectiveDockItems());
13654 } else {
13655 primary?.replaceItems(plugin);
13656 }
13657 repaintIcons();
13658 },
13659 destroy: () => {
13660 tearDownDocks();
13661 removeSideDockEl();
13662 }
13663 };
13664 deps2.shellRoot.setAttribute("data-desktop-mode-layout", layout);
13665 buildDocksForCurrentLayout();
13666 repaintIcons();
13667 let lastResolvedId = resolveActive()?.id ?? null;
13668 subscribe$3(() => {
13669 const nextId2 = resolveActive()?.id ?? null;
13670 if (nextId2 === lastResolvedId) {
13671 return;
13672 }
13673 lastResolvedId = nextId2;
13674 buildDocksForCurrentLayout();
13675 repaintIcons();
13676 document.dispatchEvent(
13677 new CustomEvent("desktop-mode-layout-changed", {
13678 detail: {
13679 layout,
13680 primary: primaryDock,
13681 side: sideDock
13682 }
13683 })
13684 );
13685 });
13686 return dispatcher;
13687 }
13688 function loadImpl(scriptUrl) {
13689 if (window.desktopModeCreateAiAssistant) {
13690 return Promise.resolve(window.desktopModeCreateAiAssistant);
13691 }
13692 return new Promise((resolve2, reject) => {
13693 const existing = document.querySelector(
13694 `script[data-desktop-mode-ai="1"]`
13695 );
13696 const finish = () => {
13697 const factory = window.desktopModeCreateAiAssistant;
13698 if (!factory) {
13699 reject(
13700 new Error(
13701 "[desktop-mode] ai-assistant bundle loaded but did not register desktopModeCreateAiAssistant"
13702 )
13703 );
13704 return;
13705 }
13706 resolve2(factory);
13707 };
13708 if (existing) {
13709 if (window.desktopModeCreateAiAssistant) {
13710 finish();
13711 } else {
13712 existing.addEventListener("load", finish);
13713 existing.addEventListener(
13714 "error",
13715 () => reject(new Error("failed to load ai-assistant bundle"))
13716 );
13717 }
13718 return;
13719 }
13720 const s = document.createElement("script");
13721 s.src = scriptUrl;
13722 s.async = true;
13723 s.dataset.desktopModeAi = "1";
13724 s.addEventListener("load", finish);
13725 s.addEventListener(
13726 "error",
13727 () => reject(new Error("failed to load ai-assistant bundle"))
13728 );
13729 document.head.appendChild(s);
13730 });
13731 }
13732 class AiAssistantStub {
13733 constructor(config, scriptUrl) {
13734 this._real = null;
13735 this._loadPromise = null;
13736 this._pendingAsk = null;
13737 this._intendOpen = false;
13738 this.ask = (...args) => {
13739 return this._ensure().then((r) => r.ask(...args));
13740 };
13741 this._config = config;
13742 this._scriptUrl = scriptUrl;
13743 }
13744 _ensure() {
13745 if (this._loadPromise) {
13746 return this._loadPromise;
13747 }
13748 this._loadPromise = loadImpl(this._scriptUrl).then((factory) => {
13749 const real = factory(this._config);
13750 if (this._pendingAsk) {
13751 real.attachAsk(this._pendingAsk);
13752 }
13753 this._real = real;
13754 return real;
13755 });
13756 return this._loadPromise;
13757 }
13758 open() {
13759 this._intendOpen = true;
13760 void this._ensure().then((r) => r.open());
13761 }
13762 close() {
13763 this._intendOpen = false;
13764 if (this._real) {
13765 this._real.close();
13766 }
13767 }
13768 toggle() {
13769 if (this.isOpen) {
13770 this.close();
13771 } else {
13772 this.open();
13773 }
13774 }
13775 get isOpen() {
13776 return this._real ? this._real.isOpen : this._intendOpen;
13777 }
13778 /**
13779 * Late-bind the programmatic `ask` callback. Mirrors the real
13780 * class's `attachAsk` signature so `desktop.ts`'s call site is
13781 * identical whether it's wiring the stub or the impl.
13782 */
13783 attachAsk(fn) {
13784 this._pendingAsk = fn;
13785 if (this._real) {
13786 this._real.attachAsk(fn);
13787 }
13788 }
13789 }
13790 const isAbortError = (err) => {
13791 if (!err || typeof err !== "object") {
13792 return false;
13793 }
13794 return err.name === "AbortError";
13795 };
13796 const normaliseToolsOpt = (tools) => {
13797 if (!tools) {
13798 return [];
13799 }
13800 const all2 = listAiCallableCommands();
13801 if (tools === true || tools === "aiCallable") {
13802 return all2;
13803 }
13804 if (Array.isArray(tools)) {
13805 const allowed = new Set(tools.map((s) => s.toLowerCase()));
13806 return all2.filter((c) => allowed.has(c.slug));
13807 }
13808 if (typeof tools === "function") {
13809 return all2.filter((c) => {
13810 try {
13811 return tools(c.slug) === true;
13812 } catch {
13813 return false;
13814 }
13815 });
13816 }
13817 return [];
13818 };
13819 const normaliseSystemPrompt = (sp) => {
13820 if (!sp) {
13821 return null;
13822 }
13823 if (typeof sp === "string") {
13824 return { text: sp, mode: "append" };
13825 }
13826 if (typeof sp === "object" && typeof sp.text === "string" && sp.text !== "") {
13827 return {
13828 text: sp.text,
13829 mode: sp.mode === "replace" ? "replace" : "append"
13830 };
13831 }
13832 return null;
13833 };
13834 function liftMessage(payloadMessage, result) {
13835 const seed2 = payloadMessage ?? "";
13836 if (seed2 !== "") {
13837 return seed2;
13838 }
13839 if (typeof result === "string" && result !== "") {
13840 return result;
13841 }
13842 if (result && typeof result === "object" && "message" in result && typeof result.message === "string") {
13843 return result.message;
13844 }
13845 return "";
13846 }
13847 function serialiseOutcome(result) {
13848 if (result === void 0) {
13849 return { value: null };
13850 }
13851 if (typeof result === "object" && result !== null) {
13852 return result;
13853 }
13854 return { value: result };
13855 }
13856 function createAsk(deps2) {
13857 const postToSearch = async (body, signal) => {
13858 const config = deps2.config();
13859 const url = config.aiSearchUrl ?? "";
13860 const nonce = config.restNonce ?? "";
13861 if (!url || !nonce) {
13862 throw new Error(
13863 "[desktop-mode] wp.desktop.ai.ask: aiSearchUrl / restNonce missing from config. AI Copilot may not be enabled."
13864 );
13865 }
13866 try {
13867 return await trackedFetch$1(
13868 url,
13869 {
13870 method: "POST",
13871 credentials: "same-origin",
13872 headers: {
13873 "Content-Type": "application/json",
13874 "X-WP-Nonce": nonce
13875 },
13876 body: JSON.stringify(body),
13877 signal
13878 },
13879 { source: "desktop-mode/ai-ask" }
13880 );
13881 } catch (err) {
13882 if (isAbortError(err)) {
13883 throw err;
13884 }
13885 throw new Error(
13886 `[desktop-mode] wp.desktop.ai.ask: network error — ${String(
13887 err?.message ?? err
13888 )}`
13889 );
13890 }
13891 };
13892 const dispatchToolCall = async (payload, opts) => {
13893 const slug = payload.tool?.slug ?? "";
13894 const args = payload.tool?.args ?? "";
13895 const cmd = findCommand(slug);
13896 if (!cmd) {
13897 return {
13898 ok: false,
13899 response: {
13900 answer_type: "tool_call",
13901 message: `Command /${slug} was not registered on this page.`,
13902 entity: null,
13903 admin_links: null,
13904 toolCall: {
13905 slug,
13906 args,
13907 result: { error: "command_not_found" }
13908 },
13909 request_id: payload.request_id
13910 }
13911 };
13912 }
13913 const ctx = opts.commandContext ?? deps2.fallbackContext();
13914 let result;
13915 try {
13916 result = await Promise.resolve(cmd.run(args, ctx));
13917 } catch (err) {
13918 result = { error: String(err?.message ?? err) };
13919 }
13920 return { ok: true, slug, args, result };
13921 };
13922 const composeFollowUp = async (text, slug, args, result, sp, signal) => {
13923 const body = {
13924 query: text,
13925 follow_up: {
13926 tool: { slug, args },
13927 result: serialiseOutcome(result)
13928 }
13929 };
13930 if (sp) {
13931 body.system_prompt_text = sp.text;
13932 body.system_prompt_mode = sp.mode;
13933 }
13934 let res;
13935 try {
13936 res = await postToSearch(body, signal);
13937 } catch (err) {
13938 if (isAbortError(err)) {
13939 throw err;
13940 }
13941 return null;
13942 }
13943 if (!res.ok) {
13944 return null;
13945 }
13946 const payload = await res.json().catch(() => ({}));
13947 const message = typeof payload.message === "string" ? payload.message.trim() : "";
13948 return message !== "" ? payload.message ?? null : null;
13949 };
13950 return async function ask(query, opts = {}) {
13951 const text = (query ?? "").trim();
13952 if (text === "") {
13953 const hasMeaningfulOpts = opts.tools !== void 0 || opts.systemPrompt !== void 0 || opts.followUp === true || opts.resumeTool !== void 0 || opts.commandContext !== void 0;
13954 if (hasMeaningfulOpts) {
13955 throw new Error(
13956 "[desktop-mode] wp.desktop.ai.ask: empty query passed with non-default options — likely a caller bug. Provide a query or call without options."
13957 );
13958 }
13959 return {
13960 answer_type: "chat",
13961 message: "",
13962 entity: null,
13963 admin_links: null
13964 };
13965 }
13966 const commandTools = normaliseToolsOpt(opts.tools);
13967 const sp = normaliseSystemPrompt(opts.systemPrompt);
13968 const body = { query: text };
13969 if (opts.resumeTool) {
13970 body.resume_tool = opts.resumeTool;
13971 }
13972 if (typeof opts.startOffset === "number") {
13973 body.start_offset = opts.startOffset;
13974 }
13975 if (commandTools.length > 0) {
13976 body.command_tools = commandTools;
13977 }
13978 if (sp) {
13979 body.system_prompt_text = sp.text;
13980 body.system_prompt_mode = sp.mode;
13981 }
13982 const res = await postToSearch(body, opts.signal);
13983 if (!res.ok) {
13984 const detail = await res.json().catch(() => ({ message: res.statusText }));
13985 throw new Error(
13986 `[desktop-mode] wp.desktop.ai.ask: HTTP ${res.status} — ${detail.message ?? res.statusText}`
13987 );
13988 }
13989 const payload = await res.json();
13990 if (payload.answer_type !== "tool_call" || !payload.tool) {
13991 return {
13992 answer_type: payload.answer_type,
13993 message: payload.message ?? "",
13994 entity: payload.entity ?? null,
13995 admin_links: payload.admin_links ?? null,
13996 request_id: payload.request_id,
13997 continue: payload.continue ?? null
13998 };
13999 }
14000 const dispatch2 = await dispatchToolCall(payload, opts);
14001 if (!dispatch2.ok) {
14002 return dispatch2.response;
14003 }
14004 const { slug, args, result } = dispatch2;
14005 let message = liftMessage(payload.message, result);
14006 if (opts.followUp === true) {
14007 const composed = await composeFollowUp(
14008 text,
14009 slug,
14010 args,
14011 result,
14012 sp,
14013 opts.signal
14014 );
14015 if (composed !== null) {
14016 message = composed;
14017 }
14018 }
14019 return {
14020 answer_type: "tool_call",
14021 message,
14022 entity: null,
14023 admin_links: null,
14024 toolCall: { slug, args, result },
14025 request_id: payload.request_id
14026 };
14027 };
14028 }
14029 const EVENT_NAME = "desktop-mode-broadcast";
14030 const POSTMESSAGE_TYPE = "desktop-mode-broadcast";
14031 const ORIGIN = window.location.origin;
14032 let _manager = null;
14033 function attachBroadcastBus(manager) {
14034 _manager = manager;
14035 }
14036 function broadcast(topic, payload) {
14037 const filteredTopic = String(
14038 applyFilters("desktop-mode.broadcast.topic", topic, { payload }) ?? topic
14039 );
14040 const filteredPayload = applyFilters(
14041 "desktop-mode.broadcast.payload",
14042 payload,
14043 { topic: filteredTopic }
14044 );
14045 const detail = {
14046 topic: filteredTopic,
14047 payload: filteredPayload
14048 };
14049 document.dispatchEvent(new CustomEvent(EVENT_NAME, { detail }));
14050 doAction(HOOKS.BROADCAST, detail);
14051 activity.publish(
14052 filteredTopic,
14053 filteredPayload
14054 );
14055 if (!_manager) {
14056 return;
14057 }
14058 const message = {
14059 type: POSTMESSAGE_TYPE,
14060 topic: filteredTopic,
14061 payload: filteredPayload
14062 };
14063 for (const win of _manager._stack) {
14064 const target2 = win.iframe?.contentWindow;
14065 if (!target2) {
14066 continue;
14067 }
14068 try {
14069 target2.postMessage(message, ORIGIN);
14070 } catch (err) {
14071 }
14072 }
14073 }
14074 function subscribe$2(topic, cb) {
14075 const handler = (e) => {
14076 const detail = e.detail;
14077 if (!detail) {
14078 return;
14079 }
14080 if (topic !== "*" && detail.topic !== topic) {
14081 return;
14082 }
14083 try {
14084 cb(detail.payload, { topic: detail.topic });
14085 } catch (err) {
14086 doAction(HOOKS.SHELL_ERROR, {
14087 scope: "broadcast-subscriber",
14088 topic: detail.topic,
14089 error: err
14090 });
14091 }
14092 };
14093 document.addEventListener(EVENT_NAME, handler);
14094 return () => document.removeEventListener(EVENT_NAME, handler);
14095 }
14096 function installBroadcastReceiver() {
14097 window.addEventListener("message", (e) => {
14098 if (e.origin !== ORIGIN) {
14099 return;
14100 }
14101 const data = e.data;
14102 if (!data || data.type !== POSTMESSAGE_TYPE) {
14103 return;
14104 }
14105 if (data._fromParent) {
14106 return;
14107 }
14108 if (typeof data.topic !== "string") {
14109 return;
14110 }
14111 broadcast(data.topic, data.payload);
14112 });
14113 }
14114 const LOG_PREFIX = "[desktop-mode-bin badge]";
14115 function log(...args) {
14116 try {
14117 if (window.localStorage?.getItem("desktopModeBinDebug")) {
14118 console.info(LOG_PREFIX, ...args);
14119 }
14120 } catch {
14121 }
14122 }
14123 function warn(...args) {
14124 console.warn(LOG_PREFIX, ...args);
14125 }
14126 const TARGET_ID = "desktop-mode-recycle-bin";
14127 const HEARTBEAT_FIELD$1 = "desktop_mode_recycle_bin_seen_ts";
14128 function getDesktopApi() {
14129 return window.wp?.desktop;
14130 }
14131 const store$3 = createSharedStore(
14132 "desktop-mode/recycle-bin/badge",
14133 () => ({
14134 current: 0,
14135 seenTs: 0,
14136 started: false,
14137 countUrl: ""
14138 })
14139 );
14140 function setRecycleBinBadge(next) {
14141 const safe = Math.max(0, Math.floor(next));
14142 const prev = store$3.state.current;
14143 store$3.state.current = safe;
14144 log("setRecycleBinBadge", { prev, next: safe });
14145 paintBadge(safe);
14146 }
14147 function adjustRecycleBinBadge(delta) {
14148 setRecycleBinBadge(store$3.state.current + delta);
14149 }
14150 function _currentRecycleBinBadge() {
14151 return store$3.state.current;
14152 }
14153 function paintBadge(count) {
14154 const desktop = getDesktopApi();
14155 const active2 = isBinWindowActive();
14156 const visible = active2 ? 0 : count;
14157 log("paintBadge", { count, visible, active: active2 });
14158 desktop?.dock?.setBadge?.(TARGET_ID, visible);
14159 desktop?.taskbar?.setBadge?.(TARGET_ID, visible);
14160 desktop?.icons?.setBadge?.(TARGET_ID, visible);
14161 }
14162 function isBinWindowActive() {
14163 return !!getDesktopApi()?.windowManager?.isActive?.(TARGET_ID);
14164 }
14165 function startRecycleBinBadge(initialRaw, countUrl = "") {
14166 const initial = Number(initialRaw) || 0;
14167 const cfg = window.desktopModeConfig;
14168 const cfgCount = cfg?.recycleBinCount;
14169 const cfgUrl = cfg?.recycleBinCountUrl;
14170 const cfgDebug = cfg?.desktopModeBinDebug;
14171 log("startRecycleBinBadge entry", {
14172 initial,
14173 countUrl,
14174 alreadyStarted: store$3.state.started,
14175 cfgCount,
14176 cfgUrl,
14177 cfgDebug,
14178 readyState: document.readyState
14179 });
14180 const cfgCountNum = Number(cfgCount);
14181 const cfgCountIsHealthy = (typeof cfgCount === "number" || typeof cfgCount === "string") && Number.isFinite(cfgCountNum);
14182 if (!cfgCountIsHealthy) {
14183 warn(
14184 "desktopModeConfig.recycleBinCount is missing — PHP filter `desktop_mode_shell_config` did not deliver. Check your PHP error log for `[desktop-mode-bin debug]` lines.",
14185 { cfg }
14186 );
14187 }
14188 if (store$3.state.started) {
14189 setRecycleBinBadge(initial);
14190 return;
14191 }
14192 store$3.state.started = true;
14193 store$3.state.countUrl = countUrl;
14194 store$3.state.seenTs = Date.now();
14195 setRecycleBinBadge(initial);
14196 wireDockTileSignal();
14197 wireDesktopIconsSignal();
14198 wireBroadcastDeltas();
14199 wirePostMessageFastPath();
14200 wireHeartbeatProbe();
14201 wireWindowLifecycleSignals();
14202 }
14203 function wireWindowLifecycleSignals() {
14204 const ns = "desktop-mode/recycle-bin/badge-lifecycle";
14205 const repaint = (payload) => {
14206 const detail = payload;
14207 if (detail?.windowId !== TARGET_ID) {
14208 return;
14209 }
14210 paintBadge(store$3.state.current);
14211 };
14212 addAction(HOOKS.WINDOW_OPENED, ns, repaint);
14213 addAction(HOOKS.WINDOW_FOCUSED, ns, repaint);
14214 addAction(HOOKS.WINDOW_BLURRED, ns, repaint);
14215 addAction(HOOKS.WINDOW_MINIMIZED, ns, repaint);
14216 addAction(HOOKS.WINDOW_RESTORED, ns, repaint);
14217 addAction(HOOKS.WINDOW_CLOSED, ns, repaint);
14218 addAction(HOOKS.WINDOW_REOPENED, ns, repaint);
14219 }
14220 function wireDockTileSignal() {
14221 addAction(
14222 HOOKS.DOCK_ITEM_APPENDED,
14223 "desktop-mode/recycle-bin/badge",
14224 (payload) => {
14225 if (payload?.id === TARGET_ID) {
14226 paintBadge(store$3.state.current);
14227 }
14228 }
14229 );
14230 }
14231 function wireDesktopIconsSignal() {
14232 addAction(
14233 HOOKS.DESKTOP_ICONS_RENDERED,
14234 "desktop-mode/recycle-bin/badge",
14235 (payload) => {
14236 if (payload?.ids?.includes(TARGET_ID)) {
14237 paintBadge(store$3.state.current);
14238 }
14239 }
14240 );
14241 }
14242 function wireBroadcastDeltas() {
14243 const onDomain = (payload) => {
14244 const detail = payload;
14245 if (!detail) {
14246 return;
14247 }
14248 const ids = Array.isArray(detail.ids) ? detail.ids.length : 0;
14249 switch (detail.action) {
14250 case "trashed":
14251 adjustRecycleBinBadge(+ids);
14252 break;
14253 case "untrashed":
14254 case "deleted":
14255 adjustRecycleBinBadge(-ids);
14256 break;
14257 }
14258 };
14259 subscribe$2("desktop-mode.post.changed", onDomain);
14260 subscribe$2("desktop-mode.page.changed", onDomain);
14261 subscribe$2("desktop-mode.attachment.changed", onDomain);
14262 subscribe$2("desktop-mode.comment.changed", onDomain);
14263 subscribe$2("desktop-mode.placement.changed", onDomain);
14264 subscribe$2("desktop-mode.shortcut.changed", onDomain);
14265 subscribe$2("desktop-mode.folder.changed", onDomain);
14266 }
14267 function wirePostMessageFastPath() {
14268 const expectedOrigin = window.location.origin;
14269 window.addEventListener("message", (e) => {
14270 if (e.origin !== expectedOrigin) {
14271 return;
14272 }
14273 const data = e.data;
14274 if (!data || data.type !== "desktop-mode-recycle-bin-changed") {
14275 return;
14276 }
14277 const ts = typeof data.ts === "number" ? data.ts : Date.now();
14278 if (ts <= store$3.state.seenTs) {
14279 log("postMessage skipped (ts <= seenTs)", { ts, seenTs: store$3.state.seenTs });
14280 return;
14281 }
14282 log("postMessage triggers refetch", { ts, prevSeenTs: store$3.state.seenTs });
14283 store$3.state.seenTs = ts;
14284 void refetchCount();
14285 });
14286 }
14287 function wireHeartbeatProbe() {
14288 const $ = window.jQuery;
14289 if (!$) {
14290 warn("wireHeartbeatProbe: window.jQuery not available — heartbeat path disabled");
14291 return;
14292 }
14293 log("wireHeartbeatProbe: jQuery + heartbeat hooks attached");
14294 $(document).on("heartbeat-send", (...args) => {
14295 const data = args[1];
14296 if (data) {
14297 data[HEARTBEAT_FIELD$1] = store$3.state.seenTs;
14298 }
14299 });
14300 $(document).on("heartbeat-tick", (...args) => {
14301 const response = args[1];
14302 const block = response?.desktop_mode_recycle_bin;
14303 log("heartbeat-tick", { hasBlock: !!block, block });
14304 if (!block) {
14305 return;
14306 }
14307 if (typeof block.ts === "number" && block.ts > store$3.state.seenTs) {
14308 store$3.state.seenTs = block.ts;
14309 }
14310 if (typeof block.count === "number") {
14311 setRecycleBinBadge(block.count);
14312 }
14313 });
14314 }
14315 async function refetchCount() {
14316 if (!store$3.state.countUrl) {
14317 log("refetchCount: no countUrl, skip");
14318 return;
14319 }
14320 log("refetchCount: hitting", store$3.state.countUrl);
14321 try {
14322 const response = await fetch(store$3.state.countUrl, {
14323 credentials: "same-origin",
14324 headers: { Accept: "application/json" }
14325 });
14326 if (!response.ok) {
14327 warn("refetchCount: non-OK", response.status, response.statusText);
14328 return;
14329 }
14330 const json = await response.json();
14331 log("refetchCount: response", json);
14332 if (typeof json.count === "number") {
14333 setRecycleBinBadge(json.count);
14334 }
14335 } catch (err) {
14336 warn("refetchCount: fetch failed", err);
14337 }
14338 }
14339 const OS_SETTINGS_ID = "desktop-mode-os-settings";
14340 const RECYCLE_BIN_ID = "desktop-mode-recycle-bin";
14341 function registerBuiltInPeekRenderers(opts) {
14342 const wpHooks = getWpHooks();
14343 if (!wpHooks) {
14344 return;
14345 }
14346 wpHooks.addFilter(
14347 "desktop-mode.dock.peek-card-content",
14348 "desktop-mode/built-in-peek-renderers",
14349 (body, ctx) => {
14350 const context = ctx;
14351 const id = context.window.id;
14352 if (id === OS_SETTINGS_ID) {
14353 return renderOsSettings();
14354 }
14355 if (id === RECYCLE_BIN_ID) {
14356 return renderRecycleBin(context, opts.getRecycleBinCount);
14357 }
14358 return body;
14359 }
14360 );
14361 }
14362 function renderOsSettings(_ctx) {
14363 const root = document.createElement("span");
14364 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--os-settings";
14365 root.setAttribute("aria-hidden", "true");
14366 const hero = document.createElement("span");
14367 hero.className = "desktop-mode-dock-peek__os-hero dashicons dashicons-admin-generic";
14368 root.appendChild(hero);
14369 const subtitle = document.createElement("span");
14370 subtitle.className = "desktop-mode-dock-peek__os-subtitle";
14371 subtitle.textContent = __("System Preferences");
14372 root.appendChild(subtitle);
14373 const tabs = document.createElement("span");
14374 tabs.className = "desktop-mode-dock-peek__os-tabs";
14375 for (const cls of [
14376 "dashicons-art",
14377 "dashicons-admin-customizer",
14378 "dashicons-editor-help"
14379 ]) {
14380 const tab = document.createElement("span");
14381 tab.className = `desktop-mode-dock-peek__os-tab dashicons ${cls}`;
14382 tabs.appendChild(tab);
14383 }
14384 root.appendChild(tabs);
14385 return root;
14386 }
14387 function renderRecycleBin(_ctx, getCount) {
14388 const root = document.createElement("span");
14389 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--recycle-bin";
14390 root.setAttribute("aria-hidden", "true");
14391 const count = Math.max(0, Math.floor(getCount() || 0));
14392 root.dataset.empty = count === 0 ? "true" : "false";
14393 const stage = document.createElement("span");
14394 stage.className = "desktop-mode-dock-peek__bin-stage";
14395 const stack = document.createElement("span");
14396 stack.className = "desktop-mode-dock-peek__bin-stack";
14397 for (let i = 0; i < 3; i++) {
14398 const slip = document.createElement("span");
14399 slip.className = "desktop-mode-dock-peek__bin-slip";
14400 stack.appendChild(slip);
14401 }
14402 stage.appendChild(stack);
14403 const icon = document.createElement("span");
14404 icon.className = `desktop-mode-dock-peek__bin-icon dashicons ${count === 0 ? "dashicons-trash" : "dashicons-trash"}`;
14405 stage.appendChild(icon);
14406 root.appendChild(stage);
14407 const label = document.createElement("span");
14408 label.className = "desktop-mode-dock-peek__bin-label";
14409 if (count === 0) {
14410 label.textContent = __("Recycle Bin — empty");
14411 } else if (count === 1) {
14412 label.textContent = __("1 item");
14413 } else if (count > 99) {
14414 label.textContent = "99+ items";
14415 } else {
14416 label.textContent = `${count} items`;
14417 }
14418 root.appendChild(label);
14419 return root;
14420 }
14421 function getWpHooks() {
14422 const wp = window.wp;
14423 return wp?.hooks ?? null;
14424 }
14425 const BUG_REPORT_WINDOW_ID = "desktop-mode-bug-report";
14426 const REPO_OWNER = "WordPress";
14427 const REPO_NAME = "desktop-mode";
14428 const MAX_BODY_LENGTH = 6e3;
14429 function renderBugReport(body) {
14430 body.classList.add("desktop-mode-bug-report");
14431 body.replaceChildren();
14432 const form = document.createElement("form");
14433 form.className = "desktop-mode-bug-report__form";
14434 form.setAttribute("novalidate", "");
14435 const intro = document.createElement("p");
14436 intro.className = "desktop-mode-bug-report__intro";
14437 intro.textContent = __(
14438 "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."
14439 );
14440 form.appendChild(intro);
14441 form.appendChild(buildTypeField());
14442 form.appendChild(buildTextField("title", __("Title"), {
14443 placeholder: __("A short summary"),
14444 required: true
14445 }));
14446 form.appendChild(buildTextareaField("description", __("What happened? What did you expect?"), {
14447 placeholder: __("Describe the issue or the feature you have in mind."),
14448 rows: 5,
14449 required: true
14450 }));
14451 form.appendChild(buildTextareaField("steps", __("Steps to reproduce (bug only)"), {
14452 placeholder: __("One step per line"),
14453 rows: 4
14454 }));
14455 const meta = buildMetadataPreview();
14456 form.appendChild(meta);
14457 const actions = document.createElement("div");
14458 actions.className = "desktop-mode-bug-report__actions";
14459 const submit = document.createElement("button");
14460 submit.type = "submit";
14461 submit.className = "desktop-mode-bug-report__submit";
14462 submit.textContent = __("Open issue on GitHub");
14463 actions.appendChild(submit);
14464 const hint = document.createElement("span");
14465 hint.className = "desktop-mode-bug-report__hint";
14466 hint.textContent = __("You will review and submit on GitHub.");
14467 actions.appendChild(hint);
14468 form.appendChild(actions);
14469 form.addEventListener("submit", (e) => {
14470 e.preventDefault();
14471 const state2 = readFormState(form);
14472 if (!state2.title.trim() || !state2.description.trim()) {
14473 showInlineError(form, __("Title and description are both required."));
14474 return;
14475 }
14476 const url = buildGithubIssueUrl(state2);
14477 window.open(url, "_blank", "noopener");
14478 });
14479 body.appendChild(form);
14480 }
14481 function buildTypeField() {
14482 const wrap = document.createElement("div");
14483 wrap.className = "desktop-mode-bug-report__field desktop-mode-bug-report__field--type";
14484 const label = document.createElement("span");
14485 label.className = "desktop-mode-bug-report__label";
14486 label.textContent = __("Type");
14487 wrap.appendChild(label);
14488 const group = document.createElement("div");
14489 group.className = "desktop-mode-bug-report__radio-group";
14490 group.setAttribute("role", "radiogroup");
14491 const options = [
14492 { value: "bug", label: __("Bug"), checked: true },
14493 { value: "feature", label: __("Feature request") },
14494 { value: "question", label: __("Question") }
14495 ];
14496 for (const opt of options) {
14497 const radioLabel = document.createElement("label");
14498 radioLabel.className = "desktop-mode-bug-report__radio";
14499 const input = document.createElement("input");
14500 input.type = "radio";
14501 input.name = "type";
14502 input.value = opt.value;
14503 if (opt.checked) {
14504 input.checked = true;
14505 }
14506 radioLabel.appendChild(input);
14507 const text = document.createElement("span");
14508 text.textContent = opt.label;
14509 radioLabel.appendChild(text);
14510 group.appendChild(radioLabel);
14511 }
14512 wrap.appendChild(group);
14513 return wrap;
14514 }
14515 function buildTextField(name, labelText, opts = {}) {
14516 const wrap = document.createElement("div");
14517 wrap.className = "desktop-mode-bug-report__field";
14518 const label = document.createElement("label");
14519 label.className = "desktop-mode-bug-report__label";
14520 label.textContent = labelText;
14521 wrap.appendChild(label);
14522 const input = document.createElement("input");
14523 input.type = "text";
14524 input.name = name;
14525 input.className = "desktop-mode-bug-report__input";
14526 if (opts.placeholder) {
14527 input.placeholder = opts.placeholder;
14528 }
14529 if (opts.required) {
14530 input.setAttribute("aria-required", "true");
14531 }
14532 label.appendChild(input);
14533 return wrap;
14534 }
14535 function buildTextareaField(name, labelText, opts = {}) {
14536 const wrap = document.createElement("div");
14537 wrap.className = "desktop-mode-bug-report__field";
14538 const label = document.createElement("label");
14539 label.className = "desktop-mode-bug-report__label";
14540 label.textContent = labelText;
14541 wrap.appendChild(label);
14542 const textarea = document.createElement("textarea");
14543 textarea.name = name;
14544 textarea.className = "desktop-mode-bug-report__textarea";
14545 textarea.rows = opts.rows ?? 4;
14546 if (opts.placeholder) {
14547 textarea.placeholder = opts.placeholder;
14548 }
14549 if (opts.required) {
14550 textarea.setAttribute("aria-required", "true");
14551 }
14552 label.appendChild(textarea);
14553 return wrap;
14554 }
14555 function buildMetadataPreview() {
14556 const details = document.createElement("details");
14557 details.className = "desktop-mode-bug-report__metadata";
14558 const summary = document.createElement("summary");
14559 summary.textContent = __("Environment included with the report");
14560 details.appendChild(summary);
14561 const pre = document.createElement("pre");
14562 pre.className = "desktop-mode-bug-report__metadata-body";
14563 pre.textContent = formatMetadata(collectMetadata());
14564 details.appendChild(pre);
14565 return details;
14566 }
14567 function showInlineError(form, msg) {
14568 let banner = form.querySelector(".desktop-mode-bug-report__error");
14569 if (!banner) {
14570 banner = document.createElement("div");
14571 banner.className = "desktop-mode-bug-report__error";
14572 banner.setAttribute("role", "alert");
14573 form.prepend(banner);
14574 }
14575 banner.textContent = msg;
14576 }
14577 function readFormState(form) {
14578 const data = new FormData(form);
14579 return {
14580 type: data.get("type") ?? "bug",
14581 title: data.get("title") ?? "",
14582 description: data.get("description") ?? "",
14583 steps: data.get("steps") ?? ""
14584 };
14585 }
14586 function buildGithubIssueUrl(state2) {
14587 const labels = labelsForType(state2.type);
14588 const body = composeIssueBody(state2);
14589 const params = new URLSearchParams();
14590 params.set("title", state2.title.trim());
14591 params.set("body", body);
14592 if (labels.length) {
14593 params.set("labels", labels.join(","));
14594 }
14595 return `https://github.com/${REPO_OWNER}/${REPO_NAME}/issues/new?${params.toString()}`;
14596 }
14597 function labelsForType(type) {
14598 switch (type) {
14599 case "bug":
14600 return ["bug"];
14601 case "feature":
14602 return ["enhancement"];
14603 case "question":
14604 return ["question"];
14605 default:
14606 return [];
14607 }
14608 }
14609 function composeIssueBody(state2) {
14610 const parts = [];
14611 parts.push(state2.description.trim());
14612 if (state2.type === "bug" && state2.steps.trim()) {
14613 parts.push("");
14614 parts.push("## Steps to reproduce");
14615 parts.push("");
14616 parts.push(state2.steps.trim());
14617 }
14618 parts.push("");
14619 parts.push("<details><summary>Environment</summary>");
14620 parts.push("");
14621 parts.push("```");
14622 parts.push(formatMetadata(collectMetadata()));
14623 parts.push("```");
14624 parts.push("");
14625 parts.push("</details>");
14626 let out = parts.join("\n");
14627 if (out.length > MAX_BODY_LENGTH) {
14628 out = out.slice(0, MAX_BODY_LENGTH) + "\n\n…(truncated to fit GitHub URL length limit)";
14629 }
14630 return out;
14631 }
14632 function collectMetadata() {
14633 const cfg = window.wp?.desktop?.config;
14634 return {
14635 pluginVersion: cfg?.pluginVersion ?? "unknown",
14636 wordpressVersion: cfg?.wordpressVersion ?? "unknown",
14637 userAgent: navigator.userAgent,
14638 viewport: `${window.innerWidth}x${window.innerHeight}`,
14639 platform: navigator.platform || "unknown",
14640 currentUrl: window.location.href
14641 };
14642 }
14643 function formatMetadata(m) {
14644 return [
14645 `Plugin version: ${m.pluginVersion}`,
14646 `WordPress version: ${m.wordpressVersion}`,
14647 `User agent: ${m.userAgent}`,
14648 `Viewport: ${m.viewport}`,
14649 `Platform: ${m.platform}`,
14650 `Current URL: ${m.currentUrl}`
14651 ].join("\n");
14652 }
14653 let _config = null;
14654 let _state = {
14655 installHintDismissed: false,
14656 notificationsEnabled: false
14657 };
14658 const _listeners = /* @__PURE__ */ new Set();
14659 function initPwaState(config) {
14660 if (!config) {
14661 _config = null;
14662 return;
14663 }
14664 _config = config;
14665 _state = { ...config.state };
14666 notify$4();
14667 }
14668 function getPwaState() {
14669 return { ..._state };
14670 }
14671 function updatePwaState(patch) {
14672 _state = { ..._state, ...patch };
14673 notify$4();
14674 if (!_config) {
14675 return getPwaState();
14676 }
14677 const body = JSON.stringify(patch);
14678 const nonce = readRestNonce$2();
14679 void fetch(_config.stateUrl, {
14680 method: "POST",
14681 credentials: "same-origin",
14682 headers: {
14683 "Content-Type": "application/json",
14684 ...nonce ? { "X-WP-Nonce": nonce } : {}
14685 },
14686 body
14687 }).catch((err) => {
14688 if (typeof console !== "undefined") {
14689 console.warn("[desktop-mode] pwa-state write failed:", err);
14690 }
14691 });
14692 return getPwaState();
14693 }
14694 function subscribePwaState(cb) {
14695 _listeners.add(cb);
14696 return () => {
14697 _listeners.delete(cb);
14698 };
14699 }
14700 function notify$4() {
14701 const snapshot = getPwaState();
14702 for (const cb of Array.from(_listeners)) {
14703 try {
14704 cb(snapshot);
14705 } catch (err) {
14706 if (typeof console !== "undefined") {
14707 console.error(
14708 "[desktop-mode] pwa-state listener threw:",
14709 err
14710 );
14711 }
14712 }
14713 }
14714 }
14715 function readRestNonce$2() {
14716 const cfg = window.desktopModeConfig;
14717 return cfg?.restNonce ?? "";
14718 }
14719 const state = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
14720 __proto__: null,
14721 getPwaState,
14722 initPwaState,
14723 subscribePwaState,
14724 updatePwaState
14725 }, Symbol.toStringTag, { value: "Module" }));
14726 let _registration = null;
14727 let _registrationFailed = false;
14728 let _controllerChangeBound = false;
14729 let _reloadingForSwUpdate = false;
14730 let _status = "pending";
14731 function bindControllerChangeReload() {
14732 if (_controllerChangeBound) {
14733 return;
14734 }
14735 _controllerChangeBound = true;
14736 const hadInitialController = !!navigator.serviceWorker.controller;
14737 navigator.serviceWorker.addEventListener("controllerchange", () => {
14738 if (!hadInitialController) {
14739 return;
14740 }
14741 if (_reloadingForSwUpdate) {
14742 return;
14743 }
14744 if (wasRecentlyReloadedForSwUpdate()) {
14745 return;
14746 }
14747 markReloadedForSwUpdate();
14748 _reloadingForSwUpdate = true;
14749 setTimeout(() => window.location.reload(), 0);
14750 });
14751 }
14752 const SW_RELOAD_THROTTLE_KEY = "wpd-sw-reload-ts";
14753 const SW_RELOAD_THROTTLE_MS = 3e4;
14754 function wasRecentlyReloadedForSwUpdate() {
14755 try {
14756 const raw = sessionStorage.getItem(SW_RELOAD_THROTTLE_KEY);
14757 const last = raw ? Number.parseInt(raw, 10) : 0;
14758 if (!Number.isFinite(last) || last <= 0) {
14759 return false;
14760 }
14761 return Date.now() - last < SW_RELOAD_THROTTLE_MS;
14762 } catch {
14763 return false;
14764 }
14765 }
14766 function markReloadedForSwUpdate() {
14767 try {
14768 sessionStorage.setItem(SW_RELOAD_THROTTLE_KEY, String(Date.now()));
14769 } catch {
14770 }
14771 }
14772 async function registerServiceWorker(config, options = {}) {
14773 if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
14774 _status = "unsupported";
14775 return null;
14776 }
14777 if (!config?.swUrl) {
14778 _status = "unsupported";
14779 return null;
14780 }
14781 if (!window.isSecureContext) {
14782 _status = "unsupported";
14783 return null;
14784 }
14785 if (_registration || _registrationFailed) {
14786 return _registration;
14787 }
14788 if (!options.forceReplace) {
14789 const existing = await navigator.serviceWorker.getRegistrations().catch(() => []);
14790 const foreign = existing.find((reg) => {
14791 const url = reg.active?.scriptURL ?? reg.installing?.scriptURL ?? "";
14792 return url !== "" && url !== config.swUrl;
14793 });
14794 if (foreign) {
14795 _status = "foreign-sw";
14796 if (typeof console !== "undefined") {
14797 console.warn(
14798 "[desktop-mode] another service worker is already registered (" + foreign.scope + "); skipping desktop-mode SW. Set desktop_mode_pwa_force_replace_sw=true to override."
14799 );
14800 }
14801 return null;
14802 }
14803 }
14804 try {
14805 _registration = await navigator.serviceWorker.register(config.swUrl, {
14806 scope: "/",
14807 updateViaCache: "none"
14808 });
14809 _status = "registered";
14810 bindControllerChangeReload();
14811 return _registration;
14812 } catch (err) {
14813 _registrationFailed = true;
14814 _status = "failed";
14815 if (typeof console !== "undefined") {
14816 console.warn("[desktop-mode] SW registration failed:", err);
14817 }
14818 return null;
14819 }
14820 }
14821 function getSwRegistrationStatus() {
14822 return _status;
14823 }
14824 const PWA_INSTALL_TILE_ID = "desktop-mode-pwa-install";
14825 function isStandaloneDisplay() {
14826 if (typeof window === "undefined") {
14827 return false;
14828 }
14829 if (window.matchMedia?.("(display-mode: standalone)").matches) {
14830 return true;
14831 }
14832 const nav = window.navigator;
14833 return nav.standalone === true;
14834 }
14835 async function isLikelyInstalled() {
14836 if (isStandaloneDisplay()) {
14837 return true;
14838 }
14839 const nav = window.navigator;
14840 if (typeof nav.getInstalledRelatedApps !== "function") {
14841 return false;
14842 }
14843 try {
14844 const apps = await nav.getInstalledRelatedApps();
14845 return Array.isArray(apps) && apps.length > 0;
14846 } catch {
14847 return false;
14848 }
14849 }
14850 let _deferred = null;
14851 function installPwaInstallAffordance(siteName, showToast2) {
14852 if (typeof window === "undefined") {
14853 return;
14854 }
14855 window.removeEventListener(
14856 "beforeinstallprompt",
14857 _handleBeforeInstall
14858 );
14859 window.addEventListener(
14860 "beforeinstallprompt",
14861 _handleBeforeInstall
14862 );
14863 window.removeEventListener("appinstalled", _handleAppInstalled);
14864 window.addEventListener("appinstalled", _handleAppInstalled);
14865 function _handleBeforeInstall(ev) {
14866 ev.preventDefault();
14867 _deferred = ev;
14868 }
14869 function _handleAppInstalled() {
14870 _deferred = null;
14871 showToast2({
14872 message: sprintf(
14873 /* translators: %s: site name */
14874 __("Installed %s as an app."),
14875 siteName
14876 )
14877 });
14878 }
14879 }
14880 function getInstallTileDef(siteName, showToast2) {
14881 return {
14882 id: PWA_INSTALL_TILE_ID,
14883 title: sprintf(
14884 /* translators: %s: site name */
14885 __("Install %s as an app"),
14886 siteName
14887 ),
14888 // Dashicons class — the dock renderer prefers Dashicons
14889 // strings. `dashicons-download` is the closest match for
14890 // "install" in the WordPress glyph set without shipping
14891 // bespoke artwork.
14892 icon: "dashicons-download",
14893 onOpen: () => {
14894 void onTileClick(siteName, showToast2);
14895 }
14896 };
14897 }
14898 async function onTileClick(siteName, showToast2) {
14899 if (_deferred) {
14900 const event = _deferred;
14901 _deferred = null;
14902 try {
14903 await event.prompt();
14904 const choice = await event.userChoice;
14905 if (choice.outcome === "dismissed") {
14906 showToast2({
14907 message: __("Install cancelled.")
14908 });
14909 }
14910 } catch (err) {
14911 if (typeof console !== "undefined") {
14912 console.warn(
14913 "[desktop-mode] install prompt failed:",
14914 err
14915 );
14916 }
14917 }
14918 return;
14919 }
14920 if (await isLikelyInstalled()) {
14921 showToast2({
14922 message: sprintf(
14923 /* translators: %s: site name */
14924 __(
14925 "%s is already installed. Open it from your apps menu or home screen."
14926 ),
14927 siteName
14928 )
14929 });
14930 return;
14931 }
14932 if (getSwRegistrationStatus() === "foreign-sw") {
14933 showToast2({
14934 message: __(
14935 "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."
14936 )
14937 });
14938 return;
14939 }
14940 showToast2({
14941 message: __(
14942 "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."
14943 )
14944 });
14945 }
14946 async function promptInstall() {
14947 if (!_deferred) {
14948 return "unavailable";
14949 }
14950 const event = _deferred;
14951 _deferred = null;
14952 try {
14953 await event.prompt();
14954 const choice = await event.userChoice;
14955 return choice.outcome;
14956 } catch {
14957 return "unavailable";
14958 }
14959 }
14960 function undismissInstallHint() {
14961 Promise.resolve().then(() => state).then((m) => {
14962 m.updatePwaState({ installHintDismissed: false });
14963 });
14964 }
14965 function notify$3(options) {
14966 const intent = activity.filter(
14967 "desktop-mode/notification-requested",
14968 { ...options }
14969 );
14970 if (!intent || intent.cancel === true || !intent.title) {
14971 return () => void 0;
14972 }
14973 let dismissed = false;
14974 let dismissNative = null;
14975 let dismissToast = null;
14976 const dismiss = () => {
14977 if (dismissed) {
14978 return;
14979 }
14980 dismissed = true;
14981 if (dismissNative) {
14982 dismissNative();
14983 }
14984 if (dismissToast) {
14985 dismissToast();
14986 }
14987 };
14988 const fallback = () => {
14989 dismissToast = showToast({
14990 message: intent.body ? intent.title + " — " + intent.body : intent.title
14991 });
14992 activity.publish("desktop-mode/notification-shown", {
14993 ...intent,
14994 fallback: "toast"
14995 });
14996 };
14997 if (typeof window === "undefined" || typeof Notification === "undefined") {
14998 fallback();
14999 return dismiss;
15000 }
15001 const perm = Notification.permission;
15002 if (perm === "granted") {
15003 dismissNative = renderNative(intent);
15004 return dismiss;
15005 }
15006 if (perm === "denied") {
15007 fallback();
15008 return dismiss;
15009 }
15010 void Notification.requestPermission().then((result) => {
15011 if (dismissed) {
15012 return;
15013 }
15014 if (result === "granted") {
15015 updatePwaState({ notificationsEnabled: true });
15016 dismissNative = renderNative(intent);
15017 return;
15018 }
15019 fallback();
15020 });
15021 return dismiss;
15022 }
15023 function renderNative(intent) {
15024 let n = null;
15025 try {
15026 n = new Notification(intent.title, {
15027 body: intent.body,
15028 icon: intent.icon,
15029 tag: intent.tag,
15030 requireInteraction: intent.requireInteraction
15031 });
15032 } catch (err) {
15033 if (typeof console !== "undefined") {
15034 console.warn("[desktop-mode] Notification ctor threw:", err);
15035 }
15036 return () => void 0;
15037 }
15038 if (intent.onClick) {
15039 const handler = intent.onClick;
15040 n.onclick = () => {
15041 try {
15042 handler(n);
15043 } catch (hErr) {
15044 if (typeof console !== "undefined") {
15045 console.error(
15046 "[desktop-mode] notification onClick threw:",
15047 hErr
15048 );
15049 }
15050 }
15051 };
15052 }
15053 activity.publish("desktop-mode/notification-shown", {
15054 ...intent,
15055 fallback: null
15056 });
15057 return () => {
15058 if (n) {
15059 n.close();
15060 }
15061 };
15062 }
15063 async function requestNotificationPermission() {
15064 if (typeof Notification === "undefined") {
15065 return "unsupported";
15066 }
15067 if (Notification.permission !== "default") {
15068 return Notification.permission;
15069 }
15070 const result = await Notification.requestPermission();
15071 if (result === "granted") {
15072 updatePwaState({ notificationsEnabled: true });
15073 }
15074 return result;
15075 }
15076 function getNotificationPermission() {
15077 if (typeof Notification === "undefined") {
15078 return "unsupported";
15079 }
15080 return Notification.permission;
15081 }
15082 function bootstrapPwa(config, showToast2) {
15083 if (!config.pwa) {
15084 return;
15085 }
15086 initPwaState(config.pwa);
15087 installPwaInstallAffordance(
15088 config.pwa.appName || "WordPress",
15089 showToast2
15090 );
15091 void registerServiceWorker(config.pwa, {
15092 forceReplace: !!config.pwa.forceReplaceSw
15093 });
15094 }
15095 const DRAG_BRIDGE_EVENTS = {
15096 START: "desktop-mode-cross-frame-drag-start",
15097 END: "desktop-mode-cross-frame-drag-end"
15098 };
15099 function isStart(m) {
15100 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-start" && !!m.payload && typeof m.payload === "object";
15101 }
15102 function isEnd(m) {
15103 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-end";
15104 }
15105 function isPayloadRequest(m) {
15106 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-payload-request";
15107 }
15108 function normalizeLegacyPayload(payload) {
15109 const obj = payload;
15110 if (obj.kind !== void 0 && obj.kind !== null) {
15111 return payload;
15112 }
15113 if (typeof obj.id === "number" && typeof obj.url === "string" && typeof obj.mime === "string") {
15114 return {
15115 kind: "attachment",
15116 id: obj.id,
15117 url: obj.url,
15118 title: typeof obj.title === "string" ? obj.title : "",
15119 alt: typeof obj.alt === "string" ? obj.alt : "",
15120 mime: obj.mime,
15121 thumbnailUrl: typeof obj.thumbnailUrl === "string" ? obj.thumbnailUrl : void 0,
15122 sizes: obj.sizes && typeof obj.sizes === "object" ? obj.sizes : void 0
15123 };
15124 }
15125 return payload;
15126 }
15127 class DragBridge {
15128 constructor() {
15129 this._payload = null;
15130 this._onMessage = (e) => {
15131 if (e.origin !== this._origin) {
15132 return;
15133 }
15134 const msg = e.data;
15135 if (isStart(msg)) {
15136 this._startDrag(msg.payload);
15137 return;
15138 }
15139 if (isEnd(msg)) {
15140 this._endDrag();
15141 return;
15142 }
15143 if (isPayloadRequest(msg) && this._payload && e.source) {
15144 try {
15145 e.source.postMessage(
15146 { type: "desktop-mode-drag-payload", payload: this._payload },
15147 this._origin
15148 );
15149 } catch {
15150 }
15151 }
15152 };
15153 this._origin = window.location.origin;
15154 window.addEventListener("message", this._onMessage);
15155 }
15156 getPayload() {
15157 return this._payload;
15158 }
15159 isDragging() {
15160 return this._payload !== null;
15161 }
15162 start(payload) {
15163 if (this._payload === payload) {
15164 return;
15165 }
15166 this._startDrag(payload);
15167 }
15168 end() {
15169 this._endDrag();
15170 }
15171 _startDrag(payload) {
15172 const normalized = normalizeLegacyPayload(payload);
15173 this._payload = normalized;
15174 document.dispatchEvent(
15175 new CustomEvent(DRAG_BRIDGE_EVENTS.START, {
15176 detail: { payload: normalized }
15177 })
15178 );
15179 }
15180 _endDrag() {
15181 if (this._payload === null) {
15182 return;
15183 }
15184 const payload = this._payload;
15185 this._payload = null;
15186 document.dispatchEvent(
15187 new CustomEvent(DRAG_BRIDGE_EVENTS.END, { detail: { payload } })
15188 );
15189 }
15190 }
15191 class DropTargetRegistry {
15192 constructor() {
15193 this._targets = /* @__PURE__ */ new Map();
15194 this._byElement = /* @__PURE__ */ new Map();
15195 }
15196 register(target2) {
15197 const prev = this._targets.get(target2.id);
15198 if (prev) {
15199 this._byElement.delete(prev.element);
15200 }
15201 this._targets.set(target2.id, target2);
15202 this._byElement.set(target2.element, target2);
15203 return () => {
15204 const cur = this._targets.get(target2.id);
15205 if (cur === target2) {
15206 this._targets.delete(target2.id);
15207 this._byElement.delete(target2.element);
15208 }
15209 };
15210 }
15211 list() {
15212 return Array.from(this._targets.values());
15213 }
15214 clear() {
15215 this._targets.clear();
15216 this._byElement.clear();
15217 }
15218 /**
15219 * Find the deepest registered target whose element is `el` or an
15220 * ancestor of `el`. Walks the DOM tree once (O(depth)).
15221 *
15222 * Window claim boundary: if the walk crosses a `.desktop-mode-window`
15223 * element BEFORE finding a registered target, hit-testing stops
15224 * there and returns null. This is the rule that makes "drag over
15225 * a Gutenberg admin window" produce reject feedback instead of
15226 * silently routing the drop to the wallpaper canvas underneath.
15227 *
15228 * A window can opt INTO accepting drops by registering a target
15229 * on its own body (e.g. Recycle Bin's `[data-desktop-mode-recycle-bin-root]`):
15230 * since that element sits inside the window, the walk hits it
15231 * before reaching the window boundary and the body's target wins.
15232 */
15233 hitTest(el) {
15234 let cur = el;
15235 while (cur) {
15236 if (cur instanceof HTMLElement) {
15237 const t = this._byElement.get(cur);
15238 if (t) {
15239 return t;
15240 }
15241 if (cur.classList.contains("desktop-mode-window")) {
15242 return null;
15243 }
15244 }
15245 cur = cur.parentElement;
15246 }
15247 return null;
15248 }
15249 /**
15250 * Convenience: pick the target at viewport `(clientX, clientY)`.
15251 * Caller is responsible for hiding any obscuring ghost element
15252 * before calling — see `GhostHandle.withHidden()`.
15253 */
15254 hitTestPoint(clientX, clientY) {
15255 const el = document.elementFromPoint(clientX, clientY);
15256 const target2 = this.hitTest(el);
15257 return { target: target2, element: el, accepted: false };
15258 }
15259 }
15260 const GHOST_CLASS = "desktop-mode-drag-ghost";
15261 const GHOST_ACCEPT_CLASS = "desktop-mode-drag-ghost--accept";
15262 const GHOST_REJECT_CLASS = "desktop-mode-drag-ghost--reject";
15263 const HINT_CLASS = "desktop-mode-drag-hint";
15264 const HINT_ACCEPT_CLASS = "desktop-mode-drag-hint--accept";
15265 const HINT_REJECT_CLASS = "desktop-mode-drag-hint--reject";
15266 const HINT_NEUTRAL_CLASS = "desktop-mode-drag-hint--neutral";
15267 const HINT_OFFSET_X = 16;
15268 const HINT_OFFSET_Y = 18;
15269 function mountGhost(payload, clientX, clientY) {
15270 const ghost = buildGhost(payload);
15271 const offsetX = payload.ghost?.offsetX ?? defaultOffsetX(payload.source);
15272 const offsetY = payload.ghost?.offsetY ?? defaultOffsetY(payload.source);
15273 ghost.classList.add(GHOST_CLASS);
15274 ghost.setAttribute("aria-hidden", "true");
15275 ghost.style.position = "fixed";
15276 ghost.style.left = "0";
15277 ghost.style.top = "0";
15278 ghost.style.margin = "0";
15279 ghost.style.pointerEvents = "none";
15280 ghost.style.zIndex = "2147483647";
15281 ghost.style.willChange = "transform";
15282 document.body.appendChild(ghost);
15283 const labels = resolveHintLabels(payload);
15284 const hint = labels ? buildHintChip() : null;
15285 if (hint) {
15286 document.body.appendChild(hint);
15287 }
15288 const handle = {
15289 get element() {
15290 return ghost;
15291 },
15292 moveTo(cx, cy) {
15293 ghost.style.transform = `translate3d(${cx - offsetX}px, ${cy - offsetY}px, 0)`;
15294 if (hint) {
15295 hint.style.transform = `translate3d(${cx + HINT_OFFSET_X}px, ${cy + HINT_OFFSET_Y}px, 0)`;
15296 }
15297 },
15298 setMode(mode) {
15299 ghost.classList.remove(GHOST_ACCEPT_CLASS, GHOST_REJECT_CLASS);
15300 if (mode === "accept") {
15301 ghost.classList.add(GHOST_ACCEPT_CLASS);
15302 } else if (mode === "reject") {
15303 ghost.classList.add(GHOST_REJECT_CLASS);
15304 }
15305 if (hint && labels) {
15306 hint.classList.remove(
15307 HINT_ACCEPT_CLASS,
15308 HINT_REJECT_CLASS,
15309 HINT_NEUTRAL_CLASS
15310 );
15311 if (mode === "accept") {
15312 hint.classList.add(HINT_ACCEPT_CLASS);
15313 hint.textContent = labels.accept;
15314 } else if (mode === "reject") {
15315 hint.classList.add(HINT_REJECT_CLASS);
15316 hint.textContent = labels.reject;
15317 } else {
15318 hint.classList.add(HINT_NEUTRAL_CLASS);
15319 hint.textContent = labels.neutral;
15320 }
15321 hint.hidden = !hint.textContent;
15322 }
15323 },
15324 withHidden(fn) {
15325 const prevG = ghost.style.visibility;
15326 const prevH = hint?.style.visibility ?? "";
15327 ghost.style.visibility = "hidden";
15328 if (hint) {
15329 hint.style.visibility = "hidden";
15330 }
15331 try {
15332 return fn();
15333 } finally {
15334 ghost.style.visibility = prevG;
15335 if (hint) {
15336 hint.style.visibility = prevH;
15337 }
15338 }
15339 },
15340 dispose() {
15341 if (ghost.isConnected) {
15342 ghost.remove();
15343 }
15344 if (hint?.isConnected) {
15345 hint.remove();
15346 }
15347 }
15348 };
15349 handle.moveTo(clientX, clientY);
15350 handle.setMode("neutral");
15351 return handle;
15352 }
15353 function buildHintChip() {
15354 const chip = document.createElement("div");
15355 chip.className = HINT_CLASS;
15356 chip.setAttribute("aria-hidden", "true");
15357 chip.setAttribute("role", "presentation");
15358 chip.style.position = "fixed";
15359 chip.style.left = "0";
15360 chip.style.top = "0";
15361 chip.style.margin = "0";
15362 chip.style.pointerEvents = "none";
15363 chip.style.zIndex = "2147483647";
15364 chip.style.willChange = "transform";
15365 return chip;
15366 }
15367 function resolveHintLabels(payload) {
15368 const cfg = payload.ghost?.hint;
15369 if (cfg?.hidden) {
15370 return null;
15371 }
15372 return {
15373 accept: cfg?.accept ?? defaultAcceptLabel(payload),
15374 reject: cfg?.reject ?? defaultRejectLabel(),
15375 neutral: cfg?.neutral ?? defaultNeutralLabel(payload)
15376 };
15377 }
15378 function defaultAcceptLabel(payload) {
15379 if (payload.type === "shortcut") {
15380 return __("Drop here to create shortcut", "desktop-mode");
15381 }
15382 if (payload.type === "desktop-file") {
15383 return __("Drop here to move", "desktop-mode");
15384 }
15385 return __("Drop here", "desktop-mode");
15386 }
15387 function defaultRejectLabel(_payload) {
15388 return __("Can’t drop here", "desktop-mode");
15389 }
15390 function defaultNeutralLabel(payload) {
15391 if (payload.type === "shortcut") {
15392 return __(
15393 "Drop on the desktop or a folder",
15394 "desktop-mode"
15395 );
15396 }
15397 if (payload.type === "desktop-file") {
15398 return __("Drop in a folder", "desktop-mode");
15399 }
15400 return "";
15401 }
15402 function buildGhost(payload) {
15403 if (payload.ghost?.element) {
15404 return payload.ghost.element;
15405 }
15406 const clone = payload.source.cloneNode(true);
15407 clone.removeAttribute("id");
15408 const rect = payload.source.getBoundingClientRect();
15409 clone.style.width = `${rect.width}px`;
15410 clone.style.height = `${rect.height}px`;
15411 return clone;
15412 }
15413 function defaultOffsetX(source) {
15414 return source.offsetWidth / 2;
15415 }
15416 function defaultOffsetY(source) {
15417 return source.offsetHeight / 2;
15418 }
15419 let _installed$2 = false;
15420 function installRecovery(cancelActive) {
15421 if (_installed$2) {
15422 return;
15423 }
15424 _installed$2 = true;
15425 document.addEventListener("keydown", (e) => {
15426 if (e.key === "Escape") {
15427 cancelActive("escape");
15428 }
15429 });
15430 window.addEventListener("blur", () => {
15431 cancelActive("blur");
15432 });
15433 document.addEventListener("visibilitychange", () => {
15434 if (document.hidden) {
15435 cancelActive("visibility");
15436 }
15437 });
15438 }
15439 const DRAG_THRESHOLD_PX = 4;
15440 const DRAG_EVENTS = {
15441 START: "desktop-mode.drag.start",
15442 MOVE: "desktop-mode.drag.move",
15443 ENTER: "desktop-mode.drag.enter",
15444 LEAVE: "desktop-mode.drag.leave",
15445 REJECTED: "desktop-mode.drag.rejected",
15446 COMMIT: "desktop-mode.drag.commit",
15447 CANCEL: "desktop-mode.drag.cancel",
15448 END: "desktop-mode.drag.end"
15449 };
15450 const SOURCE_DRAGGING_CLASS = "desktop-mode-file-tile--dragging";
15451 const TARGET_DROP_ACTIVE_CLASS = "desktop-mode-file-tile--drop-target";
15452 const TRASH_DROP_ACTIVE_ATTR$1 = "data-desktop-mode-trash-drop-active";
15453 const FILES_DROP_ACTIVE_ATTR = "data-files-drop-active";
15454 const BODY_DRAGGING_ATTR = "data-desktop-mode-dragging";
15455 const BODY_DRAG_TYPE_ATTR = "data-desktop-mode-drag-type";
15456 const BODY_DRAG_MODE_ATTR = "data-desktop-mode-drag-mode";
15457 class DragManager {
15458 constructor() {
15459 this._registry = new DropTargetRegistry();
15460 this._active = null;
15461 this._docListenersAttached = false;
15462 this._lastLiftedEndAt = 0;
15463 this._onPointerMove = (e) => {
15464 const session = this._active;
15465 if (!session || session._pointerId !== e.pointerId) {
15466 return;
15467 }
15468 const dx = e.clientX - session._origin.clientX;
15469 const dy = e.clientY - session._origin.clientY;
15470 if (!session._lifted) {
15471 if (Math.abs(dx) < DRAG_THRESHOLD_PX && Math.abs(dy) < DRAG_THRESHOLD_PX) {
15472 return;
15473 }
15474 this._lift(session, e);
15475 }
15476 if (!session._ghost) {
15477 return;
15478 }
15479 session._ghost.moveTo(e.clientX, e.clientY);
15480 this._updateHover(session, e.clientX, e.clientY);
15481 dispatchOnDocument(DRAG_EVENTS.MOVE, {
15482 payload: session.payload,
15483 clientX: e.clientX,
15484 clientY: e.clientY
15485 });
15486 };
15487 this._onPointerUp = (e) => {
15488 const session = this._active;
15489 if (!session || session._pointerId !== e.pointerId) {
15490 return;
15491 }
15492 if (!session._lifted) {
15493 session._finished = true;
15494 this._active = null;
15495 try {
15496 session._callbacks.onClickOnly?.();
15497 } catch (err) {
15498 console.error("[desktop-mode] drag onClickOnly threw:", err);
15499 }
15500 return;
15501 }
15502 const hit = this._hitTestNow(session, e.clientX, e.clientY);
15503 if (hit && hit.accepted && hit.target) {
15504 this._commit(session, hit.target, e.clientX, e.clientY);
15505 return;
15506 }
15507 this._cancel(session, hit && hit.target ? "rejected" : "no-target");
15508 };
15509 this._onPointerCancel = (e) => {
15510 const session = this._active;
15511 if (!session || session._pointerId !== e.pointerId) {
15512 return;
15513 }
15514 this._cancel(session, "pointercancel");
15515 };
15516 }
15517 start(opts) {
15518 if (this._active) {
15519 return null;
15520 }
15521 if (opts.origin.button !== 0) {
15522 return null;
15523 }
15524 const session = {
15525 payload: opts.payload,
15526 isFinished: () => session._finished,
15527 cancel: (reason) => this._cancel(session, reason ?? "caller"),
15528 _origin: opts.origin,
15529 _pointerId: opts.origin.pointerId,
15530 _lifted: false,
15531 _finished: false,
15532 _callbacks: {
15533 onClickOnly: opts.onClickOnly,
15534 onCancel: opts.onCancel,
15535 onCommit: opts.onCommit
15536 },
15537 _ghost: null,
15538 _currentTarget: null,
15539 _currentAccepted: false
15540 };
15541 this._active = session;
15542 this._ensureDocListeners();
15543 installRecovery((reason) => {
15544 if (this._active) {
15545 this._cancel(this._active, reason);
15546 }
15547 });
15548 return session;
15549 }
15550 registerDropTarget(target2) {
15551 return this._registry.register(target2);
15552 }
15553 isDragging() {
15554 return this._active !== null && this._active._lifted;
15555 }
15556 /**
15557 * Whether a real (lifted) drag ended within `withinMs` of now.
15558 * Surfaces that bind plain `click` listeners use this to ignore
15559 * the synthesized click that fires after a drop. 500 ms is a
15560 * generous default — browsers fire the click within 10–50 ms of
15561 * pointerup, but plugins may chain post-drag work into a
15562 * `requestAnimationFrame` and call back into a click-driven API.
15563 *
15564 * @public
15565 * @since 0.18.x
15566 */
15567 recentlyEndedDrag(withinMs = 500) {
15568 if (this._lastLiftedEndAt === 0) {
15569 return false;
15570 }
15571 return Date.now() - this._lastLiftedEndAt < withinMs;
15572 }
15573 getActive() {
15574 return this._active;
15575 }
15576 debug() {
15577 return {
15578 findOrphans: () => findOrphans(),
15579 listTargets: () => this._registry.list()
15580 };
15581 }
15582 // -----------------------------------------------------------------
15583 // Internals
15584 // -----------------------------------------------------------------
15585 _ensureDocListeners() {
15586 if (this._docListenersAttached) {
15587 return;
15588 }
15589 this._docListenersAttached = true;
15590 document.addEventListener("pointermove", this._onPointerMove, true);
15591 document.addEventListener("pointerup", this._onPointerUp, true);
15592 document.addEventListener("pointercancel", this._onPointerCancel, true);
15593 }
15594 _lift(session, e) {
15595 session._lifted = true;
15596 session.payload.source.classList.add(SOURCE_DRAGGING_CLASS);
15597 session._ghost = mountGhost(session.payload, e.clientX, e.clientY);
15598 if (typeof document !== "undefined" && document.body) {
15599 document.body.setAttribute(BODY_DRAGGING_ATTR, "");
15600 document.body.setAttribute(
15601 BODY_DRAG_TYPE_ATTR,
15602 String(session.payload.type)
15603 );
15604 document.body.setAttribute(BODY_DRAG_MODE_ATTR, "neutral");
15605 }
15606 dispatchOnDocument(DRAG_EVENTS.START, { payload: session.payload });
15607 }
15608 _hitTestNow(session, clientX, clientY) {
15609 const run = () => {
15610 const el = document.elementFromPoint(clientX, clientY);
15611 const target2 = this._registry.hitTest(el);
15612 if (!target2) {
15613 return { target: null, accepted: false };
15614 }
15615 let accepted = false;
15616 try {
15617 accepted = target2.accept(session.payload);
15618 } catch (err) {
15619 console.error("[desktop-mode] drop target accept() threw:", target2.id, err);
15620 }
15621 return { target: target2, accepted };
15622 };
15623 if (session._ghost) {
15624 return session._ghost.withHidden(run);
15625 }
15626 return run();
15627 }
15628 _updateHover(session, clientX, clientY) {
15629 const next = this._hitTestNow(session, clientX, clientY);
15630 const prevTarget = session._currentTarget;
15631 if (next.target === prevTarget && next.accepted === session._currentAccepted) {
15632 return;
15633 }
15634 if (prevTarget) {
15635 fireLeave(prevTarget, session);
15636 }
15637 session._currentTarget = next.target;
15638 session._currentAccepted = next.accepted;
15639 let mode;
15640 if (next.target) {
15641 if (next.accepted) {
15642 fireEnter(next.target, session);
15643 session._ghost?.setMode("accept");
15644 mode = "accept";
15645 } else {
15646 session._ghost?.setMode("reject");
15647 dispatchOnDocument(DRAG_EVENTS.REJECTED, {
15648 payload: session.payload,
15649 targetId: next.target.id
15650 });
15651 mode = "reject";
15652 }
15653 } else {
15654 session._ghost?.setMode("reject");
15655 mode = "reject";
15656 }
15657 if (typeof document !== "undefined" && document.body) {
15658 document.body.setAttribute(BODY_DRAG_MODE_ATTR, mode);
15659 }
15660 }
15661 _commit(session, target2, clientX, clientY) {
15662 session._finished = true;
15663 this._lastLiftedEndAt = Date.now();
15664 fireLeave(target2, session);
15665 this._cleanupDom(session);
15666 const prevActive = this._active;
15667 this._active = null;
15668 try {
15669 void target2.onDrop(session, { clientX, clientY });
15670 } catch (err) {
15671 console.error("[desktop-mode] drop target onDrop threw:", target2.id, err);
15672 }
15673 try {
15674 session._callbacks.onCommit?.(target2);
15675 } catch (err) {
15676 console.error("[desktop-mode] drag onCommit threw:", err);
15677 }
15678 dispatchOnDocument(DRAG_EVENTS.COMMIT, {
15679 payload: session.payload,
15680 targetId: target2.id
15681 });
15682 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason: "commit" });
15683 if (this._active === prevActive) {
15684 this._active = null;
15685 }
15686 }
15687 _cancel(session, reason) {
15688 if (session._finished) {
15689 return;
15690 }
15691 session._finished = true;
15692 if (session._lifted) {
15693 this._lastLiftedEndAt = Date.now();
15694 }
15695 if (session._currentTarget) {
15696 fireLeave(session._currentTarget, session);
15697 }
15698 this._cleanupDom(session);
15699 this._active = null;
15700 try {
15701 session._callbacks.onCancel?.(reason);
15702 } catch (err) {
15703 console.error("[desktop-mode] drag onCancel threw:", err);
15704 }
15705 dispatchOnDocument(DRAG_EVENTS.CANCEL, { payload: session.payload, reason });
15706 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason });
15707 }
15708 _cleanupDom(session) {
15709 try {
15710 session.payload.source.classList.remove(SOURCE_DRAGGING_CLASS);
15711 } catch {
15712 }
15713 session._ghost?.dispose();
15714 session._ghost = null;
15715 session._currentTarget = null;
15716 session._currentAccepted = false;
15717 if (typeof document !== "undefined" && document.body) {
15718 document.body.removeAttribute(BODY_DRAGGING_ATTR);
15719 document.body.removeAttribute(BODY_DRAG_TYPE_ATTR);
15720 document.body.removeAttribute(BODY_DRAG_MODE_ATTR);
15721 }
15722 scrubOrphans();
15723 }
15724 }
15725 function dispatchOnDocument(type, detail) {
15726 if (typeof document === "undefined") {
15727 return;
15728 }
15729 document.dispatchEvent(new CustomEvent(type, { detail }));
15730 }
15731 function fireEnter(target2, session) {
15732 try {
15733 target2.onEnter?.(session);
15734 } catch (err) {
15735 console.error("[desktop-mode] drop target onEnter threw:", target2.id, err);
15736 }
15737 dispatchOnDocument(DRAG_EVENTS.ENTER, {
15738 payload: session.payload,
15739 targetId: target2.id
15740 });
15741 }
15742 function fireLeave(target2, session) {
15743 try {
15744 target2.onLeave?.(session);
15745 } catch (err) {
15746 console.error("[desktop-mode] drop target onLeave threw:", target2.id, err);
15747 }
15748 dispatchOnDocument(DRAG_EVENTS.LEAVE, {
15749 payload: session.payload,
15750 targetId: target2.id
15751 });
15752 }
15753 function findOrphans() {
15754 if (typeof document === "undefined") {
15755 return [];
15756 }
15757 const out = [];
15758 for (const sel of [
15759 `.${SOURCE_DRAGGING_CLASS}`,
15760 `.${TARGET_DROP_ACTIVE_CLASS}`,
15761 `[${TRASH_DROP_ACTIVE_ATTR$1}]`,
15762 `[${FILES_DROP_ACTIVE_ATTR}]`
15763 ]) {
15764 document.querySelectorAll(sel).forEach((el) => out.push(el));
15765 }
15766 return out;
15767 }
15768 function scrubOrphans() {
15769 for (const el of findOrphans()) {
15770 el.classList.remove(SOURCE_DRAGGING_CLASS, TARGET_DROP_ACTIVE_CLASS);
15771 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR$1);
15772 el.removeAttribute(FILES_DROP_ACTIVE_ATTR);
15773 }
15774 }
15775 const TARGET_ID_PREFIX = "desktop-mode-iframe-drop-";
15776 const IFRAME_SELECTOR = "iframe.desktop-mode-window__iframe";
15777 const DROP_ACTIVE_ATTR = "data-desktop-mode-iframe-drop-active";
15778 let _installed$1 = false;
15779 let _dragManager = null;
15780 const _suppressedIframes = /* @__PURE__ */ new Map();
15781 const _activeRegistrations = /* @__PURE__ */ new Map();
15782 let _bridgeInterceptPayload = null;
15783 let _lastHoveredBridgeIframe = null;
15784 function suppressIframePointerEventsBridge() {
15785 const iframes = document.querySelectorAll(
15786 IFRAME_SELECTOR
15787 );
15788 iframes.forEach((iframe) => {
15789 if (_suppressedIframes.has(iframe)) {
15790 return;
15791 }
15792 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
15793 iframe.style.pointerEvents = "none";
15794 });
15795 }
15796 function restoreIframePointerEvents() {
15797 _suppressedIframes.forEach((prev, iframe) => {
15798 iframe.style.pointerEvents = prev;
15799 });
15800 _suppressedIframes.clear();
15801 }
15802 function findIframeAtCursor(clientX, clientY) {
15803 const el = document.elementFromPoint(clientX, clientY);
15804 if (!el) {
15805 return null;
15806 }
15807 const win = el.closest(".desktop-mode-window");
15808 if (!(win instanceof HTMLElement)) {
15809 return null;
15810 }
15811 const iframe = win.querySelector(IFRAME_SELECTOR);
15812 return iframe instanceof HTMLIFrameElement ? iframe : null;
15813 }
15814 const onBridgeDragOver = (e) => {
15815 if (!_bridgeInterceptPayload) {
15816 return;
15817 }
15818 e.preventDefault();
15819 if (e.dataTransfer) {
15820 e.dataTransfer.dropEffect = "copy";
15821 }
15822 const iframe = findIframeAtCursor(e.clientX, e.clientY);
15823 if (iframe === _lastHoveredBridgeIframe) {
15824 return;
15825 }
15826 if (_lastHoveredBridgeIframe) {
15827 postIntoIframe(_lastHoveredBridgeIframe, {
15828 type: "desktop-mode-drag-leave"
15829 });
15830 }
15831 _lastHoveredBridgeIframe = iframe;
15832 if (iframe) {
15833 postIntoIframe(iframe, {
15834 type: "desktop-mode-drag-over",
15835 payload: _bridgeInterceptPayload
15836 });
15837 }
15838 };
15839 const onBridgeDrop = (e) => {
15840 if (!_bridgeInterceptPayload) {
15841 return;
15842 }
15843 e.preventDefault();
15844 e.stopPropagation();
15845 if (typeof e.stopImmediatePropagation === "function") {
15846 e.stopImmediatePropagation();
15847 }
15848 const iframe = findIframeAtCursor(e.clientX, e.clientY);
15849 const payload = _bridgeInterceptPayload;
15850 stopBridgeIntercept();
15851 if (!iframe) {
15852 return;
15853 }
15854 const rect = iframe.getBoundingClientRect();
15855 postIntoIframe(iframe, {
15856 type: "desktop-mode-drop",
15857 payload,
15858 position: {
15859 x: e.clientX - rect.left,
15860 y: e.clientY - rect.top
15861 }
15862 });
15863 };
15864 const onBridgeDragEnd = () => {
15865 stopBridgeIntercept();
15866 };
15867 function startBridgeIntercept(payload) {
15868 if (_bridgeInterceptPayload) {
15869 _bridgeInterceptPayload = payload;
15870 return;
15871 }
15872 _bridgeInterceptPayload = payload;
15873 suppressIframePointerEventsBridge();
15874 document.addEventListener("dragover", onBridgeDragOver, true);
15875 document.addEventListener("drop", onBridgeDrop, true);
15876 document.addEventListener("dragend", onBridgeDragEnd, true);
15877 }
15878 function stopBridgeIntercept() {
15879 if (!_bridgeInterceptPayload) {
15880 return;
15881 }
15882 _bridgeInterceptPayload = null;
15883 if (_lastHoveredBridgeIframe) {
15884 postIntoIframe(_lastHoveredBridgeIframe, {
15885 type: "desktop-mode-drag-leave"
15886 });
15887 _lastHoveredBridgeIframe = null;
15888 }
15889 document.removeEventListener("dragover", onBridgeDragOver, true);
15890 document.removeEventListener("drop", onBridgeDrop, true);
15891 document.removeEventListener("dragend", onBridgeDragEnd, true);
15892 restoreIframePointerEvents();
15893 }
15894 function extractBridgePayload(payload) {
15895 if (!payload || typeof payload !== "object") {
15896 return void 0;
15897 }
15898 const obj = payload;
15899 if (obj.type !== "shortcut" && obj.type !== "desktop-file") {
15900 return void 0;
15901 }
15902 const data = obj.data;
15903 return data?.bridgePayload;
15904 }
15905 function postIntoIframe(iframe, msg) {
15906 const w = iframe.contentWindow;
15907 if (!w) {
15908 return;
15909 }
15910 try {
15911 w.postMessage(msg, window.location.origin);
15912 } catch {
15913 }
15914 }
15915 function registerDropTargetFor(dragManager, iframe, target2, windowId) {
15916 return dragManager.registerDropTarget({
15917 id: `${TARGET_ID_PREFIX}${windowId}`,
15918 element: target2,
15919 accept: (payload) => !!extractBridgePayload(payload),
15920 onEnter: (session) => {
15921 const bridge = extractBridgePayload(session.payload);
15922 if (!bridge) {
15923 return;
15924 }
15925 target2.setAttribute(DROP_ACTIVE_ATTR, "");
15926 postIntoIframe(iframe, {
15927 type: "desktop-mode-drag-over",
15928 payload: bridge
15929 });
15930 },
15931 onLeave: () => {
15932 target2.removeAttribute(DROP_ACTIVE_ATTR);
15933 postIntoIframe(iframe, { type: "desktop-mode-drag-leave" });
15934 },
15935 onDrop: (session, ev) => {
15936 target2.removeAttribute(DROP_ACTIVE_ATTR);
15937 const bridge = extractBridgePayload(session.payload);
15938 if (!bridge) {
15939 return;
15940 }
15941 const rect = iframe.getBoundingClientRect();
15942 postIntoIframe(iframe, {
15943 type: "desktop-mode-drop",
15944 payload: bridge,
15945 position: {
15946 x: ev.clientX - rect.left,
15947 y: ev.clientY - rect.top
15948 }
15949 });
15950 }
15951 });
15952 }
15953 function deriveWindowIdFromIframe(iframe) {
15954 let cur = iframe.parentElement;
15955 while (cur) {
15956 if (cur.id.startsWith("wp-window-")) {
15957 return cur.id.slice("wp-window-".length);
15958 }
15959 cur = cur.parentElement;
15960 }
15961 return `unknown-${Math.random().toString(36).slice(2, 10)}`;
15962 }
15963 function onDragStart(payload) {
15964 const dragManager = _dragManager;
15965 if (!dragManager) {
15966 return;
15967 }
15968 const iframes = document.querySelectorAll(IFRAME_SELECTOR);
15969 const isBridgeable = !!extractBridgePayload(payload);
15970 console.info(
15971 "[desktop-mode] drag-start: suppressing %d iframe(s); bridgeable=%s",
15972 iframes.length,
15973 isBridgeable,
15974 payload
15975 );
15976 iframes.forEach((iframe) => {
15977 if (!_suppressedIframes.has(iframe)) {
15978 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
15979 iframe.style.pointerEvents = "none";
15980 }
15981 if (!isBridgeable) {
15982 return;
15983 }
15984 if (_activeRegistrations.has(iframe)) {
15985 return;
15986 }
15987 const dropTargetEl = iframe.parentElement;
15988 if (!dropTargetEl) {
15989 return;
15990 }
15991 const windowId = deriveWindowIdFromIframe(iframe);
15992 const deregister = registerDropTargetFor(
15993 dragManager,
15994 iframe,
15995 dropTargetEl,
15996 windowId
15997 );
15998 _activeRegistrations.set(iframe, deregister);
15999 });
16000 }
16001 function onDragEnd() {
16002 _suppressedIframes.forEach((prev, iframe) => {
16003 iframe.style.pointerEvents = prev;
16004 });
16005 _suppressedIframes.clear();
16006 _activeRegistrations.forEach((deregister) => {
16007 try {
16008 deregister();
16009 } catch {
16010 }
16011 });
16012 _activeRegistrations.clear();
16013 }
16014 function installIframeDropTargets(dragManager) {
16015 if (_installed$1) {
16016 return;
16017 }
16018 _installed$1 = true;
16019 _dragManager = dragManager;
16020 document.addEventListener(DRAG_EVENTS.START, (e) => {
16021 const detail = e.detail;
16022 onDragStart(detail?.payload);
16023 });
16024 document.addEventListener(DRAG_EVENTS.END, () => {
16025 onDragEnd();
16026 });
16027 document.addEventListener(DRAG_BRIDGE_EVENTS.START, (e) => {
16028 const detail = e.detail;
16029 if (!detail?.payload) {
16030 return;
16031 }
16032 startBridgeIntercept(detail.payload);
16033 });
16034 document.addEventListener(DRAG_BRIDGE_EVENTS.END, () => {
16035 stopBridgeIntercept();
16036 });
16037 addAction(
16038 HOOKS.WINDOW_CLOSED,
16039 "desktop-mode/drag/iframe-drop-targets-window-close",
16040 () => {
16041 for (const [iframe] of Array.from(_suppressedIframes)) {
16042 if (!iframe.isConnected) {
16043 _suppressedIframes.delete(iframe);
16044 }
16045 }
16046 for (const [iframe, deregister] of Array.from(_activeRegistrations)) {
16047 if (!iframe.isConnected) {
16048 try {
16049 deregister();
16050 } catch {
16051 }
16052 _activeRegistrations.delete(iframe);
16053 }
16054 }
16055 }
16056 );
16057 window.__desktopModeIframeDropDebug = () => ({
16058 installed: _installed$1,
16059 iframesInDom: document.querySelectorAll(IFRAME_SELECTOR).length,
16060 suppressedCount: _suppressedIframes.size,
16061 registeredCount: _activeRegistrations.size,
16062 suppressedIframeIds: Array.from(_suppressedIframes.keys()).map(
16063 deriveWindowIdFromIframe
16064 )
16065 });
16066 }
16067 function collectOpenables() {
16068 const desktop = window.wp?.desktop;
16069 if (!desktop) {
16070 return [];
16071 }
16072 const wm = desktop.windowManager;
16073 const config = desktop.config;
16074 if (!wm || !config) {
16075 return [];
16076 }
16077 const items = [];
16078 const fromMenu = (item, group) => ({
16079 id: item.id,
16080 label: item.title,
16081 description: group,
16082 icon: item.icon,
16083 open: () => wm.open({
16084 id: item.id,
16085 baseId: item.id,
16086 url: item.url,
16087 title: item.title,
16088 icon: item.icon
16089 })
16090 });
16091 for (const item of config.dockItems ?? []) {
16092 items.push(fromMenu(item, "Admin menu"));
16093 }
16094 const filtered = applyFilters(
16095 "desktop-mode.open-command.items",
16096 items
16097 );
16098 return Array.isArray(filtered) ? filtered : items;
16099 }
16100 const openCommand = {
16101 slug: "open",
16102 label: "Open",
16103 description: "Open an admin page or registered window.",
16104 hint: "[window]",
16105 icon: "dashicons-external",
16106 /**
16107 * Suggest matching windows as the user types args. Simple
16108 * case-insensitive substring match against label AND id so
16109 * "add" finds "Add New Post" and "jorvy" finds Jorvy whether
16110 * the plugin listed it with a friendly label or the slug.
16111 */
16112 suggest(args) {
16113 const q = args.trim().toLowerCase();
16114 const list2 = collectOpenables();
16115 const hits = q === "" ? list2 : list2.filter(
16116 (w) => w.label.toLowerCase().includes(q) || w.id.toLowerCase().includes(q)
16117 );
16118 return hits.slice(0, 12).map((w) => ({
16119 value: w.label,
16120 label: w.label,
16121 description: w.description,
16122 icon: w.icon ?? "dashicons-external"
16123 }));
16124 },
16125 run(args, ctx) {
16126 const q = args.trim();
16127 if (!q) {
16128 return "Type the name of a window to open, for example `/open Posts`.";
16129 }
16130 const list2 = collectOpenables();
16131 const ql = q.toLowerCase();
16132 const match = list2.find((w) => w.label.toLowerCase() === ql || w.id.toLowerCase() === ql) ?? list2.find(
16133 (w) => w.label.toLowerCase().includes(ql) || w.id.toLowerCase().includes(ql)
16134 );
16135 if (!match) {
16136 return `No window matching **${q}** — try \`/open\` alone to see available options.`;
16137 }
16138 match.open();
16139 ctx.close();
16140 }
16141 };
16142 function registerBuiltInCommands() {
16143 registerCommand(openCommand);
16144 }
16145 const palettes = [];
16146 const listeners$2 = /* @__PURE__ */ new Set();
16147 function registerPalette(p) {
16148 if (!p || typeof p.id !== "string" || p.id === "") {
16149 return () => {
16150 };
16151 }
16152 if (typeof p.open !== "function" || typeof p.close !== "function" || typeof p.isOpen !== "function") {
16153 return () => {
16154 };
16155 }
16156 const idx = palettes.findIndex((x) => x.id === p.id);
16157 if (idx >= 0) {
16158 palettes[idx] = p;
16159 } else {
16160 palettes.push(p);
16161 }
16162 notify$2();
16163 return () => {
16164 const i = palettes.findIndex((x) => x.id === p.id);
16165 if (i >= 0) {
16166 palettes.splice(i, 1);
16167 notify$2();
16168 }
16169 };
16170 }
16171 function unregisterPalette(id) {
16172 const idx = palettes.findIndex((x) => x.id === id);
16173 if (idx >= 0) {
16174 palettes.splice(idx, 1);
16175 notify$2();
16176 }
16177 }
16178 function listPalettes() {
16179 return palettes.slice();
16180 }
16181 function notify$2() {
16182 for (const cb of Array.from(listeners$2)) {
16183 try {
16184 cb();
16185 } catch (err) {
16186 if (typeof console !== "undefined") {
16187 console.error("[desktop-mode] palette-registry listener threw:", err);
16188 }
16189 }
16190 }
16191 }
16192 function cyclePalettes() {
16193 if (palettes.length === 0) {
16194 return;
16195 }
16196 const cur = palettes.findIndex((p) => {
16197 try {
16198 return p.isOpen();
16199 } catch {
16200 return false;
16201 }
16202 });
16203 if (cur === -1) {
16204 try {
16205 palettes[0].open();
16206 } catch {
16207 }
16208 return;
16209 }
16210 try {
16211 palettes[cur].close();
16212 } catch {
16213 }
16214 const next = cur + 1;
16215 if (next < palettes.length) {
16216 try {
16217 palettes[next].open();
16218 } catch {
16219 }
16220 }
16221 }
16222 function openPaletteOnly(id) {
16223 const target2 = palettes.find((p) => p.id === id);
16224 if (!target2) {
16225 return;
16226 }
16227 for (const p of palettes) {
16228 if (p.id !== id) {
16229 try {
16230 if (p.isOpen()) {
16231 p.close();
16232 }
16233 } catch {
16234 }
16235 }
16236 }
16237 try {
16238 target2.open();
16239 } catch {
16240 }
16241 }
16242 let installed$1 = false;
16243 function installPaletteShortcut() {
16244 if (installed$1) {
16245 return;
16246 }
16247 installed$1 = true;
16248 document.addEventListener(
16249 "keydown",
16250 (e) => {
16251 if (!(e.metaKey || e.ctrlKey) || e.key !== "k") {
16252 return;
16253 }
16254 if (e.shiftKey || e.altKey) {
16255 return;
16256 }
16257 e.preventDefault();
16258 e.stopImmediatePropagation();
16259 cyclePalettes();
16260 },
16261 true
16262 );
16263 const origin = window.location.origin;
16264 window.addEventListener("message", (e) => {
16265 if (e.origin !== origin) {
16266 return;
16267 }
16268 const data = e.data;
16269 if (data && data.type === "desktop-mode-palette-cycle") {
16270 cyclePalettes();
16271 }
16272 });
16273 }
16274 const suppliers = /* @__PURE__ */ new Map();
16275 const subscribers = /* @__PURE__ */ new Map();
16276 let booted$2 = false;
16277 const heartbeat = {
16278 contribute(field, supplier) {
16279 suppliers.set(field, supplier);
16280 return () => {
16281 if (suppliers.get(field) === supplier) {
16282 suppliers.delete(field);
16283 }
16284 };
16285 },
16286 subscribe(field, cb) {
16287 let set = subscribers.get(field);
16288 if (!set) {
16289 set = /* @__PURE__ */ new Set();
16290 subscribers.set(field, set);
16291 }
16292 set.add(cb);
16293 return () => {
16294 set.delete(cb);
16295 };
16296 }
16297 };
16298 function bootHeartbeatBus() {
16299 if (booted$2) {
16300 return;
16301 }
16302 booted$2 = true;
16303 const $ = window.jQuery;
16304 if (!$) {
16305 console.warn(
16306 "[desktop-mode/heartbeat] jQuery missing — Heartbeat bus disabled."
16307 );
16308 return;
16309 }
16310 $(document).on("heartbeat-send", (...args) => {
16311 const data = args[1];
16312 if (!data) {
16313 return;
16314 }
16315 for (const [field, supplier] of suppliers) {
16316 try {
16317 data[field] = supplier();
16318 } catch (err) {
16319 console.error(
16320 `[desktop-mode/heartbeat] supplier for "${field}" threw:`,
16321 err
16322 );
16323 }
16324 }
16325 });
16326 $(document).on("heartbeat-tick", (...args) => {
16327 const response = args[1];
16328 if (!response) {
16329 return;
16330 }
16331 for (const [field, set] of subscribers) {
16332 const value = response[field];
16333 if (value === void 0) {
16334 continue;
16335 }
16336 for (const cb of set) {
16337 try {
16338 cb(value);
16339 } catch (err) {
16340 console.error(
16341 `[desktop-mode/heartbeat] subscriber for "${field}" threw:`,
16342 err
16343 );
16344 }
16345 }
16346 }
16347 });
16348 }
16349 const store$2 = createSharedStore(
16350 "desktop-mode/presence",
16351 () => ({ byUser: /* @__PURE__ */ new Map(), serverTimeMs: 0 })
16352 );
16353 const ACTIVE_THRESHOLD_MS = 5 * 60 * 1e3;
16354 let lastInputMs = Date.now();
16355 let booted$1 = false;
16356 function noteUserActivity() {
16357 lastInputMs = Date.now();
16358 }
16359 function applySnapshot(block) {
16360 if (!block || !block.snapshot) {
16361 return;
16362 }
16363 const previous = store$2.state.byUser;
16364 const next = new Map(previous);
16365 const transitions = [];
16366 for (const [rawId, raw] of Object.entries(block.snapshot)) {
16367 const userId = Number(rawId);
16368 if (!Number.isFinite(userId) || userId <= 0) {
16369 continue;
16370 }
16371 const status = raw?.status ?? "offline";
16372 const entry = {
16373 status,
16374 lastSeenMs: Number(raw?.lastSeenMs ?? 0) || 0,
16375 lastActiveMs: Number(raw?.lastActiveMs ?? 0) || 0
16376 };
16377 const old = previous.get(userId);
16378 next.set(userId, entry);
16379 if (!old || old.status !== entry.status) {
16380 transitions.push({
16381 userId,
16382 oldStatus: old ? old.status : null,
16383 newStatus: entry.status,
16384 entry
16385 });
16386 }
16387 }
16388 store$2.state.byUser = next;
16389 if (typeof block.serverTimeMs === "number") {
16390 store$2.state.serverTimeMs = block.serverTimeMs;
16391 }
16392 store$2.notify();
16393 for (const t of transitions) {
16394 const detail = {
16395 userId: t.userId,
16396 oldStatus: t.oldStatus,
16397 newStatus: t.newStatus,
16398 lastSeenMs: t.entry.lastSeenMs,
16399 lastActiveMs: t.entry.lastActiveMs
16400 };
16401 document.dispatchEvent(
16402 new CustomEvent("desktop-mode-presence-changed", { detail })
16403 );
16404 activity.publish("desktop-mode/presence-changed", detail);
16405 }
16406 activity.publish("desktop-mode/presence-snapshot-applied", {
16407 applied: Object.keys(block.snapshot).length,
16408 transitions: transitions.length
16409 });
16410 }
16411 function bootPresenceProbe() {
16412 if (booted$1) {
16413 return;
16414 }
16415 booted$1 = true;
16416 document.addEventListener("pointerdown", noteUserActivity, {
16417 capture: true,
16418 passive: true
16419 });
16420 document.addEventListener("keydown", noteUserActivity, {
16421 capture: true,
16422 passive: true
16423 });
16424 document.addEventListener("visibilitychange", () => {
16425 if (!document.hidden) {
16426 noteUserActivity();
16427 }
16428 });
16429 heartbeat.contribute("desktop_mode_presence_active", () => true);
16430 heartbeat.contribute(
16431 "desktop_mode_user_active",
16432 () => Date.now() - lastInputMs < ACTIVE_THRESHOLD_MS
16433 );
16434 heartbeat.subscribe("desktop_mode_presence", (block) => {
16435 applySnapshot(block);
16436 });
16437 }
16438 function getStatus(userId) {
16439 const entry = store$2.state.byUser.get(userId);
16440 return entry ? entry.status : "offline";
16441 }
16442 function getAll() {
16443 return new Map(store$2.state.byUser);
16444 }
16445 function getEntry(userId) {
16446 return store$2.state.byUser.get(userId) ?? null;
16447 }
16448 function subscribe$1(cb) {
16449 return store$2.subscribe((s) => cb(s));
16450 }
16451 function markActive() {
16452 noteUserActivity();
16453 }
16454 function applyPresenceBatch(updates) {
16455 if (!Array.isArray(updates) || updates.length === 0) {
16456 return;
16457 }
16458 const previous = store$2.state.byUser;
16459 const next = new Map(previous);
16460 const transitions = [];
16461 for (const u of updates) {
16462 const userId = Number(u.userId);
16463 if (!Number.isFinite(userId) || userId <= 0) {
16464 continue;
16465 }
16466 const old = previous.get(userId);
16467 const entry = {
16468 status: u.status,
16469 lastSeenMs: typeof u.lastSeenMs === "number" ? u.lastSeenMs : old?.lastSeenMs ?? 0,
16470 lastActiveMs: typeof u.lastActiveMs === "number" ? u.lastActiveMs : old?.lastActiveMs ?? 0
16471 };
16472 next.set(userId, entry);
16473 if (!old || old.status !== entry.status) {
16474 transitions.push({
16475 userId,
16476 oldStatus: old ? old.status : null,
16477 newStatus: entry.status,
16478 entry
16479 });
16480 }
16481 }
16482 if (transitions.length === 0 && next.size === previous.size) {
16483 return;
16484 }
16485 store$2.state.byUser = next;
16486 store$2.notify();
16487 for (const t of transitions) {
16488 const detail = {
16489 userId: t.userId,
16490 oldStatus: t.oldStatus,
16491 newStatus: t.newStatus,
16492 lastSeenMs: t.entry.lastSeenMs,
16493 lastActiveMs: t.entry.lastActiveMs
16494 };
16495 document.dispatchEvent(
16496 new CustomEvent("desktop-mode-presence-changed", { detail })
16497 );
16498 activity.publish("desktop-mode/presence-changed", detail);
16499 }
16500 activity.publish("desktop-mode/presence-snapshot-applied", {
16501 applied: updates.length,
16502 transitions: transitions.length
16503 });
16504 }
16505 const presenceApi = Object.freeze({
16506 getStatus,
16507 getAll,
16508 getEntry,
16509 subscribe: subscribe$1,
16510 markActive,
16511 applyBatch: applyPresenceBatch
16512 });
16513 const HEARTBEAT_FIELD = "desktop_mode_nonces";
16514 const targets = /* @__PURE__ */ new Map();
16515 let booted = false;
16516 function registerNonceTarget(action, updater) {
16517 if (typeof action !== "string" || action === "") {
16518 return () => {
16519 };
16520 }
16521 let set = targets.get(action);
16522 if (!set) {
16523 set = /* @__PURE__ */ new Set();
16524 targets.set(action, set);
16525 }
16526 set.add(updater);
16527 return () => {
16528 set.delete(updater);
16529 };
16530 }
16531 function bootNonceRefresh() {
16532 if (booted) {
16533 return;
16534 }
16535 booted = true;
16536 heartbeat.subscribe(HEARTBEAT_FIELD, (payload) => {
16537 if (!payload || typeof payload !== "object") {
16538 return;
16539 }
16540 for (const [action, value] of Object.entries(payload)) {
16541 if (typeof value !== "string" || value === "") {
16542 continue;
16543 }
16544 const set = targets.get(action);
16545 if (!set) {
16546 continue;
16547 }
16548 for (const updater of set) {
16549 try {
16550 updater(value);
16551 } catch (err) {
16552 console.error(
16553 `[desktop-mode/nonce-refresh] updater for "${action}" threw:`,
16554 err
16555 );
16556 }
16557 }
16558 }
16559 });
16560 registerShellAndPluginsWindowTargets();
16561 }
16562 function registerShellAndPluginsWindowTargets() {
16563 registerNonceTarget("wp_rest", updateAllRestNonces);
16564 registerNonceTarget("desktop-mode-plugins", (fresh) => {
16565 writeWindowConfigField("desktop-mode-plugins", "ajaxNonce", fresh);
16566 });
16567 registerNonceTarget("updates", (fresh) => {
16568 writeWindowConfigField("desktop-mode-plugins", "updatesNonce", fresh);
16569 });
16570 }
16571 function updateAllRestNonces(fresh) {
16572 const cfg = readShellConfig();
16573 if (cfg && typeof cfg.restNonce === "string") {
16574 cfg.restNonce = fresh;
16575 }
16576 const windowConfigs = readWindowConfigs();
16577 if (!windowConfigs) {
16578 return;
16579 }
16580 for (const blob of Object.values(windowConfigs)) {
16581 if (blob && typeof blob === "object" && typeof blob.restNonce === "string") {
16582 blob.restNonce = fresh;
16583 }
16584 }
16585 }
16586 function writeWindowConfigField(windowId, field, value) {
16587 const blobs = readWindowConfigs();
16588 const blob = blobs?.[windowId];
16589 if (blob && typeof blob === "object") {
16590 blob[field] = value;
16591 }
16592 }
16593 function readShellConfig() {
16594 if (typeof window === "undefined") {
16595 return void 0;
16596 }
16597 return window.desktopModeConfig;
16598 }
16599 function readWindowConfigs() {
16600 if (typeof window === "undefined") {
16601 return void 0;
16602 }
16603 return window.desktopModeWindowConfig;
16604 }
16605 const VIEWPORT_CLAMP_MARGIN = 12;
16606 function findDockEntryForUrl(url, config) {
16607 const windowId = deriveWindowId(url, config.adminUrl);
16608 return (config.dockItems || []).find(
16609 (i) => deriveWindowId(i.url, config.adminUrl) === windowId || (i.submenu || []).some(
16610 (s) => deriveWindowId(s.url, config.adminUrl) === windowId
16611 )
16612 );
16613 }
16614 function clampGeometryToViewport(win, rect) {
16615 const maxW = Math.max(200, rect.width - VIEWPORT_CLAMP_MARGIN * 2);
16616 const maxH = Math.max(200, rect.height - VIEWPORT_CLAMP_MARGIN * 2);
16617 const width = Math.min(win.width, maxW);
16618 const height = Math.min(win.height, maxH);
16619 const maxX = Math.max(0, rect.width - width - VIEWPORT_CLAMP_MARGIN);
16620 const maxY = Math.max(0, rect.height - height - VIEWPORT_CLAMP_MARGIN);
16621 const x = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.x, maxX));
16622 const y = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.y, maxY));
16623 return { x, y, width, height };
16624 }
16625 const INITIAL_ORIGIN$1 = window.location.origin;
16626 function bindTopWindowLinkInterceptor(manager, config) {
16627 document.addEventListener(
16628 "click",
16629 (e) => {
16630 if (e.defaultPrevented) {
16631 return;
16632 }
16633 if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
16634 return;
16635 }
16636 const target2 = e.target;
16637 const link = target2 && target2.closest ? target2.closest("a[href]") : null;
16638 if (!link) {
16639 return;
16640 }
16641 const anchor = link;
16642 const linkTarget = anchor.getAttribute("target");
16643 if (linkTarget && linkTarget !== "" && linkTarget !== "_self") {
16644 return;
16645 }
16646 if (anchor.hasAttribute("download")) {
16647 return;
16648 }
16649 const rawHref = anchor.getAttribute("href");
16650 if (!rawHref || rawHref.charAt(0) === "#") {
16651 return;
16652 }
16653 if (/^(mailto:|tel:|javascript:|data:)/i.test(rawHref)) {
16654 return;
16655 }
16656 let url;
16657 try {
16658 url = new URL(rawHref, window.location.href);
16659 } catch (err) {
16660 if (typeof console !== "undefined") {
16661 console.warn(
16662 "[desktop-mode] Couldn’t parse href; letting the browser handle the click:",
16663 rawHref,
16664 err
16665 );
16666 }
16667 return;
16668 }
16669 if (url.origin !== INITIAL_ORIGIN$1) {
16670 return;
16671 }
16672 let adminPath;
16673 try {
16674 adminPath = new URL(config.adminUrl).pathname;
16675 } catch (err) {
16676 if (typeof console !== "undefined") {
16677 console.error(
16678 "[desktop-mode] config.adminUrl is not a valid URL; falling back to /wp-admin/:",
16679 config.adminUrl,
16680 err
16681 );
16682 }
16683 adminPath = "/wp-admin/";
16684 }
16685 if (!url.pathname.startsWith(adminPath)) {
16686 return;
16687 }
16688 if (/\/(admin-post|admin-ajax)\.php$/.test(url.pathname)) {
16689 return;
16690 }
16691 if (url.searchParams.has("action") && url.searchParams.get("action") === "logout") {
16692 return;
16693 }
16694 if (url.searchParams.has("desktop_mode_classic")) {
16695 return;
16696 }
16697 e.preventDefault();
16698 e.stopPropagation();
16699 if (tryNativeUrlRemap(url.href)) {
16700 return;
16701 }
16702 const windowId = deriveWindowId(url.href, config.adminUrl);
16703 const dockEntry = findDockEntryForUrl(url.href, config);
16704 const fallbackTitle = (anchor.textContent || "").trim() || dockEntry?.title || "";
16705 void manager.open({
16706 id: windowId,
16707 baseId: windowId,
16708 multi: !!dockEntry?.multi,
16709 url: url.href,
16710 parentUrl: dockEntry?.url ?? url.href,
16711 title: dockEntry?.title || fallbackTitle,
16712 icon: dockEntry?.icon || "dashicons-admin-generic",
16713 submenu: dockEntry?.submenu
16714 });
16715 },
16716 true
16717 );
16718 }
16719 const REGISTRY_CHANGED_EVENT = "desktop-mode-registry-changed";
16720 function diffIds(prev, next) {
16721 const prevIds = /* @__PURE__ */ new Set();
16722 if (Array.isArray(prev)) {
16723 for (const item of prev) {
16724 if (item && typeof item.id === "string") {
16725 prevIds.add(item.id);
16726 }
16727 }
16728 }
16729 const nextIds = /* @__PURE__ */ new Set();
16730 for (const item of next) {
16731 if (item && typeof item.id === "string") {
16732 nextIds.add(item.id);
16733 }
16734 }
16735 const added = [];
16736 for (const id of nextIds) {
16737 if (!prevIds.has(id)) {
16738 added.push(id);
16739 }
16740 }
16741 const removed = [];
16742 for (const id of prevIds) {
16743 if (!nextIds.has(id)) {
16744 removed.push(id);
16745 }
16746 }
16747 return { added, removed };
16748 }
16749 function emitRegistryChanged(registry2, prev, next) {
16750 const { added, removed } = diffIds(prev, next);
16751 if (added.length === 0 && removed.length === 0) {
16752 return;
16753 }
16754 if (typeof document === "undefined") {
16755 return;
16756 }
16757 const detail = { registry: registry2, added, removed };
16758 document.dispatchEvent(
16759 new CustomEvent(REGISTRY_CHANGED_EVENT, { detail })
16760 );
16761 }
16762 function createApplyPayload(deps2) {
16763 const {
16764 applyDockItems,
16765 config,
16766 syncNativeWindows,
16767 syncServerWidgets,
16768 syncServerWallpapers,
16769 syncServerCommands,
16770 syncServerSettingsTabs,
16771 syncServerTitleBarButtons,
16772 syncServerDockRailRenderers,
16773 renderIcons
16774 } = deps2;
16775 return function applyPayload(payload) {
16776 const dockItems = payload.dockItems;
16777 const nativeWindows = payload.nativeWindows;
16778 const serverWidgets = payload.serverWidgets;
16779 const serverWallpapers = payload.serverWallpapers;
16780 const serverCommandScripts = payload.serverCommandScripts;
16781 const serverCommands = payload.serverCommands;
16782 const serverSettingsTabScripts = payload.serverSettingsTabScripts;
16783 const serverSettingsTabs = payload.serverSettingsTabs;
16784 const serverDockRailRendererScripts = payload.serverDockRailRendererScripts;
16785 const serverTitleBarButtonScripts = payload.serverTitleBarButtonScripts;
16786 const serverWindowNotices = payload.serverWindowNotices;
16787 const desktopIcons = payload.desktopIcons;
16788 if (!Array.isArray(dockItems) || dockItems.length === 0) {
16789 return;
16790 }
16791 const prevDockItems = config.dockItems;
16792 applyDockItems(dockItems);
16793 config.dockItems = dockItems;
16794 emitRegistryChanged(
16795 "dock-items",
16796 prevDockItems,
16797 dockItems
16798 );
16799 if (Array.isArray(nativeWindows)) {
16800 const prevNativeWindows = config.nativeWindows;
16801 void syncNativeWindows(
16802 nativeWindows
16803 );
16804 config.nativeWindows = nativeWindows;
16805 emitRegistryChanged(
16806 "native-windows",
16807 prevNativeWindows,
16808 nativeWindows
16809 );
16810 }
16811 if (Array.isArray(serverWidgets)) {
16812 void syncServerWidgets(
16813 serverWidgets
16814 );
16815 config.serverWidgets = serverWidgets;
16816 }
16817 if (Array.isArray(serverWallpapers)) {
16818 void syncServerWallpapers(
16819 serverWallpapers
16820 );
16821 config.serverWallpapers = serverWallpapers;
16822 }
16823 if (Array.isArray(serverCommandScripts)) {
16824 void syncServerCommands(
16825 serverCommandScripts,
16826 Array.isArray(serverCommands) ? serverCommands : void 0
16827 );
16828 config.serverCommandScripts = serverCommandScripts;
16829 if (Array.isArray(serverCommands)) {
16830 config.serverCommands = serverCommands;
16831 }
16832 }
16833 if (Array.isArray(serverSettingsTabScripts)) {
16834 void syncServerSettingsTabs(
16835 serverSettingsTabScripts,
16836 Array.isArray(serverSettingsTabs) ? serverSettingsTabs : void 0
16837 );
16838 config.serverSettingsTabScripts = serverSettingsTabScripts;
16839 if (Array.isArray(serverSettingsTabs)) {
16840 config.serverSettingsTabs = serverSettingsTabs;
16841 }
16842 }
16843 if (Array.isArray(serverTitleBarButtonScripts)) {
16844 void syncServerTitleBarButtons(
16845 serverTitleBarButtonScripts
16846 );
16847 config.serverTitleBarButtonScripts = serverTitleBarButtonScripts;
16848 }
16849 if (Array.isArray(serverDockRailRendererScripts)) {
16850 void syncServerDockRailRenderers(
16851 serverDockRailRendererScripts
16852 );
16853 config.serverDockRailRendererScripts = serverDockRailRendererScripts;
16854 }
16855 if (Array.isArray(serverWindowNotices)) {
16856 applyServerWindowNotices(
16857 serverWindowNotices
16858 );
16859 config.serverWindowNotices = serverWindowNotices;
16860 }
16861 if (Array.isArray(desktopIcons)) {
16862 const prevDesktopIcons = config.desktopIcons;
16863 renderIcons(desktopIcons);
16864 config.desktopIcons = desktopIcons;
16865 emitRegistryChanged(
16866 "desktop-icons",
16867 prevDesktopIcons,
16868 desktopIcons
16869 );
16870 }
16871 };
16872 }
16873 const MENU_REFRESH_TIMEOUT_MS = 8e3;
16874 function bindMenuRefresh(deps2) {
16875 const {
16876 layoutDispatcher,
16877 config,
16878 syncNativeWindows,
16879 syncServerWidgets,
16880 syncServerWallpapers,
16881 syncServerCommands,
16882 syncServerSettingsTabs,
16883 syncServerTitleBarButtons,
16884 syncServerDockRailRenderers,
16885 renderIcons
16886 } = deps2;
16887 const applyPayload = createApplyPayload({
16888 applyDockItems: (items) => layoutDispatcher?.applyDockItems(items),
16889 config,
16890 syncNativeWindows,
16891 syncServerWidgets,
16892 syncServerWallpapers,
16893 syncServerCommands,
16894 syncServerSettingsTabs,
16895 syncServerTitleBarButtons,
16896 syncServerDockRailRenderers,
16897 renderIcons
16898 });
16899 window.addEventListener("message", (e) => {
16900 if (e.origin !== INITIAL_ORIGIN$1) {
16901 return;
16902 }
16903 const data = e.data;
16904 if (!data || data.type !== "desktop-mode-plugins-changed") {
16905 return;
16906 }
16907 if (data.payload) {
16908 applyPayload(data.payload);
16909 }
16910 });
16911 const refresh = () => {
16912 if (!config.adminUrl) {
16913 return Promise.resolve();
16914 }
16915 const probeUrl = (() => {
16916 try {
16917 const url = new URL("admin.php", config.adminUrl);
16918 url.searchParams.set("desktop_mode_chromeless", "1");
16919 url.searchParams.set("desktop_mode_menu_refresh", "1");
16920 return url.toString();
16921 } catch (_err) {
16922 return null;
16923 }
16924 })();
16925 if (!probeUrl) {
16926 return Promise.resolve();
16927 }
16928 return new Promise((resolve2) => {
16929 const iframe = document.createElement("iframe");
16930 iframe.setAttribute("aria-hidden", "true");
16931 iframe.tabIndex = -1;
16932 iframe.style.cssText = "position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;border:0;opacity:0;pointer-events:none;";
16933 iframe.src = probeUrl;
16934 let done = false;
16935 const cleanup = () => {
16936 if (done) {
16937 return;
16938 }
16939 done = true;
16940 window.clearTimeout(timeoutId);
16941 window.removeEventListener("message", onMessage);
16942 if (iframe.parentNode) {
16943 iframe.parentNode.removeChild(iframe);
16944 }
16945 resolve2();
16946 };
16947 const onMessage = (e) => {
16948 if (e.source !== iframe.contentWindow) {
16949 return;
16950 }
16951 const data = e.data;
16952 if (!data || data.type !== "desktop-mode-plugins-changed") {
16953 return;
16954 }
16955 cleanup();
16956 };
16957 const timeoutId = window.setTimeout(() => {
16958 doAction(HOOKS.SHELL_ERROR, {
16959 scope: "menu-refresh",
16960 error: new Error("menu refresh probe timed out")
16961 });
16962 cleanup();
16963 }, MENU_REFRESH_TIMEOUT_MS);
16964 window.addEventListener("message", onMessage);
16965 document.body.appendChild(iframe);
16966 });
16967 };
16968 return refresh;
16969 }
16970 function hasRestorableSession(session) {
16971 if (!session) {
16972 return false;
16973 }
16974 if (Array.isArray(session.windows) && session.windows.length > 0) {
16975 return true;
16976 }
16977 if (typeof session.updated !== "number" || session.updated <= 0 || !Array.isArray(session.desktops) || session.desktops.length === 0) {
16978 return false;
16979 }
16980 if (session.desktops.length > 1) {
16981 return true;
16982 }
16983 const onlyDesktop = session.desktops[0];
16984 if (onlyDesktop?.id && onlyDesktop.id !== "desktop-1") {
16985 return true;
16986 }
16987 return !!session.activeDesktop && session.activeDesktop !== "desktop-1";
16988 }
16989 async function restoreSession(manager, config, desktopArea) {
16990 const rect = desktopArea.getBoundingClientRect();
16991 if (Array.isArray(config.session.desktops) && config.session.desktops.length > 0) {
16992 manager.seedDesktops(
16993 config.session.desktops,
16994 config.session.activeDesktop || config.session.desktops[0].id
16995 );
16996 }
16997 for (const win of config.session.windows) {
16998 const clamped = clampGeometryToViewport(win, rect);
16999 const dockEntry = findDockEntryForUrl(win.url, config);
17000 const opened = await manager.open({
17001 id: win.id,
17002 baseId: win.baseId || win.id,
17003 desktopId: win.desktopId,
17004 multi: !!dockEntry?.multi,
17005 url: win.url,
17006 // `dockEntry?.url` is the parent menu's landing page —
17007 // recover it so the synthetic "back to parent" tab in
17008 // the in-window strip points at the dock URL even when
17009 // the saved `win.url` is a sub-page (e.g. theme-install.php
17010 // under Appearance, or a deep wc-admin route under
17011 // WooCommerce). Without this the dedup check in
17012 // `dom.ts` sees the iframe URL match a submenu entry
17013 // and suppresses the parent tab — losing the only
17014 // affordance to navigate back.
17015 parentUrl: dockEntry?.url ?? win.url,
17016 title: win.title,
17017 icon: win.icon || "dashicons-admin-generic",
17018 x: clamped.x,
17019 y: clamped.y,
17020 width: clamped.width,
17021 height: clamped.height,
17022 initialState: win.state,
17023 submenu: dockEntry?.submenu
17024 });
17025 if (Array.isArray(win.externalTabs)) {
17026 for (const ext of win.externalTabs) {
17027 if (ext && typeof ext.url === "string" && ext.url !== "") {
17028 opened.addExternalTab(
17029 ext.url,
17030 typeof ext.label === "string" && ext.label !== "" ? ext.label : ext.url
17031 );
17032 }
17033 }
17034 }
17035 }
17036 if (config.session.focused) {
17037 const focused = manager.getById(config.session.focused);
17038 if (focused) {
17039 manager.focus(focused);
17040 }
17041 }
17042 }
17043 async function openCurrentPage(manager, config) {
17044 if (tryNativeUrlRemap(config.currentPage)) {
17045 return;
17046 }
17047 const windowId = deriveWindowId(config.currentPage, config.adminUrl);
17048 const dockEntry = findDockEntryForUrl(config.currentPage, config);
17049 await manager.open({
17050 id: windowId,
17051 baseId: windowId,
17052 multi: !!dockEntry?.multi,
17053 url: config.currentPage,
17054 parentUrl: dockEntry?.url ?? config.currentPage,
17055 title: config.currentTitle,
17056 icon: config.currentIcon,
17057 submenu: dockEntry?.submenu
17058 });
17059 }
17060 function shouldAutoOpenCurrentPage(inputs) {
17061 const suppress = inputs.fromPortal && !inputs.fromPortalIntent && (inputs.hasSession || !inputs.defaultEnabled || inputs.isNativeDefault);
17062 return !suppress;
17063 }
17064 function trackedFetch(manager, input, requestInit, opts) {
17065 const finalInit = injectRestNonce(input, requestInit);
17066 const promise = window.fetch(input, finalInit);
17067 if (opts?.silent) {
17068 return promise;
17069 }
17070 let target2 = opts?.window;
17071 if (!target2 && opts?.windowId) {
17072 target2 = manager.getById(opts.windowId) ?? null;
17073 }
17074 if (!target2) {
17075 target2 = manager.getFocused();
17076 }
17077 if (target2 && typeof target2.trackActivity === "function") {
17078 void target2.trackActivity(promise).catch(() => {
17079 });
17080 }
17081 return promise;
17082 }
17083 const SESSION_SAVE_DEBOUNCE_MS = 500;
17084 function createSessionSaver(manager, config) {
17085 let debounceTimer = null;
17086 let inFlight = false;
17087 const doSave = async () => {
17088 if (inFlight) {
17089 return;
17090 }
17091 const payload = manager.snapshot();
17092 inFlight = true;
17093 try {
17094 await trackedFetch(
17095 manager,
17096 config.sessionUrl,
17097 {
17098 method: "POST",
17099 credentials: "same-origin",
17100 headers: {
17101 "Content-Type": "application/json",
17102 "X-WP-Nonce": config.restNonce
17103 },
17104 body: JSON.stringify({ session: payload }),
17105 // Best-effort: we don't block the UI on persistence.
17106 keepalive: true
17107 },
17108 { silent: true }
17109 );
17110 } catch (err) {
17111 doAction(HOOKS.SHELL_ERROR, { scope: "session-save", error: err });
17112 } finally {
17113 inFlight = false;
17114 }
17115 };
17116 const flushImmediately = () => {
17117 if (debounceTimer !== null) {
17118 clearTimeout(debounceTimer);
17119 debounceTimer = null;
17120 }
17121 const payload = manager.snapshot();
17122 const body = new Blob(
17123 [JSON.stringify({ session: payload })],
17124 { type: "application/json" }
17125 );
17126 const beaconUrl = config.sessionUrl + (config.sessionUrl.includes("?") ? "&" : "?") + "_wpnonce=" + encodeURIComponent(config.restNonce);
17127 if (navigator.sendBeacon && navigator.sendBeacon(beaconUrl, body)) {
17128 return;
17129 }
17130 void doSave();
17131 };
17132 const schedule = () => {
17133 if (debounceTimer !== null) {
17134 clearTimeout(debounceTimer);
17135 }
17136 debounceTimer = window.setTimeout(() => {
17137 debounceTimer = null;
17138 void doSave();
17139 }, SESSION_SAVE_DEBOUNCE_MS);
17140 };
17141 window.addEventListener("pagehide", flushImmediately);
17142 document.addEventListener("visibilitychange", () => {
17143 if (document.visibilityState === "hidden") {
17144 flushImmediately();
17145 }
17146 });
17147 return schedule;
17148 }
17149 const SHELL_RESIZE_DEBOUNCE_MS = 120;
17150 function wireSessionEvents(save) {
17151 document.addEventListener("desktop-mode-window-opened", save);
17152 document.addEventListener("desktop-mode-window-closed", save);
17153 document.addEventListener("desktop-mode-window-focused", save);
17154 document.addEventListener("desktop-mode-window-changed", save);
17155 addAction(HOOKS.DESKTOP_CREATED, "desktop-mode/session-save", save);
17156 addAction(HOOKS.DESKTOP_CLOSED, "desktop-mode/session-save", save);
17157 addAction(HOOKS.DESKTOP_SWITCHED, "desktop-mode/session-save", save);
17158 }
17159 function bindShellLifecycle() {
17160 const shellEl = document.getElementById("desktop-mode-shell");
17161 let resizeTimer = null;
17162 const fireShellResize = () => {
17163 resizeTimer = null;
17164 const rect = shellEl ? shellEl.getBoundingClientRect() : null;
17165 doAction(HOOKS.SHELL_RESIZED, {
17166 width: rect ? Math.round(rect.width) : window.innerWidth,
17167 height: rect ? Math.round(rect.height) : window.innerHeight
17168 });
17169 };
17170 window.addEventListener("resize", () => {
17171 if (resizeTimer !== null) {
17172 window.clearTimeout(resizeTimer);
17173 }
17174 resizeTimer = window.setTimeout(
17175 fireShellResize,
17176 SHELL_RESIZE_DEBOUNCE_MS
17177 );
17178 });
17179 document.addEventListener("visibilitychange", () => {
17180 doAction(HOOKS.SHELL_VISIBILITY, {
17181 state: document.hidden ? "hidden" : "visible"
17182 });
17183 });
17184 }
17185 function applyTileClasses(baseClasses, item, ctx) {
17186 const fullCtx = {
17187 rail: ctx.rail ?? "dock",
17188 orientation: ctx.orientation,
17189 dockId: ctx.dockId,
17190 container: ctx.container ?? document.body,
17191 item,
17192 isSystem: ctx.isSystem
17193 };
17194 return applyFilters(
17195 HOOKS.DOCK_TILE_CLASS,
17196 baseClasses,
17197 fullCtx
17198 );
17199 }
17200 function applyTileElement(tile2, item, ctx) {
17201 const fullCtx = {
17202 rail: ctx.rail ?? "dock",
17203 orientation: ctx.orientation,
17204 dockId: ctx.dockId,
17205 container: ctx.container ?? document.body,
17206 item,
17207 isSystem: ctx.isSystem
17208 };
17209 return applyFilters(
17210 HOOKS.DOCK_TILE_ELEMENT,
17211 tile2,
17212 fullCtx
17213 );
17214 }
17215 function applyTileTooltip(label, item, ctx) {
17216 const fullCtx = {
17217 rail: ctx.rail ?? "dock",
17218 orientation: ctx.orientation,
17219 dockId: ctx.dockId,
17220 container: ctx.container ?? document.body,
17221 item,
17222 isSystem: ctx.isSystem
17223 };
17224 return applyFilters(
17225 HOOKS.DOCK_TILE_TOOLTIP,
17226 label,
17227 fullCtx
17228 );
17229 }
17230 function dispatchTileRendered(el, item, ctx) {
17231 const fullCtx = {
17232 rail: ctx.rail ?? "dock",
17233 orientation: ctx.orientation,
17234 dockId: ctx.dockId,
17235 container: ctx.container ?? document.body,
17236 item,
17237 isSystem: ctx.isSystem
17238 };
17239 doAction(HOOKS.DOCK_TILE_RENDERED, { ...fullCtx, el });
17240 }
17241 const DEFAULT_DOCK_SELECTOR = [
17242 ".desktop-mode-dock",
17243 "#desktop-mode-dock",
17244 "#desktop-mode-side-dock",
17245 ".desktop-mode-dock__tooltip",
17246 ".desktop-mode-dock-submenu"
17247 ].join(",");
17248 const customSelectors = /* @__PURE__ */ new Set();
17249 function isDockElement(target2) {
17250 if (!target2 || typeof target2.closest !== "function") {
17251 return false;
17252 }
17253 const el = target2;
17254 if (el.closest(DEFAULT_DOCK_SELECTOR)) {
17255 return true;
17256 }
17257 for (const selector of customSelectors) {
17258 if (el.closest(selector)) {
17259 return true;
17260 }
17261 }
17262 return false;
17263 }
17264 function registerDockSelector(selector) {
17265 if (typeof selector !== "string" || selector.trim() === "") {
17266 return () => void 0;
17267 }
17268 customSelectors.add(selector);
17269 return () => {
17270 customSelectors.delete(selector);
17271 };
17272 }
17273 const states = /* @__PURE__ */ new Map();
17274 const INITIAL_ORIGIN = window.location.origin;
17275 function ensureState(windowId) {
17276 let s = states.get(windowId);
17277 if (!s) {
17278 s = {
17279 headers: /* @__PURE__ */ new Map(),
17280 observers: /* @__PURE__ */ new Set(),
17281 observeCount: 0,
17282 loadHandler: null,
17283 loadHandlerTarget: null
17284 };
17285 states.set(windowId, s);
17286 }
17287 ensureLoadHandler(windowId, s);
17288 return s;
17289 }
17290 function ensureLoadHandler(windowId, s) {
17291 const iframe = findIframe(windowId);
17292 if (!iframe) {
17293 return;
17294 }
17295 if (s.loadHandlerTarget === iframe && s.loadHandler) {
17296 return;
17297 }
17298 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
17299 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
17300 }
17301 if (typeof iframe.addEventListener !== "function") {
17302 return;
17303 }
17304 const handler = () => {
17305 queueMicrotask(() => pushInstrumentation(windowId));
17306 };
17307 iframe.addEventListener("load", handler);
17308 s.loadHandler = handler;
17309 s.loadHandlerTarget = iframe;
17310 }
17311 function detachLoadHandler(s) {
17312 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
17313 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
17314 }
17315 s.loadHandler = null;
17316 s.loadHandlerTarget = null;
17317 }
17318 function findIframe(windowId) {
17319 const wpd = window.wp?.desktop?.windowManager;
17320 if (wpd && typeof wpd.getById === "function") {
17321 const win = wpd.getById(windowId);
17322 if (win?.iframe) {
17323 return win.iframe;
17324 }
17325 if (win?.element) {
17326 const synth = win.element.querySelector("iframe");
17327 if (synth) {
17328 return synth;
17329 }
17330 }
17331 }
17332 const fallback = document.getElementById(`wp-window-${windowId}`);
17333 return fallback?.querySelector("iframe") ?? null;
17334 }
17335 function snapshotHeaders(s) {
17336 const out = {};
17337 for (const [name, contributions] of s.headers) {
17338 const parts = [];
17339 for (const c of contributions) {
17340 let v;
17341 try {
17342 v = typeof c.value === "function" ? c.value() : c.value;
17343 } catch {
17344 continue;
17345 }
17346 if (typeof v === "string" && v !== "") {
17347 parts.push(v);
17348 }
17349 }
17350 if (parts.length > 0) {
17351 out[name] = parts.join(", ");
17352 }
17353 }
17354 return out;
17355 }
17356 function pushInstrumentation(windowId) {
17357 const iframe = findIframe(windowId);
17358 if (!iframe || !iframe.contentWindow) {
17359 return;
17360 }
17361 const s = states.get(windowId);
17362 const headers = s ? snapshotHeaders(s) : {};
17363 const observe = !!s && s.observeCount > 0;
17364 try {
17365 iframe.contentWindow.postMessage(
17366 {
17367 type: "desktop-mode-instrument-set",
17368 headers,
17369 observe
17370 },
17371 INITIAL_ORIGIN
17372 );
17373 } catch {
17374 }
17375 }
17376 addAction(HOOKS.IFRAME_READY, "desktop-mode/devtools/replay", (payload) => {
17377 const p = payload;
17378 if (p && typeof p.windowId === "string" && states.has(p.windowId)) {
17379 pushInstrumentation(p.windowId);
17380 }
17381 });
17382 addAction(
17383 HOOKS.IFRAME_NETWORK_COMPLETED,
17384 "desktop-mode/devtools/dispatch",
17385 (payload) => {
17386 const p = payload;
17387 if (!p || typeof p.windowId !== "string") {
17388 return;
17389 }
17390 const s = states.get(p.windowId);
17391 if (!s) {
17392 return;
17393 }
17394 for (const cb of s.observers) {
17395 try {
17396 cb(p);
17397 } catch {
17398 }
17399 }
17400 }
17401 );
17402 const sessions = /* @__PURE__ */ new Map();
17403 const POLL_INTERVAL_MS = 1e3;
17404 function pollOnce(sessionId, restUrl2, restNonce) {
17405 const sp = sessions.get(sessionId);
17406 if (!sp || sp.inflight) {
17407 return;
17408 }
17409 sp.inflight = true;
17410 const u = new URL(restUrl2 + "desktop-mode/v1/debug", window.location.origin);
17411 u.searchParams.set("sessionId", sessionId);
17412 u.searchParams.set("since", String(sp.cursor));
17413 for (const ch of sp.channels.keys()) {
17414 u.searchParams.append("channels[]", ch);
17415 }
17416 const url = u.toString();
17417 fetch(url, {
17418 credentials: "same-origin",
17419 headers: { "X-WP-Nonce": restNonce }
17420 }).then((r) => r.ok ? r.json() : { events: [], cursor: sp.cursor }).then((body) => {
17421 sp.inflight = false;
17422 if (!sessions.has(sessionId)) {
17423 return;
17424 }
17425 if (typeof body.cursor === "number") {
17426 sp.cursor = body.cursor;
17427 }
17428 for (const ev of body.events || []) {
17429 const bucket2 = sp.channels.get(ev.channel);
17430 if (!bucket2) {
17431 continue;
17432 }
17433 for (const cb of bucket2) {
17434 try {
17435 cb(ev);
17436 } catch {
17437 }
17438 }
17439 }
17440 }).catch(() => {
17441 sp.inflight = false;
17442 }).finally(() => {
17443 const stillThere = sessions.get(sessionId);
17444 if (stillThere && stillThere.channels.size > 0) {
17445 stillThere.timer = setTimeout(
17446 () => pollOnce(sessionId, restUrl2, restNonce),
17447 POLL_INTERVAL_MS
17448 );
17449 }
17450 });
17451 }
17452 function getRestEndpoint() {
17453 const cfg = window.desktopModeConfig;
17454 if (!cfg || !cfg.restUrl || !cfg.restNonce) {
17455 return null;
17456 }
17457 return { restUrl: cfg.restUrl, restNonce: cfg.restNonce };
17458 }
17459 function dispatchLocal(sessionId, ev) {
17460 const sp = sessions.get(sessionId);
17461 if (!sp) {
17462 return;
17463 }
17464 const bucket2 = sp.channels.get(ev.channel);
17465 if (!bucket2) {
17466 return;
17467 }
17468 for (const cb of bucket2) {
17469 try {
17470 cb(ev);
17471 } catch {
17472 }
17473 }
17474 }
17475 let _localEventCounter = 0;
17476 const debugBus = {
17477 startSession() {
17478 const cryptoApi = window.crypto;
17479 if (cryptoApi && typeof cryptoApi.randomUUID === "function") {
17480 return cryptoApi.randomUUID();
17481 }
17482 return "wpdbg-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
17483 },
17484 publish(sessionId, channel, payload) {
17485 dispatchLocal(sessionId, {
17486 id: ++_localEventCounter,
17487 t: Date.now(),
17488 channel,
17489 payload
17490 });
17491 },
17492 subscribe(sessionId, channel, cb) {
17493 let sp = sessions.get(sessionId);
17494 const startedFresh = !sp;
17495 if (!sp) {
17496 sp = {
17497 channels: /* @__PURE__ */ new Map(),
17498 cursor: 0,
17499 timer: null,
17500 inflight: false
17501 };
17502 sessions.set(sessionId, sp);
17503 }
17504 let bucket2 = sp.channels.get(channel);
17505 if (!bucket2) {
17506 bucket2 = /* @__PURE__ */ new Set();
17507 sp.channels.set(channel, bucket2);
17508 }
17509 bucket2.add(cb);
17510 if (startedFresh) {
17511 const ep = getRestEndpoint();
17512 if (ep) {
17513 pollOnce(sessionId, ep.restUrl, ep.restNonce);
17514 }
17515 }
17516 return () => {
17517 const cur = sessions.get(sessionId);
17518 if (!cur) {
17519 return;
17520 }
17521 const b = cur.channels.get(channel);
17522 if (b) {
17523 b.delete(cb);
17524 if (b.size === 0) {
17525 cur.channels.delete(channel);
17526 }
17527 }
17528 if (cur.channels.size === 0) {
17529 if (cur.timer) {
17530 clearTimeout(cur.timer);
17531 }
17532 sessions.delete(sessionId);
17533 }
17534 };
17535 }
17536 };
17537 const devtools = {
17538 addRequestHeader(windowId, name, value) {
17539 if (typeof windowId !== "string" || windowId === "") {
17540 return () => {
17541 };
17542 }
17543 if (typeof name !== "string" || name === "") {
17544 return () => {
17545 };
17546 }
17547 const s = ensureState(windowId);
17548 const contribution = { value };
17549 let bucket2 = s.headers.get(name);
17550 if (!bucket2) {
17551 bucket2 = [];
17552 s.headers.set(name, bucket2);
17553 }
17554 bucket2.push(contribution);
17555 pushInstrumentation(windowId);
17556 return () => {
17557 const cur = states.get(windowId);
17558 if (!cur) {
17559 return;
17560 }
17561 const b = cur.headers.get(name);
17562 if (!b) {
17563 return;
17564 }
17565 const i = b.indexOf(contribution);
17566 if (i >= 0) {
17567 b.splice(i, 1);
17568 }
17569 if (b.length === 0) {
17570 cur.headers.delete(name);
17571 }
17572 pushInstrumentation(windowId);
17573 gcWindowState(windowId);
17574 };
17575 },
17576 onRequest(windowId, cb, opts) {
17577 if (typeof windowId !== "string" || windowId === "") {
17578 return () => {
17579 };
17580 }
17581 if (typeof cb !== "function") {
17582 return () => {
17583 };
17584 }
17585 const s = ensureState(windowId);
17586 s.observers.add(cb);
17587 const wantsObserve = !!opts?.observe;
17588 if (wantsObserve) {
17589 s.observeCount++;
17590 pushInstrumentation(windowId);
17591 }
17592 return () => {
17593 const cur = states.get(windowId);
17594 if (!cur) {
17595 return;
17596 }
17597 cur.observers.delete(cb);
17598 if (wantsObserve) {
17599 cur.observeCount = Math.max(0, cur.observeCount - 1);
17600 pushInstrumentation(windowId);
17601 }
17602 gcWindowState(windowId);
17603 };
17604 },
17605 reloadWithDebugSession(windowId, sessionId, opts) {
17606 if (typeof windowId !== "string" || windowId === "" || typeof sessionId !== "string" || sessionId === "") {
17607 return null;
17608 }
17609 const iframe = findIframe(windowId);
17610 if (!iframe) {
17611 return null;
17612 }
17613 const headerName = opts?.headerName || "X-WP-Debug-Session";
17614 const queryArg = opts?.queryArg || "wp_debug_session";
17615 const stopHeader = devtools.addRequestHeader(windowId, headerName, sessionId);
17616 try {
17617 const currentSrc = iframe.getAttribute("src") || iframe.src || "";
17618 const u = new URL(currentSrc, window.location.origin);
17619 u.searchParams.set(queryArg, sessionId);
17620 iframe.src = u.toString();
17621 } catch {
17622 }
17623 return {
17624 dispose: () => {
17625 stopHeader();
17626 }
17627 };
17628 },
17629 debug: debugBus
17630 };
17631 function gcWindowState(windowId) {
17632 const s = states.get(windowId);
17633 if (!s) {
17634 return;
17635 }
17636 if (s.headers.size === 0 && s.observers.size === 0) {
17637 detachLoadHandler(s);
17638 states.delete(windowId);
17639 }
17640 }
17641 async function wpdConfirm(options) {
17642 await ensureShellOverlaysLoaded(shellOverlaysBundleUrl());
17643 return new Promise((resolve2) => {
17644 const dialog2 = document.createElement("wpd-confirm-dialog");
17645 dialog2.setAttribute("open", "");
17646 if (options.title) {
17647 dialog2.setAttribute("title", options.title);
17648 }
17649 dialog2.setAttribute("message", options.message);
17650 if (options.confirmLabel) {
17651 dialog2.setAttribute("confirm-label", options.confirmLabel);
17652 }
17653 if (options.cancelLabel) {
17654 dialog2.setAttribute("cancel-label", options.cancelLabel);
17655 }
17656 if (options.danger) {
17657 dialog2.setAttribute("danger", "");
17658 }
17659 if (options.hideCancel) {
17660 dialog2.setAttribute("hide-cancel", "");
17661 }
17662 if (options.dismissable) {
17663 dialog2.setAttribute("dismissable", "");
17664 }
17665 const cleanup = (ok) => {
17666 dialog2.remove();
17667 resolve2(ok);
17668 };
17669 dialog2.addEventListener("wpd-confirm", () => cleanup(true));
17670 dialog2.addEventListener("wpd-cancel", () => cleanup(false));
17671 document.body.appendChild(dialog2);
17672 const inner = dialog2.shadowRoot?.querySelector(".dialog");
17673 (inner ?? dialog2).focus?.();
17674 });
17675 }
17676 function collectWallpaperSurfaces(manager) {
17677 const seed2 = [];
17678 for (const w of manager.getVisibleRects()) {
17679 if (w.state === "minimized") {
17680 continue;
17681 }
17682 if (w.element.offsetParent === null) {
17683 continue;
17684 }
17685 const r = w.element.getBoundingClientRect();
17686 seed2.push({
17687 id: `window:${w.windowId}`,
17688 kind: "window",
17689 rect: rectFromDom(r),
17690 face: "top",
17691 element: w.element
17692 });
17693 }
17694 const shellEl = document.getElementById("desktop-mode-shell");
17695 if (shellEl) {
17696 const r = shellEl.getBoundingClientRect();
17697 seed2.push({
17698 id: "shell:floor",
17699 kind: "shell",
17700 rect: {
17701 x: r.left,
17702 y: r.bottom - 1,
17703 width: r.width,
17704 height: 1
17705 },
17706 face: "top",
17707 element: shellEl
17708 });
17709 }
17710 const dockEls = document.querySelectorAll(
17711 ".desktop-mode-dock"
17712 );
17713 let dockIndex = 0;
17714 for (const dockEl of Array.from(dockEls)) {
17715 const r = dockEl.getBoundingClientRect();
17716 if (r.width <= 0 || r.height <= 0) {
17717 continue;
17718 }
17719 const placement = dockEl.getAttribute("data-desktop-mode-dock-placement") ?? "bottom";
17720 const id = dockIndex === 0 ? "dock:edge" : `dock:edge:${dockIndex}`;
17721 dockIndex++;
17722 if (placement === "bottom") {
17723 seed2.push({
17724 id,
17725 kind: "dock",
17726 rect: { x: r.left, y: r.top, width: r.width, height: 1 },
17727 face: "top",
17728 element: dockEl
17729 });
17730 } else if (placement === "right") {
17731 seed2.push({
17732 id,
17733 kind: "dock",
17734 rect: { x: r.left, y: r.top, width: 1, height: r.height },
17735 face: "left",
17736 element: dockEl
17737 });
17738 } else {
17739 seed2.push({
17740 id,
17741 kind: "dock",
17742 rect: {
17743 x: r.right - 1,
17744 y: r.top,
17745 width: 1,
17746 height: r.height
17747 },
17748 face: "right",
17749 element: dockEl
17750 });
17751 }
17752 }
17753 const widgetCards = document.querySelectorAll(
17754 ".desktop-mode-widgets__card"
17755 );
17756 let widgetIndex = 0;
17757 widgetCards.forEach((card) => {
17758 const r = card.getBoundingClientRect();
17759 if (r.width === 0 && r.height === 0) {
17760 return;
17761 }
17762 const id = card.dataset.widgetId ?? String(widgetIndex++);
17763 seed2.push({
17764 id: `widget:${id}`,
17765 kind: "widget",
17766 rect: rectFromDom(r),
17767 face: "top",
17768 element: card
17769 });
17770 });
17771 const filtered = applyFilters(HOOKS.WALLPAPER_SURFACES, seed2);
17772 return Array.isArray(filtered) ? filtered : seed2;
17773 }
17774 function rectFromDom(r) {
17775 return {
17776 x: r.left,
17777 y: r.top,
17778 width: r.width,
17779 height: r.height
17780 };
17781 }
17782 const NODE_KEY_PROP = "__desktop_modeKeyedListKey";
17783 const NODE_DATA_PROP = "__desktop_modeKeyedListData";
17784 function getHostState(host) {
17785 const cached = host.__desktop_modeKeyedList;
17786 if (cached) {
17787 return cached;
17788 }
17789 const fresh = { byKey: /* @__PURE__ */ new Map() };
17790 host.__desktop_modeKeyedList = fresh;
17791 return fresh;
17792 }
17793 function renderKeyedList(host, items, opts) {
17794 const state2 = getHostState(host);
17795 const prev = state2.byKey;
17796 const next = /* @__PURE__ */ new Map();
17797 const ordered = [];
17798 const seenKeys = /* @__PURE__ */ new Set();
17799 for (const item of items) {
17800 const key = String(opts.keyOf(item));
17801 if (seenKeys.has(key)) {
17802 console.warn(
17803 "[desktop-mode/keyed-list] duplicate key — only the last item with this key will render:",
17804 key
17805 );
17806 }
17807 seenKeys.add(key);
17808 const reused = prev.get(key);
17809 if (reused) {
17810 const prevData = reused.data;
17811 opts.updateItem?.(reused.el, item, prevData);
17812 reused.data = item;
17813 next.set(key, reused);
17814 ordered.push(reused.el);
17815 continue;
17816 }
17817 const el = opts.buildItem(item);
17818 el[NODE_KEY_PROP] = key;
17819 el[NODE_DATA_PROP] = item;
17820 next.set(key, { el, data: item });
17821 ordered.push(el);
17822 }
17823 for (const [key, entry] of prev) {
17824 if (!next.has(key)) {
17825 entry.el.remove();
17826 }
17827 }
17828 for (let i = 0; i < ordered.length; i++) {
17829 const desired = ordered[i];
17830 const live = host.children[i];
17831 if (live === desired) {
17832 continue;
17833 }
17834 host.insertBefore(desired, live ?? null);
17835 }
17836 state2.byKey = next;
17837 }
17838 function clearKeyedList(host) {
17839 const cached = host.__desktop_modeKeyedList;
17840 if (!cached) {
17841 return;
17842 }
17843 for (const entry of cached.byKey.values()) {
17844 entry.el.remove();
17845 }
17846 cached.byKey.clear();
17847 delete host.__desktop_modeKeyedList;
17848 }
17849 function createInfiniteList(options) {
17850 const {
17851 root,
17852 fetchPage,
17853 getId,
17854 renderItem,
17855 rootMargin = "200px",
17856 initialCursor = null,
17857 onLoadingChange = () => void 0,
17858 onError = (err) => {
17859 if (typeof console !== "undefined") {
17860 console.error("[desktop-mode] createInfiniteList:", err);
17861 }
17862 }
17863 } = options;
17864 let sentinel = options.sentinel ?? null;
17865 if (!sentinel) {
17866 sentinel = document.createElement("div");
17867 sentinel.dataset.wpdInfiniteListSentinel = "";
17868 sentinel.style.height = "1px";
17869 root.appendChild(sentinel);
17870 }
17871 const seen = /* @__PURE__ */ new Set();
17872 let cursor = initialCursor;
17873 let hasMoreInternal = true;
17874 let loading = false;
17875 let controller = null;
17876 let renderedCount = 0;
17877 let destroyed = false;
17878 let observer = null;
17879 const setLoading = (next) => {
17880 if (loading === next) {
17881 return;
17882 }
17883 loading = next;
17884 try {
17885 onLoadingChange(next);
17886 } catch (err) {
17887 onError(err);
17888 }
17889 };
17890 const detachObserver = () => {
17891 if (observer) {
17892 observer.disconnect();
17893 observer = null;
17894 }
17895 };
17896 const ensureObserver = () => {
17897 if (observer || !sentinel || destroyed) {
17898 return;
17899 }
17900 observer = new IntersectionObserver(
17901 (entries) => {
17902 for (const entry of entries) {
17903 if (entry.isIntersecting) {
17904 void loadMore();
17905 }
17906 }
17907 },
17908 { rootMargin }
17909 );
17910 observer.observe(sentinel);
17911 };
17912 const loadMore = async () => {
17913 if (destroyed || loading || !hasMoreInternal) {
17914 return;
17915 }
17916 setLoading(true);
17917 controller = new AbortController();
17918 const localController = controller;
17919 try {
17920 const page = await fetchPage(cursor, localController.signal);
17921 if (destroyed || localController !== controller) {
17922 return;
17923 }
17924 let appended = 0;
17925 const frag = document.createDocumentFragment();
17926 for (const item of page.items ?? []) {
17927 const key = String(getId(item));
17928 if (seen.has(key)) {
17929 continue;
17930 }
17931 seen.add(key);
17932 const el = renderItem(item, renderedCount + appended);
17933 frag.appendChild(el);
17934 appended++;
17935 }
17936 if (appended > 0) {
17937 if (sentinel && sentinel.parentNode === root) {
17938 root.insertBefore(frag, sentinel);
17939 } else {
17940 root.appendChild(frag);
17941 }
17942 renderedCount += appended;
17943 }
17944 cursor = page.nextCursor ?? null;
17945 if (!cursor) {
17946 hasMoreInternal = false;
17947 detachObserver();
17948 }
17949 } catch (err) {
17950 if (err?.name === "AbortError") {
17951 return;
17952 }
17953 onError(err);
17954 } finally {
17955 if (localController === controller) {
17956 setLoading(false);
17957 controller = null;
17958 }
17959 }
17960 };
17961 const reset = () => {
17962 if (destroyed) {
17963 return;
17964 }
17965 controller?.abort();
17966 controller = null;
17967 seen.clear();
17968 cursor = initialCursor;
17969 hasMoreInternal = true;
17970 renderedCount = 0;
17971 const sentinelInRoot = sentinel && sentinel.parentNode === root;
17972 while (root.firstChild) {
17973 root.removeChild(root.firstChild);
17974 }
17975 if (sentinelInRoot && sentinel) {
17976 root.appendChild(sentinel);
17977 }
17978 setLoading(false);
17979 ensureObserver();
17980 void loadMore();
17981 };
17982 const destroy = () => {
17983 if (destroyed) {
17984 return;
17985 }
17986 destroyed = true;
17987 detachObserver();
17988 controller?.abort();
17989 controller = null;
17990 if (!options.sentinel && sentinel && sentinel.parentNode === root) {
17991 root.removeChild(sentinel);
17992 }
17993 sentinel = null;
17994 setLoading(false);
17995 };
17996 ensureObserver();
17997 void loadMore();
17998 return {
17999 reset,
18000 loadMore,
18001 hasMore: () => hasMoreInternal,
18002 isLoading: () => loading,
18003 destroy
18004 };
18005 }
18006 const POPUP_DEFAULT_WIDTH = 520;
18007 const POPUP_DEFAULT_HEIGHT = 720;
18008 const POPUP_CLOSE_POLL_MS = 500;
18009 function startOAuth(service, options = {}) {
18010 if (typeof service !== "string" || service === "") {
18011 return Promise.reject(
18012 new Error("[desktop-mode] startOAuth requires a non-empty service slug.")
18013 );
18014 }
18015 const restRoot2 = readRestRoot$1();
18016 const restNonce = readRestNonce$1();
18017 return trackedFetch$1(
18018 joinRestUrl(restRoot2, "desktop-mode/v1/oauth/start"),
18019 {
18020 method: "POST",
18021 headers: {
18022 "Content-Type": "application/json",
18023 "X-WP-Nonce": restNonce ?? ""
18024 },
18025 body: JSON.stringify({ service })
18026 },
18027 { source: "desktop-mode/oauth-start" }
18028 ).then(async (res) => {
18029 if (!res.ok) {
18030 const text = await res.text().catch(() => "");
18031 throw new Error(
18032 `[desktop-mode] OAuth start failed (${res.status}): ${text}`
18033 );
18034 }
18035 return await res.json();
18036 }).then((startBody) => openPopupAndWait(startBody, service, options));
18037 }
18038 function openPopupAndWait(body, service, options) {
18039 return new Promise((resolve2, reject) => {
18040 const width = options.width ?? POPUP_DEFAULT_WIDTH;
18041 const height = options.height ?? POPUP_DEFAULT_HEIGHT;
18042 const left = Math.max(0, Math.floor((window.screen.width - width) / 2));
18043 const top = Math.max(0, Math.floor((window.screen.height - height) / 2));
18044 const features = [
18045 `width=${width}`,
18046 `height=${height}`,
18047 `left=${left}`,
18048 `top=${top}`,
18049 "menubar=no",
18050 "toolbar=no",
18051 "location=yes",
18052 "status=no",
18053 "resizable=yes",
18054 "scrollbars=yes"
18055 ].join(",");
18056 const popup = window.open(
18057 body.authorize_url,
18058 `desktop-mode-oauth-${service}`,
18059 features
18060 );
18061 if (!popup) {
18062 reject(
18063 new Error(
18064 "[desktop-mode] OAuth popup blocked. Tell users to allow popups for this site."
18065 )
18066 );
18067 return;
18068 }
18069 const expectedOrigin = window.location.origin;
18070 let pollTimer = null;
18071 let detached = false;
18072 const cleanup = () => {
18073 if (detached) {
18074 return;
18075 }
18076 detached = true;
18077 window.removeEventListener("message", onMessage);
18078 if (pollTimer !== null) {
18079 window.clearInterval(pollTimer);
18080 pollTimer = null;
18081 }
18082 };
18083 const onMessage = (e) => {
18084 if (e.origin !== expectedOrigin) {
18085 return;
18086 }
18087 const data = e.data;
18088 if (!data || data.type !== "desktop-mode-oauth-callback") {
18089 return;
18090 }
18091 const payload = data.payload;
18092 cleanup();
18093 if (payload && payload.ok) {
18094 resolve2(payload);
18095 } else {
18096 const reason = payload?.reason ?? "unknown";
18097 const message = payload?.message ?? "OAuth flow failed";
18098 const err = new Error(
18099 `[desktop-mode] startOAuth(${service}) failed: ${reason} — ${message}`
18100 );
18101 err.cause = payload;
18102 reject(err);
18103 }
18104 };
18105 window.addEventListener("message", onMessage);
18106 pollTimer = window.setInterval(() => {
18107 if (popup.closed) {
18108 cleanup();
18109 reject(
18110 new Error(
18111 `[desktop-mode] startOAuth(${service}) cancelled — popup closed before completing.`
18112 )
18113 );
18114 }
18115 }, POPUP_CLOSE_POLL_MS);
18116 });
18117 }
18118 function readDesktopConfig() {
18119 return window.desktopModeConfig ?? {};
18120 }
18121 function readRestRoot$1() {
18122 const root = readDesktopConfig().restRoot;
18123 if (typeof root === "string" && root !== "") {
18124 return root;
18125 }
18126 return `${window.location.origin}/wp-json/`;
18127 }
18128 function readRestNonce$1() {
18129 const nonce = readDesktopConfig().restNonce;
18130 return typeof nonce === "string" && nonce !== "" ? nonce : null;
18131 }
18132 const RESERVED_NAMESPACE_KEYS = /* @__PURE__ */ new Set([
18133 "windowManager",
18134 "dock",
18135 "taskbar",
18136 "icons",
18137 "saveSession",
18138 "hooks",
18139 "HOOKS",
18140 "isActive",
18141 "registerWallpaper",
18142 "registerWidget",
18143 "widgetLayer",
18144 "widgets",
18145 "registerSystemTile",
18146 "registerWindow",
18147 "openWindow",
18148 "cloneTemplate",
18149 "onWindow",
18150 "loadVendorScript",
18151 "getWallpaperSurfaces",
18152 "registerModule",
18153 "loadModules",
18154 "whenReady",
18155 "ready",
18156 "isReady",
18157 "setDefaultWindow",
18158 "refreshMenu",
18159 "config",
18160 "ai",
18161 "dragBridge",
18162 "dragManager",
18163 "registerCommand",
18164 "unregisterCommand",
18165 "listCommands",
18166 "registerDestructiveAdminAction",
18167 "unregisterDestructiveAdminAction",
18168 "listDestructiveAdminActions",
18169 "registerSettingsTab",
18170 "unregisterSettingsTab",
18171 "listSettingsTabs",
18172 "registerDockRailRenderer",
18173 "unregisterDockRailRenderer",
18174 "listDockRailRenderers",
18175 "openOsSettings",
18176 "getOsSettings",
18177 "subscribeOsSettings",
18178 "updateOsSettings",
18179 "deriveWindowId",
18180 "listSystemTiles",
18181 "getSystemTile",
18182 "getMenuItems",
18183 "renderIcon",
18184 "applyTileClasses",
18185 "applyTileElement",
18186 "applyTileTooltip",
18187 "dispatchTileRendered",
18188 "isDockElement",
18189 "registerDockSelector",
18190 "registerTitleBarButton",
18191 "unregisterTitleBarButton",
18192 "listTitleBarButtons",
18193 "registerWindowTheme",
18194 "unregisterWindowTheme",
18195 "listWindowThemes",
18196 "applyWindowTheme",
18197 "registerWindowControl",
18198 "unregisterWindowControl",
18199 "listWindowControls",
18200 "applyWindowControls",
18201 "registerWindowSlot",
18202 "unregisterWindowSlot",
18203 "listWindowSlots",
18204 "applyWindowSlot",
18205 "registerWindowNotice",
18206 "unregisterWindowNotice",
18207 "listWindowNotices",
18208 "dismissWindowNotice",
18209 "undismissWindowNotice",
18210 "registerWindowChrome",
18211 "unregisterWindowChrome",
18212 "listWindowChromes",
18213 "applyWindowChrome",
18214 "connect",
18215 "broadcast",
18216 "subscribe",
18217 "registerPalette",
18218 "unregisterPalette",
18219 "listPalettes",
18220 "openPalette",
18221 "devtools",
18222 "createSharedStore",
18223 "presence",
18224 "activity",
18225 "heartbeat",
18226 "showToast",
18227 "renderKeyedList",
18228 "clearKeyedList",
18229 "registerNamespace",
18230 "notify",
18231 "pwa",
18232 "getWindowConfig",
18233 "debug",
18234 "fetch"
18235 ]);
18236 function buildPublicApi(deps2) {
18237 const {
18238 manager,
18239 dock,
18240 layoutDispatcher,
18241 osSettings,
18242 iconsApi: iconsApi2,
18243 filesApi: filesApi2,
18244 saveSession,
18245 widgetLayer,
18246 registerWindow,
18247 openWindowById,
18248 openNewWindowById,
18249 placeSystemTile,
18250 setDefaultWindow,
18251 refreshMenu,
18252 openOsSettings,
18253 aiAssistant,
18254 dragBridge,
18255 dragManager,
18256 connect,
18257 getConnection,
18258 config
18259 } = deps2;
18260 const desktopApi = {
18261 windowManager: manager,
18262 dock,
18263 sideDock: layoutDispatcher?.getSide() ?? null,
18264 desktopLayout: osSettings.getOsSettingsSnapshot().desktopLayout,
18265 icons: iconsApi2,
18266 files: filesApi2,
18267 confirm: wpdConfirm,
18268 saveSession,
18269 hooks: rawHooks(),
18270 HOOKS,
18271 isActive: () => !!document.getElementById("desktop-mode-shell"),
18272 registerWallpaper: (def) => {
18273 register$2(def);
18274 osSettings.apply();
18275 },
18276 registerWidget: (def) => {
18277 register(def);
18278 },
18279 widgetLayer,
18280 widgets: {
18281 redock: (id) => {
18282 widgetLayer?.redock(id);
18283 }
18284 },
18285 loadVendorScript,
18286 getWallpaperSurfaces: () => collectWallpaperSurfaces(manager),
18287 registerWindow,
18288 openWindow: openWindowById,
18289 openNewWindow: openNewWindowById,
18290 fetch: (input, requestInit, opts) => trackedFetch(manager, input, requestInit, opts),
18291 repaintLoadingOverlays,
18292 cloneTemplate,
18293 onWindow,
18294 createInfiniteList,
18295 startOAuth,
18296 registerSystemTile: (item) => {
18297 placeSystemTile(item);
18298 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: item.id });
18299 },
18300 registerModule,
18301 loadModules,
18302 whenReady,
18303 ready: whenReady,
18304 isReady,
18305 setDefaultWindow,
18306 refreshMenu,
18307 config,
18308 ai: aiAssistant,
18309 dragBridge,
18310 dragManager,
18311 registerCommand,
18312 unregisterCommand,
18313 listCommands,
18314 registerDestructiveAdminAction,
18315 unregisterDestructiveAdminAction,
18316 listDestructiveAdminActions,
18317 registerSettingsTab,
18318 unregisterSettingsTab,
18319 listSettingsTabs,
18320 registerDockRailRenderer: register$1,
18321 unregisterDockRailRenderer: unregister$1,
18322 listDockRailRenderers: list,
18323 openOsSettings,
18324 getOsSettings: () => osSettings.getOsSettingsSnapshot(),
18325 subscribeOsSettings: (cb) => osSettings.subscribeOsSettings(cb),
18326 updateOsSettings: (patch, opts = {}) => {
18327 if (typeof patch.wallpaper === "string") {
18328 osSettings.state.wallpaper = patch.wallpaper;
18329 }
18330 if (typeof patch.accent === "string") {
18331 osSettings.state.accent = patch.accent;
18332 }
18333 if (typeof patch.dockSize === "string") {
18334 osSettings.state.dockSize = patch.dockSize;
18335 }
18336 if (typeof patch.desktopLayout === "string") {
18337 osSettings.state.desktopLayout = patch.desktopLayout;
18338 }
18339 if (typeof patch.dockRailRenderer === "string") {
18340 osSettings.state.dockRailRenderer = patch.dockRailRenderer;
18341 }
18342 if (patch.ai && typeof patch.ai === "object") {
18343 osSettings.state.ai = { ...osSettings.state.ai, ...patch.ai };
18344 }
18345 if (typeof patch.nativePostsEnabled === "boolean") {
18346 osSettings.state.nativePostsEnabled = patch.nativePostsEnabled;
18347 }
18348 if (typeof patch.nativePagesEnabled === "boolean") {
18349 osSettings.state.nativePagesEnabled = patch.nativePagesEnabled;
18350 }
18351 if (typeof patch.nativeUsersEnabled === "boolean") {
18352 osSettings.state.nativeUsersEnabled = patch.nativeUsersEnabled;
18353 }
18354 if (typeof patch.nativePluginsEnabled === "boolean") {
18355 osSettings.state.nativePluginsEnabled = patch.nativePluginsEnabled;
18356 }
18357 if (typeof patch.nativeCommentsEnabled === "boolean") {
18358 osSettings.state.nativeCommentsEnabled = patch.nativeCommentsEnabled;
18359 }
18360 if (typeof patch.foldersSharingEnabled === "boolean") {
18361 osSettings.state.foldersSharingEnabled = patch.foldersSharingEnabled;
18362 }
18363 if (Array.isArray(patch.nativePostsHiddenColumns)) {
18364 osSettings.state.nativePostsHiddenColumns = patch.nativePostsHiddenColumns.filter(
18365 (v) => typeof v === "string" && v !== ""
18366 ).slice(0, 32);
18367 }
18368 if (patch.itemVisibility && typeof patch.itemVisibility === "object") {
18369 const allowed = ["both", "dock", "desktop", "hidden"];
18370 const next = {};
18371 for (const [k, v] of Object.entries(
18372 patch.itemVisibility
18373 )) {
18374 if (typeof k !== "string" || k === "") {
18375 continue;
18376 }
18377 if (typeof v !== "string" || !allowed.includes(v)) {
18378 continue;
18379 }
18380 next[k] = v;
18381 }
18382 osSettings.state.itemVisibility = next;
18383 }
18384 if (Array.isArray(patch.dockOrder)) {
18385 osSettings.state.dockOrder = patch.dockOrder.filter(
18386 (v) => typeof v === "string" && v !== ""
18387 ).slice(0, 256);
18388 }
18389 if (patch.dockPromotedPositions && typeof patch.dockPromotedPositions === "object") {
18390 const MAX_COORD = 1e5;
18391 const next = {};
18392 for (const [k, v] of Object.entries(
18393 patch.dockPromotedPositions
18394 )) {
18395 if (typeof k !== "string" || k === "") {
18396 continue;
18397 }
18398 if (!v || typeof v !== "object") {
18399 continue;
18400 }
18401 const pos = v;
18402 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) {
18403 continue;
18404 }
18405 next[k] = { x: pos.x, y: pos.y };
18406 if (Object.keys(next).length >= 256) {
18407 break;
18408 }
18409 }
18410 osSettings.state.dockPromotedPositions = next;
18411 }
18412 osSettings.save(opts);
18413 if (patch.itemVisibility || patch.dockOrder) {
18414 layoutDispatcher?.refresh();
18415 }
18416 },
18417 deriveWindowId: (url, overrideAdminUrl) => deriveWindowId(url, overrideAdminUrl ?? config.adminUrl),
18418 listSystemTiles: () => layoutDispatcher?.listSystemTiles() ?? [],
18419 getSystemTile: (id) => layoutDispatcher?.getSystemTile(id) ?? null,
18420 getMenuItems: () => layoutDispatcher?.getMenuItems() ?? [],
18421 renderIcon,
18422 applyTileClasses,
18423 applyTileElement,
18424 applyTileTooltip,
18425 dispatchTileRendered,
18426 isDockElement,
18427 registerDockSelector,
18428 registerTitleBarButton,
18429 unregisterTitleBarButton,
18430 listTitleBarButtons,
18431 registerWindowTheme,
18432 unregisterWindowTheme,
18433 listWindowThemes,
18434 applyWindowTheme: (windowId, override) => {
18435 const win = manager.getById(windowId);
18436 if (!win) {
18437 return;
18438 }
18439 win.setAppearanceTheme(override);
18440 },
18441 registerWindowControl,
18442 unregisterWindowControl,
18443 listWindowControls,
18444 applyWindowControls: (windowId, override) => {
18445 const win = manager.getById(windowId);
18446 if (!win) {
18447 return;
18448 }
18449 win.setAppearanceControls(override);
18450 },
18451 registerWindowSlot,
18452 unregisterWindowSlot,
18453 listWindowSlots,
18454 applyWindowSlot: (windowId, slot, slotConfig) => {
18455 const win = manager.getById(windowId);
18456 if (!win) {
18457 return;
18458 }
18459 win.setAppearanceSlot(slot, slotConfig);
18460 },
18461 registerWindowNotice,
18462 unregisterWindowNotice,
18463 listWindowNotices,
18464 dismissWindowNotice,
18465 undismissWindowNotice,
18466 registerWindowChrome,
18467 unregisterWindowChrome,
18468 listWindowChromes,
18469 applyWindowChrome: (windowId, chromeId) => {
18470 const win = manager.getById(windowId);
18471 if (!win) {
18472 return;
18473 }
18474 win.setAppearanceChrome(chromeId);
18475 },
18476 connect,
18477 getConnection,
18478 broadcast,
18479 subscribe: subscribe$2,
18480 registerPalette,
18481 unregisterPalette,
18482 listPalettes,
18483 openPalette: openPaletteOnly,
18484 devtools,
18485 createSharedStore,
18486 presence: presenceApi,
18487 activity,
18488 heartbeat,
18489 showToast,
18490 notify: notify$3,
18491 pwa: {
18492 promptInstall,
18493 undismissInstallHint,
18494 getState: getPwaState,
18495 subscribe: subscribePwaState,
18496 requestNotificationPermission,
18497 getNotificationPermission
18498 },
18499 renderKeyedList,
18500 clearKeyedList,
18501 registerNamespace: (name, api) => {
18502 if (typeof name !== "string" || name === "") {
18503 console.warn(
18504 "[desktop-mode] registerNamespace: name must be a non-empty string"
18505 );
18506 return;
18507 }
18508 if (!api || typeof api !== "object") {
18509 console.warn(
18510 `[desktop-mode] registerNamespace("${name}"): api must be an object`
18511 );
18512 return;
18513 }
18514 if (RESERVED_NAMESPACE_KEYS.has(name)) {
18515 console.warn(
18516 `[desktop-mode] registerNamespace("${name}"): name is reserved by the shell — pick a plugin-specific key`
18517 );
18518 return;
18519 }
18520 desktopApi[name] = api;
18521 },
18522 getWindowConfig: (id) => {
18523 const store2 = window.desktopModeWindowConfig;
18524 if (!store2 || typeof store2 !== "object") {
18525 return void 0;
18526 }
18527 const value = store2[id];
18528 return value === void 0 ? void 0 : value;
18529 },
18530 debug: {
18531 window: (id) => {
18532 const entry = (config.nativeWindows ?? []).find(
18533 (e) => e.id === id
18534 );
18535 if (!entry) {
18536 return null;
18537 }
18538 const url = entry.scriptUrl || "";
18539 let loadPath = "unknown";
18540 let tagInDom = false;
18541 if (url) {
18542 const lazyTag = document.querySelector(
18543 `script[data-desktop-mode-vendor="${url.replace(/"/g, '\\"')}"]`
18544 );
18545 if (lazyTag) {
18546 loadPath = "lazy";
18547 tagInDom = true;
18548 } else {
18549 const eagerTag = Array.from(
18550 document.querySelectorAll(
18551 "script[src]"
18552 )
18553 ).find((s) => s.src === url);
18554 if (eagerTag) {
18555 loadPath = "eager";
18556 tagInDom = true;
18557 }
18558 }
18559 }
18560 const cfgStore = window.desktopModeWindowConfig;
18561 const configPresent = !!(cfgStore && typeof cfgStore === "object" && Object.prototype.hasOwnProperty.call(cfgStore, id));
18562 return {
18563 id,
18564 scriptHandle: entry.scriptHandle || "",
18565 scriptUrl: url,
18566 loadPath,
18567 tagInDom,
18568 configPresent,
18569 extras: {
18570 hasTranslations: !!entry.scriptTranslations,
18571 l10nCount: (entry.scriptL10n ?? []).length,
18572 beforeCount: (entry.scriptBefore ?? []).length,
18573 afterCount: (entry.scriptAfter ?? []).length
18574 }
18575 };
18576 }
18577 }
18578 };
18579 return desktopApi;
18580 }
18581 function installPublicApi(api) {
18582 if (!window.wp) {
18583 window.wp = {};
18584 }
18585 if (!window.wp.desktop) {
18586 window.wp.desktop = api;
18587 return;
18588 }
18589 Object.assign(
18590 window.wp.desktop,
18591 api
18592 );
18593 }
18594 const store$1 = createSharedStore("desktop-mode/layout", () => ({
18595 // Default mirrors the OsSettingsSnapshot default; the shell
18596 // re-publishes the persisted value as soon as it boots.
18597 layout: "classic"
18598 }));
18599 function setCurrentLayout(layout) {
18600 if (store$1.state.layout === layout) {
18601 return;
18602 }
18603 store$1.state.layout = layout;
18604 store$1.notify();
18605 }
18606 class DesktopFile {
18607 constructor(shape) {
18608 this.shape = shape;
18609 }
18610 /** Title shown under the tile. Defaults to `shape.title`. */
18611 title() {
18612 return this.shape.title;
18613 }
18614 /** Dashicon class or data URI. Defaults to `shape.icon`. */
18615 icon() {
18616 return this.shape.icon;
18617 }
18618 /** Optional preview-image URL. Defaults to `shape.previewUrl`. */
18619 previewUrl() {
18620 return this.shape.previewUrl;
18621 }
18622 /** Reference (id, URL, …). */
18623 ref() {
18624 return this.shape.ref;
18625 }
18626 /** Whether the underlying entity still exists. */
18627 exists() {
18628 return this.shape.exists;
18629 }
18630 }
18631 class DefaultDesktopFile extends DesktopFile {
18632 constructor(shape, typeSlug) {
18633 super(shape);
18634 this.typeSlug = typeSlug;
18635 }
18636 type() {
18637 return this.typeSlug;
18638 }
18639 }
18640 const seed$1 = /* @__PURE__ */ new Map();
18641 const listeners$1 = /* @__PURE__ */ new Set();
18642 function registerType(def) {
18643 if (!def.type) {
18644 throw new Error("[desktop-mode] registerType: `type` is required.");
18645 }
18646 if (!def.label) {
18647 throw new Error("[desktop-mode] registerType: `label` is required.");
18648 }
18649 seed$1.set(def.type, {
18650 type: def.type,
18651 label: def.label,
18652 sort: typeof def.sort === "number" ? def.sort : 100,
18653 DesktopFile: def.DesktopFile
18654 });
18655 doAction("desktop-mode.files.type-registered", def.type, def);
18656 notify$1();
18657 }
18658 function unregisterType(typeSlug) {
18659 if (seed$1.delete(typeSlug)) {
18660 doAction("desktop-mode.files.type-unregistered", typeSlug);
18661 notify$1();
18662 }
18663 }
18664 function getType(typeSlug) {
18665 const entry = seed$1.get(typeSlug);
18666 return entry ? entry : null;
18667 }
18668 function getTypes() {
18669 const list2 = Array.from(seed$1.values()).slice();
18670 const filtered = applyFilters(
18671 "desktop-mode.files.types",
18672 list2
18673 );
18674 const arr = Array.isArray(filtered) ? filtered : list2;
18675 arr.sort((a, b) => {
18676 if (a.sort !== b.sort) {
18677 return a.sort - b.sort;
18678 }
18679 return a.label.localeCompare(b.label);
18680 });
18681 return arr;
18682 }
18683 function resolve(shape) {
18684 const entry = seed$1.get(shape.type);
18685 if (entry?.DesktopFile) {
18686 return new entry.DesktopFile(shape);
18687 }
18688 return new DefaultDesktopFile(shape, shape.type);
18689 }
18690 function subscribe(cb) {
18691 listeners$1.add(cb);
18692 return () => listeners$1.delete(cb);
18693 }
18694 function notify$1() {
18695 for (const cb of listeners$1) {
18696 try {
18697 cb();
18698 } catch (err) {
18699 console.error("[desktop-mode] files registry subscriber threw:", err);
18700 }
18701 }
18702 }
18703 const seed = /* @__PURE__ */ new Map();
18704 const listeners = /* @__PURE__ */ new Set();
18705 let userAssociations = {};
18706 function setUserAssociations(map) {
18707 userAssociations = { ...map };
18708 notify();
18709 }
18710 function getUserAssociations() {
18711 return { ...userAssociations };
18712 }
18713 function registerOpener(def) {
18714 if (!def.id) {
18715 throw new Error("[desktop-mode] registerOpener: `id` is required.");
18716 }
18717 if (!def.label) {
18718 throw new Error("[desktop-mode] registerOpener: `label` is required.");
18719 }
18720 if (!Array.isArray(def.types) || def.types.length === 0) {
18721 throw new Error("[desktop-mode] registerOpener: `types` must be a non-empty array.");
18722 }
18723 if (!def.handler || typeof def.handler !== "object") {
18724 throw new Error("[desktop-mode] registerOpener: `handler` is required.");
18725 }
18726 seed.set(def.id, {
18727 id: def.id,
18728 label: def.label,
18729 types: def.types.slice(),
18730 isDefault: !!def.isDefault,
18731 sort: typeof def.sort === "number" ? def.sort : 100,
18732 handler: def.handler
18733 });
18734 doAction("desktop-mode.files.opener-registered", def.id, def);
18735 notify();
18736 }
18737 function unregisterOpener(id) {
18738 if (seed.delete(id)) {
18739 doAction("desktop-mode.files.opener-unregistered", id);
18740 notify();
18741 }
18742 }
18743 function getOpener(id) {
18744 return seed.get(id) ?? null;
18745 }
18746 function getOpeners() {
18747 const list2 = Array.from(seed.values()).slice();
18748 const filtered = applyFilters(
18749 "desktop-mode.files.openers",
18750 list2
18751 );
18752 const arr = Array.isArray(filtered) ? filtered : list2;
18753 arr.sort((a, b) => {
18754 const sa = typeof a.sort === "number" ? a.sort : 100;
18755 const sb = typeof b.sort === "number" ? b.sort : 100;
18756 if (sa !== sb) {
18757 return sa - sb;
18758 }
18759 return a.label.localeCompare(b.label);
18760 });
18761 return arr;
18762 }
18763 function getOpenersForType(type) {
18764 return getOpeners().filter((e) => e.types.includes(type));
18765 }
18766 function resolveOpener(type) {
18767 const candidates = getOpenersForType(type);
18768 if (candidates.length === 0) {
18769 return null;
18770 }
18771 const override = userAssociations[type];
18772 let resolved = null;
18773 if (override) {
18774 resolved = candidates.find((e) => e.id === override) ?? null;
18775 }
18776 if (!resolved) {
18777 resolved = candidates.find((e) => e.isDefault) ?? null;
18778 }
18779 if (!resolved) {
18780 resolved = candidates[0];
18781 }
18782 const filtered = applyFilters(
18783 "desktop-mode.files.resolve-opener",
18784 resolved,
18785 type
18786 );
18787 return filtered ?? null;
18788 }
18789 function subscribeOpeners(cb) {
18790 listeners.add(cb);
18791 return () => listeners.delete(cb);
18792 }
18793 function notify() {
18794 for (const cb of listeners) {
18795 try {
18796 cb();
18797 } catch (err) {
18798 console.error("[desktop-mode] openers subscriber threw:", err);
18799 }
18800 }
18801 }
18802 let deps$1 = null;
18803 function installOpenDeps(next) {
18804 deps$1 = next;
18805 }
18806 async function openFile(file, ctx) {
18807 if (!deps$1) {
18808 console.warn(
18809 "[desktop-mode] wp.desktop.files.open() called before the shell installed open deps. The file will not open."
18810 );
18811 return false;
18812 }
18813 const opener = resolveOpener(file.type());
18814 if (!opener) {
18815 doAction("desktop-mode.files.open-failed", {
18816 reason: "no-opener",
18817 type: file.type(),
18818 ref: file.ref()
18819 });
18820 return false;
18821 }
18822 doAction("desktop-mode.files.opening", { file, openerId: opener.id });
18823 try {
18824 const handler = opener.handler;
18825 if (handler.kind === "url") {
18826 const url = await handler.url(file);
18827 if (!url) {
18828 return false;
18829 }
18830 const id = handler.windowId ? handler.windowId(file) : deps$1.deriveWindowId(url);
18831 const title = handler.title ? handler.title(file) : file.title();
18832 const icon = file.icon();
18833 const opened = deps$1.openUrl({ id, url, title, icon });
18834 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "url" });
18835 return opened;
18836 }
18837 if (handler.kind === "window") {
18838 const config = handler.config ? handler.config(file) : void 0;
18839 const opened = deps$1.openNativeWindow(handler.windowId, config);
18840 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "window" });
18841 return opened;
18842 }
18843 await handler.open(file, ctx);
18844 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "js" });
18845 return true;
18846 } catch (err) {
18847 doAction("desktop-mode.files.open-failed", {
18848 reason: "handler-threw",
18849 type: file.type(),
18850 ref: file.ref(),
18851 openerId: opener.id,
18852 error: err
18853 });
18854 console.error("[desktop-mode] file opener threw:", err);
18855 return false;
18856 }
18857 }
18858 function registerBuiltInFileTypes() {
18859 registerType({ type: "shortcut", label: "Plugin shortcut", sort: 1 });
18860 registerType({ type: "folder", label: "Folder", sort: 5 });
18861 registerType({ type: "post", label: "Post", sort: 10 });
18862 registerType({ type: "attachment", label: "Media", sort: 20 });
18863 registerType({ type: "user", label: "User", sort: 30 });
18864 registerType({ type: "term", label: "Taxonomy term", sort: 40 });
18865 registerType({ type: "comment", label: "Comment", sort: 50 });
18866 registerType({ type: "bookmark", label: "Bookmark", sort: 60 });
18867 registerType({ type: "link", label: "Web link", sort: 70 });
18868 registerType({ type: "embed", label: "Embedded web window", sort: 80 });
18869 }
18870 let deps = null;
18871 function installRestDeps(next) {
18872 deps = next;
18873 }
18874 function ensureDeps() {
18875 if (!deps) {
18876 throw new Error("[desktop-mode] files REST client called before installRestDeps().");
18877 }
18878 return deps;
18879 }
18880 class FilesConflictError extends Error {
18881 constructor(detail) {
18882 super(
18883 `Row was changed by ${detail.actor.name || "another session"} (parent="${detail.current.parentName}")`
18884 );
18885 this.name = "FilesConflictError";
18886 this.status = 409;
18887 this.detail = detail;
18888 }
18889 }
18890 async function call(path, init2) {
18891 const { baseUrl, nonce } = ensureDeps();
18892 const url = joinRestUrl(baseUrl, path);
18893 const headers = new Headers(init2.headers ?? {});
18894 headers.set("X-WP-Nonce", nonce);
18895 if (init2.body && !headers.has("Content-Type")) {
18896 headers.set("Content-Type", "application/json");
18897 }
18898 const res = await trackedFetch$1(
18899 url,
18900 { ...init2, headers, credentials: "same-origin" },
18901 { source: "desktop-mode/files" }
18902 );
18903 const text = await res.text();
18904 let body = null;
18905 let parseError = null;
18906 if (text) {
18907 try {
18908 body = JSON.parse(text);
18909 } catch (e) {
18910 body = null;
18911 parseError = e;
18912 }
18913 }
18914 if (!res.ok) {
18915 if (res.status === 409) {
18916 const data = body?.data?.data ?? body?.data;
18917 if (data && typeof data === "object") {
18918 throw new FilesConflictError(data);
18919 }
18920 }
18921 const err = body;
18922 throw new Error(
18923 `[desktop-mode] files REST ${res.status}: ${err?.code ?? ""} ${err?.message ?? ""}`.trim()
18924 );
18925 }
18926 if (null === body) {
18927 if (parseError && text) {
18928 const head = text.slice(0, 120).replace(/\s+/g, " ");
18929 throw new Error(
18930 `[desktop-mode] files REST ${res.status} returned non-JSON body — ${parseError.message}. First 120 chars: ${head}`
18931 );
18932 }
18933 throw new Error(
18934 `[desktop-mode] files REST ${res.status}: empty or unparseable body.`
18935 );
18936 }
18937 return body;
18938 }
18939 function listPlacements(folderId = 0) {
18940 return call(
18941 `/placements?folder=${encodeURIComponent(String(folderId))}`,
18942 { method: "GET" }
18943 );
18944 }
18945 function createPlacement(body) {
18946 return call("/placements", {
18947 method: "POST",
18948 body: JSON.stringify(body)
18949 });
18950 }
18951 function updatePlacement(id, body, ifMatchMs) {
18952 const headers = {};
18953 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
18954 headers["If-Match"] = String(ifMatchMs);
18955 }
18956 return call(`/placements/${id}`, {
18957 method: "PATCH",
18958 body: JSON.stringify(body),
18959 headers
18960 });
18961 }
18962 function deletePlacement(id) {
18963 return call(`/placements/${id}`, { method: "DELETE" });
18964 }
18965 async function restoreTrashedItem(id, type) {
18966 const { baseUrl, nonce } = ensureDeps();
18967 const root = baseUrl.replace(/\/files\/?$/, "");
18968 const url = `${root}/recycle-bin/restore`;
18969 const res = await trackedFetch$1(
18970 url,
18971 {
18972 method: "POST",
18973 headers: {
18974 "Content-Type": "application/json",
18975 "X-WP-Nonce": nonce
18976 },
18977 credentials: "same-origin",
18978 body: JSON.stringify({ items: [{ id, type }] })
18979 },
18980 { source: "desktop-mode/files" }
18981 );
18982 if (!res.ok) {
18983 throw new Error(`[desktop-mode] restore ${res.status}`);
18984 }
18985 return await res.json();
18986 }
18987 function listFolders() {
18988 return call("/folders", { method: "GET" });
18989 }
18990 function createFolder(body) {
18991 return call("/folders", {
18992 method: "POST",
18993 body: JSON.stringify(body)
18994 });
18995 }
18996 function updateFolder(id, body, ifMatchMs) {
18997 const headers = {};
18998 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
18999 headers["If-Match"] = String(ifMatchMs);
19000 }
19001 return call(`/folders/${id}`, {
19002 method: "PATCH",
19003 body: JSON.stringify(body),
19004 headers
19005 });
19006 }
19007 function deleteFolder(id) {
19008 return call(`/folders/${id}`, { method: "DELETE" });
19009 }
19010 function saveAssociations(associations) {
19011 return call("/associations", {
19012 method: "PUT",
19013 body: JSON.stringify({ associations })
19014 });
19015 }
19016 function listShares(folderId) {
19017 return call(`/folders/${folderId}/shares`, { method: "GET" });
19018 }
19019 function inviteShare(folderId, body) {
19020 return call(`/folders/${folderId}/shares`, {
19021 method: "POST",
19022 body: JSON.stringify(body)
19023 });
19024 }
19025 function updateShareCapability(folderId, shareId, capability) {
19026 return call(`/folders/${folderId}/shares/${shareId}`, {
19027 method: "PATCH",
19028 body: JSON.stringify({ capability })
19029 });
19030 }
19031 function revokeShare(folderId, shareId) {
19032 return call(`/folders/${folderId}/shares/${shareId}`, {
19033 method: "DELETE"
19034 });
19035 }
19036 function acceptShare(folderId, shareId) {
19037 return call(`/folders/${folderId}/shares/${shareId}/accept`, {
19038 method: "POST"
19039 });
19040 }
19041 function denyShare(folderId, shareId) {
19042 return call(`/folders/${folderId}/shares/${shareId}/deny`, {
19043 method: "POST"
19044 });
19045 }
19046 function leaveShare(folderId) {
19047 return call(`/folders/${folderId}/leave`, {
19048 method: "POST"
19049 });
19050 }
19051 function purgeFolderSharingTables() {
19052 return call(
19053 "/folder-sharing-tables/purge",
19054 { method: "POST" }
19055 );
19056 }
19057 const filesRest = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
19058 __proto__: null,
19059 FilesConflictError,
19060 acceptShare,
19061 createFolder,
19062 createPlacement,
19063 deleteFolder,
19064 deletePlacement,
19065 denyShare,
19066 installRestDeps,
19067 inviteShare,
19068 leaveShare,
19069 listFolders,
19070 listPlacements,
19071 listShares,
19072 purgeFolderSharingTables,
19073 restoreTrashedItem,
19074 revokeShare,
19075 saveAssociations,
19076 updateFolder,
19077 updatePlacement,
19078 updateShareCapability
19079 }, Symbol.toStringTag, { value: "Module" }));
19080 const STORE_KEY = "desktop-mode/files";
19081 function getFilesStore() {
19082 return createSharedStore(STORE_KEY, () => ({
19083 placementsByFolder: /* @__PURE__ */ new Map(),
19084 folders: /* @__PURE__ */ new Map(),
19085 hydratedFolders: /* @__PURE__ */ new Set()
19086 }));
19087 }
19088 function fireChanged(detail) {
19089 if (typeof document === "undefined") {
19090 return;
19091 }
19092 document.dispatchEvent(
19093 new CustomEvent("desktop-mode-files-changed", {
19094 detail: { source: "local", ...detail }
19095 })
19096 );
19097 }
19098 function setFolderPlacements(folderId, placements) {
19099 const store2 = getFilesStore();
19100 const next = new Map(store2.state.placementsByFolder);
19101 next.set(folderId, placements.slice());
19102 const hydrated = new Set(store2.state.hydratedFolders);
19103 hydrated.add(folderId);
19104 store2.state = { ...store2.state, placementsByFolder: next, hydratedFolders: hydrated };
19105 store2.notify();
19106 fireChanged({ kind: "placements-set", folderId });
19107 }
19108 function upsertPlacement(placement, source = "local") {
19109 if (!placement || typeof placement.id !== "number") {
19110 console.warn(
19111 "[desktop-mode] upsertPlacement called with a non-placement value; ignoring.",
19112 placement
19113 );
19114 return;
19115 }
19116 const store2 = getFilesStore();
19117 const next = new Map(store2.state.placementsByFolder);
19118 for (const [folderId, list2] of next) {
19119 const idx2 = list2.findIndex((p) => p && p.id === placement.id);
19120 if (idx2 >= 0 && folderId !== placement.parentId) {
19121 const copy = list2.filter(Boolean);
19122 const removeAt = copy.findIndex((p) => p.id === placement.id);
19123 if (removeAt >= 0) {
19124 copy.splice(removeAt, 1);
19125 }
19126 next.set(folderId, copy);
19127 }
19128 }
19129 const rawTarget = next.get(placement.parentId)?.slice() ?? [];
19130 const target2 = rawTarget.filter(Boolean);
19131 const idx = target2.findIndex((p) => p.id === placement.id);
19132 if (idx >= 0) {
19133 target2[idx] = placement;
19134 } else {
19135 target2.push(placement);
19136 }
19137 next.set(placement.parentId, target2);
19138 store2.state = { ...store2.state, placementsByFolder: next };
19139 store2.notify();
19140 fireChanged({ kind: "placement-upserted", placementId: placement.id, folderId: placement.parentId, source });
19141 }
19142 function removePlacement(placementId, source = "local") {
19143 const store2 = getFilesStore();
19144 const next = new Map(store2.state.placementsByFolder);
19145 let touchedFolder;
19146 for (const [folderId, list2] of next) {
19147 const idx = list2.findIndex((p) => p && p.id === placementId);
19148 if (idx >= 0) {
19149 const copy = list2.filter(Boolean).filter(
19150 (p) => p.id !== placementId
19151 );
19152 next.set(folderId, copy);
19153 touchedFolder = folderId;
19154 }
19155 }
19156 if (touchedFolder === void 0) {
19157 return;
19158 }
19159 store2.state = { ...store2.state, placementsByFolder: next };
19160 store2.notify();
19161 fireChanged({ kind: "placement-removed", placementId, folderId: touchedFolder, source });
19162 }
19163 function setFolders(folders) {
19164 const store2 = getFilesStore();
19165 const next = /* @__PURE__ */ new Map();
19166 for (const f of folders) {
19167 next.set(f.id, f);
19168 }
19169 store2.state = { ...store2.state, folders: next };
19170 store2.notify();
19171 fireChanged({ kind: "folders-set" });
19172 }
19173 function upsertFolder(folder, source = "local") {
19174 const store2 = getFilesStore();
19175 const next = new Map(store2.state.folders);
19176 next.set(folder.id, folder);
19177 store2.state = { ...store2.state, folders: next };
19178 store2.notify();
19179 fireChanged({ kind: "folder-upserted", folderRowId: folder.id, source });
19180 }
19181 function removeFolder(folderId, source = "local") {
19182 const store2 = getFilesStore();
19183 const folders = new Map(store2.state.folders);
19184 folders.delete(folderId);
19185 const placements = new Map(store2.state.placementsByFolder);
19186 placements.delete(folderId);
19187 store2.state = { ...store2.state, folders, placementsByFolder: placements };
19188 store2.notify();
19189 fireChanged({ kind: "folder-removed", folderRowId: folderId, source });
19190 }
19191 function subscribeFilesStore(cb) {
19192 const store2 = getFilesStore();
19193 const off = store2.subscribe(cb);
19194 return off;
19195 }
19196 function getFilesState() {
19197 return getFilesStore().getState();
19198 }
19199 const store = {
19200 getState: getFilesState,
19201 subscribe: subscribeFilesStore,
19202 setFolderPlacements,
19203 upsertPlacement,
19204 upsertFolder,
19205 removePlacement,
19206 removeFolder
19207 };
19208 const styles$5 = css`:host{display:inline-block}`;
19209 const styles$4 = 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 )}`;
19210 const _WpdRibbon = class _WpdRibbon extends Component {
19211 render() {
19212 return html`<span class="banner" part="banner"><slot></slot></span>`;
19213 }
19214 };
19215 _WpdRibbon.props = ["placement", "tone"];
19216 _WpdRibbon.styles = [styles$4];
19217 _WpdRibbon.help = {
19218 title: "Ribbon",
19219 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.",
19220 status: "experimental",
19221 since: "0.20.0",
19222 props: [
19223 {
19224 name: "placement",
19225 type: '"top-end" | "top-start" | "bottom-end" | "bottom-start"',
19226 description: "Which corner of the parent the ribbon hugs. Defaults to `top-end` (logical right in LTR, left in RTL)."
19227 },
19228 {
19229 name: "tone",
19230 type: '"primary" | "success" | "warning" | "danger" | "info" | "neutral"',
19231 description: "Background color tone. Defaults to `primary` (the admin theme accent)."
19232 }
19233 ],
19234 slots: [{ name: "(default)", description: "Ribbon label text. Keep short." }],
19235 cssProps: [
19236 { name: "--wpd-ribbon-size", default: "90px", description: "Square clipping window edge." },
19237 { name: "--wpd-ribbon-banner-width", default: "140px", description: "Width of the rotated strip." },
19238 { name: "--wpd-ribbon-banner-offset", default: "20px", description: "Distance from corner to strip center." },
19239 { name: "--wpd-ribbon-banner-pull", default: "-36px", description: "How far the strip overhangs the clip edge." },
19240 { name: "--wpd-ribbon-bg", default: "var(--wp-admin-theme-color, #2271b1)" },
19241 { name: "--wpd-ribbon-fg", default: "#fff" },
19242 { name: "--wpd-ribbon-shadow", default: "0 2px 4px rgba(0,0,0,0.2)" },
19243 { name: "--wpd-ribbon-padding", default: "4px 0" },
19244 { name: "--wpd-ribbon-font", default: "700 10px/1.4 system-ui" },
19245 { name: "--wpd-ribbon-tracking", default: "0.06em" },
19246 { name: "--wpd-ribbon-z", default: "2" }
19247 ],
19248 example: html`
19249 <div
19250 style="position: relative; width: 240px; height: 120px;
19251 border: 1px solid #ccc; border-radius: 8px;
19252 padding: 16px; box-sizing: border-box;"
19253 >
19254 <wpd-ribbon>Featured</wpd-ribbon>
19255 Card body…
19256 </div>
19257 `
19258 };
19259 let WpdRibbon = _WpdRibbon;
19260 defineComponent("wpd-ribbon", WpdRibbon);
19261 const TILE_CLASS = "desktop-mode-file-tile";
19262 const STATUS_LABEL = {
19263 draft: "Draft",
19264 pending: "Pending",
19265 private: "Private",
19266 future: "Scheduled"
19267 };
19268 function statusRibbonsEnabled() {
19269 const get2 = window.wp?.desktop?.getOsSettings;
19270 if (typeof get2 !== "function") {
19271 return true;
19272 }
19273 try {
19274 return get2()?.showPostStatusRibbons !== false;
19275 } catch {
19276 return true;
19277 }
19278 }
19279 function getDragManager$1() {
19280 const api = window.wp?.desktop?.dragManager;
19281 return api ?? null;
19282 }
19283 const REACTIVE_PROPS = [
19284 "type",
19285 "ref",
19286 "label",
19287 "icon",
19288 "thumbnail",
19289 "kind",
19290 "status",
19291 "selected",
19292 "missing",
19293 "access-gated",
19294 "drag-kind",
19295 "drag-title",
19296 "drag-icon"
19297 ];
19298 const _WpdTile = class _WpdTile extends Component {
19299 constructor() {
19300 super(...arguments);
19301 this._pointerdownHandler = null;
19302 this._keydownHandler = null;
19303 }
19304 connectedCallback() {
19305 super.connectedCallback();
19306 if (!this._keydownHandler) {
19307 this._keydownHandler = (e) => {
19308 if (e.key === "Enter" || e.key === " ") {
19309 e.preventDefault();
19310 this.click();
19311 }
19312 };
19313 this.addEventListener("keydown", this._keydownHandler);
19314 }
19315 this._paint();
19316 }
19317 disconnectedCallback() {
19318 if (this._pointerdownHandler) {
19319 this.removeEventListener(
19320 "pointerdown",
19321 this._pointerdownHandler
19322 );
19323 this._pointerdownHandler = null;
19324 }
19325 if (this._keydownHandler) {
19326 this.removeEventListener(
19327 "keydown",
19328 this._keydownHandler
19329 );
19330 this._keydownHandler = null;
19331 }
19332 }
19333 /**
19334 * Bypass the templated render loop. Lit-html's `render(template,
19335 * root)` would wipe the host's light-DOM children every tick —
19336 * including the visual / label / ribbon `_paint()` just
19337 * inserted. We override `requestUpdate` directly so attribute
19338 * changes call `_paint` (idempotent) without lit-html getting
19339 * involved.
19340 */
19341 requestUpdate() {
19342 if (!this.isConnected) {
19343 return;
19344 }
19345 this._paint();
19346 }
19347 render() {
19348 return html``;
19349 }
19350 _paint() {
19351 const type = this.getAttribute("type") ?? "";
19352 const ref = this.getAttribute("ref") ?? "";
19353 const label = this.getAttribute("label") ?? "";
19354 const icon = this.getAttribute("icon") ?? "";
19355 const thumbnail = this.getAttribute("thumbnail") ?? "";
19356 const kind = this.getAttribute("kind") ?? "entry";
19357 const status = this.getAttribute("status") ?? "";
19358 const selected = this.hasAttribute("selected");
19359 const missing = this.hasAttribute("missing");
19360 const accessGated = this.hasAttribute("access-gated");
19361 const ownedClasses = [
19362 TILE_CLASS,
19363 `${TILE_CLASS}--folder`,
19364 `${TILE_CLASS}--missing`,
19365 `${TILE_CLASS}--access-gated`,
19366 `${TILE_CLASS}--selected`
19367 ];
19368 for (const c of ownedClasses) {
19369 this.classList.remove(c);
19370 }
19371 this.classList.add(TILE_CLASS);
19372 if (kind === "folder") {
19373 this.classList.add(`${TILE_CLASS}--folder`);
19374 }
19375 if (missing) {
19376 this.classList.add(`${TILE_CLASS}--missing`);
19377 }
19378 if (accessGated) {
19379 this.classList.add(`${TILE_CLASS}--access-gated`);
19380 }
19381 if (selected) {
19382 this.classList.add(`${TILE_CLASS}--selected`);
19383 }
19384 this.dataset.fileType = type;
19385 this.dataset.fileRef = ref;
19386 if (kind) {
19387 this.dataset.role = kind;
19388 }
19389 this.setAttribute("role", "listitem");
19390 this.setAttribute("aria-label", label);
19391 if (!this.hasAttribute("tabindex")) {
19392 this.setAttribute("tabindex", "0");
19393 }
19394 const accessGatedTitle = "You don’t have permission to open this — ask the folder owner for access.";
19395 if (accessGated) {
19396 this.title = accessGatedTitle;
19397 this.setAttribute("aria-disabled", "true");
19398 } else {
19399 this.removeAttribute("aria-disabled");
19400 if (this.title === accessGatedTitle) {
19401 this.removeAttribute("title");
19402 }
19403 }
19404 const SLOTS = [
19405 `${TILE_CLASS}__visual`,
19406 `${TILE_CLASS}__label`,
19407 `${TILE_CLASS}__lock`
19408 ];
19409 for (const cls of SLOTS) {
19410 this.querySelectorAll(`:scope > .${cls}`).forEach(
19411 (n) => n.remove()
19412 );
19413 }
19414 this.querySelectorAll(":scope > wpd-ribbon").forEach(
19415 (n) => n.remove()
19416 );
19417 const visual = document.createElement("span");
19418 visual.className = `${TILE_CLASS}__visual`;
19419 if (thumbnail) {
19420 const img = document.createElement("img");
19421 img.src = thumbnail;
19422 img.alt = "";
19423 img.loading = "lazy";
19424 img.decoding = "async";
19425 img.className = `${TILE_CLASS}__preview`;
19426 img.draggable = false;
19427 visual.appendChild(img);
19428 } else if (icon) {
19429 const iconNode = renderIcon(icon, {
19430 title: label,
19431 className: `${TILE_CLASS}__icon`
19432 });
19433 visual.appendChild(iconNode);
19434 }
19435 this.appendChild(visual);
19436 const labelNode = document.createElement("span");
19437 labelNode.className = `${TILE_CLASS}__label`;
19438 labelNode.textContent = label;
19439 this.appendChild(labelNode);
19440 if (accessGated) {
19441 const lock = document.createElement("span");
19442 lock.className = `${TILE_CLASS}__lock dashicons dashicons-lock`;
19443 lock.setAttribute("aria-hidden", "true");
19444 this.appendChild(lock);
19445 }
19446 if (status && status !== "publish" && STATUS_LABEL[status] && statusRibbonsEnabled()) {
19447 const ribbon = document.createElement("wpd-ribbon");
19448 ribbon.setAttribute("placement", "top-end");
19449 ribbon.setAttribute("tone", ribbonToneFor(status));
19450 ribbon.textContent = STATUS_LABEL[status];
19451 this.appendChild(ribbon);
19452 }
19453 applyTileEntryStagger(this);
19454 doAction("desktop-mode.tile.rendered", { tile: this });
19455 this._wireDragOut();
19456 }
19457 _wireDragOut() {
19458 if (this._pointerdownHandler) {
19459 this.removeEventListener(
19460 "pointerdown",
19461 this._pointerdownHandler
19462 );
19463 this._pointerdownHandler = null;
19464 }
19465 const dragKind = this.getAttribute("drag-kind");
19466 if (!dragKind) {
19467 return;
19468 }
19469 const handler = (e) => {
19470 if (e.button !== 0) {
19471 return;
19472 }
19473 const dragManager = getDragManager$1();
19474 if (!dragManager) {
19475 return;
19476 }
19477 const ref = this.getAttribute("ref") ?? "";
19478 const title = this.getAttribute("drag-title") ?? this.getAttribute("label") ?? void 0;
19479 const icon = this.getAttribute("drag-icon") ?? this.getAttribute("icon") ?? void 0;
19480 const rect = this.getBoundingClientRect();
19481 dragManager.start({
19482 payload: {
19483 type: "shortcut",
19484 source: this,
19485 data: {
19486 kind: dragKind,
19487 ref,
19488 title,
19489 icon
19490 },
19491 ghost: {
19492 offsetX: e.clientX - rect.left,
19493 offsetY: e.clientY - rect.top
19494 }
19495 },
19496 origin: e
19497 });
19498 };
19499 this._pointerdownHandler = handler;
19500 this.addEventListener("pointerdown", handler);
19501 }
19502 };
19503 _WpdTile.shadow = false;
19504 _WpdTile.props = REACTIVE_PROPS;
19505 _WpdTile.styles = [styles$5];
19506 _WpdTile.help = {
19507 title: "Tile",
19508 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.",
19509 status: "experimental",
19510 since: "0.21.0",
19511 props: [
19512 { name: "type", type: "string" },
19513 { name: "ref", type: "string" },
19514 { name: "label", type: "string" },
19515 { name: "icon", type: "string", description: "Dashicon class / URL / data URI. Ignored when `thumbnail` is set." },
19516 { name: "thumbnail", type: "string", description: "Preview image URL. Renders as `<img>` and wins over `icon`." },
19517 { name: "kind", type: "`entry` | `folder`" },
19518 { name: "status", type: "`draft` | `pending` | `private` | `future` | `publish`" },
19519 { name: "selected", type: "boolean" },
19520 { name: "missing", type: "boolean" },
19521 { name: "access-gated", type: "boolean" },
19522 { name: "drag-kind", type: "string", description: "When set, the component wires pointerdown → DragManager." },
19523 { name: "drag-title", type: "string" },
19524 { name: "drag-icon", type: "string" }
19525 ]
19526 };
19527 let WpdTile = _WpdTile;
19528 function ribbonToneFor(status) {
19529 switch (status) {
19530 case "draft":
19531 return "warning";
19532 case "pending":
19533 return "info";
19534 case "private":
19535 return "danger";
19536 case "future":
19537 return "primary";
19538 default:
19539 return "primary";
19540 }
19541 }
19542 defineComponent("wpd-tile", WpdTile);
19543 function buildTileFromSpec(spec) {
19544 const tile2 = document.createElement("wpd-tile");
19545 tile2.setAttribute("type", spec.type);
19546 tile2.setAttribute("ref", spec.ref);
19547 tile2.setAttribute("label", spec.label);
19548 if (spec.icon) {
19549 tile2.setAttribute("icon", spec.icon);
19550 }
19551 if (spec.thumbnail) {
19552 tile2.setAttribute("thumbnail", spec.thumbnail);
19553 }
19554 if (spec.role) {
19555 tile2.setAttribute("kind", spec.role);
19556 }
19557 if (spec.status) {
19558 tile2.setAttribute("status", spec.status);
19559 }
19560 if (spec.missing) {
19561 tile2.setAttribute("missing", "");
19562 }
19563 if (spec.accessGated) {
19564 tile2.setAttribute("access-gated", "");
19565 }
19566 if (spec.dataset) {
19567 for (const [key, raw] of Object.entries(spec.dataset)) {
19568 if (raw === void 0 || raw === null) {
19569 continue;
19570 }
19571 tile2.dataset[key] = String(raw);
19572 }
19573 }
19574 if (Array.isArray(spec.extraClasses)) {
19575 for (const c of spec.extraClasses) {
19576 if (c) {
19577 tile2.classList.add(c);
19578 }
19579 }
19580 }
19581 const classFiltered = applyFilters(
19582 "desktop-mode.tile.class",
19583 tile2.className,
19584 spec
19585 );
19586 if (classFiltered && classFiltered !== tile2.className) {
19587 tile2.className = classFiltered;
19588 }
19589 if (typeof spec.x === "number" && typeof spec.y === "number") {
19590 tile2.style.position = "absolute";
19591 tile2.style.left = `${spec.x}px`;
19592 tile2.style.top = `${spec.y}px`;
19593 }
19594 return tile2;
19595 }
19596 function placementToSpec(placement, folderId) {
19597 const file = resolve(placement.file);
19598 const previewUrl = file.previewUrl();
19599 const metaName = placement.meta && typeof placement.meta.name === "string" ? placement.meta.name.trim() : "";
19600 const label = metaName !== "" ? metaName : file.title();
19601 const metaIconUrl = placement.meta && typeof placement.meta.iconUrl === "string" ? placement.meta.iconUrl.trim() : "";
19602 return {
19603 type: placement.file.type,
19604 ref: placement.file.ref,
19605 label,
19606 // Preview wins over icon (matches the previous behavior).
19607 thumbnail: previewUrl || void 0,
19608 icon: previewUrl ? void 0 : metaIconUrl || file.icon(),
19609 x: placement.x,
19610 y: placement.y,
19611 dataset: {
19612 placementId: placement.id,
19613 folderId
19614 },
19615 meta: placement.meta,
19616 missing: !placement.file.exists,
19617 accessGated: Boolean(placement.accessGated),
19618 ariaLabel: label
19619 };
19620 }
19621 function buildTile(placement, folderId) {
19622 const file = resolve(placement.file);
19623 const tile2 = buildTileFromSpec(placementToSpec(placement, folderId));
19624 const classFiltered = applyFilters(
19625 "desktop-mode.files.tile-class",
19626 TILE_CLASS,
19627 placement
19628 );
19629 if (classFiltered && classFiltered !== TILE_CLASS) {
19630 tile2.className = classFiltered;
19631 }
19632 const extra = applyFilters(
19633 "desktop-mode.files.tile-element",
19634 null,
19635 placement
19636 );
19637 if (extra instanceof Element) {
19638 tile2.appendChild(extra);
19639 }
19640 tile2.addEventListener("dblclick", (e) => {
19641 e.preventDefault();
19642 e.stopPropagation();
19643 if (placement.accessGated) {
19644 showToast({
19645 message: `You don’t have permission to open "${placement.file.title || file.title()}". Ask the folder owner if you need access to this item.`,
19646 duration: 6e3
19647 });
19648 return;
19649 }
19650 void openFile(file, {
19651 placement: {
19652 id: placement.id,
19653 x: placement.x,
19654 y: placement.y,
19655 meta: placement.meta
19656 }
19657 });
19658 });
19659 doAction("desktop-mode.files.tile-rendered", { tile: tile2, placement });
19660 return tile2;
19661 }
19662 function setTilePosition(tile2, x, y) {
19663 tile2.style.left = `${x}px`;
19664 tile2.style.top = `${y}px`;
19665 }
19666 function attachDismissable(host, options) {
19667 const onAway = (e) => {
19668 if (e.target instanceof Node && host.contains(e.target)) {
19669 return;
19670 }
19671 if (e.target instanceof Node) {
19672 for (const sel of options.siblingSelectors ?? []) {
19673 const matches = Array.from(
19674 document.querySelectorAll(sel)
19675 );
19676 for (const m of matches) {
19677 if (m.contains(e.target)) {
19678 return;
19679 }
19680 }
19681 }
19682 }
19683 if (options.excludeOutsideTarget && e.target instanceof Node && options.excludeOutsideTarget.contains(e.target)) {
19684 return;
19685 }
19686 options.close();
19687 };
19688 const onKey = (e) => {
19689 if (e.key === "Escape") {
19690 options.close();
19691 }
19692 };
19693 document.addEventListener("mousedown", onAway, { capture: true });
19694 document.addEventListener("keydown", onKey);
19695 return () => {
19696 document.removeEventListener("mousedown", onAway, { capture: true });
19697 document.removeEventListener("keydown", onKey);
19698 };
19699 }
19700 const MENU_CLASS$2 = "desktop-mode-wallpaper-menu";
19701 let activeMenu$2 = null;
19702 function closeTileMenu() {
19703 if (!activeMenu$2) {
19704 return;
19705 }
19706 activeMenu$2.dispatchEvent(new CustomEvent("tile-menu-closed"));
19707 activeMenu$2.remove();
19708 activeMenu$2 = null;
19709 doAction("desktop-mode.files.tile-menu.closed", {});
19710 }
19711 let openGeneration$1 = 0;
19712 function openTileMenu(pos, opts) {
19713 closeTileMenu();
19714 const myGen = ++openGeneration$1;
19715 openWithShellOverlays(
19716 () => myGen === openGeneration$1,
19717 () => openTileMenuImmediate(pos, opts)
19718 );
19719 }
19720 function openTileMenuImmediate(pos, { placement, items }) {
19721 const list2 = applyFilters(
19722 "desktop-mode.files.tile-menu",
19723 items.slice(),
19724 placement
19725 );
19726 const sorted = (Array.isArray(list2) ? list2 : items).slice().sort((a, b) => {
19727 const sa = typeof a.sort === "number" ? a.sort : 100;
19728 const sb = typeof b.sort === "number" ? b.sort : 100;
19729 if (sa !== sb) {
19730 return sa - sb;
19731 }
19732 return a.label.localeCompare(b.label);
19733 });
19734 if (sorted.length === 0) {
19735 return;
19736 }
19737 const menu = document.createElement("wpd-context-menu");
19738 menu.setAttribute("open", "");
19739 menu.classList.add(MENU_CLASS$2);
19740 menu.dataset.placementId = String(placement.id);
19741 menu.style.left = `${pos.x}px`;
19742 menu.style.top = `${pos.y}px`;
19743 const itemById = /* @__PURE__ */ new Map();
19744 for (const item of sorted) {
19745 itemById.set(item.id, item);
19746 const opt = document.createElement("wpd-context-menu-option");
19747 opt.dataset.menuItemId = item.id;
19748 opt.setAttribute("value", item.id);
19749 if (item.danger) {
19750 opt.setAttribute("danger", "");
19751 }
19752 if (item.disabled) {
19753 opt.setAttribute("disabled", "");
19754 }
19755 if (item.icon) {
19756 opt.setAttribute("icon", sanitizeClass$2(item.icon));
19757 }
19758 opt.textContent = item.label;
19759 menu.appendChild(opt);
19760 }
19761 menu.addEventListener("wpd-context-menu-pick", (e) => {
19762 const detail = e.detail;
19763 const item = itemById.get(detail.id);
19764 if (!item) {
19765 return;
19766 }
19767 closeTileMenu();
19768 void item.onClick(new MouseEvent("click"));
19769 });
19770 document.body.appendChild(menu);
19771 activeMenu$2 = menu;
19772 const rect = menu.getBoundingClientRect();
19773 if (rect.right > window.innerWidth) {
19774 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
19775 }
19776 if (rect.bottom > window.innerHeight) {
19777 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
19778 }
19779 const detach = attachDismissable(menu, {
19780 close: () => closeTileMenu()
19781 });
19782 menu.addEventListener("tile-menu-closed", detach);
19783 doAction("desktop-mode.files.tile-menu.opened", {
19784 placementId: placement.id,
19785 items: sorted.map((i) => i.id)
19786 });
19787 }
19788 function sanitizeClass$2(raw) {
19789 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
19790 }
19791 const ROOT_CLASS$3 = "desktop-mode-create-folder-dialog";
19792 let active$1 = null;
19793 function closeCreateFolderDialog() {
19794 if (!active$1) {
19795 return;
19796 }
19797 active$1.dispatchEvent(new CustomEvent("create-folder-dialog-closed"));
19798 active$1.remove();
19799 active$1 = null;
19800 doAction("desktop-mode.files.create-folder.closed", {});
19801 }
19802 function openCreateFolderDialog(options) {
19803 closeCreateFolderDialog();
19804 const decision = applyFilters(
19805 "desktop-mode.files.create-folder.dialog",
19806 null,
19807 options
19808 );
19809 if (decision === false) {
19810 return;
19811 }
19812 const initial = (options.initialName ?? "Untitled folder").trim();
19813 const overlay = document.createElement("div");
19814 overlay.className = `${ROOT_CLASS$3}__overlay`;
19815 overlay.setAttribute("role", "presentation");
19816 const dialog2 = document.createElement("div");
19817 dialog2.className = ROOT_CLASS$3;
19818 dialog2.setAttribute("role", "dialog");
19819 dialog2.setAttribute("aria-modal", "true");
19820 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS$3}-title`);
19821 const title = document.createElement("h2");
19822 title.id = `${ROOT_CLASS$3}-title`;
19823 title.className = `${ROOT_CLASS$3}__title`;
19824 title.textContent = options.title ?? "New folder";
19825 dialog2.appendChild(title);
19826 const label = document.createElement("label");
19827 label.className = `${ROOT_CLASS$3}__label`;
19828 label.htmlFor = `${ROOT_CLASS$3}-input`;
19829 label.textContent = options.label ?? "Folder name";
19830 dialog2.appendChild(label);
19831 const input = document.createElement("input");
19832 input.type = "text";
19833 input.id = `${ROOT_CLASS$3}-input`;
19834 input.className = `${ROOT_CLASS$3}__input`;
19835 input.value = initial;
19836 input.setAttribute("autocomplete", "off");
19837 input.setAttribute("spellcheck", "false");
19838 dialog2.appendChild(input);
19839 const error = document.createElement("p");
19840 error.className = `${ROOT_CLASS$3}__error`;
19841 error.hidden = true;
19842 error.setAttribute("role", "alert");
19843 dialog2.appendChild(error);
19844 const actions = document.createElement("div");
19845 actions.className = `${ROOT_CLASS$3}__actions`;
19846 const cancel = document.createElement("button");
19847 cancel.type = "button";
19848 cancel.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--secondary`;
19849 cancel.textContent = "Cancel";
19850 const submit = document.createElement("button");
19851 submit.type = "button";
19852 submit.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--primary`;
19853 submit.textContent = options.submitLabel ?? "Create";
19854 actions.appendChild(cancel);
19855 actions.appendChild(submit);
19856 dialog2.appendChild(actions);
19857 overlay.appendChild(dialog2);
19858 document.body.appendChild(overlay);
19859 active$1 = overlay;
19860 input.focus();
19861 input.select();
19862 doAction("desktop-mode.files.create-folder.opened", {});
19863 const setBusy = (busy) => {
19864 input.disabled = busy;
19865 cancel.disabled = busy;
19866 submit.disabled = busy;
19867 dialog2.classList.toggle(`${ROOT_CLASS$3}--busy`, busy);
19868 };
19869 const showError = (msg) => {
19870 error.textContent = msg;
19871 error.hidden = false;
19872 };
19873 const doCancel = () => {
19874 closeCreateFolderDialog();
19875 options.onCancel?.();
19876 };
19877 const doSubmit = async () => {
19878 const name = input.value.trim();
19879 if (!name) {
19880 showError("Please enter a name.");
19881 input.focus();
19882 return;
19883 }
19884 error.hidden = true;
19885 setBusy(true);
19886 try {
19887 await options.onSubmit(name);
19888 closeCreateFolderDialog();
19889 } catch (err) {
19890 setBusy(false);
19891 showError(
19892 err instanceof Error ? err.message : "Could not create the folder."
19893 );
19894 input.focus();
19895 input.select();
19896 }
19897 };
19898 cancel.addEventListener("click", () => doCancel());
19899 submit.addEventListener("click", () => void doSubmit());
19900 overlay.addEventListener("click", (e) => {
19901 if (e.target === overlay) {
19902 doCancel();
19903 }
19904 });
19905 const onKey = (e) => {
19906 if (e.key === "Escape") {
19907 e.preventDefault();
19908 doCancel();
19909 } else if (e.key === "Enter" && !e.isComposing) {
19910 e.preventDefault();
19911 void doSubmit();
19912 }
19913 };
19914 dialog2.addEventListener("keydown", onKey);
19915 overlay.addEventListener("create-folder-dialog-closed", () => {
19916 dialog2.removeEventListener("keydown", onKey);
19917 });
19918 }
19919 const GRID_PADDING = 16;
19920 const GRID_CELL_W = 96;
19921 const GRID_CELL_H = 110;
19922 function pointToCell(x, y) {
19923 const col = Math.max(0, Math.round((x - GRID_PADDING) / GRID_CELL_W));
19924 const row = Math.max(0, Math.round((y - GRID_PADDING) / GRID_CELL_H));
19925 return cellToPos(col, row);
19926 }
19927 function cellToPos(col, row) {
19928 return {
19929 col,
19930 row,
19931 x: GRID_PADDING + col * GRID_CELL_W,
19932 y: GRID_PADDING + row * GRID_CELL_H
19933 };
19934 }
19935 function snapToEmptyCell(x, y, occupied, host) {
19936 const target2 = pointToCell(x, y);
19937 if (!occupied.has(cellKey(target2.col, target2.row))) {
19938 return target2;
19939 }
19940 const maxRows = host ? Math.max(1, Math.floor((host.clientHeight - GRID_PADDING) / GRID_CELL_H)) : 999;
19941 for (let col = 0; col < 999; col++) {
19942 for (let row = 0; row < maxRows; row++) {
19943 if (!occupied.has(cellKey(col, row))) {
19944 return cellToPos(col, row);
19945 }
19946 }
19947 }
19948 return target2;
19949 }
19950 function nextRowMajorCell(occupied, host) {
19951 const cols = host ? Math.max(
19952 1,
19953 Math.floor((host.clientWidth - GRID_PADDING) / GRID_CELL_W)
19954 ) : 4;
19955 const maxCols = Math.max(1, cols);
19956 for (let row = 0; row < 999; row++) {
19957 for (let col = 0; col < maxCols; col++) {
19958 if (!occupied.has(cellKey(col, row))) {
19959 return cellToPos(col, row);
19960 }
19961 }
19962 }
19963 return cellToPos(0, 0);
19964 }
19965 function buildOccupiedSet(placements, excludeId) {
19966 const out = /* @__PURE__ */ new Set();
19967 for (const p of placements) {
19968 const cell = pointToCell(p.x, p.y);
19969 out.add(cellKey(cell.col, cell.row));
19970 }
19971 return out;
19972 }
19973 function cellKey(col, row) {
19974 return `${col},${row}`;
19975 }
19976 function isConflict(err) {
19977 return err instanceof FilesConflictError;
19978 }
19979 function buildReason(err) {
19980 const actor = err.detail.actor.name || "Someone else";
19981 const where = err.detail.current.parentName || "another folder";
19982 if (err.detail.reason === "trashed") {
19983 return "This item is in the recycle bin.";
19984 }
19985 if (err.detail.reason === "forbidden") {
19986 return "You no longer have access.";
19987 }
19988 if (err.detail.reason === "gone") {
19989 return "This item was deleted.";
19990 }
19991 return `${actor} moved this to "${where}".`;
19992 }
19993 function showConflictToast(err) {
19994 const reason = buildReason(err);
19995 const targetParentId = err.detail.current.parentId;
19996 let action;
19997 if (targetParentId > 0) {
19998 action = {
19999 label: "View folder",
20000 onClick: () => {
20001 const winId = `desktop-mode-folder-${targetParentId}`;
20002 const mgr = window.desktopMode?.windowManager;
20003 if (mgr?.focus) {
20004 const w = mgr.focus(winId);
20005 if (w) {
20006 return;
20007 }
20008 }
20009 if (mgr?.open) {
20010 void mgr.open(winId);
20011 }
20012 }
20013 };
20014 }
20015 showToast({
20016 message: reason,
20017 action,
20018 duration: 7e3
20019 });
20020 }
20021 function broadcastFilesChange(kind, action, ids) {
20022 const api = window.wp?.desktop;
20023 api?.broadcast?.(`desktop-mode.${kind}.changed`, {
20024 source: "desktop-files",
20025 action,
20026 ids
20027 });
20028 }
20029 function showTrashErrorToast(err) {
20030 const api = window.wp?.desktop;
20031 if (!api?.showToast) {
20032 return;
20033 }
20034 const raw = err instanceof Error ? err.message : String(err);
20035 const friendly = raw.replace(/^\[desktop-mode\][^:]*:\s*/, "").replace(/^desktop_mode_files_[a-z_]+\s*/, "");
20036 api.showToast({
20037 message: friendly || "Could not move this item to the recycle bin.",
20038 duration: 5e3
20039 });
20040 }
20041 function showTrashedToast(message, onUndo) {
20042 const api = window.wp?.desktop;
20043 if (!api?.showToast) {
20044 return;
20045 }
20046 api.showToast({
20047 message,
20048 duration: 6e3,
20049 action: {
20050 label: "Undo",
20051 onClick: onUndo
20052 }
20053 });
20054 }
20055 async function trashPlacementWithUndo(placement) {
20056 const placementId = placement.id;
20057 const parentId = placement.parentId;
20058 const title = placement.file?.title ?? "Item";
20059 const kind = placement.file?.type === "shortcut" ? "shortcut" : "placement";
20060 store.removePlacement(placementId);
20061 try {
20062 await deletePlacement(placementId);
20063 broadcastFilesChange(kind, "trashed", [placementId]);
20064 showTrashedToast(`"${title}" moved to Trash`, async () => {
20065 try {
20066 await restoreTrashedItem(placementId, "placement");
20067 const res = await listPlacements(parentId);
20068 store.setFolderPlacements(parentId, res.placements);
20069 broadcastFilesChange(kind, "untrashed", [placementId]);
20070 } catch (err) {
20071 console.error("[desktop-mode] restore failed:", err);
20072 }
20073 });
20074 } catch (err) {
20075 console.error("[desktop-mode] deletePlacement failed:", err);
20076 showTrashErrorToast(err);
20077 void listPlacements(parentId).then((res) => {
20078 store.setFolderPlacements(parentId, res.placements);
20079 });
20080 }
20081 }
20082 async function trashFolderWithUndo(placement) {
20083 const folderId = parseInt(placement.file.ref, 10);
20084 if (!folderId) {
20085 return;
20086 }
20087 const placementId = placement.id;
20088 const parentId = placement.parentId;
20089 const title = placement.file?.title ?? "Folder";
20090 store.removePlacement(placementId);
20091 store.removeFolder(folderId);
20092 try {
20093 await deleteFolder(folderId);
20094 broadcastFilesChange("folder", "trashed", [folderId]);
20095 showTrashedToast(`"${title}" moved to Trash`, async () => {
20096 try {
20097 await restoreTrashedItem(folderId, "folder");
20098 const res = await listPlacements(parentId);
20099 store.setFolderPlacements(parentId, res.placements);
20100 broadcastFilesChange("folder", "untrashed", [folderId]);
20101 } catch (err) {
20102 console.error("[desktop-mode] restore folder failed:", err);
20103 }
20104 });
20105 } catch (err) {
20106 console.error("[desktop-mode] deleteFolder failed:", err);
20107 showTrashErrorToast(err);
20108 void listPlacements(parentId).then((res) => {
20109 store.setFolderPlacements(parentId, res.placements);
20110 });
20111 }
20112 }
20113 function trashByFileType(placement) {
20114 if (placement.file?.type === "folder") {
20115 return trashFolderWithUndo(placement);
20116 }
20117 return trashPlacementWithUndo(placement);
20118 }
20119 function buildBridgePayloadFromPlacement(placement) {
20120 const file = placement.file;
20121 if (!file) {
20122 return void 0;
20123 }
20124 const id = parseInt(String(file.ref ?? ""), 10);
20125 if (!Number.isFinite(id) || id <= 0) {
20126 return void 0;
20127 }
20128 const title = String(file.title ?? "");
20129 if (file.type === "attachment") {
20130 const url = String(file.sourceUrl ?? file.previewUrl ?? "");
20131 return {
20132 kind: "attachment",
20133 id,
20134 url,
20135 title,
20136 alt: String(file.alt ?? ""),
20137 mime: String(file.mime ?? ""),
20138 thumbnailUrl: file.previewUrl ? String(file.previewUrl) : void 0
20139 };
20140 }
20141 if (file.type === "post") {
20142 return {
20143 kind: "post",
20144 id,
20145 postType: String(file.postType ?? "post"),
20146 url: String(file.link ?? ""),
20147 title
20148 };
20149 }
20150 if (file.type === "user") {
20151 return {
20152 kind: "user",
20153 id,
20154 url: String(file.link ?? ""),
20155 title
20156 };
20157 }
20158 return void 0;
20159 }
20160 function getDragManager() {
20161 const api = window.wp?.desktop?.dragManager;
20162 return api ?? null;
20163 }
20164 const LAYER_CLASS = "desktop-mode-files-layer";
20165 function mountFilesLayer(host, folderId = 0) {
20166 const container = document.createElement("div");
20167 container.className = LAYER_CLASS;
20168 container.setAttribute("role", "list");
20169 container.dataset.folderId = String(folderId);
20170 host.appendChild(container);
20171 let lastFingerprint = "";
20172 let selectedId = null;
20173 const selectionListeners = /* @__PURE__ */ new Set();
20174 const notifySelection = (placement) => {
20175 for (const cb of selectionListeners) {
20176 try {
20177 cb(placement);
20178 } catch (err) {
20179 console.error(
20180 "[desktop-mode] files: selection listener threw:",
20181 err
20182 );
20183 }
20184 }
20185 };
20186 const setSelected = (placement) => {
20187 const newId = placement ? placement.id : null;
20188 if (newId === selectedId) {
20189 return;
20190 }
20191 container.querySelectorAll(`.${TILE_CLASS}--selected`).forEach((n) => n.removeAttribute("selected"));
20192 if (placement) {
20193 const tile2 = container.querySelector(
20194 `[data-placement-id="${placement.id}"]`
20195 );
20196 tile2?.setAttribute("selected", "");
20197 }
20198 selectedId = newId;
20199 notifySelection(placement);
20200 };
20201 const computeLayout = (list2) => {
20202 const pinnedSlots = /* @__PURE__ */ new Map();
20203 const occupiedCells = /* @__PURE__ */ new Set();
20204 let pinnedIdx = 0;
20205 for (const placement of list2) {
20206 if (!isPinned(placement)) {
20207 continue;
20208 }
20209 const slot = cellToPos(0, pinnedIdx);
20210 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
20211 occupiedCells.add(cellKey(slot.col, slot.row));
20212 pinnedIdx += 1;
20213 }
20214 const displaced = /* @__PURE__ */ new Map();
20215 for (const placement of list2) {
20216 if (pinnedSlots.has(placement.id)) {
20217 continue;
20218 }
20219 const target2 = pointToCell(placement.x, placement.y);
20220 const key = cellKey(target2.col, target2.row);
20221 if (!occupiedCells.has(key)) {
20222 occupiedCells.add(key);
20223 continue;
20224 }
20225 const free = snapToEmptyCell(
20226 placement.x,
20227 placement.y,
20228 occupiedCells,
20229 host
20230 );
20231 occupiedCells.add(cellKey(free.col, free.row));
20232 displaced.set(placement.id, { x: free.x, y: free.y });
20233 }
20234 return { pinnedSlots, displaced };
20235 };
20236 const applyTilePosition = (tile2, placement, pinnedSlots, displaced) => {
20237 const pinned = pinnedSlots.get(placement.id);
20238 const moved = displaced.get(placement.id);
20239 if (pinned) {
20240 setTilePosition(tile2, pinned.x, pinned.y);
20241 } else if (moved) {
20242 setTilePosition(tile2, moved.x, moved.y);
20243 } else {
20244 setTilePosition(tile2, placement.x, placement.y);
20245 }
20246 };
20247 const wireTile = (placement, pinnedSlots, displaced) => {
20248 const tile2 = buildTile(placement, folderId);
20249 const pinnedSlot = pinnedSlots.get(placement.id);
20250 if (pinnedSlot) {
20251 setTilePosition(tile2, pinnedSlot.x, pinnedSlot.y);
20252 tile2.classList.add(`${TILE_CLASS}--pinned`);
20253 attachContextMenu(tile2, placement);
20254 attachSelectOnClick(tile2, placement);
20255 if (shouldRejectTileDrops(placement)) {
20256 const dragManager = getDragManager();
20257 if (dragManager) {
20258 const deregister = dragManager.registerDropTarget({
20259 id: `desktop-mode-files-tile-${placement.id}-reject`,
20260 element: tile2,
20261 accept: () => false,
20262 onDrop: () => {
20263 }
20264 });
20265 tileRejectDeregisters.set(placement.id, deregister);
20266 }
20267 }
20268 return tile2;
20269 }
20270 const moved = displaced.get(placement.id);
20271 if (moved) {
20272 setTilePosition(tile2, moved.x, moved.y);
20273 }
20274 attachTileDrag(tile2, placement, folderId);
20275 attachContextMenu(tile2, placement);
20276 attachSelectOnClick(tile2, placement);
20277 if (placement.file.type === "folder") {
20278 const targetFolderId = parseInt(placement.file.ref, 10);
20279 if (targetFolderId > 0) {
20280 const dragManager = getDragManager();
20281 if (dragManager) {
20282 const deregister = registerFolderDropTarget(
20283 dragManager,
20284 tile2,
20285 targetFolderId
20286 );
20287 folderDropDeregisters.set(placement.id, deregister);
20288 }
20289 }
20290 } else if (shouldRejectTileDrops(placement)) {
20291 const dragManager = getDragManager();
20292 if (dragManager) {
20293 const deregister = dragManager.registerDropTarget({
20294 id: `desktop-mode-files-tile-${placement.id}-reject`,
20295 element: tile2,
20296 accept: () => false,
20297 onDrop: () => {
20298 }
20299 });
20300 tileRejectDeregisters.set(placement.id, deregister);
20301 }
20302 }
20303 return tile2;
20304 };
20305 const tryPatchIncremental = (list2) => {
20306 const existing = /* @__PURE__ */ new Map();
20307 for (const tile2 of container.querySelectorAll(
20308 "[data-placement-id]"
20309 )) {
20310 const raw = tile2.dataset.placementId ?? "";
20311 const id = parseInt(raw, 10);
20312 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
20313 return false;
20314 }
20315 existing.set(id, tile2);
20316 }
20317 const wantIds = /* @__PURE__ */ new Set();
20318 for (const placement of list2) {
20319 wantIds.add(placement.id);
20320 }
20321 for (const placement of list2) {
20322 const tile2 = existing.get(placement.id);
20323 if (!tile2) {
20324 continue;
20325 }
20326 if (tile2.dataset.fileType !== placement.file.type) {
20327 return false;
20328 }
20329 if (tile2.dataset.fileRef !== placement.file.ref) {
20330 return false;
20331 }
20332 const wasPinned = tile2.classList.contains(
20333 `${TILE_CLASS}--pinned`
20334 );
20335 if (wasPinned !== isPinned(placement)) {
20336 return false;
20337 }
20338 }
20339 for (const [id, tile2] of existing) {
20340 if (wantIds.has(id)) {
20341 continue;
20342 }
20343 const folderDereg = folderDropDeregisters.get(id);
20344 if (folderDereg) {
20345 try {
20346 folderDereg();
20347 } catch {
20348 }
20349 folderDropDeregisters.delete(id);
20350 }
20351 const rejectDereg = tileRejectDeregisters.get(id);
20352 if (rejectDereg) {
20353 try {
20354 rejectDereg();
20355 } catch {
20356 }
20357 tileRejectDeregisters.delete(id);
20358 }
20359 tile2.remove();
20360 }
20361 const { pinnedSlots, displaced } = computeLayout(list2);
20362 for (const placement of list2) {
20363 const tile2 = existing.get(placement.id);
20364 if (tile2) {
20365 applyTilePosition(tile2, placement, pinnedSlots, displaced);
20366 continue;
20367 }
20368 container.appendChild(
20369 wireTile(placement, pinnedSlots, displaced)
20370 );
20371 }
20372 if (selectedId !== null && !container.querySelector(
20373 `[data-placement-id="${selectedId}"]`
20374 )) {
20375 selectedId = null;
20376 notifySelection(null);
20377 }
20378 doAction("desktop-mode.files.grid-rendered", {
20379 folderId,
20380 count: list2.length
20381 });
20382 return true;
20383 };
20384 const repaint = (state2) => {
20385 const raw = state2.placementsByFolder.get(folderId) ?? [];
20386 const list2 = raw.slice().sort((a, b) => {
20387 const ap = isPinned(a) ? 0 : 1;
20388 const bp = isPinned(b) ? 0 : 1;
20389 return ap - bp;
20390 });
20391 const fp = fingerprint(list2);
20392 if (fp === lastFingerprint) {
20393 return;
20394 }
20395 lastFingerprint = fp;
20396 if (tryPatchPositions(list2, container, host)) {
20397 return;
20398 }
20399 if (tryPatchIncremental(list2)) {
20400 return;
20401 }
20402 container.replaceChildren();
20403 for (const [, deregister] of folderDropDeregisters) {
20404 try {
20405 deregister();
20406 } catch {
20407 }
20408 }
20409 folderDropDeregisters.clear();
20410 for (const [, deregister] of tileRejectDeregisters) {
20411 try {
20412 deregister();
20413 } catch {
20414 }
20415 }
20416 tileRejectDeregisters.clear();
20417 const { pinnedSlots, displaced } = computeLayout(list2);
20418 for (const placement of list2) {
20419 container.appendChild(
20420 wireTile(placement, pinnedSlots, displaced)
20421 );
20422 }
20423 if (selectedId !== null && !container.querySelector(`[data-placement-id="${selectedId}"]`)) {
20424 selectedId = null;
20425 notifySelection(null);
20426 } else if (selectedId !== null) {
20427 const tile2 = container.querySelector(
20428 `[data-placement-id="${selectedId}"]`
20429 );
20430 tile2?.setAttribute("selected", "");
20431 }
20432 doAction("desktop-mode.files.grid-rendered", {
20433 folderId,
20434 count: list2.length
20435 });
20436 };
20437 const dropTargetDeregisters = [];
20438 const folderDropDeregisters = /* @__PURE__ */ new Map();
20439 const tileRejectDeregisters = /* @__PURE__ */ new Map();
20440 let dropPreviewEl = null;
20441 let dropPreviewMoveHandler = null;
20442 const installCanvasDropPreview = (session) => {
20443 if (dropPreviewEl) {
20444 return;
20445 }
20446 if (session.payload.type !== "desktop-file") {
20447 return;
20448 }
20449 const previewEl = document.createElement("div");
20450 previewEl.className = "desktop-mode-files-drop-preview";
20451 previewEl.setAttribute("aria-hidden", "true");
20452 container.appendChild(previewEl);
20453 dropPreviewEl = previewEl;
20454 const ghost = session.payload.ghost;
20455 const offsetX = ghost?.offsetX ?? 0;
20456 const offsetY = ghost?.offsetY ?? 0;
20457 const data = session.payload.data;
20458 const movingId = data?.placement?.id;
20459 const updatePreview = (clientX, clientY) => {
20460 const rect = container.getBoundingClientRect();
20461 const rawX = Math.max(0, clientX - rect.left - offsetX);
20462 const rawY = Math.max(0, clientY - rect.top - offsetY);
20463 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
20464 const occupied = buildVisualOccupiedSet(peers, movingId);
20465 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
20466 previewEl.style.transform = `translate3d(${cell.x}px, ${cell.y}px, 0)`;
20467 };
20468 const sourceRect = session.payload.source.getBoundingClientRect();
20469 updatePreview(
20470 sourceRect.left + offsetX,
20471 sourceRect.top + offsetY
20472 );
20473 const moveHandler = (ev) => {
20474 updatePreview(ev.clientX, ev.clientY);
20475 };
20476 document.addEventListener("pointermove", moveHandler);
20477 dropPreviewMoveHandler = moveHandler;
20478 };
20479 const teardownCanvasDropPreview = () => {
20480 if (dropPreviewMoveHandler) {
20481 document.removeEventListener("pointermove", dropPreviewMoveHandler);
20482 dropPreviewMoveHandler = null;
20483 }
20484 if (dropPreviewEl) {
20485 dropPreviewEl.remove();
20486 dropPreviewEl = null;
20487 }
20488 };
20489 const canvasDropTarget = {
20490 id: `desktop-mode-files-canvas-${folderId}`,
20491 element: host,
20492 accept: (payload) => {
20493 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
20494 return false;
20495 }
20496 if (folderId > 0 && payload.type === "desktop-file") {
20497 const data = payload.data;
20498 if (data.placement.file?.type === "folder") {
20499 const movingFolderId = parseInt(data.placement.file.ref, 10);
20500 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, folderId)) {
20501 return false;
20502 }
20503 }
20504 }
20505 return true;
20506 },
20507 onEnter: (session) => {
20508 host.setAttribute("data-files-drop-active", "");
20509 installCanvasDropPreview(session);
20510 },
20511 onLeave: () => {
20512 host.removeAttribute("data-files-drop-active");
20513 teardownCanvasDropPreview();
20514 },
20515 onDrop: (session, ev) => {
20516 host.removeAttribute("data-files-drop-active");
20517 teardownCanvasDropPreview();
20518 const rect = container.getBoundingClientRect();
20519 const ghost = session.payload.ghost;
20520 const offsetX = ghost?.offsetX ?? 0;
20521 const offsetY = ghost?.offsetY ?? 0;
20522 const rawX = Math.max(0, ev.clientX - rect.left - offsetX);
20523 const rawY = Math.max(0, ev.clientY - rect.top - offsetY);
20524 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
20525 if (session.payload.type === "desktop-file") {
20526 const data = session.payload.data;
20527 const occupied = buildVisualOccupiedSet(peers, data.placement.id);
20528 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
20529 const next = {
20530 ...data.placement,
20531 x: cell.x,
20532 y: cell.y,
20533 parentId: folderId
20534 };
20535 store.upsertPlacement(next);
20536 doAction("desktop-mode.files.tile-manually-placed", {
20537 folderId,
20538 placementId: data.placement.id
20539 });
20540 if (isSyntheticPlacement(data.placement)) {
20541 const dockItemId = readSynthSource(data.placement);
20542 if (dockItemId) {
20543 persistDockPromotedPosition(
20544 dockItemId,
20545 cell.x,
20546 cell.y
20547 );
20548 }
20549 return;
20550 }
20551 void updatePlacement(
20552 data.placement.id,
20553 {
20554 x: cell.x,
20555 y: cell.y,
20556 parentId: folderId
20557 },
20558 data.placement.updatedAtMs
20559 ).then((server) => {
20560 store.upsertPlacement(server, "remote");
20561 }).catch((err) => {
20562 if (isConflict(err)) {
20563 showConflictToast(err);
20564 } else {
20565 console.error(
20566 "[desktop-mode] files: drag persist failed",
20567 err
20568 );
20569 }
20570 store.upsertPlacement(data.placement);
20571 });
20572 return;
20573 }
20574 if (session.payload.type === "shortcut") {
20575 const data = session.payload.data;
20576 const occupied = buildVisualOccupiedSet(peers);
20577 const cell = nextRowMajorCell(occupied, host);
20578 void createPlacement({
20579 parentId: folderId,
20580 type: data.kind,
20581 ref: data.ref,
20582 x: cell.x,
20583 y: cell.y
20584 }).then((placement) => {
20585 store.upsertPlacement(placement);
20586 doAction("desktop-mode.files.shortcut-dropped", {
20587 folderId,
20588 placement
20589 });
20590 }).catch((err) => {
20591 console.error(
20592 "[desktop-mode] shortcut drop failed:",
20593 err
20594 );
20595 });
20596 }
20597 }
20598 };
20599 const dragManagerForLayer = getDragManager();
20600 if (dragManagerForLayer) {
20601 dropTargetDeregisters.push(
20602 dragManagerForLayer.registerDropTarget(canvasDropTarget)
20603 );
20604 }
20605 const onCanvasClick = (e) => {
20606 if (e.target instanceof Element && e.target.closest(`.${TILE_CLASS}`)) {
20607 return;
20608 }
20609 setSelected(null);
20610 };
20611 host.addEventListener("click", onCanvasClick);
20612 function attachSelectOnClick(tile2, placement) {
20613 tile2.addEventListener("click", (e) => {
20614 e.stopPropagation();
20615 setSelected(placement);
20616 });
20617 }
20618 repaint(store.getState());
20619 const off = store.subscribe(repaint);
20620 let resolveHydrated = () => void 0;
20621 const hydrated = new Promise((resolve2) => {
20622 resolveHydrated = resolve2;
20623 });
20624 if (!store.getState().hydratedFolders.has(folderId)) {
20625 void listPlacements(folderId).then((res) => {
20626 store.setFolderPlacements(folderId, res.placements);
20627 }).catch((err) => {
20628 console.error("[desktop-mode] files: failed to hydrate folder", folderId, err);
20629 }).finally(() => {
20630 resolveHydrated();
20631 });
20632 } else {
20633 queueMicrotask(resolveHydrated);
20634 }
20635 const colsForWidth = () => {
20636 const w = host.clientWidth > 0 ? host.clientWidth : 4 * GRID_CELL_W;
20637 return Math.max(1, Math.floor((w - GRID_PADDING) / GRID_CELL_W));
20638 };
20639 const sortPlacements = (list2, mode) => {
20640 const sorted = list2.slice();
20641 switch (mode) {
20642 case "name-asc":
20643 sorted.sort(
20644 (a, b) => a.file.title.localeCompare(b.file.title)
20645 );
20646 break;
20647 case "name-desc":
20648 sorted.sort(
20649 (a, b) => b.file.title.localeCompare(a.file.title)
20650 );
20651 break;
20652 case "date-asc":
20653 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
20654 break;
20655 case "date-desc":
20656 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
20657 break;
20658 }
20659 return sorted;
20660 };
20661 const sort = (mode) => {
20662 const live = store.getState().placementsByFolder.get(folderId);
20663 if (!live || live.length === 0) {
20664 return;
20665 }
20666 const pinned = live.filter((p) => isPinned(p));
20667 const draggable = live.filter((p) => !isPinned(p));
20668 const sorted = sortPlacements(draggable, mode);
20669 const cols = colsForWidth();
20670 const occupied = /* @__PURE__ */ new Set();
20671 for (let i = 0; i < pinned.length; i += 1) {
20672 occupied.add(cellKey(0, i));
20673 }
20674 let idx = 0;
20675 const nextCell = () => {
20676 while (true) {
20677 const row = Math.floor(idx / cols);
20678 const col = idx % cols;
20679 idx += 1;
20680 if (!occupied.has(cellKey(col, row))) {
20681 return { col, row };
20682 }
20683 }
20684 };
20685 sorted.forEach((p, i) => {
20686 const cell = nextCell();
20687 const x = GRID_PADDING + cell.col * GRID_CELL_W;
20688 const y = GRID_PADDING + cell.row * GRID_CELL_H;
20689 const next = {
20690 ...p,
20691 x,
20692 y,
20693 sortOrder: i
20694 };
20695 store.upsertPlacement(next);
20696 if (isSyntheticPlacement(p)) {
20697 return;
20698 }
20699 void updatePlacement(p.id, { x, y, sortOrder: i }).catch((err) => {
20700 console.error(
20701 "[desktop-mode] files: sort persist failed",
20702 err
20703 );
20704 });
20705 });
20706 };
20707 const reflow = () => {
20708 const live = store.getState().placementsByFolder.get(folderId);
20709 if (!live || live.length === 0) {
20710 return;
20711 }
20712 const w = host.clientWidth > 0 ? host.clientWidth : Infinity;
20713 const overflowing = live.some((p) => {
20714 const right = p.x + GRID_CELL_W;
20715 return right > w;
20716 });
20717 if (!overflowing) {
20718 return;
20719 }
20720 const cols = colsForWidth();
20721 const pinned = live.filter((p) => isPinned(p));
20722 const draggable = live.filter((p) => !isPinned(p));
20723 const occupied = /* @__PURE__ */ new Set();
20724 for (let i = 0; i < pinned.length; i += 1) {
20725 occupied.add(cellKey(0, i));
20726 }
20727 let idx = 0;
20728 const nextCell = () => {
20729 while (true) {
20730 const row = Math.floor(idx / cols);
20731 const col = idx % cols;
20732 idx += 1;
20733 if (!occupied.has(cellKey(col, row))) {
20734 return { col, row };
20735 }
20736 }
20737 };
20738 for (const p of draggable) {
20739 const cell = nextCell();
20740 const x = GRID_PADDING + cell.col * GRID_CELL_W;
20741 const y = GRID_PADDING + cell.row * GRID_CELL_H;
20742 const tile2 = container.querySelector(
20743 `[data-placement-id="${p.id}"]`
20744 );
20745 if (tile2) {
20746 setTilePosition(tile2, x, y);
20747 }
20748 }
20749 };
20750 let lastWidth = host.clientWidth;
20751 let resizeObserver = null;
20752 if (typeof ResizeObserver !== "undefined") {
20753 resizeObserver = new ResizeObserver(() => {
20754 const w = host.clientWidth;
20755 if (w === lastWidth) {
20756 return;
20757 }
20758 lastWidth = w;
20759 reflow();
20760 });
20761 resizeObserver.observe(host);
20762 }
20763 return {
20764 host,
20765 folderId,
20766 onSelectionChange(cb) {
20767 selectionListeners.add(cb);
20768 return () => {
20769 selectionListeners.delete(cb);
20770 };
20771 },
20772 sort,
20773 reflow,
20774 hydrated,
20775 dispose() {
20776 off();
20777 resizeObserver?.disconnect();
20778 resizeObserver = null;
20779 for (const deregister of dropTargetDeregisters) {
20780 try {
20781 deregister();
20782 } catch {
20783 }
20784 }
20785 dropTargetDeregisters.length = 0;
20786 for (const deregister of folderDropDeregisters.values()) {
20787 try {
20788 deregister();
20789 } catch {
20790 }
20791 }
20792 folderDropDeregisters.clear();
20793 for (const deregister of tileRejectDeregisters.values()) {
20794 try {
20795 deregister();
20796 } catch {
20797 }
20798 }
20799 tileRejectDeregisters.clear();
20800 host.removeEventListener("click", onCanvasClick);
20801 selectionListeners.clear();
20802 container.remove();
20803 }
20804 };
20805 }
20806 function fingerprint(list2) {
20807 if (list2.length === 0) {
20808 return "0";
20809 }
20810 const parts = [];
20811 for (const p of list2) {
20812 parts.push(
20813 `${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}`
20814 );
20815 }
20816 return parts.join("|");
20817 }
20818 function isPinned(placement) {
20819 return Boolean(placement.file.pinned);
20820 }
20821 function readSynthSource(placement) {
20822 const meta = placement.meta;
20823 if (!meta || typeof meta !== "object") {
20824 return null;
20825 }
20826 const v = meta.__synthFromDockItem;
20827 return typeof v === "string" && v !== "" ? v : null;
20828 }
20829 function isSyntheticPlacement(placement) {
20830 return placement.id <= 0 || readSynthSource(placement) !== null;
20831 }
20832 const RECYCLE_BIN_REF = "desktop-mode-recycle-bin";
20833 function shouldRejectTileDrops(placement) {
20834 if (placement.file?.type === "folder") {
20835 return false;
20836 }
20837 if (placement.file?.ref === RECYCLE_BIN_REF) {
20838 return false;
20839 }
20840 return true;
20841 }
20842 function buildVisualOccupiedSet(placements, excludeId) {
20843 const sorted = placements.slice().sort((a, b) => {
20844 const ap = isPinned(a) ? 0 : 1;
20845 const bp = isPinned(b) ? 0 : 1;
20846 return ap - bp;
20847 });
20848 const set = /* @__PURE__ */ new Set();
20849 let pinnedIdx = 0;
20850 for (const p of sorted) {
20851 if (excludeId !== void 0 && p.id === excludeId) {
20852 continue;
20853 }
20854 if (isPinned(p)) {
20855 set.add(cellKey(0, pinnedIdx));
20856 pinnedIdx += 1;
20857 } else {
20858 const cell = pointToCell(p.x, p.y);
20859 set.add(cellKey(cell.col, cell.row));
20860 }
20861 }
20862 return set;
20863 }
20864 function wouldCreateFolderCycle(movingFolderId, targetParentId) {
20865 if (targetParentId <= 0 || movingFolderId <= 0) {
20866 return false;
20867 }
20868 if (movingFolderId === targetParentId) {
20869 return true;
20870 }
20871 const parentByFolderId = /* @__PURE__ */ new Map();
20872 const state2 = store.getState();
20873 for (const bucket2 of state2.placementsByFolder.values()) {
20874 for (const p of bucket2) {
20875 if (p.file?.type !== "folder") {
20876 continue;
20877 }
20878 const fid = parseInt(p.file.ref, 10);
20879 if (Number.isNaN(fid) || fid <= 0) {
20880 continue;
20881 }
20882 if (!parentByFolderId.has(fid)) {
20883 parentByFolderId.set(fid, p.parentId);
20884 }
20885 }
20886 }
20887 const visited = /* @__PURE__ */ new Set();
20888 let cursor = targetParentId;
20889 let maxDepth = 256;
20890 while (cursor > 0 && maxDepth-- > 0) {
20891 if (cursor === movingFolderId) {
20892 return true;
20893 }
20894 if (visited.has(cursor)) {
20895 return true;
20896 }
20897 visited.add(cursor);
20898 const next = parentByFolderId.get(cursor);
20899 if (next === void 0) {
20900 return false;
20901 }
20902 cursor = next;
20903 }
20904 return false;
20905 }
20906 function persistDockPromotedPosition(dockItemId, x, y) {
20907 const api = window.wp?.desktop;
20908 if (!api?.getOsSettings || !api?.updateOsSettings) {
20909 return;
20910 }
20911 const current = api.getOsSettings().dockPromotedPositions ?? {};
20912 api.updateOsSettings({
20913 dockPromotedPositions: {
20914 ...current,
20915 [dockItemId]: { x, y }
20916 }
20917 });
20918 }
20919 function tryPatchPositions(list2, container, host) {
20920 const tiles = Array.from(
20921 container.querySelectorAll("[data-placement-id]")
20922 );
20923 if (tiles.length !== list2.length) {
20924 return false;
20925 }
20926 const byId = /* @__PURE__ */ new Map();
20927 for (const tile2 of tiles) {
20928 const raw = tile2.dataset.placementId ?? "";
20929 const id = parseInt(raw, 10);
20930 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
20931 return false;
20932 }
20933 byId.set(id, tile2);
20934 }
20935 for (const placement of list2) {
20936 const tile2 = byId.get(placement.id);
20937 if (!tile2) {
20938 return false;
20939 }
20940 if (tile2.dataset.fileType !== placement.file.type) {
20941 return false;
20942 }
20943 if (tile2.dataset.fileRef !== placement.file.ref) {
20944 return false;
20945 }
20946 const wasPinned = tile2.classList.contains(`${TILE_CLASS}--pinned`);
20947 if (wasPinned !== isPinned(placement)) {
20948 return false;
20949 }
20950 }
20951 const pinnedSlots = /* @__PURE__ */ new Map();
20952 const occupiedCells = /* @__PURE__ */ new Set();
20953 let pinnedIdx = 0;
20954 for (const placement of list2) {
20955 if (!isPinned(placement)) {
20956 continue;
20957 }
20958 const slot = cellToPos(0, pinnedIdx);
20959 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
20960 occupiedCells.add(cellKey(slot.col, slot.row));
20961 pinnedIdx += 1;
20962 }
20963 const displaced = /* @__PURE__ */ new Map();
20964 for (const placement of list2) {
20965 if (pinnedSlots.has(placement.id)) {
20966 continue;
20967 }
20968 const target2 = pointToCell(placement.x, placement.y);
20969 const key = cellKey(target2.col, target2.row);
20970 if (!occupiedCells.has(key)) {
20971 occupiedCells.add(key);
20972 continue;
20973 }
20974 const free = snapToEmptyCell(
20975 placement.x,
20976 placement.y,
20977 occupiedCells,
20978 host
20979 );
20980 occupiedCells.add(cellKey(free.col, free.row));
20981 displaced.set(placement.id, { x: free.x, y: free.y });
20982 }
20983 for (const placement of list2) {
20984 const tile2 = byId.get(placement.id);
20985 if (!tile2) {
20986 continue;
20987 }
20988 const pinned = pinnedSlots.get(placement.id);
20989 const disp = displaced.get(placement.id);
20990 if (pinned) {
20991 setTilePosition(tile2, pinned.x, pinned.y);
20992 } else if (disp) {
20993 setTilePosition(tile2, disp.x, disp.y);
20994 } else {
20995 setTilePosition(tile2, placement.x, placement.y);
20996 }
20997 }
20998 return true;
20999 }
21000 function hidePromotedDockItem(dockItemId) {
21001 const api = window.wp?.desktop;
21002 if (!api?.getOsSettings || !api?.updateOsSettings) {
21003 return;
21004 }
21005 const current = api.getOsSettings().itemVisibility ?? {};
21006 const next = { ...current, [dockItemId]: "dock" };
21007 api.updateOsSettings({ itemVisibility: next });
21008 }
21009 function registerFolderDropTarget(dragManager, tile2, targetFolderId, currentFolderId) {
21010 const target2 = {
21011 id: `desktop-mode-files-folder-${targetFolderId}-tile-${tile2.dataset.placementId ?? "?"}`,
21012 element: tile2,
21013 accept: (payload) => {
21014 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
21015 return false;
21016 }
21017 if (payload.type === "desktop-file") {
21018 const data = payload.data;
21019 if (data.placement.file.type === "folder" && parseInt(data.placement.file.ref, 10) === targetFolderId) {
21020 return false;
21021 }
21022 if (data.placement.parentId === targetFolderId) {
21023 return false;
21024 }
21025 if (isSyntheticPlacement(data.placement)) {
21026 return false;
21027 }
21028 if (data.placement.file.type === "folder") {
21029 const movingFolderId = parseInt(data.placement.file.ref, 10);
21030 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, targetFolderId)) {
21031 return false;
21032 }
21033 }
21034 }
21035 return true;
21036 },
21037 onEnter: () => {
21038 tile2.classList.add(`${TILE_CLASS}--drop-target`);
21039 },
21040 onLeave: () => {
21041 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
21042 },
21043 onDrop: (session) => {
21044 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
21045 if (session.payload.type === "desktop-file") {
21046 const data = session.payload.data;
21047 const next = {
21048 ...data.placement,
21049 parentId: targetFolderId
21050 };
21051 store.upsertPlacement(next);
21052 void updatePlacement(
21053 data.placement.id,
21054 { parentId: targetFolderId },
21055 data.placement.updatedAtMs
21056 ).then((server) => {
21057 store.upsertPlacement(server, "remote");
21058 }).catch((err) => {
21059 if (isConflict(err)) {
21060 showConflictToast(err);
21061 } else {
21062 console.error(
21063 "[desktop-mode] files: move-into-folder persist failed",
21064 err
21065 );
21066 }
21067 store.upsertPlacement(data.placement);
21068 });
21069 return;
21070 }
21071 if (session.payload.type === "shortcut") {
21072 const data = session.payload.data;
21073 const peers = store.getState().placementsByFolder.get(targetFolderId) ?? [];
21074 const cell = nextRowMajorCell(buildVisualOccupiedSet(peers));
21075 void createPlacement({
21076 parentId: targetFolderId,
21077 type: data.kind,
21078 ref: data.ref,
21079 x: cell.x,
21080 y: cell.y
21081 }).then((placement) => {
21082 store.upsertPlacement(placement);
21083 doAction("desktop-mode.files.shortcut-dropped", {
21084 folderId: targetFolderId,
21085 placement
21086 });
21087 }).catch((err) => {
21088 console.error(
21089 "[desktop-mode] shortcut drop into folder failed:",
21090 err
21091 );
21092 });
21093 }
21094 }
21095 };
21096 return dragManager.registerDropTarget(target2);
21097 }
21098 function attachTileDrag(tile2, placement, folderId) {
21099 tile2.addEventListener("pointerdown", (e) => {
21100 if (e.button !== 0) {
21101 return;
21102 }
21103 const dragManager = getDragManager();
21104 if (!dragManager) {
21105 return;
21106 }
21107 const liveBucket = store.getState().placementsByFolder.get(folderId);
21108 const livePlacement = liveBucket?.find((p) => p.id === placement.id) ?? placement;
21109 parseFloat(tile2.style.left) || livePlacement.x;
21110 parseFloat(tile2.style.top) || livePlacement.y;
21111 dragManager.start({
21112 payload: {
21113 type: "desktop-file",
21114 source: tile2,
21115 data: {
21116 placement: livePlacement,
21117 sourceFolderId: folderId,
21118 // Synthesize a cross-frame bridge payload from the
21119 // placement's file shape so a wallpaper-placed
21120 // shortcut can be dropped into an open Gutenberg
21121 // iframe and inserted as the matching block. The
21122 // PHP serialize() methods (`Desktop_Mode_Post_File`,
21123 // `Desktop_Mode_User_File`, `Desktop_Mode_Attachment_File`)
21124 // surface the URL fields this needs.
21125 bridgePayload: buildBridgePayloadFromPlacement(livePlacement)
21126 },
21127 ghost: {
21128 offsetX: e.clientX - tile2.getBoundingClientRect().left,
21129 offsetY: e.clientY - tile2.getBoundingClientRect().top
21130 }
21131 },
21132 origin: e
21133 // `onClickOnly` intentionally empty — a tile click is
21134 // handled by the dedicated `attachSelectOnClick` listener
21135 // below, which fires from the regular `click` event after
21136 // a sub-threshold pointerup. The manager won't fire a
21137 // `click` itself; the browser does.
21138 });
21139 });
21140 }
21141 function attachContextMenu(tile2, placement) {
21142 tile2.addEventListener("contextmenu", (e) => {
21143 e.preventDefault();
21144 e.stopPropagation();
21145 const items = [
21146 {
21147 id: "open",
21148 label: "Open",
21149 icon: "dashicons-external",
21150 sort: 10,
21151 onClick: () => {
21152 const file = resolve(placement.file);
21153 void openFile(file);
21154 }
21155 }
21156 ];
21157 if (placement.file.type === "post") {
21158 items.push({
21159 id: "navigate-into",
21160 label: "Navigate into",
21161 icon: "dashicons-category",
21162 sort: 20,
21163 onClick: () => {
21164 const postId = parseInt(placement.file.ref, 10);
21165 if (!postId) {
21166 return;
21167 }
21168 const api = window.wp?.desktop?.myWordpress;
21169 const postType = typeof placement.file.postType === "string" ? placement.file.postType : "post";
21170 const entityId = postType === "page" ? "pages" : "posts";
21171 api?.openDetail({
21172 entityId,
21173 postId,
21174 postTitle: placement.file.title || `#${postId}`
21175 });
21176 }
21177 });
21178 }
21179 const isFolder = placement.file.type === "folder";
21180 if (isFolder) {
21181 items.push({
21182 id: "rename-folder",
21183 label: "Rename…",
21184 icon: "dashicons-edit",
21185 sort: 30,
21186 onClick: () => {
21187 const folderId = parseInt(placement.file.ref, 10);
21188 if (!folderId) {
21189 return;
21190 }
21191 openCreateFolderDialog({
21192 title: "Rename folder",
21193 label: "New name",
21194 submitLabel: "Rename",
21195 initialName: placement.file.title,
21196 onSubmit: async (name) => {
21197 const trimmed = name.trim();
21198 if (!trimmed || trimmed === placement.file.title) {
21199 return;
21200 }
21201 const previousTitle = placement.file.title;
21202 const optimistic = {
21203 ...placement,
21204 file: { ...placement.file, title: trimmed }
21205 };
21206 store.upsertPlacement(optimistic);
21207 try {
21208 const folderUpdatedAtMs = store.getState().folders.get(folderId)?.updatedAtMs ?? 0;
21209 const updated = await updateFolder(
21210 folderId,
21211 { name: trimmed },
21212 folderUpdatedAtMs
21213 );
21214 store.upsertFolder(updated);
21215 const refreshed = await listPlacements(
21216 placement.parentId
21217 );
21218 store.setFolderPlacements(
21219 placement.parentId,
21220 refreshed.placements
21221 );
21222 } catch (err) {
21223 console.error(
21224 "[desktop-mode] rename folder failed:",
21225 err
21226 );
21227 store.upsertPlacement({
21228 ...placement,
21229 file: {
21230 ...placement.file,
21231 title: previousTitle
21232 }
21233 });
21234 }
21235 }
21236 });
21237 }
21238 });
21239 if (placement.canTrash !== false) {
21240 items.push({
21241 id: "delete-folder",
21242 label: "Move folder to Trash",
21243 icon: "dashicons-trash",
21244 sort: 90,
21245 danger: true,
21246 onClick: () => trashFolderWithUndo(placement)
21247 });
21248 }
21249 } else {
21250 const synthFromDockItem = readSynthSource(placement);
21251 const isRegisteredIcon = placement.file.type === "shortcut";
21252 if (synthFromDockItem || isRegisteredIcon) {
21253 const hideId = synthFromDockItem ?? placement.file.ref;
21254 items.push({
21255 id: "hide-from-desktop",
21256 label: "Hide from desktop",
21257 icon: "dashicons-hidden",
21258 sort: 90,
21259 onClick: () => hidePromotedDockItem(hideId)
21260 });
21261 } else if (placement.canTrash !== false) {
21262 items.push({
21263 id: "remove",
21264 label: "Move to Trash",
21265 icon: "dashicons-trash",
21266 sort: 90,
21267 danger: true,
21268 onClick: () => trashPlacementWithUndo(placement)
21269 });
21270 }
21271 }
21272 openTileMenu({ x: e.clientX, y: e.clientY }, { placement, items });
21273 });
21274 }
21275 const STATUS_BAR_CLASS = "desktop-mode-folder-status-bar";
21276 const ROOT_CLASS$2 = STATUS_BAR_CLASS;
21277 function mountFolderStatusBar(host, folderId) {
21278 const bar = document.createElement("div");
21279 bar.className = ROOT_CLASS$2;
21280 bar.setAttribute("role", "status");
21281 bar.dataset.folderId = String(folderId);
21282 host.appendChild(bar);
21283 const repaint = () => {
21284 const list2 = getFilesState().placementsByFolder.get(folderId) ?? [];
21285 const folders = list2.filter((p) => p.file.type === "folder").length;
21286 const files = list2.length - folders;
21287 const ctx = {
21288 folderId,
21289 totals: { files, folders, total: list2.length }
21290 };
21291 const segments = computeSegments(ctx);
21292 render(bar, segments);
21293 };
21294 repaint();
21295 const off = subscribeFilesStore(() => repaint());
21296 return {
21297 dispose() {
21298 off();
21299 bar.remove();
21300 }
21301 };
21302 }
21303 function computeSegments(ctx) {
21304 const { folders, files } = ctx.totals;
21305 const builtIns = [
21306 {
21307 id: "count",
21308 label: pluralize(files, "file", "files") + (folders > 0 ? `, ${pluralize(folders, "folder", "folders")}` : ""),
21309 align: "start",
21310 sort: 10
21311 }
21312 ];
21313 const filtered = applyFilters(
21314 "desktop-mode.files.folder-window.status-bar",
21315 builtIns,
21316 ctx
21317 );
21318 return Array.isArray(filtered) ? filtered : builtIns;
21319 }
21320 function render(bar, segments) {
21321 const sort = (a, b) => {
21322 const sa = typeof a.sort === "number" ? a.sort : 100;
21323 const sb = typeof b.sort === "number" ? b.sort : 100;
21324 if (sa !== sb) {
21325 return sa - sb;
21326 }
21327 return a.label.localeCompare(b.label);
21328 };
21329 const start = segments.filter((s) => (s.align ?? "start") === "start").sort(sort);
21330 const end = segments.filter((s) => s.align === "end").sort(sort);
21331 bar.replaceChildren();
21332 bar.appendChild(buildCluster("start", start));
21333 bar.appendChild(buildCluster("end", end));
21334 }
21335 function buildCluster(align, segs) {
21336 const cluster = document.createElement("div");
21337 cluster.className = `${ROOT_CLASS$2}__cluster ${ROOT_CLASS$2}__cluster--${align}`;
21338 for (const seg of segs) {
21339 cluster.appendChild(buildSegment(seg));
21340 }
21341 return cluster;
21342 }
21343 function buildSegment(seg) {
21344 const interactive = typeof seg.onClick === "function";
21345 const el = document.createElement(interactive ? "button" : "span");
21346 el.className = `${ROOT_CLASS$2}__segment`;
21347 el.dataset.segmentId = seg.id;
21348 if (interactive) {
21349 el.type = "button";
21350 el.addEventListener("click", (e) => seg.onClick(e));
21351 }
21352 if (seg.icon) {
21353 const icon = document.createElement("span");
21354 icon.className = `${ROOT_CLASS$2}__icon dashicons ${seg.icon.replace(/[^a-zA-Z0-9_-]/g, "")}`;
21355 icon.setAttribute("aria-hidden", "true");
21356 el.appendChild(icon);
21357 }
21358 const label = document.createElement("span");
21359 label.className = `${ROOT_CLASS$2}__label`;
21360 label.textContent = seg.label;
21361 el.appendChild(label);
21362 return el;
21363 }
21364 function pluralize(n, singular, plural) {
21365 return `${n} ${n === 1 ? singular : plural}`;
21366 }
21367 const MENU_CLASS$1 = "desktop-mode-icon-canvas-menu";
21368 let activeMenu$1 = null;
21369 let activeFlyout = null;
21370 let activeCanvas = null;
21371 let outsideHandler = null;
21372 let escHandler = null;
21373 function attachIconCanvasMenu(canvas, deps2) {
21374 deps2.openOnBackgroundClick !== false;
21375 const onContextMenu = (e) => {
21376 if (isInsideTile(e.target) || isInsideMenu(e.target)) {
21377 return;
21378 }
21379 e.preventDefault();
21380 toggle(e.clientX, e.clientY);
21381 };
21382 let toggleGen = 0;
21383 const toggle = (x, y) => {
21384 if (activeCanvas === canvas && activeMenu$1) {
21385 closeMenu();
21386 return;
21387 }
21388 const items = buildItems(deps2);
21389 const filtered = applyFilters(
21390 "desktop-mode.icon-canvas.menu",
21391 items,
21392 deps2.scope
21393 );
21394 const finalItems = Array.isArray(filtered) ? filtered : items;
21395 const myGen = ++toggleGen;
21396 openWithShellOverlays(
21397 () => myGen === toggleGen,
21398 () => openMenu(finalItems, { x, y }, canvas)
21399 );
21400 };
21401 canvas.addEventListener("contextmenu", onContextMenu);
21402 return {
21403 dispose: () => {
21404 canvas.removeEventListener("contextmenu", onContextMenu);
21405 closeMenu();
21406 }
21407 };
21408 }
21409 function isInsideTile(target2) {
21410 if (!(target2 instanceof Element)) {
21411 return false;
21412 }
21413 return target2.closest(".desktop-mode-file-tile") !== null;
21414 }
21415 function isInsideMenu(target2) {
21416 if (!(target2 instanceof Element)) {
21417 return false;
21418 }
21419 return target2.closest(`.${MENU_CLASS$1}`) !== null;
21420 }
21421 function buildItems(deps2) {
21422 const sortItem = {
21423 id: "sort-by",
21424 label: __("Sort by", "desktop-mode"),
21425 icon: "dashicons-sort",
21426 sort: 10,
21427 children: [
21428 {
21429 id: "sort-name-asc",
21430 label: __("Name (A → Z)", "desktop-mode"),
21431 sort: 10,
21432 onClick: () => deps2.onSort("name-asc")
21433 },
21434 {
21435 id: "sort-name-desc",
21436 label: __("Name (Z → A)", "desktop-mode"),
21437 sort: 20,
21438 onClick: () => deps2.onSort("name-desc")
21439 },
21440 {
21441 id: "sort-date-desc",
21442 label: __("Newest first", "desktop-mode"),
21443 sort: 30,
21444 onClick: () => deps2.onSort("date-desc")
21445 },
21446 {
21447 id: "sort-date-asc",
21448 label: __("Oldest first", "desktop-mode"),
21449 sort: 40,
21450 onClick: () => deps2.onSort("date-asc")
21451 }
21452 ]
21453 };
21454 const items = [sortItem];
21455 if (Array.isArray(deps2.extraItems)) {
21456 items.push(...deps2.extraItems);
21457 }
21458 return items;
21459 }
21460 function sortItems(items) {
21461 return items.slice().sort((a, b) => {
21462 const sa = typeof a.sort === "number" ? a.sort : 100;
21463 const sb = typeof b.sort === "number" ? b.sort : 100;
21464 if (sa !== sb) {
21465 return sa - sb;
21466 }
21467 return a.label.localeCompare(b.label);
21468 });
21469 }
21470 function openMenu(items, pos, canvas) {
21471 closeMenu();
21472 if (items.length === 0) {
21473 return;
21474 }
21475 activeCanvas = canvas;
21476 const sorted = sortItems(items);
21477 const menu = document.createElement("wpd-context-menu");
21478 menu.setAttribute("open", "");
21479 menu.classList.add(MENU_CLASS$1);
21480 menu.style.left = `${pos.x}px`;
21481 menu.style.top = `${pos.y}px`;
21482 const itemById = /* @__PURE__ */ new Map();
21483 for (const item of sorted) {
21484 itemById.set(item.id, item);
21485 const opt = appendOption(menu, item);
21486 if (hasChildren(item)) {
21487 opt.addEventListener("mouseenter", () => {
21488 openFlyout(item, opt);
21489 });
21490 }
21491 }
21492 menu.addEventListener("wpd-context-menu-pick", (e) => {
21493 const detail = e.detail;
21494 const item = itemById.get(detail.id);
21495 if (!item) {
21496 return;
21497 }
21498 if (hasChildren(item)) {
21499 e.stopPropagation();
21500 const anchor = menu.querySelector(
21501 `[data-menu-item-id="${item.id}"]`
21502 );
21503 if (anchor) {
21504 openFlyout(item, anchor);
21505 }
21506 return;
21507 }
21508 closeMenu();
21509 item.onClick?.();
21510 });
21511 document.body.appendChild(menu);
21512 activeMenu$1 = menu;
21513 clampToViewport(menu);
21514 queueMicrotask(() => {
21515 outsideHandler = (e) => {
21516 if (isInsideMenu(e.target)) {
21517 return;
21518 }
21519 closeMenu();
21520 };
21521 escHandler = (e) => {
21522 if (e.key === "Escape") {
21523 closeMenu();
21524 }
21525 };
21526 document.addEventListener("mousedown", outsideHandler);
21527 document.addEventListener("keydown", escHandler);
21528 });
21529 }
21530 function appendOption(host, item) {
21531 const opt = document.createElement("wpd-context-menu-option");
21532 opt.dataset.menuItemId = item.id;
21533 opt.setAttribute("value", item.id);
21534 if (item.heading) {
21535 opt.setAttribute("heading", "");
21536 }
21537 if (item.disabled) {
21538 opt.setAttribute("disabled", "");
21539 }
21540 if (item.icon) {
21541 opt.setAttribute("icon", sanitizeClass$1(item.icon));
21542 }
21543 if (hasChildren(item)) {
21544 opt.setAttribute("has-children", "");
21545 }
21546 opt.textContent = item.label;
21547 host.appendChild(opt);
21548 return opt;
21549 }
21550 function openFlyout(parent, anchor) {
21551 closeFlyout();
21552 if (!hasChildren(parent)) {
21553 return;
21554 }
21555 const fly = document.createElement("wpd-context-menu");
21556 fly.setAttribute("open", "");
21557 fly.classList.add(MENU_CLASS$1, `${MENU_CLASS$1}--flyout`);
21558 const childById = /* @__PURE__ */ new Map();
21559 for (const child of sortItems(parent.children ?? [])) {
21560 childById.set(child.id, child);
21561 appendOption(fly, child);
21562 }
21563 fly.addEventListener("wpd-context-menu-pick", (e) => {
21564 const detail = e.detail;
21565 const child = childById.get(detail.id);
21566 if (!child) {
21567 return;
21568 }
21569 e.stopPropagation();
21570 closeMenu();
21571 child.onClick?.();
21572 });
21573 document.body.appendChild(fly);
21574 activeFlyout = fly;
21575 positionFlyout(fly, anchor);
21576 }
21577 function positionFlyout(fly, anchor) {
21578 const ar = anchor.getBoundingClientRect();
21579 fly.style.position = "fixed";
21580 fly.style.left = `${ar.right}px`;
21581 fly.style.top = `${ar.top}px`;
21582 const fr = fly.getBoundingClientRect();
21583 if (fr.right > window.innerWidth) {
21584 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
21585 }
21586 if (fr.bottom > window.innerHeight) {
21587 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
21588 }
21589 }
21590 function clampToViewport(menu) {
21591 const rect = menu.getBoundingClientRect();
21592 if (rect.right > window.innerWidth) {
21593 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
21594 }
21595 if (rect.bottom > window.innerHeight) {
21596 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
21597 }
21598 }
21599 function hasChildren(item) {
21600 return Array.isArray(item.children) && item.children.length > 0;
21601 }
21602 function closeFlyout() {
21603 if (activeFlyout) {
21604 activeFlyout.remove();
21605 activeFlyout = null;
21606 }
21607 }
21608 function closeMenu() {
21609 closeFlyout();
21610 if (activeMenu$1) {
21611 activeMenu$1.remove();
21612 activeMenu$1 = null;
21613 }
21614 activeCanvas = null;
21615 if (outsideHandler) {
21616 document.removeEventListener("mousedown", outsideHandler);
21617 outsideHandler = null;
21618 }
21619 if (escHandler) {
21620 document.removeEventListener("keydown", escHandler);
21621 escHandler = null;
21622 }
21623 }
21624 function sanitizeClass$1(raw) {
21625 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
21626 }
21627 const ROOT_CLASS$1 = "desktop-mode-breadcrumbs";
21628 function renderBreadcrumbs(host, segments, opts = {}) {
21629 host.replaceChildren();
21630 host.classList.add(ROOT_CLASS$1);
21631 if (opts.onBack) {
21632 const back = document.createElement("button");
21633 back.type = "button";
21634 back.className = `${ROOT_CLASS$1}__back`;
21635 back.setAttribute("aria-label", __("Back", "desktop-mode"));
21636 back.title = __("Back", "desktop-mode");
21637 const arrow = document.createElement("span");
21638 arrow.className = "dashicons dashicons-arrow-left-alt2";
21639 arrow.setAttribute("aria-hidden", "true");
21640 back.appendChild(arrow);
21641 if (opts.backDisabled) {
21642 back.disabled = true;
21643 }
21644 const onBack = opts.onBack;
21645 back.addEventListener("click", () => {
21646 if (back.disabled) {
21647 return;
21648 }
21649 onBack();
21650 });
21651 host.appendChild(back);
21652 }
21653 const nav = document.createElement("nav");
21654 nav.className = `${ROOT_CLASS$1}__crumbs`;
21655 nav.setAttribute("aria-label", __("Breadcrumb", "desktop-mode"));
21656 segments.forEach((seg, idx) => {
21657 if (idx > 0) {
21658 const sep = document.createElement("span");
21659 sep.className = `${ROOT_CLASS$1}__sep`;
21660 sep.setAttribute("aria-hidden", "true");
21661 sep.textContent = "›";
21662 nav.appendChild(sep);
21663 }
21664 if (!seg.onClick) {
21665 const here = document.createElement("span");
21666 here.className = `${ROOT_CLASS$1}__crumb ${ROOT_CLASS$1}__crumb--current`;
21667 here.setAttribute("aria-current", "page");
21668 here.textContent = seg.label;
21669 nav.appendChild(here);
21670 return;
21671 }
21672 const btn = document.createElement("button");
21673 btn.type = "button";
21674 btn.className = `${ROOT_CLASS$1}__crumb`;
21675 btn.textContent = seg.label;
21676 const onClick = seg.onClick;
21677 btn.addEventListener("click", () => {
21678 onClick();
21679 });
21680 nav.appendChild(btn);
21681 });
21682 host.appendChild(nav);
21683 }
21684 async function getJson(url, init2 = {}) {
21685 const response = await trackedFetch$1(url, {
21686 credentials: "same-origin",
21687 headers: {
21688 Accept: "application/json",
21689 "X-WP-Nonce": readRestNonce(),
21690 ...init2.headers ?? {}
21691 },
21692 ...init2
21693 });
21694 if (!response.ok) {
21695 throw new Error(`${response.status} ${response.statusText}`);
21696 }
21697 return await response.json();
21698 }
21699 function readRestNonce() {
21700 const cfg = window.wp?.desktop?.config;
21701 return cfg?.restNonce ?? "";
21702 }
21703 function readRestRoot() {
21704 const cfg = window.wp?.desktop?.config;
21705 if (cfg?.restUrl) {
21706 return cfg.restUrl.endsWith("/") ? cfg.restUrl : cfg.restUrl + "/";
21707 }
21708 return `${window.location.origin}/wp-json/`;
21709 }
21710 function restUrl(path) {
21711 return joinRestUrl(readRestRoot(), path);
21712 }
21713 function renderPlacementPreview(placement, host) {
21714 const filtered = applyFilters(
21715 "desktop-mode.files.preview",
21716 null,
21717 placement
21718 );
21719 if (filtered instanceof HTMLElement) {
21720 host.replaceChildren(filtered);
21721 return;
21722 }
21723 if (placement.accessGated) {
21724 host.replaceChildren(renderAccessGated(placement));
21725 return;
21726 }
21727 host.replaceChildren(renderLoading());
21728 void renderByType(placement).then((node) => {
21729 host.replaceChildren(node);
21730 }).catch((err) => {
21731 host.replaceChildren(renderError(err));
21732 });
21733 }
21734 function renderAccessGated(placement) {
21735 const wrap = document.createElement("div");
21736 wrap.className = "desktop-mode-files__access-gated";
21737 const ring = document.createElement("div");
21738 ring.className = "desktop-mode-files__access-gated-ring";
21739 const glyph = document.createElement("span");
21740 glyph.className = "dashicons dashicons-lock desktop-mode-files__access-gated-glyph";
21741 glyph.setAttribute("aria-hidden", "true");
21742 ring.appendChild(glyph);
21743 wrap.appendChild(ring);
21744 const title = document.createElement("h2");
21745 title.className = "desktop-mode-files__access-gated-title";
21746 title.textContent = "No permission to view";
21747 wrap.appendChild(title);
21748 const sub = document.createElement("p");
21749 sub.className = "desktop-mode-files__access-gated-sub";
21750 const target2 = placement.file.title || placement.file.type;
21751 sub.textContent = `You don’t have access to "${target2}". The folder owner shared this folder with you, but your role doesn’t include permission to open this item.`;
21752 wrap.appendChild(sub);
21753 const hint = document.createElement("p");
21754 hint.className = "desktop-mode-files__access-gated-hint";
21755 hint.textContent = "Ask the owner to grant access on the underlying item, or to remove it from the shared folder.";
21756 wrap.appendChild(hint);
21757 return wrap;
21758 }
21759 async function renderByType(placement) {
21760 const file = placement.file;
21761 switch (file.type) {
21762 case "post":
21763 return renderPostPreview(file.ref, file);
21764 case "folder":
21765 return renderFolderPreview(file);
21766 case "shortcut":
21767 return renderShortcutPreview(file);
21768 case "attachment":
21769 return renderAttachmentPreview(file.ref, file);
21770 case "user":
21771 return renderUserSummary(file.ref, file);
21772 case "term":
21773 return renderTermSummary(file);
21774 case "comment":
21775 return renderCommentSummary(file.ref, file);
21776 case "bookmark":
21777 return renderBookmarkPreview(file);
21778 default:
21779 return renderGenericPreview(file);
21780 }
21781 }
21782 async function renderPostPreview(ref, file) {
21783 const id = parseInt(ref, 10);
21784 if (!id) {
21785 return renderGenericPreview(file);
21786 }
21787 let data = null;
21788 for (const path of ["wp/v2/posts", "wp/v2/pages"]) {
21789 try {
21790 data = await getJson(
21791 restUrl(
21792 `${path}/${id}?_fields=id,title,content,date,link,status`
21793 )
21794 );
21795 break;
21796 } catch {
21797 }
21798 }
21799 if (!data) {
21800 return renderGenericPreview(file);
21801 }
21802 const wrap = articleShell();
21803 const h = document.createElement("h2");
21804 h.className = "desktop-mode-my-wordpress__article-title";
21805 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
21806 wrap.appendChild(h);
21807 const meta = document.createElement("p");
21808 meta.className = "desktop-mode-my-wordpress__article-meta";
21809 const parts = [];
21810 parts.push(formatDate(data.date));
21811 if (data.status && data.status !== "publish") {
21812 parts.push(data.status);
21813 }
21814 meta.textContent = parts.join(" · ");
21815 wrap.appendChild(meta);
21816 if (data.content?.rendered) {
21817 const body = document.createElement("div");
21818 body.className = "desktop-mode-my-wordpress__article-content";
21819 body.innerHTML = data.content.rendered;
21820 wrap.appendChild(body);
21821 }
21822 const footer = document.createElement("footer");
21823 footer.className = "desktop-mode-my-wordpress__article-footer";
21824 const myWordpressApi = window.wp?.desktop?.myWordpress;
21825 if (myWordpressApi) {
21826 const exploreBtn = document.createElement("wpd-button");
21827 exploreBtn.setAttribute("variant", "secondary");
21828 exploreBtn.textContent = __("Explore details", "desktop-mode");
21829 exploreBtn.title = __(
21830 "See author, comments, categories, tags, attached media, and revisions for this entry.",
21831 "desktop-mode"
21832 );
21833 exploreBtn.addEventListener("click", () => {
21834 const postType = typeof file.postType === "string" ? file.postType : "post";
21835 myWordpressApi.openDetail({
21836 entityId: postType === "page" ? "pages" : "posts",
21837 postId: id,
21838 postTitle: stripTags(data.title.rendered) || `#${id}`
21839 });
21840 });
21841 footer.appendChild(exploreBtn);
21842 }
21843 const editBtn = document.createElement("wpd-button");
21844 editBtn.setAttribute("variant", "primary");
21845 editBtn.textContent = __("Open in editor", "desktop-mode");
21846 editBtn.addEventListener("click", () => {
21847 const adminUrl = window.wp?.desktop?.config?.adminUrl;
21848 if (!adminUrl) {
21849 return;
21850 }
21851 const editUrl = `${adminUrl}post.php?post=${id}&action=edit`;
21852 const wm = window.wp?.desktop?.windowManager;
21853 const postType = typeof file.postType === "string" ? file.postType : "post";
21854 const entityId = postType === "page" ? "pages" : "posts";
21855 wm?.open({
21856 id: `${entityId}-edit-${id}`,
21857 url: editUrl,
21858 title: stripTags(data.title.rendered),
21859 icon: file.icon
21860 });
21861 });
21862 footer.appendChild(editBtn);
21863 wrap.appendChild(footer);
21864 return wrap;
21865 }
21866 async function renderUserSummary(ref, file) {
21867 const id = parseInt(ref, 10);
21868 if (!id) {
21869 return renderGenericPreview(file);
21870 }
21871 let data = null;
21872 try {
21873 data = await getJson(
21874 restUrl(`desktop-mode/v1/user-stats/${id}`)
21875 );
21876 } catch {
21877 return renderGenericPreview(file);
21878 }
21879 const wrap = articleShell("desktop-mode-my-wordpress__user");
21880 const header = document.createElement("header");
21881 header.className = "desktop-mode-my-wordpress__user-header";
21882 if (data.profile.avatarUrl) {
21883 const img = document.createElement("img");
21884 img.className = "desktop-mode-my-wordpress__user-avatar";
21885 img.src = data.profile.avatarUrl;
21886 img.alt = "";
21887 header.appendChild(img);
21888 }
21889 const head = document.createElement("div");
21890 head.className = "desktop-mode-my-wordpress__user-headline";
21891 const h = document.createElement("h2");
21892 h.className = "desktop-mode-my-wordpress__article-title";
21893 h.textContent = data.profile.name || file.title || `#${id}`;
21894 head.appendChild(h);
21895 if (data.profile.roleLabels && data.profile.roleLabels.length > 0) {
21896 const roles = document.createElement("div");
21897 roles.className = "desktop-mode-my-wordpress__user-roles";
21898 for (const r of data.profile.roleLabels) {
21899 const badge = document.createElement("span");
21900 badge.className = "desktop-mode-my-wordpress__user-role";
21901 badge.textContent = r;
21902 roles.appendChild(badge);
21903 }
21904 head.appendChild(roles);
21905 }
21906 header.appendChild(head);
21907 wrap.appendChild(header);
21908 if (data.profile.description) {
21909 const bio = document.createElement("div");
21910 bio.className = "desktop-mode-my-wordpress__user-bio";
21911 bio.textContent = data.profile.description;
21912 wrap.appendChild(bio);
21913 }
21914 const cards = document.createElement("div");
21915 cards.className = "desktop-mode-my-wordpress__user-stats";
21916 cards.appendChild(
21917 statCard(
21918 data.counts.posts.total.toLocaleString(),
21919 __("Posts", "desktop-mode")
21920 )
21921 );
21922 cards.appendChild(
21923 statCard(
21924 data.counts.pages.total.toLocaleString(),
21925 __("Pages", "desktop-mode")
21926 )
21927 );
21928 cards.appendChild(
21929 statCard(
21930 data.counts.commentsReceived.toLocaleString(),
21931 __("Comments received", "desktop-mode")
21932 )
21933 );
21934 wrap.appendChild(cards);
21935 return wrap;
21936 }
21937 async function renderTermSummary(file) {
21938 const id = parseInt(file.ref, 10);
21939 const taxonomy = typeof file.taxonomy === "string" && file.taxonomy ? file.taxonomy : "category";
21940 if (!id) {
21941 return renderGenericPreview(file);
21942 }
21943 let data = null;
21944 try {
21945 data = await getJson(
21946 restUrl(`desktop-mode/v1/term-stats/${taxonomy}/${id}`)
21947 );
21948 } catch {
21949 return renderGenericPreview(file);
21950 }
21951 const wrap = articleShell();
21952 const h = document.createElement("h2");
21953 h.className = "desktop-mode-my-wordpress__article-title";
21954 h.textContent = data.profile.name || file.title || `#${id}`;
21955 wrap.appendChild(h);
21956 const meta = document.createElement("p");
21957 meta.className = "desktop-mode-my-wordpress__article-meta";
21958 meta.textContent = data.profile.taxonomyLabel || data.profile.taxonomy;
21959 wrap.appendChild(meta);
21960 if (data.profile.description) {
21961 const desc = document.createElement("div");
21962 desc.className = "desktop-mode-my-wordpress__article-content";
21963 desc.innerHTML = data.profile.description;
21964 wrap.appendChild(desc);
21965 }
21966 const cards = document.createElement("div");
21967 cards.className = "desktop-mode-my-wordpress__user-stats";
21968 cards.appendChild(
21969 statCard(
21970 data.counts.posts.total.toLocaleString(),
21971 __("Posts", "desktop-mode")
21972 )
21973 );
21974 cards.appendChild(
21975 statCard(
21976 data.counts.commentsReceived.toLocaleString(),
21977 __("Comments", "desktop-mode")
21978 )
21979 );
21980 cards.appendChild(
21981 statCard(
21982 data.counts.distinctAuthors.toLocaleString(),
21983 __("Authors", "desktop-mode")
21984 )
21985 );
21986 wrap.appendChild(cards);
21987 return wrap;
21988 }
21989 async function renderCommentSummary(ref, file) {
21990 const id = parseInt(ref, 10);
21991 if (!id) {
21992 return renderGenericPreview(file);
21993 }
21994 let data = null;
21995 try {
21996 data = await getJson(
21997 restUrl(`desktop-mode/v1/comment-stats/${id}`)
21998 );
21999 } catch {
22000 return renderGenericPreview(file);
22001 }
22002 const wrap = articleShell();
22003 const header = document.createElement("header");
22004 header.className = "desktop-mode-my-wordpress__user-header";
22005 if (data.author.avatarUrl) {
22006 const img = document.createElement("img");
22007 img.className = "desktop-mode-my-wordpress__user-avatar";
22008 img.src = data.author.avatarUrl;
22009 img.alt = "";
22010 header.appendChild(img);
22011 }
22012 const head = document.createElement("div");
22013 head.className = "desktop-mode-my-wordpress__user-headline";
22014 const h = document.createElement("h2");
22015 h.className = "desktop-mode-my-wordpress__article-title";
22016 h.textContent = data.author.name;
22017 head.appendChild(h);
22018 const sub = document.createElement("p");
22019 sub.className = "desktop-mode-my-wordpress__article-meta";
22020 sub.textContent = `${formatDate(data.comment.date)} · ${data.comment.status}`;
22021 head.appendChild(sub);
22022 header.appendChild(head);
22023 wrap.appendChild(header);
22024 const body = document.createElement("div");
22025 body.className = "desktop-mode-my-wordpress__article-content";
22026 body.innerHTML = data.comment.rendered;
22027 wrap.appendChild(body);
22028 if (data.post) {
22029 const card = document.createElement("div");
22030 card.className = "desktop-mode-my-wordpress__comment-post";
22031 const link = document.createElement("a");
22032 link.className = "desktop-mode-my-wordpress__comment-post-title";
22033 link.href = data.post.link;
22034 link.target = "_blank";
22035 link.rel = "noopener noreferrer";
22036 link.textContent = data.post.title;
22037 card.appendChild(link);
22038 wrap.appendChild(card);
22039 }
22040 return wrap;
22041 }
22042 async function renderAttachmentPreview(ref, file) {
22043 const id = parseInt(ref, 10);
22044 if (!id) {
22045 return renderGenericPreview(file);
22046 }
22047 let data = null;
22048 try {
22049 data = await getJson(
22050 restUrl(
22051 `wp/v2/media/${id}?_fields=id,title,source_url,mime_type,alt_text,media_details`
22052 )
22053 );
22054 } catch {
22055 return renderGenericPreview(file);
22056 }
22057 const wrap = articleShell();
22058 const h = document.createElement("h2");
22059 h.className = "desktop-mode-my-wordpress__article-title";
22060 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
22061 wrap.appendChild(h);
22062 const meta = document.createElement("p");
22063 meta.className = "desktop-mode-my-wordpress__article-meta";
22064 meta.textContent = data.mime_type;
22065 wrap.appendChild(meta);
22066 if (data.mime_type.startsWith("image/")) {
22067 const img = document.createElement("img");
22068 img.className = "desktop-mode-my-wordpress__article-hero";
22069 const sizes = data.media_details?.sizes;
22070 img.src = sizes?.large?.source_url ?? sizes?.medium?.source_url ?? data.source_url;
22071 img.alt = data.alt_text ?? "";
22072 wrap.appendChild(img);
22073 } else {
22074 const p = document.createElement("p");
22075 const a = document.createElement("a");
22076 a.href = data.source_url;
22077 a.textContent = data.source_url;
22078 a.target = "_blank";
22079 a.rel = "noopener noreferrer";
22080 p.appendChild(a);
22081 wrap.appendChild(p);
22082 }
22083 return wrap;
22084 }
22085 function renderFolderPreview(file) {
22086 const wrap = articleShell();
22087 const h = document.createElement("h2");
22088 h.className = "desktop-mode-my-wordpress__article-title";
22089 h.textContent = file.title || __("(folder)", "desktop-mode");
22090 wrap.appendChild(h);
22091 const meta = document.createElement("p");
22092 meta.className = "desktop-mode-my-wordpress__article-meta";
22093 meta.textContent = __("Double-click to open.", "desktop-mode");
22094 wrap.appendChild(meta);
22095 return wrap;
22096 }
22097 function renderShortcutPreview(file) {
22098 const wrap = articleShell();
22099 const h = document.createElement("h2");
22100 h.className = "desktop-mode-my-wordpress__article-title";
22101 h.textContent = file.title || __("Shortcut", "desktop-mode");
22102 wrap.appendChild(h);
22103 const meta = document.createElement("p");
22104 meta.className = "desktop-mode-my-wordpress__article-meta";
22105 meta.textContent = __("Plugin shortcut. Double-click to open.", "desktop-mode");
22106 wrap.appendChild(meta);
22107 return wrap;
22108 }
22109 function renderBookmarkPreview(file) {
22110 const wrap = articleShell();
22111 const h = document.createElement("h2");
22112 h.className = "desktop-mode-my-wordpress__article-title";
22113 h.textContent = file.title || __("Bookmark", "desktop-mode");
22114 wrap.appendChild(h);
22115 const url = typeof file.url === "string" ? file.url : "";
22116 if (url) {
22117 const a = document.createElement("a");
22118 a.href = url;
22119 a.textContent = url;
22120 a.target = "_blank";
22121 a.rel = "noopener noreferrer";
22122 wrap.appendChild(a);
22123 }
22124 return wrap;
22125 }
22126 function renderGenericPreview(file) {
22127 const wrap = articleShell();
22128 const h = document.createElement("h2");
22129 h.className = "desktop-mode-my-wordpress__article-title";
22130 h.textContent = file.title || file.type;
22131 wrap.appendChild(h);
22132 const meta = document.createElement("p");
22133 meta.className = "desktop-mode-my-wordpress__article-meta";
22134 meta.textContent = sprintf(
22135 // translators: %s is a file-type slug.
22136 __("Type: %s", "desktop-mode"),
22137 file.type
22138 );
22139 wrap.appendChild(meta);
22140 if (!file.exists) {
22141 const warn2 = document.createElement("p");
22142 warn2.className = "desktop-mode-my-wordpress__article-meta";
22143 warn2.textContent = __(
22144 "The underlying entity is no longer available.",
22145 "desktop-mode"
22146 );
22147 wrap.appendChild(warn2);
22148 }
22149 return wrap;
22150 }
22151 function articleShell(extraClass = "") {
22152 const article = document.createElement("article");
22153 article.className = "desktop-mode-my-wordpress__article" + (extraClass ? " " + extraClass : "");
22154 return article;
22155 }
22156 function statCard(value, label) {
22157 const card = document.createElement("div");
22158 card.className = "desktop-mode-my-wordpress__user-stat";
22159 const v = document.createElement("span");
22160 v.className = "desktop-mode-my-wordpress__user-stat-value";
22161 v.textContent = value;
22162 card.appendChild(v);
22163 const l = document.createElement("span");
22164 l.className = "desktop-mode-my-wordpress__user-stat-label";
22165 l.textContent = label;
22166 card.appendChild(l);
22167 return card;
22168 }
22169 function renderLoading() {
22170 const wrap = document.createElement("div");
22171 wrap.className = "desktop-mode-my-wordpress__preview-loading";
22172 const spinner = document.createElement("wpd-spinner");
22173 wrap.appendChild(spinner);
22174 return wrap;
22175 }
22176 function renderError(err) {
22177 const wrap = document.createElement("div");
22178 wrap.className = "desktop-mode-my-wordpress__error";
22179 wrap.textContent = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
22180 return wrap;
22181 }
22182 function stripTags(html2) {
22183 const div = document.createElement("div");
22184 div.innerHTML = html2;
22185 return (div.textContent ?? "").trim();
22186 }
22187 function formatDate(iso) {
22188 if (!iso) {
22189 return "";
22190 }
22191 try {
22192 return new Date(iso).toLocaleString();
22193 } catch {
22194 return iso;
22195 }
22196 }
22197 function renderPreviewEmpty() {
22198 const wrap = document.createElement("div");
22199 wrap.className = "desktop-mode-my-wordpress__preview-empty";
22200 wrap.textContent = __(
22201 "Select an item to preview it here.",
22202 "desktop-mode"
22203 );
22204 return wrap;
22205 }
22206 const ID_PREFIX = "desktop-mode-embed-";
22207 const DEFAULT_W = 800;
22208 const DEFAULT_H = 600;
22209 const MIN_W = 360;
22210 const MIN_H = 240;
22211 const PADDING = 16;
22212 const lastPersisted = /* @__PURE__ */ new Map();
22213 function openEmbedWindow(file, ctx) {
22214 const url = file.ref();
22215 if (!url) {
22216 return;
22217 }
22218 const wm = window.wp?.desktop?.windowManager;
22219 if (!wm) {
22220 return;
22221 }
22222 const placement = ctx?.placement;
22223 const meta = placement?.meta ?? null;
22224 const windowId = placement ? `${ID_PREFIX}${placement.id}` : `${ID_PREFIX}anon-${hash(url)}`;
22225 const customName = meta?.name?.trim() ?? "";
22226 const title = customName !== "" ? customName : file.title();
22227 const cfg = {
22228 id: windowId,
22229 baseId: windowId,
22230 url,
22231 title,
22232 icon: file.icon(),
22233 minWidth: MIN_W,
22234 minHeight: MIN_H
22235 };
22236 const saved = meta?.window;
22237 const area = document.getElementById("desktop-mode-area");
22238 const aw = area?.clientWidth ?? window.innerWidth;
22239 const ah = area?.clientHeight ?? window.innerHeight;
22240 if (saved && Number.isFinite(saved.width) && Number.isFinite(saved.height)) {
22241 const { x, y, width, height } = clampGeometry(saved, aw, ah);
22242 cfg.x = x;
22243 cfg.y = y;
22244 cfg.width = width;
22245 cfg.height = height;
22246 } else {
22247 cfg.width = Math.min(DEFAULT_W, Math.max(MIN_W, aw - PADDING * 2));
22248 cfg.height = Math.min(DEFAULT_H, Math.max(MIN_H, ah - PADDING * 2));
22249 }
22250 if (placement) {
22251 if (saved) {
22252 lastPersisted.set(windowId, { ...saved });
22253 }
22254 }
22255 wm.open(cfg);
22256 }
22257 let installed = false;
22258 function installEmbedPersistence() {
22259 if (installed) {
22260 return;
22261 }
22262 installed = true;
22263 const onChange = (payload) => {
22264 const p = payload;
22265 const id = p?.windowId;
22266 if (!id || !id.startsWith(ID_PREFIX)) {
22267 return;
22268 }
22269 const placementIdStr = id.slice(ID_PREFIX.length);
22270 const placementId = parseInt(placementIdStr, 10);
22271 if (!placementId) {
22272 return;
22273 }
22274 const wm = window.wp?.desktop?.windowManager;
22275 const win = wm?.getById?.(id);
22276 const el = win?.element;
22277 if (!el) {
22278 return;
22279 }
22280 const next = {
22281 x: el.offsetLeft,
22282 y: el.offsetTop,
22283 width: el.offsetWidth,
22284 height: el.offsetHeight
22285 };
22286 const prev = lastPersisted.get(id);
22287 if (prev && prev.x === next.x && prev.y === next.y && prev.width === next.width && prev.height === next.height) {
22288 return;
22289 }
22290 lastPersisted.set(id, next);
22291 void persist(placementId, next);
22292 };
22293 addAction(HOOKS.WINDOW_DRAG_END, "desktop-mode-embed-persist", onChange);
22294 addAction(HOOKS.WINDOW_RESIZE_END, "desktop-mode-embed-persist", onChange);
22295 }
22296 async function persist(placementId, geo) {
22297 try {
22298 const list2 = await listPlacements(0);
22299 const row = list2.placements.find((p) => p.id === placementId);
22300 const prevMeta = row?.meta ?? {};
22301 const nextMeta = {
22302 ...prevMeta,
22303 window: geo
22304 };
22305 await updatePlacement(placementId, { meta: nextMeta });
22306 } catch (err) {
22307 console.warn("[desktop-mode] embed window persist failed:", err);
22308 }
22309 }
22310 function clampGeometry(g, areaW, areaH) {
22311 const width = Math.max(MIN_W, Math.min(g.width, areaW - PADDING));
22312 const height = Math.max(MIN_H, Math.min(g.height, areaH - PADDING));
22313 const x = Math.max(0, Math.min(g.x, Math.max(0, areaW - width)));
22314 const y = Math.max(0, Math.min(g.y, Math.max(0, areaH - height)));
22315 return { x, y, width, height };
22316 }
22317 function hash(s) {
22318 let h = 0;
22319 for (let i = 0; i < s.length; i++) {
22320 h = (Math.imul(h, 31) + s.charCodeAt(i)) % 2147483647;
22321 }
22322 return Math.abs(h).toString(36);
22323 }
22324 function adminBase() {
22325 const cfg = window.wp?.desktop?.config;
22326 const url = cfg?.adminUrl ?? "/wp-admin/";
22327 return url.endsWith("/") ? url : `${url}/`;
22328 }
22329 function registerBuiltInFileOpeners() {
22330 registerOpener({
22331 id: "wp-post-editor",
22332 label: "Block Editor",
22333 types: ["post"],
22334 isDefault: true,
22335 sort: 10,
22336 handler: {
22337 kind: "url",
22338 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
22339 }
22340 });
22341 registerOpener({
22342 id: "wp-media-editor",
22343 label: "Media editor",
22344 types: ["attachment"],
22345 isDefault: true,
22346 sort: 10,
22347 handler: {
22348 kind: "url",
22349 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
22350 }
22351 });
22352 registerOpener({
22353 id: "wp-user-profile",
22354 label: "User profile",
22355 types: ["user"],
22356 isDefault: true,
22357 sort: 10,
22358 handler: {
22359 kind: "url",
22360 url: (file) => `${adminBase()}user-edit.php?user_id=${encodeURIComponent(file.ref())}`
22361 }
22362 });
22363 registerOpener({
22364 id: "wp-term-editor",
22365 label: "Term editor",
22366 types: ["term"],
22367 isDefault: true,
22368 sort: 10,
22369 handler: {
22370 kind: "url",
22371 url: (file) => {
22372 const [taxonomy, termId] = file.ref().split(":");
22373 return `${adminBase()}term.php?taxonomy=${encodeURIComponent(taxonomy ?? "")}&tag_ID=${encodeURIComponent(termId ?? "")}`;
22374 }
22375 }
22376 });
22377 registerOpener({
22378 id: "wp-comment-editor",
22379 label: "Comment editor",
22380 types: ["comment"],
22381 isDefault: true,
22382 sort: 10,
22383 handler: {
22384 kind: "url",
22385 url: (file) => `${adminBase()}comment.php?action=editcomment&c=${encodeURIComponent(file.ref())}`
22386 }
22387 });
22388 registerOpener({
22389 id: "desktop-mode-folder-window",
22390 label: "Open folder",
22391 types: ["folder"],
22392 isDefault: true,
22393 sort: 10,
22394 handler: {
22395 kind: "js",
22396 open: (file) => {
22397 const folderId = parseInt(file.ref(), 10);
22398 if (!folderId) {
22399 return;
22400 }
22401 const wm = window.wp?.desktop?.windowManager;
22402 if (!wm) {
22403 return;
22404 }
22405 const id = `desktop-mode-folder-${folderId}`;
22406 const folderRow = store.getState().folders.get(folderId);
22407 const viewerId2 = Number(window.desktopModeConfig?.currentUserId ?? 0);
22408 const isRecipient = !!folderRow && folderRow.ownerId > 0 && folderRow.ownerId !== viewerId2;
22409 const baseTitle = file.title();
22410 const titleWithCue = isRecipient ? `${baseTitle} · Shared` : baseTitle;
22411 wm.open({
22412 id,
22413 baseId: id,
22414 url: `#folder-${folderId}`,
22415 title: titleWithCue,
22416 icon: file.icon(),
22417 native: true,
22418 render: (body) => {
22419 body.replaceChildren();
22420 body.classList.add("desktop-mode-folder-window");
22421 const routes = [
22422 { folderId, title: file.title() }
22423 ];
22424 let currentDispose = null;
22425 const breadcrumbsHost = document.createElement("header");
22426 body.appendChild(breadcrumbsHost);
22427 const bodyHost = document.createElement("div");
22428 bodyHost.style.cssText = "flex:1 1 auto;min-height:0;display:flex;flex-direction:column;";
22429 body.appendChild(bodyHost);
22430 const paintBreadcrumbs = () => {
22431 const segments = routes.map(
22432 (route, idx) => {
22433 const isCurrent = idx === routes.length - 1;
22434 if (isCurrent) {
22435 return { label: route.title };
22436 }
22437 return {
22438 label: route.title,
22439 onClick: () => {
22440 routes.length = idx + 1;
22441 mountCurrent();
22442 }
22443 };
22444 }
22445 );
22446 renderBreadcrumbs(breadcrumbsHost, segments, {
22447 onBack: () => {
22448 if (routes.length <= 1) {
22449 return;
22450 }
22451 routes.pop();
22452 mountCurrent();
22453 },
22454 backDisabled: routes.length <= 1
22455 });
22456 };
22457 const mountCurrent = () => {
22458 currentDispose?.();
22459 currentDispose = null;
22460 bodyHost.replaceChildren();
22461 const split = document.createElement("div");
22462 split.className = "desktop-mode-folder-window__split";
22463 bodyHost.appendChild(split);
22464 const layerHost = document.createElement("div");
22465 layerHost.className = "desktop-mode-folder-window__layer";
22466 split.appendChild(layerHost);
22467 const previewPane = document.createElement("div");
22468 previewPane.className = "desktop-mode-folder-window__preview";
22469 previewPane.appendChild(renderPreviewEmpty());
22470 split.appendChild(previewPane);
22471 const route = routes[routes.length - 1];
22472 const layer = mountFilesLayer(
22473 layerHost,
22474 route.folderId
22475 );
22476 const offSelection = layer.onSelectionChange(
22477 (placement) => {
22478 if (!placement) {
22479 previewPane.replaceChildren(
22480 renderPreviewEmpty()
22481 );
22482 return;
22483 }
22484 renderPlacementPreview(
22485 placement,
22486 previewPane
22487 );
22488 }
22489 );
22490 const dblClickHandler = (e) => {
22491 if (!(e.target instanceof Element)) {
22492 return;
22493 }
22494 const tile2 = e.target.closest(
22495 ".desktop-mode-file-tile"
22496 );
22497 if (!tile2) {
22498 return;
22499 }
22500 if (tile2.dataset.fileType !== "folder") {
22501 return;
22502 }
22503 const subId = parseInt(
22504 tile2.dataset.fileRef ?? "",
22505 10
22506 );
22507 if (!subId) {
22508 return;
22509 }
22510 e.preventDefault();
22511 e.stopPropagation();
22512 const subTitle = tile2.querySelector(
22513 ".desktop-mode-file-tile__label"
22514 )?.textContent ?? `#${subId}`;
22515 routes.push({
22516 folderId: subId,
22517 title: subTitle
22518 });
22519 mountCurrent();
22520 };
22521 layerHost.addEventListener(
22522 "dblclick",
22523 dblClickHandler,
22524 true
22525 );
22526 const menu = attachIconCanvasMenu(layerHost, {
22527 scope: `desktop-mode-folder:${route.folderId}`,
22528 onSort: (mode) => layer.sort(mode),
22529 extraItems: [
22530 {
22531 id: "new-folder",
22532 label: "New folder",
22533 icon: "dashicons-portfolio",
22534 sort: 5,
22535 onClick: () => {
22536 openCreateFolderDialog({
22537 onSubmit: async (name) => {
22538 const folder = await createFolder({
22539 name
22540 });
22541 const peers = store.getState().placementsByFolder.get(
22542 route.folderId
22543 ) ?? [];
22544 const occupied = buildOccupiedSet(peers);
22545 const cell = snapToEmptyCell(
22546 GRID_PADDING,
22547 GRID_PADDING,
22548 occupied,
22549 layerHost
22550 );
22551 const placement = await createPlacement({
22552 type: "folder",
22553 ref: String(folder.id),
22554 parentId: route.folderId,
22555 x: cell.x,
22556 y: cell.y
22557 });
22558 store.upsertFolder(folder);
22559 store.upsertPlacement(
22560 placement
22561 );
22562 }
22563 });
22564 }
22565 }
22566 ]
22567 });
22568 const status = mountFolderStatusBar(
22569 bodyHost,
22570 route.folderId
22571 );
22572 currentDispose = () => {
22573 offSelection();
22574 menu.dispose();
22575 status.dispose();
22576 layerHost.removeEventListener(
22577 "dblclick",
22578 dblClickHandler,
22579 true
22580 );
22581 layer.dispose();
22582 };
22583 paintBreadcrumbs();
22584 };
22585 mountCurrent();
22586 },
22587 width: 720,
22588 height: 480,
22589 minWidth: 360,
22590 minHeight: 240
22591 });
22592 }
22593 }
22594 });
22595 registerOpener({
22596 id: "desktop-mode-shortcut-opener",
22597 label: "Open shortcut",
22598 types: ["shortcut"],
22599 isDefault: true,
22600 sort: 10,
22601 handler: {
22602 kind: "js",
22603 open: (file) => {
22604 const extras = file.shape;
22605 const wp = window.wp?.desktop;
22606 if (!wp) {
22607 return;
22608 }
22609 if (extras.shortcutWindow && wp.openWindow) {
22610 wp.openWindow(extras.shortcutWindow);
22611 return;
22612 }
22613 if (extras.shortcutUrl && wp.windowManager) {
22614 try {
22615 const u = new URL(extras.shortcutUrl, window.location.origin);
22616 if (u.origin !== window.location.origin) {
22617 window.open(u.toString(), "_blank", "noopener,noreferrer");
22618 return;
22619 }
22620 const id = `desktop-icon-${file.ref()}`;
22621 wp.windowManager.open({
22622 id,
22623 baseId: id,
22624 url: u.toString(),
22625 title: file.title(),
22626 icon: file.icon()
22627 });
22628 } catch {
22629 }
22630 }
22631 }
22632 }
22633 });
22634 registerOpener({
22635 id: "browser-navigate",
22636 label: "Open in browser",
22637 types: ["bookmark"],
22638 isDefault: true,
22639 sort: 10,
22640 handler: {
22641 kind: "js",
22642 open: (file) => {
22643 const url = file.ref();
22644 if (!url) {
22645 return;
22646 }
22647 window.open(url, "_blank", "noopener,noreferrer");
22648 }
22649 }
22650 });
22651 registerOpener({
22652 id: "desktop-mode-link-opener",
22653 label: "Open in browser",
22654 types: ["link"],
22655 isDefault: true,
22656 sort: 10,
22657 handler: {
22658 kind: "js",
22659 open: (file) => {
22660 const url = file.ref();
22661 if (!url) {
22662 return;
22663 }
22664 window.open(url, "_blank", "noopener,noreferrer");
22665 }
22666 }
22667 });
22668 registerOpener({
22669 id: "desktop-mode-embed-opener",
22670 label: "Open as window",
22671 types: ["embed"],
22672 isDefault: true,
22673 sort: 10,
22674 handler: {
22675 kind: "js",
22676 open: (file, ctx) => {
22677 openEmbedWindow(file, ctx);
22678 }
22679 }
22680 });
22681 }
22682 const TAB_ID = "desktop-mode-file-associations";
22683 function registerFileAssociationsTab() {
22684 registerSettingsTab({
22685 id: TAB_ID,
22686 label: "File Associations",
22687 order: 50,
22688 render(body) {
22689 renderTab(body);
22690 }
22691 });
22692 }
22693 function renderTab(body) {
22694 body.replaceChildren();
22695 const types = getTypes();
22696 if (types.length === 0) {
22697 const empty = document.createElement("p");
22698 empty.className = "desktop-mode-file-associations__empty";
22699 empty.textContent = "No file types are registered.";
22700 body.appendChild(empty);
22701 return;
22702 }
22703 const intro = document.createElement("p");
22704 intro.className = "desktop-mode-file-associations__intro";
22705 intro.textContent = "Pick which app opens each kind of file when you double-click it on the desktop.";
22706 body.appendChild(intro);
22707 const associations = getUserAssociations();
22708 const list2 = document.createElement("div");
22709 list2.className = "desktop-mode-file-associations__list";
22710 list2.setAttribute("role", "list");
22711 for (const type of types) {
22712 list2.appendChild(buildRow(type.type, type.label, associations));
22713 }
22714 body.appendChild(list2);
22715 }
22716 function buildRow(typeSlug, typeLabel, associations) {
22717 const row = document.createElement("div");
22718 row.className = "desktop-mode-file-associations__row";
22719 row.setAttribute("role", "listitem");
22720 row.dataset.fileType = typeSlug;
22721 const label = document.createElement("label");
22722 label.className = "desktop-mode-file-associations__label";
22723 label.textContent = typeLabel;
22724 row.appendChild(label);
22725 const candidates = getOpenersForType(typeSlug);
22726 if (candidates.length === 0) {
22727 const empty = document.createElement("span");
22728 empty.className = "desktop-mode-file-associations__none";
22729 empty.textContent = "No app available";
22730 row.appendChild(empty);
22731 return row;
22732 }
22733 const resolved = resolveOpener(typeSlug);
22734 const currentId = associations[typeSlug] ?? resolved?.id ?? "";
22735 const select = document.createElement("wpd-select");
22736 select.setAttribute("value", currentId);
22737 select.setAttribute("aria-label", `Default app for ${typeLabel}`);
22738 select.className = "desktop-mode-file-associations__select";
22739 label.htmlFor = `assoc-${typeSlug}`;
22740 select.id = `assoc-${typeSlug}`;
22741 for (const o of candidates) {
22742 const opt = document.createElement("wpd-option");
22743 opt.setAttribute("value", o.id);
22744 opt.textContent = o.isDefault ? `${o.label} (default)` : o.label;
22745 select.appendChild(opt);
22746 }
22747 select.addEventListener("wpd-pick", (e) => {
22748 const next = e.detail?.value;
22749 if (!next) {
22750 return;
22751 }
22752 const merged = { ...getUserAssociations(), [typeSlug]: next };
22753 setUserAssociations(merged);
22754 void saveAssociations(merged).catch((err) => {
22755 console.error("[desktop-mode] saveAssociations failed:", err);
22756 });
22757 });
22758 row.appendChild(select);
22759 return row;
22760 }
22761 let _store$1 = null;
22762 function sharesStore() {
22763 if (!_store$1) {
22764 _store$1 = createSharedStore("desktop-files/shares", () => ({
22765 byFolder: /* @__PURE__ */ new Map(),
22766 pending: [],
22767 sharesVersion: 0,
22768 deniedFolders: /* @__PURE__ */ new Set()
22769 }));
22770 }
22771 return _store$1;
22772 }
22773 function setSharesForFolder(folderId, shares) {
22774 const s = sharesStore();
22775 s.state.byFolder.set(folderId, shares);
22776 s.notify();
22777 }
22778 function upsertShare(share) {
22779 if (!share || typeof share.folderId !== "number") {
22780 return;
22781 }
22782 const s = sharesStore();
22783 const existing = s.state.byFolder.get(share.folderId) ?? [];
22784 const next = existing.filter((r) => r.id !== share.id);
22785 next.push(share);
22786 s.state.byFolder.set(share.folderId, next);
22787 s.notify();
22788 }
22789 function removeShare(folderId, shareId) {
22790 const s = sharesStore();
22791 const existing = s.state.byFolder.get(folderId) ?? [];
22792 s.state.byFolder.set(
22793 folderId,
22794 existing.filter((r) => r.id !== shareId)
22795 );
22796 s.notify();
22797 }
22798 function inviteEquals(a, b) {
22799 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;
22800 }
22801 function ingestPendingInvites(invites) {
22802 const s = sharesStore();
22803 const existingById = new Map(s.state.pending.map((p) => [p.id, p]));
22804 let mutated = false;
22805 for (const inv of invites) {
22806 if (s.state.deniedFolders.has(inv.folderId)) {
22807 continue;
22808 }
22809 const existing = existingById.get(inv.id);
22810 if (existing) {
22811 if (inviteEquals(existing, inv)) {
22812 continue;
22813 }
22814 s.state.pending = s.state.pending.map((p) => p.id === inv.id ? inv : p);
22815 } else {
22816 s.state.pending.push(inv);
22817 }
22818 if (inv.invitedAtMs > s.state.sharesVersion) {
22819 s.state.sharesVersion = inv.invitedAtMs;
22820 }
22821 mutated = true;
22822 }
22823 if (mutated) {
22824 s.notify();
22825 }
22826 }
22827 function dropPending(shareId, opts = {}) {
22828 const s = sharesStore();
22829 s.state.pending = s.state.pending.filter((p) => p.id !== shareId);
22830 if (opts.denied && typeof opts.folderId === "number") {
22831 s.state.deniedFolders.add(opts.folderId);
22832 }
22833 s.notify();
22834 }
22835 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}`;
22836 const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
22837 const _WpdModal = class _WpdModal extends Component {
22838 constructor() {
22839 super(...arguments);
22840 this._prevFocus = null;
22841 this._onKey = (e) => {
22842 if (e.key === "Escape" && !this.hasAttribute("mandatory")) {
22843 e.preventDefault();
22844 this._cancel();
22845 return;
22846 }
22847 if (e.key === "Tab") {
22848 const f = this._focusables();
22849 if (f.length === 0) {
22850 return;
22851 }
22852 const first = f[0];
22853 const last = f[f.length - 1];
22854 const doc = this.ownerDocument;
22855 const fallback = doc ? doc.activeElement : null;
22856 const active2 = e.composedPath()[0] || fallback;
22857 if (e.shiftKey && active2 === first) {
22858 e.preventDefault();
22859 last.focus();
22860 } else if (!e.shiftKey && active2 === last) {
22861 e.preventDefault();
22862 first.focus();
22863 }
22864 }
22865 };
22866 this._onBackdrop = (e) => {
22867 if (this.hasAttribute("mandatory")) {
22868 return;
22869 }
22870 const path = e.composedPath();
22871 const original = path.length > 0 ? path[0] : e.target;
22872 if (original === this) {
22873 this._cancel();
22874 }
22875 };
22876 }
22877 connectedCallback() {
22878 super.connectedCallback();
22879 this.setAttribute("role", "dialog");
22880 this.setAttribute("aria-modal", "true");
22881 this.addEventListener("keydown", this._onKey);
22882 this.addEventListener("click", this._onBackdrop);
22883 }
22884 disconnectedCallback() {
22885 this.removeEventListener("keydown", this._onKey);
22886 this.removeEventListener("click", this._onBackdrop);
22887 }
22888 attributeChangedCallback(name, oldValue, newValue) {
22889 super.attributeChangedCallback?.(name, oldValue, newValue);
22890 if (name === "open") {
22891 if (newValue !== null) {
22892 const doc = this.ownerDocument;
22893 this._prevFocus = doc ? doc.activeElement : null;
22894 queueMicrotask(() => this._focusFirst());
22895 } else if (this._prevFocus) {
22896 try {
22897 this._prevFocus.focus();
22898 } catch (e) {
22899 }
22900 this._prevFocus = null;
22901 }
22902 }
22903 }
22904 showModal() {
22905 this.setAttribute("open", "");
22906 }
22907 hideModal() {
22908 this.removeAttribute("open");
22909 }
22910 _focusables() {
22911 const root = this.shadowRoot;
22912 if (!root) {
22913 return [];
22914 }
22915 const slotted = Array.from(this.querySelectorAll(FOCUSABLE));
22916 const inShadow = Array.from(root.querySelectorAll(FOCUSABLE));
22917 return [...slotted, ...inShadow].filter((el) => el.offsetParent !== null || el.tagName === "BUTTON");
22918 }
22919 _focusFirst() {
22920 const f = this._focusables();
22921 if (f.length > 0) {
22922 f[0].focus();
22923 } else {
22924 const inner = this.shadowRoot?.querySelector(".dialog");
22925 inner?.focus?.();
22926 }
22927 }
22928 _cancel() {
22929 const ev = new CustomEvent("wpd-modal-cancel", {
22930 bubbles: true,
22931 cancelable: true,
22932 composed: true
22933 });
22934 const allowed = this.dispatchEvent(ev);
22935 if (allowed) {
22936 this.hideModal();
22937 }
22938 }
22939 render() {
22940 const title = this.getAttribute("title") ?? "";
22941 const mandatory = this.hasAttribute("mandatory");
22942 return html`
22943 <div class="dialog" tabindex="-1">
22944 ${title ? html`
22945 <div class="header">
22946 <h2 class="title">${title}</h2>
22947 <div class="header-actions">
22948 <slot name="header-actions"></slot>
22949 ${mandatory ? html`` : html`<button
22950 type="button"
22951 class="close"
22952 aria-label="Close"
22953 @click=${() => this._cancel()}
22954 >×</button>`}
22955 </div>
22956 </div>
22957 ` : html``}
22958 <div class="body">
22959 <slot></slot>
22960 </div>
22961 <div class="footer">
22962 <slot name="footer"></slot>
22963 </div>
22964 </div>
22965 `;
22966 }
22967 };
22968 _WpdModal.props = ["open", "title", "size", "mandatory"];
22969 _WpdModal.styles = [modalStyles];
22970 _WpdModal.help = {
22971 title: "Modal overlay",
22972 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.",
22973 status: "experimental",
22974 since: "0.18.0",
22975 props: [
22976 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
22977 { name: "title", type: "string", description: "Heading shown at the top of the dialog." },
22978 { name: "size", type: "'sm' | 'md' | 'lg'", default: "md", description: "Width preset." },
22979 {
22980 name: "mandatory",
22981 type: "boolean attribute",
22982 description: "Disables ESC, click-outside and the close button."
22983 }
22984 ],
22985 slots: [
22986 { name: "(default)", description: "Body content." },
22987 { name: "footer", description: "Footer button row, right-aligned." },
22988 { name: "header-actions", description: "Extra actions next to the close button." }
22989 ],
22990 events: [
22991 {
22992 name: "wpd-modal-cancel",
22993 description: "Fires when the user dismisses the modal (ESC, click-outside, close button). Cancelable; calling `preventDefault()` keeps the modal open."
22994 }
22995 ]
22996 };
22997 let WpdModal = _WpdModal;
22998 defineComponent("wpd-modal", WpdModal);
22999 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}`;
23000 const _WpdUserSearch = class _WpdUserSearch extends Component {
23001 constructor() {
23002 super(...arguments);
23003 this._timer = null;
23004 this._abort = null;
23005 this._results = [];
23006 this._query = "";
23007 this._open = false;
23008 this._phase = "idle";
23009 this._error = "";
23010 this._dropdownStyle = "";
23011 this._onScrollOrResize = () => void 0;
23012 this._onInput = (e) => {
23013 const value = e.target.value;
23014 this._query = value;
23015 this._scheduleSearch(value);
23016 };
23017 this._onFocus = () => {
23018 if (this._results.length === 0 && this._phase === "idle") {
23019 this._scheduleSearch(this._query);
23020 return;
23021 }
23022 this._open = true;
23023 this._positionDropdown();
23024 this.requestUpdate();
23025 };
23026 this._onBlur = () => {
23027 setTimeout(() => {
23028 this._open = false;
23029 this.requestUpdate();
23030 }, 150);
23031 };
23032 this._pick = (user) => {
23033 this.emit("wpd-user-pick", { user });
23034 this._results = [];
23035 this._open = false;
23036 this._phase = "idle";
23037 this._query = "";
23038 const input = this.shadowRoot?.querySelector(".input");
23039 if (input) {
23040 input.value = "";
23041 }
23042 this.requestUpdate();
23043 };
23044 }
23045 connectedCallback() {
23046 super.connectedCallback();
23047 this._onScrollOrResize = () => {
23048 if (this._open) {
23049 this._positionDropdown();
23050 this.requestUpdate();
23051 }
23052 };
23053 window.addEventListener("resize", this._onScrollOrResize);
23054 window.addEventListener("scroll", this._onScrollOrResize, true);
23055 }
23056 disconnectedCallback() {
23057 if (this._timer) {
23058 clearTimeout(this._timer);
23059 }
23060 if (this._abort) {
23061 this._abort.abort();
23062 }
23063 window.removeEventListener("resize", this._onScrollOrResize);
23064 window.removeEventListener("scroll", this._onScrollOrResize, true);
23065 }
23066 _endpoint() {
23067 const attr = this.getAttribute("endpoint");
23068 if (attr) {
23069 return attr;
23070 }
23071 return window.desktopModeConfig?.filesUsersSearchUrl || "";
23072 }
23073 _scheduleSearch(q) {
23074 if (this._timer) {
23075 clearTimeout(this._timer);
23076 }
23077 this._phase = "loading";
23078 this._open = true;
23079 this._positionDropdown();
23080 this.requestUpdate();
23081 this._timer = setTimeout(() => this._runSearch(q), 200);
23082 }
23083 async _runSearch(q) {
23084 const url = this._endpoint();
23085 if (!url) {
23086 this._phase = "error";
23087 this._error = "Search endpoint is not configured.";
23088 this._results = [];
23089 this._open = true;
23090 this.requestUpdate();
23091 return;
23092 }
23093 if (this._abort) {
23094 this._abort.abort();
23095 }
23096 const ctrl = new AbortController();
23097 this._abort = ctrl;
23098 const exclude = this.getAttribute("exclude") || "";
23099 const full = url + "?q=" + encodeURIComponent(q) + "&exclude=" + encodeURIComponent(exclude);
23100 try {
23101 const init2 = {
23102 signal: ctrl.signal,
23103 credentials: "same-origin"
23104 };
23105 const res = await trackedFetch$1(full, init2, {
23106 source: "desktop-mode/files-user-search",
23107 silent: true
23108 });
23109 if (!res.ok) {
23110 throw new Error(`HTTP ${res.status}`);
23111 }
23112 const json = await res.json();
23113 this._results = json && Array.isArray(json.users) ? json.users : [];
23114 this._phase = "ready";
23115 this._error = "";
23116 this._open = true;
23117 } catch (e) {
23118 if (e.name === "AbortError") {
23119 return;
23120 }
23121 this._results = [];
23122 this._phase = "error";
23123 this._error = e.message || "Search failed.";
23124 this._open = true;
23125 }
23126 this._positionDropdown();
23127 this.requestUpdate();
23128 }
23129 _positionDropdown() {
23130 const input = this.shadowRoot?.querySelector(".input");
23131 if (!input) {
23132 return;
23133 }
23134 const rect = input.getBoundingClientRect();
23135 const top = rect.bottom + 4;
23136 const left = rect.left;
23137 const width = rect.width;
23138 const viewportH = window.innerHeight;
23139 const spaceBelow = viewportH - rect.bottom;
23140 const spaceAbove = rect.top;
23141 const maxHeight = Math.max(120, Math.min(280, Math.max(spaceBelow, spaceAbove) - 16));
23142 if (spaceBelow < 200 && spaceAbove > spaceBelow) {
23143 this._dropdownStyle = [
23144 "position:fixed",
23145 `left:${left}px`,
23146 `top:${rect.top - 4 - maxHeight}px`,
23147 `width:${width}px`,
23148 `max-height:${maxHeight}px`
23149 ].join(";");
23150 } else {
23151 this._dropdownStyle = [
23152 "position:fixed",
23153 `left:${left}px`,
23154 `top:${top}px`,
23155 `width:${width}px`,
23156 `max-height:${maxHeight}px`
23157 ].join(";");
23158 }
23159 }
23160 _dropdownContent() {
23161 if (this._phase === "loading") {
23162 return html`<div class="empty">Searching…</div>`;
23163 }
23164 if (this._phase === "error") {
23165 return html`<div class="empty error">${this._error}</div>`;
23166 }
23167 if (this._results.length === 0) {
23168 const message = this._query ? "No matches." : "No users available.";
23169 return html`<div class="empty">${message}</div>`;
23170 }
23171 return this._results.map(
23172 (u) => html`
23173 <button
23174 type="button"
23175 class="item"
23176 role="option"
23177 @mousedown=${(e) => e.preventDefault()}
23178 @click=${() => this._pick(u)}
23179 >
23180 <img class="avatar" src=${u.avatarUrl} alt="" />
23181 <div>
23182 <div class="name">${u.name}</div>
23183 <div class="slug">${u.slug}</div>
23184 </div>
23185 </button>
23186 `
23187 );
23188 }
23189 render() {
23190 const placeholder = this.getAttribute("placeholder") || "Search users…";
23191 return html`
23192 <input
23193 class="input"
23194 type="search"
23195 placeholder=${placeholder}
23196 autocomplete="off"
23197 @input=${this._onInput}
23198 @focus=${this._onFocus}
23199 @blur=${this._onBlur}
23200 .value=${this._query}
23201 />
23202 ${this._open ? html`
23203 <div class="dropdown" role="listbox" style=${this._dropdownStyle}>
23204 ${this._dropdownContent()}
23205 </div>
23206 ` : html``}
23207 `;
23208 }
23209 };
23210 _WpdUserSearch.props = ["placeholder", "exclude", "endpoint"];
23211 _WpdUserSearch.styles = [userSearchStyles];
23212 _WpdUserSearch.help = {
23213 title: "User autocomplete",
23214 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.",
23215 status: "experimental",
23216 since: "0.18.0",
23217 props: [
23218 { name: "placeholder", type: "string", description: "Input placeholder text." },
23219 {
23220 name: "exclude",
23221 type: "csv user ids",
23222 description: "Already-picked user ids to suppress in results."
23223 },
23224 {
23225 name: "endpoint",
23226 type: "URL",
23227 description: "Override the search URL (defaults to desktopModeConfig.filesUsersSearchUrl)."
23228 }
23229 ],
23230 events: [
23231 { name: "wpd-user-pick", description: "Emitted on pick. Detail: `{ user: SearchUser }`." }
23232 ]
23233 };
23234 let WpdUserSearch = _WpdUserSearch;
23235 defineComponent("wpd-user-search", WpdUserSearch);
23236 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}`;
23237 const _WpdRolePicker = class _WpdRolePicker extends Component {
23238 constructor() {
23239 super(...arguments);
23240 this._onToggle = (slug) => {
23241 const selected = !this._selectedSet().has(slug);
23242 this.emit("wpd-role-toggle", { slug, selected });
23243 };
23244 }
23245 _selectedSet() {
23246 const raw = this.getAttribute("selected") || "";
23247 return new Set(
23248 raw.split(",").map((s) => s.trim()).filter((s) => s !== "")
23249 );
23250 }
23251 _roles() {
23252 const attr = this.getAttribute("roles");
23253 if (attr) {
23254 try {
23255 const parsed = JSON.parse(attr);
23256 if (Array.isArray(parsed)) {
23257 return parsed;
23258 }
23259 } catch (e) {
23260 }
23261 }
23262 return window.desktopModeConfig?.shareEligibleRoles || [];
23263 }
23264 render() {
23265 const roles = this._roles();
23266 if (roles.length === 0) {
23267 return html`<span class="empty">No eligible roles.</span>`;
23268 }
23269 const set = this._selectedSet();
23270 return html`
23271 ${roles.map((r) => {
23272 const isSelected = set.has(r.slug);
23273 return html`
23274 <button
23275 type="button"
23276 class="chip"
23277 aria-pressed=${isSelected ? "true" : "false"}
23278 @click=${() => this._onToggle(r.slug)}
23279 >${r.name}</button>
23280 `;
23281 })}
23282 `;
23283 }
23284 };
23285 _WpdRolePicker.props = ["selected", "roles"];
23286 _WpdRolePicker.styles = [rolePickerStyles];
23287 _WpdRolePicker.help = {
23288 title: "Role picker",
23289 summary: "Chip multi-select for WordPress roles. Reads eligible roles from desktopModeConfig.shareEligibleRoles; emits wpd-role-toggle { slug, selected } on every change.",
23290 status: "experimental",
23291 since: "0.18.0",
23292 props: [
23293 {
23294 name: "selected",
23295 type: "csv role slugs",
23296 description: "Comma-separated role slugs that are currently selected."
23297 },
23298 {
23299 name: "roles",
23300 type: "JSON",
23301 description: "Override the source of eligible roles (defaults to the global config)."
23302 }
23303 ],
23304 events: [
23305 {
23306 name: "wpd-role-toggle",
23307 description: "Emitted on every click. Detail: `{ slug, selected }`."
23308 }
23309 ]
23310 };
23311 let WpdRolePicker = _WpdRolePicker;
23312 defineComponent("wpd-role-picker", WpdRolePicker);
23313 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}`;
23314 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}`;
23315 const _WpdSegment = class _WpdSegment extends Component {
23316 render() {
23317 this.setAttribute("role", "radio");
23318 return html`
23319 <button type="button" @click=${() => this._onPick()}>
23320 <slot></slot>
23321 </button>
23322 `;
23323 }
23324 _onPick() {
23325 this.emit("wpd-segment-pick", {
23326 value: this.value
23327 });
23328 }
23329 };
23330 _WpdSegment.props = ["value"];
23331 _WpdSegment.styles = [segmentStyles];
23332 _WpdSegment.help = {
23333 title: "Segment",
23334 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
23335 status: "stable",
23336 since: "0.9.0",
23337 props: [
23338 {
23339 name: "value",
23340 type: "string",
23341 description: "Identifier this segment contributes to the parent group selection."
23342 }
23343 ],
23344 slots: [
23345 { name: "(default)", description: "Visible segment label." }
23346 ],
23347 events: [
23348 {
23349 name: "wpd-segment-pick",
23350 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
23351 detail: "{ value: string }"
23352 }
23353 ]
23354 };
23355 let WpdSegment = _WpdSegment;
23356 defineComponent("wpd-segment", WpdSegment);
23357 const _WpdSegmented = class _WpdSegmented extends Component {
23358 connectedCallback() {
23359 super.connectedCallback();
23360 this.addEventListener("wpd-segment-pick", (e) => {
23361 const detail = e.detail;
23362 e.stopPropagation();
23363 this.value = detail.value;
23364 this.emit("wpd-pick", { value: detail.value });
23365 });
23366 }
23367 /**
23368 * Declarative item-list setter. Replaces the existing
23369 * `<wpd-segment>` children with a fresh set built from a
23370 * `{ value, label }` array; preserves the current selection
23371 * when the value still matches an entry, otherwise falls back
23372 * to the first item.
23373 *
23374 * Collapses the pre-0.11 imperative dance (clear children,
23375 * `createElement`, set `textContent`, `appendChild`, then
23376 * `setAttribute('value', …)` on the group — order matters) to
23377 * a single assignment:
23378 *
23379 * ```js
23380 * segmented.items = [
23381 * { value: 'm', label: 'm' },
23382 * { value: 'km', label: 'km' },
23383 * ];
23384 * ```
23385 *
23386 * @since 0.11.0
23387 */
23388 set items(list2) {
23389 const existing = this.querySelectorAll(":scope > wpd-segment");
23390 for (const el of Array.from(existing)) {
23391 el.remove();
23392 }
23393 for (const item of list2) {
23394 const seg = document.createElement("wpd-segment");
23395 seg.setAttribute("value", item.value);
23396 seg.textContent = item.label;
23397 this.appendChild(seg);
23398 }
23399 const current = this.value;
23400 const stillValid = current !== null && list2.some((i) => i.value === current);
23401 if (!stillValid && list2.length > 0) {
23402 this.value = list2[0].value;
23403 } else {
23404 this.requestUpdate();
23405 }
23406 }
23407 render() {
23408 const label = this.label || "";
23409 if (label) {
23410 this.setAttribute("aria-label", label);
23411 }
23412 this.setAttribute("role", "radiogroup");
23413 const current = this.value;
23414 queueMicrotask(() => {
23415 const segs = this.querySelectorAll("wpd-segment");
23416 for (const seg of Array.from(segs)) {
23417 const v = seg.getAttribute("value");
23418 seg.setAttribute(
23419 "aria-checked",
23420 v === current ? "true" : "false"
23421 );
23422 }
23423 });
23424 return html`<slot></slot>`;
23425 }
23426 };
23427 _WpdSegmented.props = ["value", "label"];
23428 _WpdSegmented.styles = [segmentedStyles];
23429 _WpdSegmented.help = {
23430 title: "Segmented",
23431 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
23432 status: "stable",
23433 since: "0.9.0",
23434 props: [
23435 {
23436 name: "value",
23437 type: "string",
23438 description: "Currently selected segment value. Mirrored onto child aria-checked."
23439 },
23440 {
23441 name: "label",
23442 type: "string",
23443 description: "aria-label for the radiogroup."
23444 }
23445 ],
23446 slots: [
23447 { name: "(default)", description: '<wpd-segment value="…"> children.' }
23448 ],
23449 events: [
23450 {
23451 name: "wpd-pick",
23452 description: "Fires when the selected segment changes.",
23453 detail: "{ value: string }"
23454 }
23455 ],
23456 cssProps: [
23457 { name: "--desktop-mode-window-bg", description: "Pill background." },
23458 { name: "--desktop-mode-text", description: "Active label colour." },
23459 { name: "--desktop-mode-muted", description: "Inactive label colour." }
23460 ],
23461 example: html`
23462 <wpd-segmented value="md" label="Dock size">
23463 <wpd-segment value="sm">Small</wpd-segment>
23464 <wpd-segment value="md">Medium</wpd-segment>
23465 <wpd-segment value="lg">Large</wpd-segment>
23466 </wpd-segmented>
23467 `
23468 };
23469 let WpdSegmented = _WpdSegmented;
23470 defineComponent("wpd-segmented", WpdSegmented);
23471 const styles$3 = 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}`;
23472 const _WpdButton = class _WpdButton extends Component {
23473 render() {
23474 const disabled = this.disabled !== null;
23475 const type = this.type || "button";
23476 return html`
23477 <button part="button" type=${type} ?disabled=${disabled}>
23478 <slot></slot>
23479 </button>
23480 `;
23481 }
23482 };
23483 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
23484 _WpdButton.styles = [styles$3];
23485 _WpdButton.help = {
23486 title: "Button",
23487 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
23488 status: "stable",
23489 since: "0.9.0",
23490 props: [
23491 {
23492 name: "variant",
23493 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
23494 default: "ghost",
23495 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
23496 },
23497 {
23498 name: "disabled",
23499 type: "boolean attribute",
23500 description: "Disable pointer + keyboard interaction and dim the chrome."
23501 },
23502 {
23503 name: "type",
23504 type: "'button' | 'submit' | 'reset'",
23505 default: "button",
23506 description: "Forwarded to the underlying native <button>."
23507 },
23508 {
23509 name: "busy",
23510 type: "boolean attribute",
23511 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
23512 },
23513 {
23514 name: "fill-cell",
23515 type: "boolean attribute",
23516 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
23517 }
23518 ],
23519 slots: [{ name: "(default)", description: "Button label." }],
23520 parts: [{ name: "button", description: "Underlying <button> element." }],
23521 cssProps: [
23522 { name: "--wpd-button-bg", description: "Background color." },
23523 { name: "--wpd-button-fg", description: "Text color." },
23524 { name: "--wpd-button-border", description: "Border shorthand." },
23525 { name: "--wpd-button-border-radius", default: "6px" },
23526 { name: "--wpd-button-padding", default: "6px 12px" },
23527 {
23528 name: "--wpd-button-min-height",
23529 description: "Minimum height when fill-cell is set."
23530 }
23531 ],
23532 example: html`
23533 <wpd-cluster gap="8">
23534 <wpd-button variant="primary">Primary</wpd-button>
23535 <wpd-button variant="secondary">Secondary</wpd-button>
23536 <wpd-button variant="ghost">Ghost</wpd-button>
23537 <wpd-button variant="danger">Danger</wpd-button>
23538 <wpd-button variant="link">Link</wpd-button>
23539 </wpd-cluster>
23540 `
23541 };
23542 let WpdButton = _WpdButton;
23543 defineComponent("wpd-button", WpdButton);
23544 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}`;
23545 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}}`;
23546 const _WpdToastContainer = class _WpdToastContainer extends Component {
23547 connectedCallback() {
23548 super.connectedCallback();
23549 this.setAttribute("aria-live", "polite");
23550 }
23551 render() {
23552 return html`<slot></slot>`;
23553 }
23554 };
23555 _WpdToastContainer.styles = [containerStyles];
23556 _WpdToastContainer.help = {
23557 title: "Toast container",
23558 summary: "Singleton stack beneath <body> that hosts transient <wpd-toast> notifications in the top-right. Created lazily by showToast(); authors rarely place one themselves.",
23559 status: "stable",
23560 since: "0.9.0",
23561 slots: [
23562 { name: "(default)", description: "<wpd-toast> children, stacked vertically." }
23563 ],
23564 cssProps: [
23565 { name: "--desktop-mode-z-fullscreen", description: "z-index base — toasts sit above fullscreen windows." }
23566 ],
23567 example: html`
23568 <wpd-toast-container>
23569 <wpd-toast state="in">Settings saved.</wpd-toast>
23570 <wpd-toast state="in" action="Undo">Theme changed.</wpd-toast>
23571 </wpd-toast-container>
23572 `
23573 };
23574 let WpdToastContainer = _WpdToastContainer;
23575 defineComponent("wpd-toast-container", WpdToastContainer);
23576 const _WpdToast = class _WpdToast extends Component {
23577 connectedCallback() {
23578 super.connectedCallback();
23579 if (!this.hasAttribute("role")) {
23580 this.setAttribute("role", "status");
23581 }
23582 }
23583 render() {
23584 const action = this.action || "";
23585 return html`
23586 <span class="wpd-toast__label"><slot></slot></span>
23587 <button
23588 type="button"
23589 ?hidden=${!action}
23590 @click=${(e) => this._onAction(e)}
23591 >
23592 ${action}
23593 </button>
23594 `;
23595 }
23596 _onAction(e) {
23597 e.preventDefault();
23598 e.stopPropagation();
23599 this.emit("wpd-toast-action", {});
23600 }
23601 };
23602 _WpdToast.props = ["action", "state"];
23603 _WpdToast.styles = [toastStyles];
23604 _WpdToast.help = {
23605 title: "Toast",
23606 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.',
23607 status: "stable",
23608 since: "0.9.0",
23609 props: [
23610 {
23611 name: "action",
23612 type: "string",
23613 description: "Optional action button label. When set, a button renders on the right and emits wpd-toast-action on click."
23614 },
23615 {
23616 name: "state",
23617 type: "'in' | 'out'",
23618 description: 'Drives the CSS fade transition. Set to "in" when rendered, flip to "out" before removal.'
23619 }
23620 ],
23621 slots: [
23622 { name: "(default)", description: "Message text." }
23623 ],
23624 events: [
23625 {
23626 name: "wpd-toast-action",
23627 description: "Fires when the action button is clicked.",
23628 detail: "{}"
23629 }
23630 ],
23631 example: html`
23632 <wpd-toast state="in" action="Undo">Post moved to trash.</wpd-toast>
23633 `
23634 };
23635 let WpdToast = _WpdToast;
23636 defineComponent("wpd-toast", WpdToast);
23637 function buildCapSegmented(initial, onChange) {
23638 const segmented = document.createElement("wpd-segmented");
23639 segmented.setAttribute("value", initial);
23640 segmented.setAttribute("label", "Capability");
23641 segmented.style.setProperty("--wpd-segmented-bg", "rgba(255,255,255,0.06)");
23642 segmented.style.setProperty(
23643 "--desktop-mode-window-bg",
23644 "var(--wp-admin-theme-color, #2271b1)"
23645 );
23646 segmented.style.setProperty("--desktop-mode-text", "#fff");
23647 segmented.style.setProperty("--desktop-mode-muted", "rgba(255,255,255,0.65)");
23648 const segRead = document.createElement("wpd-segment");
23649 segRead.setAttribute("value", "read");
23650 segRead.textContent = "Read";
23651 segmented.appendChild(segRead);
23652 const segWrite = document.createElement("wpd-segment");
23653 segWrite.setAttribute("value", "write");
23654 segWrite.textContent = "Read + Write";
23655 segmented.appendChild(segWrite);
23656 segmented.addEventListener("wpd-pick", (e) => {
23657 const detail = e.detail;
23658 onChange(detail.value);
23659 });
23660 return segmented;
23661 }
23662 function buildIconButton(label, onClick, opts = {}) {
23663 const btn = document.createElement("wpd-button");
23664 btn.setAttribute("variant", "ghost");
23665 btn.setAttribute("aria-label", opts.danger ? "Remove" : "Dismiss");
23666 btn.textContent = label;
23667 const fg = opts.danger ? "#ff8080" : "rgba(255,255,255,0.75)";
23668 const border = opts.danger ? "1px solid rgba(255,128,128,0.45)" : "1px solid rgba(255,255,255,0.18)";
23669 btn.style.setProperty("--wpd-button-fg", fg);
23670 btn.style.setProperty("--wpd-button-border", border);
23671 btn.style.setProperty("--wpd-button-padding", "6px 12px");
23672 btn.style.setProperty("--wpd-button-border-radius", "7px");
23673 btn.style.setProperty("--wpd-button-min-height", "34px");
23674 btn.style.minWidth = "34px";
23675 btn.style.fontSize = "18px";
23676 btn.style.lineHeight = "1";
23677 btn.addEventListener("click", onClick);
23678 return btn;
23679 }
23680 async function openShareSettingsModal(opts) {
23681 const modal = document.createElement("wpd-modal");
23682 modal.setAttribute("open", "");
23683 modal.setAttribute("size", "lg");
23684 modal.setAttribute("title", `Share "${opts.folderName}"`);
23685 document.body.appendChild(modal);
23686 let shares = [];
23687 let pendingPicks = [];
23688 const renderBody = () => {
23689 modal.innerHTML = "";
23690 const owner = document.createElement("div");
23691 owner.style.cssText = "opacity:0.7;margin-bottom:14px;font-size:12px;";
23692 owner.textContent = opts.ownerName ? `Owner: ${opts.ownerName} — cannot be changed` : "Owner cannot be changed";
23693 modal.appendChild(owner);
23694 const addPeople = document.createElement("div");
23695 addPeople.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
23696 const addPeopleLabel = document.createElement("div");
23697 addPeopleLabel.textContent = "Add people";
23698 addPeopleLabel.style.cssText = "font-weight:600;";
23699 addPeople.appendChild(addPeopleLabel);
23700 const userSearch = document.createElement("wpd-user-search");
23701 const excludedUserIds = shares.filter((s) => s.principalType === "user").map((s) => s.principalRef).concat(pendingPicks.filter((p) => p.kind === "user").map((p) => p.ref));
23702 userSearch.setAttribute("exclude", excludedUserIds.join(","));
23703 userSearch.setAttribute("placeholder", "Search users…");
23704 userSearch.addEventListener("wpd-user-pick", (e) => {
23705 const detail = e.detail;
23706 pendingPicks.push({
23707 kind: "user",
23708 ref: String(detail.user.id),
23709 label: detail.user.name,
23710 cap: "read"
23711 });
23712 renderBody();
23713 });
23714 addPeople.appendChild(userSearch);
23715 modal.appendChild(addPeople);
23716 const addRoles = document.createElement("div");
23717 addRoles.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
23718 const addRolesLabel = document.createElement("div");
23719 addRolesLabel.textContent = "Add roles";
23720 addRolesLabel.style.cssText = "font-weight:600;";
23721 addRoles.appendChild(addRolesLabel);
23722 const rolePicker = document.createElement("wpd-role-picker");
23723 const grantedRoles = shares.filter((s) => s.principalType === "role").map((s) => s.principalRef);
23724 const pickedRoles = pendingPicks.filter((p) => p.kind === "role").map((p) => p.ref);
23725 rolePicker.setAttribute("selected", [...grantedRoles, ...pickedRoles].join(","));
23726 rolePicker.addEventListener("wpd-role-toggle", (e) => {
23727 const detail = e.detail;
23728 const existing = shares.find(
23729 (s) => s.principalType === "role" && s.principalRef === detail.slug
23730 );
23731 if (existing) {
23732 if (!detail.selected) {
23733 void revoke(existing);
23734 }
23735 return;
23736 }
23737 if (detail.selected) {
23738 const eligible = (window.desktopModeConfig?.shareEligibleRoles ?? []).find(
23739 (r) => r.slug === detail.slug
23740 );
23741 pendingPicks.push({
23742 kind: "role",
23743 ref: detail.slug,
23744 label: eligible ? eligible.name : detail.slug,
23745 cap: "read"
23746 });
23747 } else {
23748 pendingPicks = pendingPicks.filter(
23749 (p) => !(p.kind === "role" && p.ref === detail.slug)
23750 );
23751 }
23752 renderBody();
23753 });
23754 addRoles.appendChild(rolePicker);
23755 modal.appendChild(addRoles);
23756 if (pendingPicks.length > 0) {
23757 const pendingBlock = document.createElement("div");
23758 pendingBlock.style.cssText = "border:1px dashed rgba(255,255,255,0.18);border-radius:8px;padding:10px;margin-bottom:14px;";
23759 const pendingTitle = document.createElement("div");
23760 pendingTitle.textContent = "New invites (not sent yet)";
23761 pendingTitle.style.cssText = "font-weight:600;margin-bottom:6px;font-size:12px;";
23762 pendingBlock.appendChild(pendingTitle);
23763 for (const pick of pendingPicks) {
23764 const row = document.createElement("div");
23765 row.style.cssText = "display:flex;align-items:center;gap:8px;padding:4px 0;font-size:13px;";
23766 const tag = document.createElement("span");
23767 tag.textContent = pick.kind === "role" ? `Role: ${pick.label}` : pick.label;
23768 tag.style.flex = "1";
23769 row.appendChild(tag);
23770 const capSeg = buildCapSegmented(pick.cap, (next) => {
23771 pick.cap = next;
23772 });
23773 row.appendChild(capSeg);
23774 const removeBtn = buildIconButton("×", () => {
23775 pendingPicks = pendingPicks.filter(
23776 (p) => !(p.kind === pick.kind && p.ref === pick.ref)
23777 );
23778 renderBody();
23779 });
23780 row.appendChild(removeBtn);
23781 pendingBlock.appendChild(row);
23782 }
23783 const sendBtn = document.createElement("wpd-button");
23784 sendBtn.setAttribute("variant", "primary");
23785 sendBtn.textContent = `Send ${pendingPicks.length} invite${pendingPicks.length === 1 ? "" : "s"}`;
23786 sendBtn.style.marginTop = "8px";
23787 sendBtn.addEventListener("click", async () => {
23788 if (pendingPicks.length === 0) {
23789 return;
23790 }
23791 sendBtn.setAttribute("busy", "");
23792 sendBtn.setAttribute("disabled", "");
23793 const snapshot = pendingPicks.slice();
23794 let succeeded = 0;
23795 let firstError = null;
23796 for (const pick of snapshot) {
23797 try {
23798 await inviteShare(opts.folderId, {
23799 principalType: pick.kind,
23800 principalRef: pick.ref,
23801 capability: pick.cap
23802 });
23803 succeeded++;
23804 } catch (err) {
23805 firstError = err;
23806 break;
23807 }
23808 }
23809 if (succeeded > 0) {
23810 pendingPicks = pendingPicks.slice(succeeded);
23811 }
23812 try {
23813 await refresh();
23814 } catch (_e) {
23815 }
23816 if (firstError) {
23817 showToast({
23818 message: `Could not send invites: ${firstError.message}`
23819 });
23820 } else {
23821 showToast({
23822 message: 1 === succeeded ? "Invite sent." : `${succeeded} invites sent.`
23823 });
23824 }
23825 sendBtn.removeAttribute("busy");
23826 sendBtn.removeAttribute("disabled");
23827 renderBody();
23828 });
23829 pendingBlock.appendChild(sendBtn);
23830 modal.appendChild(pendingBlock);
23831 }
23832 const listTitle = document.createElement("div");
23833 listTitle.textContent = "Who has access";
23834 listTitle.style.cssText = "font-weight:600;margin:8px 0 6px;";
23835 modal.appendChild(listTitle);
23836 if (shares.length === 0) {
23837 const empty = document.createElement("div");
23838 empty.textContent = "Only you can see this folder.";
23839 empty.style.cssText = "opacity:0.6;font-size:12px;";
23840 modal.appendChild(empty);
23841 } else {
23842 for (const s of shares) {
23843 const row = document.createElement("div");
23844 row.style.cssText = "display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.04);";
23845 const label = document.createElement("div");
23846 label.style.flex = "1";
23847 label.textContent = s.principalType === "role" ? `Role: ${s.displayName}` : s.displayName;
23848 if (s.state === "pending") {
23849 const tag = document.createElement("span");
23850 tag.textContent = " · pending";
23851 tag.style.cssText = "opacity:0.6;font-size:12px;";
23852 label.appendChild(tag);
23853 } else if (s.state === "denied") {
23854 const tag = document.createElement("span");
23855 tag.textContent = " · denied";
23856 tag.style.cssText = "color:#d63638;font-size:12px;";
23857 label.appendChild(tag);
23858 }
23859 row.appendChild(label);
23860 const cap = s.capability === "write" ? "write" : "read";
23861 const capSeg = buildCapSegmented(cap, (next) => {
23862 void changeCap(s, next);
23863 });
23864 row.appendChild(capSeg);
23865 const removeBtn = buildIconButton(
23866 "×",
23867 () => {
23868 void revoke(s);
23869 },
23870 { danger: true }
23871 );
23872 row.appendChild(removeBtn);
23873 modal.appendChild(row);
23874 }
23875 }
23876 const footer = document.createElement("div");
23877 footer.setAttribute("slot", "footer");
23878 footer.style.display = "flex";
23879 footer.style.justifyContent = "flex-end";
23880 footer.style.gap = "10px";
23881 footer.style.flexWrap = "wrap";
23882 const doneBtn = document.createElement("wpd-button");
23883 doneBtn.setAttribute("variant", "secondary");
23884 doneBtn.textContent = "Done";
23885 doneBtn.addEventListener("click", () => modal.remove());
23886 footer.appendChild(doneBtn);
23887 modal.appendChild(footer);
23888 };
23889 const refresh = async () => {
23890 try {
23891 const res = await listShares(opts.folderId);
23892 shares = res.shares;
23893 setSharesForFolder(opts.folderId, shares);
23894 } catch (err) {
23895 showToast({
23896 message: `Could not load shares: ${err.message}`
23897 });
23898 }
23899 renderBody();
23900 };
23901 const revoke = async (s) => {
23902 try {
23903 await revokeShare(opts.folderId, s.id);
23904 removeShare(opts.folderId, s.id);
23905 await refresh();
23906 showToast({ message: "Access revoked." });
23907 } catch (err) {
23908 showToast({
23909 message: `Could not revoke: ${err.message}`
23910 });
23911 }
23912 };
23913 const changeCap = async (s, cap) => {
23914 try {
23915 const next = await updateShareCapability(opts.folderId, s.id, cap);
23916 upsertShare(next);
23917 await refresh();
23918 } catch (err) {
23919 showToast({
23920 message: `Could not update capability: ${err.message}`
23921 });
23922 }
23923 };
23924 modal.addEventListener("wpd-modal-cancel", () => modal.remove());
23925 renderBody();
23926 await refresh();
23927 }
23928 function openPendingInviteModal(invite) {
23929 return new Promise((resolve2) => {
23930 const modal = document.createElement("wpd-modal");
23931 modal.setAttribute("open", "");
23932 modal.setAttribute("title", invite.folderName ? `${invite.ownerName ?? "Someone"} shared "${invite.folderName}" with you` : "Folder shared with you");
23933 const body = document.createElement("div");
23934 const capLabel = invite.capability === "write" ? "Read + Write" : "Read";
23935 body.innerHTML = `
23936 <p style="margin: 0 0 12px;">Accept the invite to add this folder to your desktop.</p>
23937 <p style="margin: 0; opacity: 0.75;">Access level: <strong>${capLabel}</strong></p>
23938 `;
23939 modal.appendChild(body);
23940 const footer = document.createElement("div");
23941 footer.setAttribute("slot", "footer");
23942 footer.style.display = "flex";
23943 footer.style.justifyContent = "flex-end";
23944 footer.style.gap = "10px";
23945 footer.style.flexWrap = "wrap";
23946 const laterBtn = document.createElement("wpd-button");
23947 laterBtn.setAttribute("variant", "secondary");
23948 laterBtn.textContent = "Decide later";
23949 laterBtn.addEventListener("click", () => {
23950 modal.remove();
23951 resolve2("dismissed");
23952 });
23953 const denyBtn = document.createElement("wpd-button");
23954 denyBtn.setAttribute("variant", "danger");
23955 denyBtn.textContent = "Deny";
23956 denyBtn.addEventListener("click", async () => {
23957 denyBtn.setAttribute("busy", "");
23958 denyBtn.setAttribute("disabled", "");
23959 try {
23960 await denyShare(invite.folderId, invite.id);
23961 sharesStore().state.deniedFolders.add(invite.folderId);
23962 sharesStore().notify();
23963 modal.remove();
23964 resolve2("denied");
23965 } catch (err) {
23966 showToast({
23967 message: `Could not deny: ${err.message}`
23968 });
23969 denyBtn.removeAttribute("busy");
23970 denyBtn.removeAttribute("disabled");
23971 }
23972 });
23973 const acceptBtn = document.createElement("wpd-button");
23974 acceptBtn.setAttribute("variant", "primary");
23975 acceptBtn.textContent = "Accept";
23976 acceptBtn.addEventListener("click", async () => {
23977 acceptBtn.setAttribute("busy", "");
23978 acceptBtn.setAttribute("disabled", "");
23979 try {
23980 await acceptShare(invite.folderId, invite.id);
23981 try {
23982 const res = await listPlacements(0);
23983 setFolderPlacements(0, res.placements);
23984 } catch (_e) {
23985 }
23986 modal.remove();
23987 resolve2("accepted");
23988 } catch (err) {
23989 showToast({
23990 message: `Could not accept: ${err.message}`
23991 });
23992 acceptBtn.removeAttribute("busy");
23993 acceptBtn.removeAttribute("disabled");
23994 }
23995 });
23996 footer.appendChild(laterBtn);
23997 footer.appendChild(denyBtn);
23998 footer.appendChild(acceptBtn);
23999 modal.appendChild(footer);
24000 modal.addEventListener("wpd-modal-cancel", () => {
24001 modal.remove();
24002 resolve2("dismissed");
24003 });
24004 document.body.appendChild(modal);
24005 });
24006 }
24007 function viewerId() {
24008 return Number(window.desktopModeConfig?.currentUserId ?? 0);
24009 }
24010 function sharingEnabled$1() {
24011 const settings = window.wp?.desktop?.getOsSettings?.();
24012 if (!settings) {
24013 return true;
24014 }
24015 return settings.foldersSharingEnabled !== false;
24016 }
24017 function folderOwnerId(folderId) {
24018 const folder = getFilesState().folders.get(folderId);
24019 return folder ? Number(folder.ownerId) : 0;
24020 }
24021 function folderIdFromBaseId(baseId) {
24022 if (typeof baseId !== "string") {
24023 return null;
24024 }
24025 const m = /^desktop-mode-folder-(\d+)$/.exec(baseId);
24026 return m ? Number(m[1]) : null;
24027 }
24028 function placementFolderId(placement) {
24029 if (placement.file.type !== "folder") {
24030 return null;
24031 }
24032 const ref = Number(placement.file.ref);
24033 if (!Number.isFinite(ref) || ref <= 0) {
24034 return null;
24035 }
24036 return ref;
24037 }
24038 function placementOwnerId(placement) {
24039 return Number(placement.file.ownerId ?? 0);
24040 }
24041 function installShareMenuItems() {
24042 addFilter(
24043 "desktop-mode.files.tile-menu",
24044 "desktop-mode/folder-share",
24045 (items, placement) => {
24046 if (!sharingEnabled$1()) {
24047 return items;
24048 }
24049 const folderId = placementFolderId(placement);
24050 if (folderId === null) {
24051 return items;
24052 }
24053 const ownerId = folderOwnerId(folderId) || placementOwnerId(placement);
24054 const viewer = viewerId();
24055 if (ownerId === viewer) {
24056 const shared = !!placement.file.shareSummary?.shared;
24057 const label = shared ? "Manage sharing…" : "Share folder…";
24058 items.push({
24059 id: "desktop-mode/folder-share",
24060 label,
24061 icon: "dashicons-share",
24062 sort: 30,
24063 onClick: () => {
24064 void openShareSettingsModal({
24065 folderId,
24066 folderName: placement.file.title || `Folder ${folderId}`
24067 });
24068 }
24069 });
24070 } else if (ownerId > 0) {
24071 items.push({
24072 id: "desktop-mode/folder-leave",
24073 label: "Leave shared folder",
24074 icon: "dashicons-exit",
24075 sort: 80,
24076 danger: true,
24077 onClick: async () => {
24078 const ok = await wpdConfirm$1({
24079 title: "Leave this folder?",
24080 message: "The folder will be removed from your desktop. The original and its contents are not deleted; the owner keeps them.",
24081 confirmLabel: "Leave",
24082 danger: true
24083 });
24084 if (!ok) {
24085 return;
24086 }
24087 try {
24088 await leaveShare(folderId);
24089 removePlacement(placement.id);
24090 try {
24091 const res = await listPlacements(0);
24092 setFolderPlacements(0, res.placements);
24093 } catch (_e) {
24094 }
24095 const winId = `desktop-mode-folder-${folderId}`;
24096 const mgr = window.desktopMode?.windowManager;
24097 mgr?.close?.(winId);
24098 showToast({ message: "You left the shared folder." });
24099 } catch (err) {
24100 showToast({
24101 message: `Could not leave: ${err.message}`
24102 });
24103 }
24104 }
24105 });
24106 }
24107 return items;
24108 }
24109 );
24110 registerTitleBarButton({
24111 id: "desktop-mode/folder-share",
24112 label: "Share folder",
24113 icon: "dashicons-share",
24114 placement: "right",
24115 order: 50,
24116 match: (w) => {
24117 if (!sharingEnabled$1()) {
24118 return false;
24119 }
24120 const base = w.config.baseId ?? w.id;
24121 const folderId = folderIdFromBaseId(base);
24122 if (folderId === null) {
24123 return false;
24124 }
24125 return folderOwnerId(folderId) === viewerId();
24126 },
24127 onClick: (w) => {
24128 const base = w.config.baseId ?? w.id;
24129 const folderId = folderIdFromBaseId(base);
24130 if (folderId === null) {
24131 return;
24132 }
24133 void openShareSettingsModal({
24134 folderId,
24135 folderName: w.config.title || `Folder ${folderId}`
24136 });
24137 }
24138 });
24139 addAction(
24140 "desktop-mode.files.tile-rendered",
24141 "desktop-mode/folder-share",
24142 (payload) => {
24143 const { tile: tile2, placement } = payload;
24144 if (placement.file.type !== "folder") {
24145 return;
24146 }
24147 const summary = placement.file.shareSummary;
24148 if (!summary?.shared) {
24149 return;
24150 }
24151 if (tile2.querySelector(".desktop-mode-file-tile__share-badge")) {
24152 return;
24153 }
24154 const badge = document.createElement("span");
24155 badge.className = "desktop-mode-file-tile__share-badge dashicons dashicons-share";
24156 badge.setAttribute("aria-label", "Shared folder");
24157 badge.title = "Shared folder";
24158 badge.style.cssText = [
24159 "position:absolute",
24160 "top:6px",
24161 "inset-inline-end:6px",
24162 "background:rgba(0,0,0,0.55)",
24163 "color:#fff",
24164 "border-radius:50%",
24165 "width:18px",
24166 "height:18px",
24167 "font-size:12px",
24168 "line-height:18px",
24169 "text-align:center",
24170 "pointer-events:none"
24171 ].join(";");
24172 tile2.appendChild(badge);
24173 }
24174 );
24175 }
24176 const prompted = /* @__PURE__ */ new Set();
24177 function sharingEnabled() {
24178 const settings = window.wp?.desktop?.getOsSettings?.();
24179 if (!settings) {
24180 return true;
24181 }
24182 return settings.foldersSharingEnabled !== false;
24183 }
24184 function installShareInviteBanner() {
24185 const store2 = sharesStore();
24186 const handle = (state2) => {
24187 if (!sharingEnabled()) {
24188 return;
24189 }
24190 for (const invite of state2.pending) {
24191 if (prompted.has(invite.id)) {
24192 continue;
24193 }
24194 prompted.add(invite.id);
24195 void openPendingInviteModal({
24196 id: invite.id,
24197 folderId: invite.folderId,
24198 folderName: invite.folderName,
24199 ownerName: invite.ownerName,
24200 capability: invite.capability
24201 }).then((decision) => {
24202 if (decision === "accepted") {
24203 dropPending(invite.id);
24204 } else if (decision === "denied") {
24205 dropPending(invite.id, { denied: true, folderId: invite.folderId });
24206 }
24207 });
24208 }
24209 };
24210 store2.subscribe(handle);
24211 handle(store2.state);
24212 }
24213 registerBuiltInFileTypes();
24214 registerBuiltInFileOpeners();
24215 installEmbedPersistence();
24216 registerFileAssociationsTab();
24217 installShareMenuItems();
24218 const seededPending = window.desktopModeConfig?.serverPendingShares;
24219 if (Array.isArray(seededPending) && seededPending.length > 0) {
24220 ingestPendingInvites(seededPending);
24221 }
24222 installShareInviteBanner();
24223 const filesApi = {
24224 DesktopFile,
24225 registerType,
24226 unregisterType,
24227 getType,
24228 getTypes,
24229 resolve,
24230 subscribe,
24231 registerOpener,
24232 unregisterOpener,
24233 getOpener,
24234 getOpeners,
24235 getOpenersForType,
24236 resolveOpener,
24237 subscribeOpeners,
24238 getUserAssociations,
24239 open: openFile,
24240 rest: filesRest,
24241 store: {
24242 get: getFilesStore,
24243 getState: getFilesState,
24244 subscribe: subscribeFilesStore,
24245 setFolderPlacements,
24246 upsertPlacement,
24247 removePlacement,
24248 setFolders,
24249 upsertFolder,
24250 removeFolder
24251 }
24252 };
24253 const SYNTH_META_KEY = "__synthFromDockItem";
24254 function hashToNegativeId(s) {
24255 let h = 0;
24256 for (let i = 0; i < s.length; i++) {
24257 h = (h * 31 + s.charCodeAt(i)) % 2147483647;
24258 }
24259 return -(h + 1);
24260 }
24261 function buildSyntheticPlacement(item, persistedPositions) {
24262 const saved = persistedPositions[item.id];
24263 return {
24264 id: hashToNegativeId(item.id),
24265 parentId: 0,
24266 x: saved ? saved.x : 0,
24267 y: saved ? saved.y : 0,
24268 sortOrder: 9999,
24269 updatedAtMs: Date.now(),
24270 meta: { [SYNTH_META_KEY]: item.id },
24271 file: {
24272 type: "shortcut",
24273 ref: `dock-promoted:${item.id}`,
24274 title: item.title,
24275 icon: item.icon,
24276 previewUrl: "",
24277 exists: true,
24278 // The shortcut opener (built-in-openers.ts) reads these
24279 // off the file shape — `shortcutUrl` is what a dock-item
24280 // promotion naturally has.
24281 shortcutUrl: item.url
24282 }
24283 };
24284 }
24285 function readDockItems() {
24286 const api = window.wp?.desktop;
24287 if (api?.getMenuItems) {
24288 const items = api.getMenuItems();
24289 return items.map((i) => ({
24290 id: i.id,
24291 title: i.title,
24292 icon: i.icon,
24293 url: i.url,
24294 badge: i.badge ?? 0,
24295 submenu: i.submenu ?? []
24296 }));
24297 }
24298 const cfg = window.desktopModeConfig;
24299 return cfg?.dockItems ?? [];
24300 }
24301 function readServerIcons() {
24302 const cfg = window.desktopModeConfig;
24303 return cfg?.desktopIcons ?? [];
24304 }
24305 let reentrant = false;
24306 const removedServerPlacementsByRef = /* @__PURE__ */ new Map();
24307 function syncShortcutsWithVisibility(visibility, positions = {}) {
24308 if (reentrant) {
24309 return;
24310 }
24311 reentrant = true;
24312 try {
24313 const dockItems = readDockItems();
24314 const serverIcons = readServerIcons();
24315 const state2 = filesApi.store.getState();
24316 const root = state2.placementsByFolder.get(0) ?? [];
24317 const currentSynth = /* @__PURE__ */ new Map();
24318 for (const p of root) {
24319 const sourceId = (p.meta ?? null) && typeof p.meta === "object" ? p.meta[SYNTH_META_KEY] : null;
24320 if (typeof sourceId === "string") {
24321 currentSynth.set(sourceId, p);
24322 }
24323 }
24324 const realByRef = /* @__PURE__ */ new Map();
24325 const registeredIconIds = new Set(
24326 serverIcons.map((i) => i.id)
24327 );
24328 for (const p of root) {
24329 const ref = p?.file?.ref;
24330 if (typeof ref === "string" && registeredIconIds.has(ref)) {
24331 realByRef.set(ref, p);
24332 }
24333 }
24334 const desiredSynth = /* @__PURE__ */ new Set();
24335 for (const item of dockItems) {
24336 const placement = visibility[item.id];
24337 if (placement === "desktop" || placement === "both") {
24338 desiredSynth.add(item.id);
24339 if (!currentSynth.has(item.id)) {
24340 filesApi.store.upsertPlacement(
24341 buildSyntheticPlacement(item, positions)
24342 );
24343 }
24344 }
24345 }
24346 for (const [sourceId, p] of currentSynth) {
24347 if (!desiredSynth.has(sourceId)) {
24348 filesApi.store.removePlacement(p.id);
24349 }
24350 }
24351 for (const icon of serverIcons) {
24352 const placement = visibility[icon.id];
24353 const inStore = realByRef.get(icon.id);
24354 if (placement === "dock" || placement === "hidden") {
24355 if (inStore) {
24356 removedServerPlacementsByRef.set(icon.id, inStore);
24357 filesApi.store.removePlacement(inStore.id);
24358 }
24359 continue;
24360 }
24361 if (!inStore) {
24362 const cached = removedServerPlacementsByRef.get(icon.id);
24363 if (cached) {
24364 filesApi.store.upsertPlacement(cached);
24365 removedServerPlacementsByRef.delete(icon.id);
24366 }
24367 }
24368 }
24369 } finally {
24370 reentrant = false;
24371 }
24372 }
24373 function installShortcutsSync(getVisibility, getPositions = () => ({})) {
24374 queueMicrotask(
24375 () => syncShortcutsWithVisibility(getVisibility(), getPositions())
24376 );
24377 const off = filesApi.store.subscribe(() => {
24378 syncShortcutsWithVisibility(getVisibility(), getPositions());
24379 });
24380 return off;
24381 }
24382 const styles$2 = css`:host{display:inline-flex;align-items:center;gap:6px;font-size:var( --wpd-save-status-font-size,11px );line-height:1;color:var( --wpd-save-status-fg,currentColor );vertical-align:middle;min-width:0;opacity:1;pointer-events:auto}.wpd-save-status__indicator{display:inline-flex;align-items:center;justify-content:center;width:12px;height:12px;border-radius:50%;flex-shrink:0;box-sizing:border-box;background:var( --wpd-save-status-bg,transparent );border:2px solid var( --wpd-save-status-idle-color,color-mix( in srgb,var( --wp-admin-theme-color,#2271b1 ) 55%,transparent ) );color:var( --wp-admin-theme-color,#2271b1 );transition:background-color 0.2s ease,border-color 0.2s ease,box-shadow 0.2s ease}:host( [ phase='pending' ] ) .wpd-save-status__indicator,:host( [ phase='saving' ] ) .wpd-save-status__indicator{background:var( --wpd-save-status-bg,var( --wp-admin-theme-color,#2271b1 ) );border-color:transparent;color:var( --wp-admin-theme-color,#2271b1 );animation:wpd-save-status-pulse 1.2s ease-in-out infinite}:host( [ animation='modem' ][ phase='pending' ] ) .wpd-save-status__indicator,:host( [ animation='modem' ][ phase='saving' ] ) .wpd-save-status__indicator{background:var( --wpd-save-status-bg,var( --wp-admin-theme-color,#2271b1 ) );border-color:transparent;color:var( --wp-admin-theme-color,#2271b1 );animation:wpd-save-status-modem-stutter 1.8s ease-in-out infinite,wpd-save-status-modem-glow 2.4s ease-in-out infinite}@keyframes wpd-save-status-modem-stutter{0%,4%{opacity:1}5%,30%{opacity:0.22}31%,36%{opacity:1}37%,39%{opacity:0.22}40%,44%{opacity:1}45%,67%{opacity:0.22}68%,76%{opacity:1}77%,100%{opacity:0.22}}@keyframes wpd-save-status-modem-glow{0%,12%{box-shadow:0 0 0 0 transparent}13%,22%{box-shadow:0 0 4px 0 currentColor}23%,50%{box-shadow:0 0 0 0 transparent}51%,58%{box-shadow:0 0 4px 0 currentColor}59%,84%{box-shadow:0 0 0 0 transparent}85%,94%{box-shadow:0 0 5px 0 currentColor}95%,100%{box-shadow:0 0 0 0 transparent}}@media ( prefers-reduced-motion:reduce ){:host( [ phase='pending' ] ) .wpd-save-status__indicator,:host( [ phase='saving' ] ) .wpd-save-status__indicator,:host( [ animation='modem' ][ phase='pending' ] ) .wpd-save-status__indicator,:host( [ animation='modem' ][ phase='saving' ] ) .wpd-save-status__indicator{animation:none;opacity:0.85}}:host( [ phase='saved' ] ) .wpd-save-status__indicator{background:var( --wpd-save-status-saved-bg,#1d6f42 );border-color:transparent;color:var( --wpd-save-status-saved-bg,#1d6f42 )}:host( [ phase='failed' ] ) .wpd-save-status__indicator{background:var( --wpd-save-status-failed-bg,#d63638 );border-color:transparent;color:var( --wpd-save-status-failed-bg,#d63638 );animation:wpd-save-status-pulse 0.8s ease-in-out 2}@keyframes wpd-save-status-pulse{0%,100%{opacity:0.55;transform:scale( 0.9 )}50%{opacity:1;transform:scale( 1 )}}:host( [ mode='pill' ] ) .wpd-save-status{display:inline-flex;align-items:center;gap:6px;padding:2px 10px;border-radius:999px;background:var( --wpd-save-status-pill-bg,transparent );font-weight:500;white-space:nowrap}:host( [ mode='pill' ][ phase='saving' ] ) .wpd-save-status,:host( [ mode='pill' ][ phase='pending' ] ) .wpd-save-status{background:var( --wpd-save-status-pill-bg,rgba( 0,0,0,0.04 ) );color:var( --wpd-save-status-pill-fg,#50575e )}:host( [ mode='pill' ][ phase='saved' ] ) .wpd-save-status{background:var( --wpd-save-status-pill-bg,rgba( 30,132,73,0.12 ) );color:var( --wpd-save-status-pill-fg,#1d6f42 )}:host( [ mode='pill' ][ phase='failed' ] ) .wpd-save-status{background:var( --wpd-save-status-pill-bg,rgba( 214,54,56,0.12 ) );color:var( --wpd-save-status-pill-fg,#a02622 )}.wpd-save-status__label{min-width:0;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host( [ phase='saved' ] ) .wpd-save-status__glyph,:host( [ phase='failed' ] ) .wpd-save-status__glyph{display:inline-block;color:#fff;width:8px;height:8px}.wpd-save-status__glyph{display:none}.wpd-save-status__glyph svg{display:block;width:100%;height:100%}`;
24383 const DEFAULT_EVENT = "desktop-mode-os-settings-save-lifecycle";
24384 const DEFAULT_AUTO_CLEAR_SAVED_MS = 2200;
24385 const DEFAULT_AUTO_CLEAR_FAILED_MS = 6e3;
24386 const _WpdSaveStatus = class _WpdSaveStatus extends Component {
24387 constructor() {
24388 super(...arguments);
24389 this._autoTimer = null;
24390 this._docListener = null;
24391 }
24392 connectedCallback() {
24393 super.connectedCallback();
24394 if (this.auto !== null) {
24395 this._installAutoListener();
24396 }
24397 }
24398 disconnectedCallback() {
24399 this._removeAutoListener();
24400 if (this._autoTimer !== null) {
24401 window.clearTimeout(this._autoTimer);
24402 this._autoTimer = null;
24403 }
24404 }
24405 attributeChangedCallback(name, oldValue, newValue) {
24406 super.attributeChangedCallback(name, oldValue, newValue);
24407 if (name === "auto" || name === "event") {
24408 this._removeAutoListener();
24409 if (this.auto !== null) {
24410 this._installAutoListener();
24411 }
24412 }
24413 if (name === "phase") {
24414 this._scheduleAutoClear();
24415 const detail = {
24416 phase: this.phase ?? "idle",
24417 error: this.error ?? void 0
24418 };
24419 this.emit("wpd-save-status-change", detail);
24420 }
24421 }
24422 render() {
24423 const phase = this.phase ?? "idle";
24424 const mode = this.mode ?? "dot";
24425 const error = this.error ?? "";
24426 const title = error || this._labelForPhase(phase);
24427 if (title) {
24428 this.setAttribute("title", title);
24429 } else {
24430 this.removeAttribute("title");
24431 }
24432 this.setAttribute("aria-live", phase === "failed" ? "assertive" : "polite");
24433 this.setAttribute("role", phase === "failed" ? "alert" : "status");
24434 return html`
24435 <span class="wpd-save-status">
24436 <span class="wpd-save-status__indicator" aria-hidden="true">
24437 <span class="wpd-save-status__glyph">${this._renderGlyph(phase)}</span>
24438 </span>
24439 ${mode === "pill" ? html`<span class="wpd-save-status__label"
24440 >${this._labelForPhase(phase)}</span
24441 >` : html``}
24442 </span>
24443 `;
24444 }
24445 _renderGlyph(phase) {
24446 if (phase === "saved") {
24447 return _iconCheck();
24448 }
24449 if (phase === "failed") {
24450 return _iconBang();
24451 }
24452 return "";
24453 }
24454 _labelForPhase(phase) {
24455 switch (phase) {
24456 case "pending":
24457 case "saving":
24458 return this["saving-label"] ?? "Saving…";
24459 case "saved":
24460 return this["saved-label"] ?? "Saved";
24461 case "failed": {
24462 const err = this.error ?? "";
24463 return err || "Couldn’t save";
24464 }
24465 default:
24466 return this["idle-label"] ?? "";
24467 }
24468 }
24469 _installAutoListener() {
24470 const eventName = this.event || DEFAULT_EVENT;
24471 this._docListener = (e) => {
24472 const detail = e.detail;
24473 if (!detail || typeof detail.phase !== "string") {
24474 return;
24475 }
24476 this.phase = detail.phase;
24477 if (detail.error) {
24478 this.error = detail.error;
24479 } else if (detail.phase !== "failed" && this.error) {
24480 this.removeAttribute("error");
24481 }
24482 };
24483 document.addEventListener(eventName, this._docListener);
24484 }
24485 _removeAutoListener() {
24486 if (!this._docListener) {
24487 return;
24488 }
24489 const eventName = this.event || DEFAULT_EVENT;
24490 document.removeEventListener(eventName, this._docListener);
24491 this._docListener = null;
24492 }
24493 _scheduleAutoClear() {
24494 if (this._autoTimer !== null) {
24495 window.clearTimeout(this._autoTimer);
24496 this._autoTimer = null;
24497 }
24498 const phase = this.phase ?? "idle";
24499 const ms = this._autoClearMsFor(phase);
24500 if (ms <= 0) {
24501 return;
24502 }
24503 this._autoTimer = window.setTimeout(() => {
24504 this._autoTimer = null;
24505 this.phase = "idle";
24506 }, ms);
24507 }
24508 _autoClearMsFor(phase) {
24509 if (phase === "saved") {
24510 const raw = this["auto-clear-saved-ms"];
24511 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_SAVED_MS;
24512 }
24513 if (phase === "failed") {
24514 const raw = this["auto-clear-failed-ms"];
24515 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_FAILED_MS;
24516 }
24517 return 0;
24518 }
24519 };
24520 _WpdSaveStatus.props = [
24521 "phase",
24522 "mode",
24523 "animation",
24524 "auto",
24525 "event",
24526 "error",
24527 "saving-label",
24528 "saved-label",
24529 "idle-label",
24530 "auto-clear-saved-ms",
24531 "auto-clear-failed-ms"
24532 ];
24533 _WpdSaveStatus.styles = [styles$2];
24534 _WpdSaveStatus.help = {
24535 title: "Save status",
24536 summary: 'Tiny status indicator for "is this change saved yet?" affordances. Three layouts (dot / icon / pill), four phases, optional auto-listen to a save-lifecycle CustomEvent so every input in the panel inherits feedback for free.',
24537 status: "experimental",
24538 since: "0.8.0",
24539 props: [
24540 {
24541 name: "phase",
24542 type: "'idle' | 'pending' | 'saving' | 'saved' | 'failed'",
24543 default: "idle",
24544 description: "Current lifecycle phase. Set manually for one-off integrations, or rely on `auto` to populate it from a CustomEvent."
24545 },
24546 {
24547 name: "mode",
24548 type: "'dot' | 'icon' | 'pill'",
24549 default: "dot",
24550 description: "Layout. `dot` is the smallest (10×10 colored dot); `icon` adds a glyph inside on saved/failed; `pill` adds an inline label."
24551 },
24552 {
24553 name: "animation",
24554 type: "'pulse' | 'modem'",
24555 default: "pulse",
24556 description: "Animation cadence during the saving phase. `pulse` (default) is a smooth ease-in-out; `modem` is an irregular activity-LED blink with a soft glow — suits a 'data-flowing' affordance in window title bars."
24557 },
24558 {
24559 name: "auto",
24560 type: "boolean attribute",
24561 description: 'Subscribe to a CustomEvent on `document` and populate phase + error from its detail. Default event name is `desktop-mode-os-settings-save-lifecycle`; override with `event="…"`.'
24562 },
24563 {
24564 name: "event",
24565 type: "string",
24566 default: "desktop-mode-os-settings-save-lifecycle",
24567 description: "CustomEvent name to listen on when `auto` is set."
24568 },
24569 {
24570 name: "error",
24571 type: "string",
24572 description: "Error message shown in `pill` mode and exposed as the host title attribute (so dot/icon modes still surface the message via tooltip)."
24573 },
24574 {
24575 name: "saving-label",
24576 type: "string",
24577 default: "Saving…",
24578 description: "Pill-mode label shown during `pending` / `saving`."
24579 },
24580 {
24581 name: "saved-label",
24582 type: "string",
24583 default: "Saved",
24584 description: "Pill-mode label shown during `saved`."
24585 },
24586 {
24587 name: "idle-label",
24588 type: "string",
24589 description: 'Optional pill-mode label shown during `idle` (e.g. "All changes saved"). When unset, the pill collapses to invisible while idle.'
24590 },
24591 {
24592 name: "auto-clear-saved-ms",
24593 type: "integer",
24594 default: "2200",
24595 description: "How long the `saved` phase stays visible before auto-fading back to `idle`."
24596 },
24597 {
24598 name: "auto-clear-failed-ms",
24599 type: "integer",
24600 default: "6000",
24601 description: "How long the `failed` phase stays visible before auto-fading back to `idle`."
24602 }
24603 ],
24604 events: [
24605 {
24606 name: "wpd-save-status-change",
24607 description: "Fires when the phase changes (manually or via auto-listen).",
24608 detail: "{ phase, error }"
24609 }
24610 ],
24611 cssProps: [
24612 {
24613 name: "--wpd-save-status-bg",
24614 description: "Indicator background color (saving/pending phase)."
24615 },
24616 {
24617 name: "--wpd-save-status-saved-bg",
24618 description: "Indicator background on saved."
24619 },
24620 {
24621 name: "--wpd-save-status-failed-bg",
24622 description: "Indicator background on failed."
24623 },
24624 {
24625 name: "--wpd-save-status-pill-bg",
24626 description: "Pill background (mode=pill)."
24627 },
24628 {
24629 name: "--wpd-save-status-pill-fg",
24630 description: "Pill foreground (mode=pill)."
24631 }
24632 ],
24633 example: html`
24634 <wpd-cluster gap="12">
24635 <wpd-save-status phase="pending"></wpd-save-status>
24636 <wpd-save-status phase="saving"></wpd-save-status>
24637 <wpd-save-status phase="saved"></wpd-save-status>
24638 <wpd-save-status phase="failed"></wpd-save-status>
24639 <wpd-save-status mode="pill" phase="saving"></wpd-save-status>
24640 <wpd-save-status mode="pill" phase="saved"></wpd-save-status>
24641 <wpd-save-status mode="pill" phase="failed" error="Network error."></wpd-save-status>
24642 </wpd-cluster>
24643 `
24644 };
24645 let WpdSaveStatus = _WpdSaveStatus;
24646 defineComponent("wpd-save-status", WpdSaveStatus);
24647 function _iconCheck() {
24648 return html`
24649 <svg
24650 viewBox="0 0 12 12"
24651 aria-hidden="true"
24652 focusable="false"
24653 fill="none"
24654 stroke="currentColor"
24655 stroke-width="2"
24656 stroke-linecap="round"
24657 stroke-linejoin="round"
24658 >
24659 <path d="M2.5 6 L5 8.5 L9.5 4" />
24660 </svg>
24661 `;
24662 }
24663 function _iconBang() {
24664 return html`
24665 <svg
24666 viewBox="0 0 12 12"
24667 aria-hidden="true"
24668 focusable="false"
24669 fill="currentColor"
24670 >
24671 <path
24672 d="M5 2 H7 V7 H5 z M5 8.5 H7 V10.5 H5 z"
24673 />
24674 </svg>
24675 `;
24676 }
24677 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}`;
24678 const _WpdTextarea = class _WpdTextarea extends Component {
24679 constructor() {
24680 super(...arguments);
24681 this._textareaEl = null;
24682 }
24683 connectedCallback() {
24684 super.connectedCallback();
24685 ensureAutoId(this);
24686 }
24687 render() {
24688 const label = this._attr("label") || "";
24689 const value = this._attr("value") ?? "";
24690 const placeholder = this._attr("placeholder") || "";
24691 const disabled = this._boolAttr("disabled");
24692 const readonly = this._boolAttr("readonly");
24693 const ariaLabel = this._attr("aria-label") || label;
24694 const name = this._attr("name") || "";
24695 const rows = Number(this._attr("rows")) || 3;
24696 const maxLength = this._attr("maxlength");
24697 const minLength = this._attr("minlength");
24698 const invalid = this._boolAttr("invalid");
24699 const hostId = this.id || "wpd-unnamed";
24700 const fieldId = `${hostId}__field`;
24701 return html`
24702 ${label ? html`<label class="wpd-textarea__label" for=${fieldId}>${label}</label>` : html``}
24703 <textarea
24704 id=${fieldId}
24705 part="textarea"
24706 .value=${value}
24707 placeholder=${placeholder}
24708 ?disabled=${disabled}
24709 ?readonly=${readonly}
24710 rows=${rows}
24711 maxlength=${maxLength ?? ""}
24712 minlength=${minLength ?? ""}
24713 name=${name}
24714 aria-invalid=${invalid ? "true" : "false"}
24715 aria-label=${ariaLabel || ""}
24716 @input=${(e) => this._onInput(e)}
24717 @change=${(e) => this._onChange(e)}
24718 @keydown=${(e) => this._onKeyDown(e)}
24719 ></textarea>
24720 `;
24721 }
24722 _attr(name) {
24723 return this.getAttribute(name);
24724 }
24725 _boolAttr(name) {
24726 return this.getAttribute(name) !== null;
24727 }
24728 _onInput(e) {
24729 const ta = e.target;
24730 this._textareaEl = ta;
24731 this.setAttribute("value", ta.value);
24732 this.emit("wpd-input-change", { value: ta.value });
24733 if (this._boolAttr("auto-grow")) {
24734 this._autosize(ta);
24735 }
24736 }
24737 _onChange(e) {
24738 const ta = e.target;
24739 this.emit("wpd-input-commit", { value: ta.value });
24740 }
24741 _onKeyDown(e) {
24742 if (!this._boolAttr("submit-on-enter")) {
24743 return;
24744 }
24745 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
24746 e.preventDefault();
24747 const ta = e.target;
24748 this.emit("wpd-submit", { value: ta.value });
24749 }
24750 }
24751 /**
24752 * Grow the textarea height to fit content, capped at `max-rows`.
24753 * Resets to scroll-height each input then clamps; cheap because
24754 * the browser caches layout.
24755 */
24756 _autosize(ta) {
24757 const maxRows = Number(this._attr("max-rows")) || 8;
24758 const cs = window.getComputedStyle(ta);
24759 const fontSize = parseFloat(cs.fontSize) || 13;
24760 const lineHeightRaw = cs.lineHeight;
24761 const lineHeight = lineHeightRaw === "normal" ? fontSize * 1.45 : parseFloat(lineHeightRaw) || fontSize * 1.45;
24762 const paddingTop = parseFloat(cs.paddingTop) || 0;
24763 const paddingBottom = parseFloat(cs.paddingBottom) || 0;
24764 const max = lineHeight * maxRows + paddingTop + paddingBottom;
24765 ta.style.height = "auto";
24766 const next = Math.min(ta.scrollHeight, max);
24767 ta.style.height = `${next}px`;
24768 }
24769 /** Public helper for callers that programmatically set `.value` and want autosize to re-run. */
24770 refreshAutosize() {
24771 if (this._textareaEl && this._boolAttr("auto-grow")) {
24772 this._autosize(this._textareaEl);
24773 }
24774 }
24775 /** Imperatively focus the underlying textarea. */
24776 focusInput() {
24777 const root = this.shadowRoot ?? this;
24778 const ta = root.querySelector("textarea");
24779 ta?.focus();
24780 }
24781 /** Imperatively clear the value. */
24782 clear() {
24783 this.setAttribute("value", "");
24784 const root = this.shadowRoot ?? this;
24785 const ta = root.querySelector("textarea");
24786 if (ta) {
24787 ta.value = "";
24788 if (this._boolAttr("auto-grow")) {
24789 this._autosize(ta);
24790 }
24791 }
24792 }
24793 };
24794 _WpdTextarea.props = [
24795 "label",
24796 "value",
24797 "placeholder",
24798 "disabled",
24799 "readonly",
24800 "ariaLabel",
24801 "name",
24802 "rows",
24803 "maxlength",
24804 "minlength",
24805 "invalid",
24806 "autoGrow",
24807 "maxRows",
24808 "submitOnEnter"
24809 ];
24810 _WpdTextarea.styles = [textareaStyles];
24811 _WpdTextarea.help = {
24812 title: "Textarea",
24813 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).",
24814 status: "stable",
24815 since: "0.22.0",
24816 props: [
24817 { name: "label", type: "string", description: "Visible label above the textarea." },
24818 { name: "value", type: "string", description: "Current value; reflected two-way." },
24819 { name: "placeholder", type: "string", description: "Native placeholder." },
24820 { name: "disabled", type: "boolean attribute" },
24821 { name: "readonly", type: "boolean attribute" },
24822 { name: "aria-label", type: "string", description: "Accessible label when no visible label is rendered." },
24823 { name: "name", type: "string", description: "Forwarded to native textarea for form submission." },
24824 { name: "rows", type: "integer (string)", default: "3", description: "Initial visible row count." },
24825 { name: "maxlength", type: "integer (string)" },
24826 { name: "minlength", type: "integer (string)" },
24827 { name: "invalid", type: "boolean attribute", description: "Sets aria-invalid + error styling." },
24828 { name: "auto-grow", type: "boolean attribute", description: "Grows up to max-rows as the user types." },
24829 { name: "max-rows", type: "integer (string)", default: "8" },
24830 {
24831 name: "submit-on-enter",
24832 type: "boolean attribute",
24833 description: "Enter fires wpd-submit; Shift+Enter inserts a newline."
24834 }
24835 ],
24836 events: [
24837 { name: "wpd-input-change", description: "Fires on every keystroke.", detail: "{ value: string }" },
24838 { name: "wpd-input-commit", description: "Fires on blur / native change.", detail: "{ value: string }" },
24839 {
24840 name: "wpd-submit",
24841 description: "Fires on Enter (without Shift) when submit-on-enter is set.",
24842 detail: "{ value: string }"
24843 }
24844 ],
24845 example: html`
24846 <wpd-textarea label="Message" rows="3" auto-grow max-rows="8" submit-on-enter></wpd-textarea>
24847 `
24848 };
24849 let WpdTextarea = _WpdTextarea;
24850 defineComponent("wpd-textarea", WpdTextarea);
24851 const styles$1 = css`:host{display:inline-flex}button{display:flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;border:none;border-radius:5px;background:transparent;color:var( --wpd-btn-color,currentColor );cursor:pointer;transition:background-color 0.15s ease,color 0.15s ease}button:hover{color:var( --wpd-btn-color-hover,currentColor );background:var( --wpd-btn-bg-hover,rgba( 0,0,0,0.06 ) )}button:focus-visible{color:var( --wpd-btn-color-hover,currentColor );background:var( --wpd-btn-bg-hover,rgba( 0,0,0,0.06 ) );outline:2px solid var( --wpd-btn-outline,currentColor );outline-offset:1px}:host( [ active ] ) button{color:var( --wpd-btn-color-hover,currentColor );background:var( --wpd-btn-bg-active,rgba( 0,0,0,0.08 ) )}:host( [ danger ] ) button:hover{color:#fff;background:var( --wpd-btn-danger-hover,#d63638 )}svg{display:block;pointer-events:none;flex-shrink:0}svg:empty{display:none}::slotted( span ){line-height:1}::slotted( svg ){display:block}`;
24852 const ICONS = {
24853 minimize: '<path d="M3 6h6" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>',
24854 maximize: '<rect x="3" y="3" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.25" fill="none"/>',
24855 fullscreen: '<path d="M4.5 2H2v2.5M10 4.5V2H7.5M4.5 10H2V7.5M10 7.5V10H7.5" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
24856 "fullscreen-exit": '<path d="M2 4.5H4.5V2M7.5 2V4.5H10M2 7.5H4.5V10M7.5 10V7.5H10" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
24857 detach: '<path d="M5 2H2.5v7.5H10V7M6.5 2H10v3.5M10 2L5.5 6.5" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" fill="none"/>',
24858 reload: (
24859 // Filled icon scaled from a 512×512 source into the 12×12 viewBox
24860 // shared with the other title-bar glyphs. The wrapping `<g>` does
24861 // the math; the inner path is dropped in unmodified so its
24862 // authoring tool can be re-edited and copy-pasted again.
24863 // `scale(0.021)` ≈ 90% of full fit, with `translate(0.6)` to
24864 // keep the result centered inside the 12×12 viewBox so the
24865 // glyph reads slightly smaller than min/max/close — closer to
24866 // the visual weight of the other title-bar buttons.
24867 '<g transform="translate(0.6 0.6) scale(0.021)" fill="currentColor"><path d="m504.554 233.704-76.447 91.467c-6.329 7.572-15.417 11.479-24.571 11.479a31.872 31.872 0 0 1-20.504-7.447l-91.467-76.447c-13.561-11.334-15.366-31.515-4.032-45.075s31.515-15.366 45.075-4.032l37.506 31.347c-10.274-74.891-74.668-132.774-152.337-132.774C132.984 102.223 64 171.207 64 256s68.984 153.777 153.777 153.777c17.673 0 32 14.327 32 32s-14.327 32-32 32c-58.17 0-112.859-22.653-153.991-63.785C22.653 368.859 0 314.17 0 256s22.653-112.859 63.786-153.992c41.132-41.132 95.821-63.785 153.991-63.785s112.859 22.653 153.992 63.785c32.517 32.516 53.471 73.508 60.829 117.991l22.849-27.339c11.334-13.56 31.515-15.364 45.075-4.032 13.56 11.335 15.365 31.516 4.032 45.076z"/></g>'
24868 ),
24869 close: '<path d="M3.25 3.25l5.5 5.5M3.25 8.75l5.5-5.5" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>',
24870 menu: '<circle cx="3" cy="6" r="1.2" fill="currentColor"/><circle cx="6" cy="6" r="1.2" fill="currentColor"/><circle cx="9" cy="6" r="1.2" fill="currentColor"/>'
24871 };
24872 const _WpdWindowButton = class _WpdWindowButton extends Component {
24873 constructor() {
24874 super(...arguments);
24875 this._activateWired = false;
24876 }
24877 render() {
24878 const iconKey = this.icon || "";
24879 const svgInner = ICONS[iconKey] || "";
24880 return html`
24881 <button type="button">
24882 <svg
24883 width="14"
24884 height="14"
24885 viewBox="0 0 12 12"
24886 aria-hidden="true"
24887 focusable="false"
24888 ></svg>
24889 <slot></slot>
24890 </button>
24891 <span data-svg-buffer style="display:none">${svgInner}</span>
24892 `;
24893 }
24894 /**
24895 * After each render, copy the raw SVG markup into the actual
24896 * `<svg>` element. The templater only writes text into slots,
24897 * so we stash the intended markup in a hidden buffer and
24898 * `innerHTML = ` the svg once here — a one-shot post-render
24899 * hook that keeps the declarative template honest.
24900 *
24901 * Also wires up the `wpd-button-activate` CustomEvent that
24902 * fires exactly once per gesture — the canonical contract
24903 * for plugin-registered title-bar buttons. Plugin authors who
24904 * use `addEventListener( 'click', cb )` directly still get
24905 * what they expect (the title bar's drag-handler now excludes
24906 * chrome buttons by class so static clicks land normally),
24907 * but `wpd-button-activate` is the documented surface that
24908 * documents the once-per-gesture contract explicitly. See
24909 * the class-level docblock for rationale.
24910 */
24911 connectedCallback() {
24912 super.connectedCallback();
24913 queueMicrotask(() => this._paintSvg());
24914 queueMicrotask(() => this._wireActivateEvent());
24915 }
24916 attributeChangedCallback(name, oldValue, newValue) {
24917 super.attributeChangedCallback(name, oldValue, newValue);
24918 queueMicrotask(() => this._paintSvg());
24919 }
24920 _paintSvg() {
24921 const root = this.shadowRoot;
24922 if (!root) {
24923 return;
24924 }
24925 const svg = root.querySelector("svg");
24926 const buffer = root.querySelector("[data-svg-buffer]");
24927 if (svg && buffer) {
24928 const markup = buffer.textContent || "";
24929 if (svg.innerHTML !== markup) {
24930 svg.innerHTML = markup;
24931 }
24932 }
24933 }
24934 _wireActivateEvent() {
24935 if (this._activateWired) {
24936 return;
24937 }
24938 const root = this.shadowRoot;
24939 if (!root) {
24940 return;
24941 }
24942 const button = root.querySelector("button");
24943 if (!button) {
24944 return;
24945 }
24946 this._activateWired = true;
24947 button.addEventListener("click", () => {
24948 this.dispatchEvent(
24949 new CustomEvent("wpd-button-activate", {
24950 bubbles: true,
24951 composed: true,
24952 cancelable: true
24953 })
24954 );
24955 });
24956 }
24957 };
24958 _WpdWindowButton.props = ["icon", "active", "danger"];
24959 _WpdWindowButton.styles = [styles$1];
24960 _WpdWindowButton.help = {
24961 title: "Window button",
24962 summary: "Chrome button used in native-window title bars. Built-in icons cover the standard controls (minimize, maximize, fullscreen, detach, close, menu). Focused/unfocused coloring is driven by --wpd-btn-* CSS custom properties the window shell owns.",
24963 status: "stable",
24964 since: "0.9.0",
24965 props: [
24966 {
24967 name: "icon",
24968 type: "'minimize' | 'maximize' | 'fullscreen' | 'fullscreen-exit' | 'detach' | 'reload' | 'close' | 'menu'",
24969 description: "Which built-in inline SVG to paint. Omit to supply your own via the slot."
24970 },
24971 {
24972 name: "active",
24973 type: "boolean attribute",
24974 description: "Applies the pressed-down look (used e.g. while a menu it triggers is open)."
24975 },
24976 {
24977 name: "danger",
24978 type: "boolean attribute",
24979 description: "Swaps the hover wash to red — used by the close button."
24980 }
24981 ],
24982 slots: [
24983 { name: "(default)", description: "Optional custom icon markup (inline SVG) when `icon` is omitted." }
24984 ],
24985 cssProps: [
24986 { name: "--wpd-btn-color", description: "Resting foreground." },
24987 { name: "--wpd-btn-color-hover", description: "Hover foreground." },
24988 { name: "--wpd-btn-bg-hover", description: "Hover background wash." },
24989 { name: "--wpd-btn-bg-active", description: "Pressed background." },
24990 { name: "--wpd-btn-danger-hover", description: "Hover background for danger variant." },
24991 { name: "--wpd-btn-outline", description: "Focus outline colour." }
24992 ],
24993 example: html`
24994 <wpd-cluster gap="2">
24995 <wpd-window-button icon="minimize"></wpd-window-button>
24996 <wpd-window-button icon="maximize"></wpd-window-button>
24997 <wpd-window-button icon="menu"></wpd-window-button>
24998 <wpd-window-button icon="close" danger></wpd-window-button>
24999 </wpd-cluster>
25000 `
25001 };
25002 let WpdWindowButton = _WpdWindowButton;
25003 defineComponent("wpd-window-button", WpdWindowButton);
25004 const DEFAULT_STICKY_TITLE = "Sticky Note";
25005 const LEGACY_METADATA_PREFIX = "<!-- wpworkspace-sticky:";
25006 const LEGACY_METADATA_SUFFIX = "-->";
25007 const TITLE_MAX = 64;
25008 const GENERATED_TITLE_MAX = 48;
25009 const EXCERPT_MAX = 180;
25010 function noteFromGuideline(guideline) {
25011 const title = titleField(guideline.title);
25012 const content = removeLegacyMetadataComment(
25013 textFieldValue(guideline.content, { stripHtmlForRendered: true })
25014 );
25015 const modifiedMs = modifiedTimeMs(guideline);
25016 return {
25017 localId: `guideline:${guideline.id}`,
25018 guidelineId: guideline.id,
25019 title,
25020 body: editorBody(title, content),
25021 modified: guideline.modified,
25022 ...modifiedMs > 0 ? { modifiedMs } : {},
25023 link: guideline.link,
25024 termIds: Array.isArray(guideline.wp_guideline_type) ? guideline.wp_guideline_type.filter(isFiniteNumber) : []
25025 };
25026 }
25027 function titleField(field) {
25028 const candidates = [];
25029 if (typeof field === "string") {
25030 candidates.push(field);
25031 } else if (field && typeof field === "object") {
25032 if (typeof field.raw === "string") {
25033 candidates.push(field.raw);
25034 }
25035 if (typeof field.rendered === "string") {
25036 candidates.push(stripHtml(field.rendered));
25037 }
25038 }
25039 for (const candidate of candidates) {
25040 const trimmed = stripHtml(candidate).trim();
25041 if (trimmed) {
25042 return trimmed;
25043 }
25044 }
25045 return DEFAULT_STICKY_TITLE;
25046 }
25047 function textFieldValue(field, options = {}) {
25048 if (typeof field === "string") {
25049 return field;
25050 }
25051 if (!field || typeof field !== "object") {
25052 return "";
25053 }
25054 if (typeof field.raw === "string" && field.raw.length > 0) {
25055 return field.raw;
25056 }
25057 if (typeof field.rendered === "string") {
25058 return options.stripHtmlForRendered ? stripHtml(field.rendered) : field.rendered;
25059 }
25060 return "";
25061 }
25062 function titleForBody(body) {
25063 const line = body.split(/\r?\n/).find((item) => item.trim().length > 0)?.trim();
25064 const title = line && line.length > 0 ? line : DEFAULT_STICKY_TITLE;
25065 return truncate(title, TITLE_MAX);
25066 }
25067 function generatedTitle(body) {
25068 const collapsed = body.replace(/\s+/g, " ").trim();
25069 const title = collapsed || DEFAULT_STICKY_TITLE;
25070 return truncate(title, GENERATED_TITLE_MAX);
25071 }
25072 function editorBody(title, content) {
25073 const trimmedTitle = title.trim();
25074 if (!trimmedTitle) {
25075 return content;
25076 }
25077 const firstLine = content.split(/\r?\n/)[0]?.trim();
25078 if (firstLine === trimmedTitle) {
25079 return content;
25080 }
25081 if (!content) {
25082 return trimmedTitle;
25083 }
25084 return `${trimmedTitle}
25085 ${content}`;
25086 }
25087 function noteComponentsForBody(editorValue, fallbackTitle = DEFAULT_STICKY_TITLE) {
25088 const fallback = fallbackTitle.trim() || DEFAULT_STICKY_TITLE;
25089 const title = titleForBody(editorValue);
25090 const firstNewline = editorValue.search(/\r?\n/);
25091 if (firstNewline === -1) {
25092 const resolvedTitle = title === DEFAULT_STICKY_TITLE ? fallback : title;
25093 return {
25094 title: resolvedTitle,
25095 content: "",
25096 excerpt: excerptFor(resolvedTitle)
25097 };
25098 }
25099 let content = editorValue.slice(firstNewline);
25100 content = content.replace(/^\r?\n/, "");
25101 if (content.startsWith("\n")) {
25102 content = content.slice(1);
25103 }
25104 return {
25105 title,
25106 content,
25107 excerpt: excerptFor(content.trim() ? content : title)
25108 };
25109 }
25110 function excerptFor(body) {
25111 const collapsed = body.replace(/[\n\t]+/g, " ").trim();
25112 return truncate(collapsed, EXCERPT_MAX);
25113 }
25114 function removeLegacyMetadataComment(content) {
25115 if (!content.startsWith(LEGACY_METADATA_PREFIX) || !content.includes(LEGACY_METADATA_SUFFIX)) {
25116 return content;
25117 }
25118 const end = content.indexOf(LEGACY_METADATA_SUFFIX);
25119 let body = content.slice(end + LEGACY_METADATA_SUFFIX.length);
25120 if (body.startsWith("\r\n")) {
25121 body = body.slice(2);
25122 } else if (body.startsWith("\n")) {
25123 body = body.slice(1);
25124 }
25125 return body;
25126 }
25127 function stripHtml(value) {
25128 if (typeof document !== "undefined") {
25129 const template = document.createElement("template");
25130 template.innerHTML = value;
25131 return (template.content.textContent ?? "").trim();
25132 }
25133 return value.replace(/<[^>]*>/g, "").trim();
25134 }
25135 function truncate(value, max) {
25136 return value.length > max ? `${value.slice(0, max)}...` : value;
25137 }
25138 function modifiedTimeMs(guideline) {
25139 if (typeof guideline.desktop_mode_modified_ms === "number" && Number.isFinite(guideline.desktop_mode_modified_ms)) {
25140 return guideline.desktop_mode_modified_ms;
25141 }
25142 if (!guideline.modified) {
25143 return 0;
25144 }
25145 const parsed = Date.parse(guideline.modified);
25146 return Number.isFinite(parsed) ? parsed : 0;
25147 }
25148 function isFiniteNumber(value) {
25149 return typeof value === "number" && Number.isFinite(value);
25150 }
25151 class StickyNotesRestError extends Error {
25152 constructor(message, status) {
25153 super(message);
25154 this.name = "StickyNotesRestError";
25155 this.status = status;
25156 }
25157 }
25158 async function resolveStickyTerms(config) {
25159 const terms = await fetchStickyTermCandidates(config);
25160 const picked = pickStickyTerms(
25161 [...terms.artifactTerms, ...terms.artifactsTerms],
25162 terms.noteTerms,
25163 terms.stickyTerms
25164 );
25165 if (picked) {
25166 return picked;
25167 }
25168 const artifact = await ensureTerm(config, {
25169 slug: "artifact",
25170 name: "Artifact",
25171 parent: 0
25172 });
25173 const note = await ensureTerm(config, {
25174 slug: "note",
25175 name: "Note",
25176 parent: artifact.id
25177 });
25178 const sticky = await ensureTerm(config, {
25179 slug: "sticky",
25180 name: "Sticky",
25181 parent: artifact.id
25182 });
25183 return {
25184 stickyTermId: sticky.id,
25185 termIds: uniqueNumbers([artifact.id, note.id, sticky.id])
25186 };
25187 }
25188 async function fetchStickyTermCandidates(config) {
25189 const [artifactTerms, artifactsTerms, noteTerms, stickyTerms] = await Promise.all([
25190 fetchTermsBySlug(config, "artifact"),
25191 fetchTermsBySlug(config, "artifacts"),
25192 fetchTermsBySlug(config, "note"),
25193 fetchTermsBySlug(config, "sticky")
25194 ]);
25195 return {
25196 artifactTerms,
25197 artifactsTerms,
25198 noteTerms,
25199 stickyTerms
25200 };
25201 }
25202 function pickStickyTerms(artifactTerms, noteTerms, stickyTerms) {
25203 if (stickyTerms.length === 0) {
25204 return null;
25205 }
25206 const artifact = artifactTerms.find(
25207 (term) => ["artifact", "artifacts"].includes(term.slug)
25208 ) ?? artifactTerms[0] ?? null;
25209 const sticky = artifact ? stickyTerms.find((term) => Number(term.parent) === artifact.id) ?? stickyTerms[0] : stickyTerms[0];
25210 if (!sticky) {
25211 return null;
25212 }
25213 const note = artifact ? noteTerms.find((term) => Number(term.parent) === artifact.id) ?? null : null;
25214 return {
25215 stickyTermId: sticky.id,
25216 termIds: uniqueNumbers([
25217 artifact?.id,
25218 note?.id,
25219 sticky.id
25220 ])
25221 };
25222 }
25223 async function fetchStickyNotes(config, stickyTermId) {
25224 const guidelines = await requestJson(
25225 config,
25226 pathWithQuery("wp/v2/guidelines", {
25227 context: "edit",
25228 status: "private",
25229 per_page: "100",
25230 orderby: "modified",
25231 order: "desc",
25232 wp_guideline_type: String(stickyTermId)
25233 }),
25234 void 0,
25235 true
25236 );
25237 return guidelines.filter(
25238 (guideline) => Array.isArray(guideline.wp_guideline_type) ? guideline.wp_guideline_type.includes(stickyTermId) : true
25239 ).map(noteFromGuideline);
25240 }
25241 async function saveStickyNote(config, note, terms) {
25242 const components = noteComponentsForBody(note.body, note.title);
25243 const payload = {
25244 status: "private",
25245 title: components.title,
25246 content: components.content,
25247 excerpt: components.excerpt
25248 };
25249 if (note.guidelineId === null) {
25250 payload.wp_guideline_type = terms.termIds;
25251 }
25252 const path = note.guidelineId === null ? "wp/v2/guidelines" : `wp/v2/guidelines/${note.guidelineId}`;
25253 const guideline = await requestJson(
25254 config,
25255 path,
25256 {
25257 method: "POST",
25258 headers: {
25259 "Content-Type": "application/json"
25260 },
25261 body: JSON.stringify(payload)
25262 },
25263 false
25264 );
25265 return noteFromGuideline(guideline);
25266 }
25267 function buildGuidelineEditUrl(adminUrl, guidelineId) {
25268 const url = new URL("post.php", adminUrl);
25269 url.searchParams.set("post", String(guidelineId));
25270 url.searchParams.set("action", "edit");
25271 return url.toString();
25272 }
25273 async function fetchTermsBySlug(config, slug) {
25274 try {
25275 return await requestJson(
25276 config,
25277 pathWithQuery("wp/v2/wp_guideline_type", {
25278 context: "edit",
25279 slug,
25280 per_page: "100"
25281 }),
25282 void 0,
25283 true
25284 );
25285 } catch (error) {
25286 if (error instanceof StickyNotesRestError && (error.status === 404 || error.status === 400)) {
25287 return [];
25288 }
25289 throw error;
25290 }
25291 }
25292 async function ensureTerm(config, term) {
25293 const existing = await fetchTermsBySlug(config, term.slug);
25294 const byParent = existing.find(
25295 (item) => Number(item.parent ?? 0) === term.parent
25296 );
25297 if (byParent) {
25298 return byParent;
25299 }
25300 if (existing[0]) {
25301 return existing[0];
25302 }
25303 try {
25304 return await requestJson(
25305 config,
25306 "wp/v2/wp_guideline_type",
25307 {
25308 method: "POST",
25309 headers: {
25310 "Content-Type": "application/json"
25311 },
25312 body: JSON.stringify(term)
25313 },
25314 true
25315 );
25316 } catch (error) {
25317 const fallback = await fetchTermsBySlug(config, term.slug);
25318 if (fallback[0]) {
25319 return fallback[0];
25320 }
25321 throw error;
25322 }
25323 }
25324 async function requestJson(config, path, init2, silent = true) {
25325 const response = await trackedFetch$1(
25326 joinRestUrl(restRoot(config), path),
25327 init2,
25328 {
25329 source: "desktop-mode/sticky-notes",
25330 silent
25331 }
25332 );
25333 if (!response.ok) {
25334 throw new StickyNotesRestError(
25335 response.statusText || `${DEFAULT_STICKY_TITLE} request failed`,
25336 response.status
25337 );
25338 }
25339 return await response.json();
25340 }
25341 function restRoot(config) {
25342 if (config.restUrl) {
25343 return config.restUrl;
25344 }
25345 return `${window.location.origin}/wp-json/`;
25346 }
25347 function pathWithQuery(path, query) {
25348 const params = new URLSearchParams();
25349 Object.entries(query).forEach(([key, value]) => {
25350 params.set(key, value);
25351 });
25352 return `${path}?${params.toString()}`;
25353 }
25354 function uniqueNumbers(values) {
25355 const out = [];
25356 values.forEach((value) => {
25357 if (typeof value === "number" && Number.isFinite(value) && !out.includes(value)) {
25358 out.push(value);
25359 }
25360 });
25361 return out;
25362 }
25363 const SUBSCRIBE_FIELD = "desktop_mode_sticky_notes_subscribe";
25364 const RESPONSE_FIELD = "desktop_mode_sticky_notes";
25365 let started$3 = false;
25366 let target = null;
25367 function startStickyNotesHeartbeat(nextTarget) {
25368 target = nextTarget;
25369 if (started$3) {
25370 return;
25371 }
25372 started$3 = true;
25373 heartbeat.contribute(
25374 SUBSCRIBE_FIELD,
25375 () => target?.getHeartbeatSubscription()
25376 );
25377 heartbeat.subscribe(
25378 RESPONSE_FIELD,
25379 (payload) => {
25380 target?.applyHeartbeatPayload(payload);
25381 }
25382 );
25383 }
25384 const GEOMETRY_KEY = "desktop-mode-sticky-notes-geometry";
25385 const DEFAULT_WIDTH = 264;
25386 const DEFAULT_HEIGHT = 176;
25387 const MIN_WIDTH = 180;
25388 const MIN_HEIGHT = 128;
25389 const EDGE_PADDING = 16;
25390 const SAVE_DEBOUNCE_MS = 1e3;
25391 class StickyNotesLayer {
25392 constructor(options) {
25393 this.root = null;
25394 this.terms = null;
25395 this.controllers = /* @__PURE__ */ new Map();
25396 this.contextMenuInstalled = false;
25397 this.desktopHooksInstalled = false;
25398 this.highWaterMs = 0;
25399 this.zIndexCounter = 0;
25400 this.host = options.host;
25401 this.config = options.config;
25402 this.openArtifact = options.openArtifact;
25403 this.getActiveDesktopId = options.getActiveDesktopId ?? (() => "desktop-1");
25404 this.onError = options.onError;
25405 }
25406 async boot() {
25407 try {
25408 this.terms = await resolveStickyTerms(this.config);
25409 if (!this.terms) {
25410 return;
25411 }
25412 this.installContextMenu();
25413 this.installDesktopHooks();
25414 const notes = await fetchStickyNotes(
25415 this.config,
25416 this.terms.stickyTermId
25417 );
25418 this.bumpHighWaterFromNotes(notes);
25419 startStickyNotesHeartbeat(this);
25420 if (notes.length === 0) {
25421 return;
25422 }
25423 this.ensureRoot();
25424 sortNotesByModified(notes).forEach(
25425 (note, index2) => this.upsert(note, index2)
25426 );
25427 } catch (error) {
25428 if (error instanceof Error) {
25429 console.debug("[desktop-mode] Sticky notes unavailable:", error.message);
25430 }
25431 }
25432 }
25433 createNote(body = "") {
25434 if (!this.terms) {
25435 return;
25436 }
25437 const note = {
25438 localId: `local:${Date.now()}:${Math.random().toString(36).slice(2)}`,
25439 guidelineId: null,
25440 title: body.trim() ? generatedTitle(body) : DEFAULT_STICKY_TITLE,
25441 body,
25442 termIds: this.terms.termIds
25443 };
25444 const controller = this.upsert(note, this.controllers.size, {
25445 activate: true
25446 });
25447 controller.focus();
25448 }
25449 upsert(note, index2, options = {}) {
25450 this.ensureRoot();
25451 const key = noteKey(note);
25452 const existing = this.controllers.get(key);
25453 if (existing) {
25454 existing.replace(note);
25455 if (options.activate) {
25456 this.bringToFront(existing);
25457 }
25458 return existing;
25459 }
25460 const controller = new StickyNoteController({
25461 layer: this,
25462 note,
25463 index: index2
25464 });
25465 this.controllers.set(key, controller);
25466 this.root?.appendChild(controller.element);
25467 this.assignZIndex(controller);
25468 this.applyDesktopVisibility(controller);
25469 if (options.activate) {
25470 this.bringToFront(controller);
25471 }
25472 return controller;
25473 }
25474 ensureRoot() {
25475 if (this.root) {
25476 return this.root;
25477 }
25478 const root = document.createElement("section");
25479 root.className = "desktop-mode-sticky-notes";
25480 root.setAttribute("aria-label", __("Sticky notes"));
25481 this.host.appendChild(root);
25482 this.root = root;
25483 return root;
25484 }
25485 installContextMenu() {
25486 if (this.contextMenuInstalled) {
25487 return;
25488 }
25489 this.contextMenuInstalled = true;
25490 addFilter(
25491 "desktop-mode.wallpaper-context-menu",
25492 "desktop-mode/sticky-notes",
25493 (items) => {
25494 if (!Array.isArray(items) || !this.terms) {
25495 return items;
25496 }
25497 if (items.some(
25498 (item) => item.id === "new-sticky-note"
25499 )) {
25500 return items;
25501 }
25502 return [
25503 ...items,
25504 {
25505 id: "new-sticky-note",
25506 label: __("New sticky note"),
25507 icon: "dashicons-edit-page",
25508 sort: 14,
25509 onClick: () => this.createNote()
25510 }
25511 ];
25512 }
25513 );
25514 }
25515 installDesktopHooks() {
25516 if (this.desktopHooksInstalled) {
25517 return;
25518 }
25519 this.desktopHooksInstalled = true;
25520 addAction(
25521 HOOKS.DESKTOP_SWITCHED,
25522 "desktop-mode/sticky-notes",
25523 () => this.refreshDesktopVisibility()
25524 );
25525 addAction(
25526 HOOKS.DESKTOP_CLOSED,
25527 "desktop-mode/sticky-notes",
25528 (detail) => {
25529 this.migrateDesktopAssignments(detail?.desktopId, detail?.migratedTo);
25530 this.refreshDesktopVisibility();
25531 }
25532 );
25533 }
25534 save(note) {
25535 if (!this.terms) {
25536 return Promise.reject(new Error(__("Sticky term is unavailable.")));
25537 }
25538 return saveStickyNote(this.config, note, this.terms);
25539 }
25540 getHeartbeatSubscription() {
25541 if (!this.terms) {
25542 return void 0;
25543 }
25544 return {
25545 stickyTermId: this.terms.stickyTermId,
25546 knownIds: this.knownGuidelineIds(),
25547 version: this.highWaterMs
25548 };
25549 }
25550 applyHeartbeatPayload(payload) {
25551 for (const guideline of payload.notes ?? []) {
25552 const note = noteFromGuideline(guideline);
25553 this.upsertRemote(note);
25554 }
25555 for (const id of payload.removed ?? []) {
25556 this.forgetGuidelineId(id);
25557 }
25558 if (typeof payload.serverTimeMs === "number" && Number.isFinite(payload.serverTimeMs) && payload.serverTimeMs > this.highWaterMs) {
25559 this.highWaterMs = payload.serverTimeMs;
25560 }
25561 if (payload.truncated) {
25562 void this.reloadFromServer();
25563 }
25564 }
25565 openNoteArtifact(note) {
25566 if (note.guidelineId === null) {
25567 return;
25568 }
25569 this.openArtifact(
25570 buildGuidelineEditUrl(this.config.adminUrl, note.guidelineId),
25571 note.title,
25572 note.guidelineId
25573 );
25574 }
25575 notifyError(message) {
25576 this.onError?.(message);
25577 }
25578 hostSize() {
25579 return {
25580 width: Math.max(1, this.host.clientWidth),
25581 height: Math.max(1, this.host.clientHeight)
25582 };
25583 }
25584 defaultGeometry(index2) {
25585 const { width: hostWidth, height: hostHeight } = this.hostSize();
25586 const width = Math.min(
25587 DEFAULT_WIDTH,
25588 Math.max(MIN_WIDTH, hostWidth - EDGE_PADDING * 2)
25589 );
25590 const height = Math.min(
25591 DEFAULT_HEIGHT,
25592 Math.max(MIN_HEIGHT, hostHeight - EDGE_PADDING * 2)
25593 );
25594 const offset = index2 % 8 * 28;
25595 const left = clamp(
25596 hostWidth - width - 32 - offset,
25597 EDGE_PADDING,
25598 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
25599 );
25600 const top = clamp(
25601 32 + offset,
25602 EDGE_PADDING,
25603 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
25604 );
25605 return {
25606 x: left / hostWidth,
25607 y: top / hostHeight,
25608 width,
25609 height
25610 };
25611 }
25612 forget(controller) {
25613 this.controllers.delete(noteKey(controller.note));
25614 controller.dispose();
25615 controller.element.remove();
25616 if (this.controllers.size === 0) {
25617 this.root?.remove();
25618 this.root = null;
25619 }
25620 }
25621 replaceControllerKey(oldKey, controller) {
25622 const newKey = noteKey(controller.note);
25623 this.controllers.delete(oldKey);
25624 this.controllers.set(newKey, controller);
25625 moveStoredGeometry(oldKey, newKey);
25626 this.applyDesktopVisibility(controller);
25627 }
25628 bumpHighWaterFromNote(note) {
25629 const modifiedMs = noteModifiedMs(note);
25630 if (modifiedMs > this.highWaterMs) {
25631 this.highWaterMs = modifiedMs;
25632 }
25633 }
25634 bringToFront(controller) {
25635 controller.setZIndex(this.nextZIndex());
25636 }
25637 geometryForNote(note, index2) {
25638 const key = noteKey(note);
25639 const loaded = loadGeometry(key);
25640 const desktopId = this.normalizeDesktopId(loaded?.desktopId);
25641 const geometry = loaded ? { ...loaded, desktopId } : { ...this.defaultGeometry(index2), desktopId };
25642 if (!loaded || loaded.desktopId !== geometry.desktopId) {
25643 saveGeometry(key, geometry);
25644 }
25645 return geometry;
25646 }
25647 upsertRemote(note) {
25648 const key = noteKey(note);
25649 const existing = this.controllers.get(key);
25650 if (existing) {
25651 if (!existing.shouldReplaceFromRemote(note)) {
25652 this.bumpHighWaterFromNote(note);
25653 return existing;
25654 }
25655 existing.replace(note);
25656 this.bumpHighWaterFromNote(note);
25657 return existing;
25658 }
25659 const controller = this.upsert(note, this.controllers.size);
25660 this.bumpHighWaterFromNote(note);
25661 return controller;
25662 }
25663 forgetGuidelineId(guidelineId) {
25664 for (const controller of this.controllers.values()) {
25665 if (controller.note.guidelineId === guidelineId) {
25666 this.forget(controller);
25667 return;
25668 }
25669 }
25670 }
25671 knownGuidelineIds() {
25672 const ids = [];
25673 for (const controller of this.controllers.values()) {
25674 if (controller.note.guidelineId !== null) {
25675 ids.push(controller.note.guidelineId);
25676 }
25677 }
25678 return ids;
25679 }
25680 bumpHighWaterFromNotes(notes) {
25681 notes.forEach((note) => this.bumpHighWaterFromNote(note));
25682 }
25683 assignZIndex(controller) {
25684 controller.setZIndex(this.nextZIndex());
25685 }
25686 nextZIndex() {
25687 this.zIndexCounter += 1;
25688 return this.zIndexCounter;
25689 }
25690 applyDesktopVisibility(controller) {
25691 controller.setVisible(this.isNoteOnActiveDesktop(controller.note));
25692 }
25693 refreshDesktopVisibility() {
25694 for (const controller of this.controllers.values()) {
25695 this.applyDesktopVisibility(controller);
25696 }
25697 }
25698 isNoteOnActiveDesktop(note) {
25699 const key = noteKey(note);
25700 const geometry = loadGeometry(key);
25701 const desktopId = this.normalizeDesktopId(geometry?.desktopId);
25702 if (geometry && geometry.desktopId !== desktopId) {
25703 saveGeometry(key, { ...geometry, desktopId });
25704 }
25705 return desktopId === this.activeDesktopId();
25706 }
25707 migrateDesktopAssignments(desktopId, migratedTo) {
25708 if (!desktopId || !migratedTo || desktopId === migratedTo) {
25709 return;
25710 }
25711 const map = readGeometryMap();
25712 let changed = false;
25713 Object.entries(map).forEach(([key, geometry]) => {
25714 if (geometry.desktopId === desktopId) {
25715 map[key] = {
25716 ...geometry,
25717 desktopId: this.normalizeDesktopId(migratedTo)
25718 };
25719 changed = true;
25720 }
25721 });
25722 if (changed) {
25723 writeGeometryMap(map);
25724 }
25725 }
25726 activeDesktopId() {
25727 try {
25728 const id = this.getActiveDesktopId();
25729 return typeof id === "string" && id ? id : "desktop-1";
25730 } catch {
25731 return "desktop-1";
25732 }
25733 }
25734 normalizeDesktopId(desktopId) {
25735 if (!desktopId) {
25736 return this.activeDesktopId();
25737 }
25738 return desktopId;
25739 }
25740 async reloadFromServer() {
25741 if (!this.terms) {
25742 return;
25743 }
25744 try {
25745 const notes = await fetchStickyNotes(
25746 this.config,
25747 this.terms.stickyTermId
25748 );
25749 const ids = /* @__PURE__ */ new Set();
25750 sortNotesByModified(notes).forEach((note) => {
25751 if (note.guidelineId !== null) {
25752 ids.add(note.guidelineId);
25753 }
25754 this.upsertRemote(note);
25755 });
25756 this.knownGuidelineIds().forEach((id) => {
25757 if (!ids.has(id)) {
25758 this.forgetGuidelineId(id);
25759 }
25760 });
25761 } catch {
25762 }
25763 }
25764 }
25765 class StickyNoteController {
25766 constructor(options) {
25767 this.saveTimer = null;
25768 this.geometryTimer = null;
25769 this.saving = false;
25770 this.saveAgain = false;
25771 this.resizeObserver = null;
25772 this.disposed = false;
25773 this.layer = options.layer;
25774 this.note = options.note;
25775 this.index = options.index;
25776 this.element = document.createElement("article");
25777 this.element.className = "desktop-mode-sticky-note";
25778 this.element.dataset.stickyNoteId = noteKey(this.note);
25779 this.titleEl = document.createElement("span");
25780 this.editor = document.createElement("wpd-textarea");
25781 this.statusEl = document.createElement("wpd-save-status");
25782 this.openButton = document.createElement("wpd-window-button");
25783 this.paint();
25784 this.applyGeometry(this.layer.geometryForNote(this.note, this.index));
25785 this.element.addEventListener(
25786 "pointerdown",
25787 () => this.layer.bringToFront(this),
25788 { capture: true }
25789 );
25790 this.element.addEventListener("focusin", () => this.layer.bringToFront(this));
25791 this.watchResize();
25792 }
25793 focus() {
25794 window.setTimeout(() => this.editor.focusInput?.(), 0);
25795 }
25796 replace(note) {
25797 this.note = note;
25798 this.element.dataset.stickyNoteId = noteKey(this.note);
25799 this.titleEl.textContent = this.note.title;
25800 this.editor.setAttribute("value", this.note.body);
25801 this.refreshOpenButton();
25802 }
25803 shouldReplaceFromRemote(note) {
25804 if (this.hasLocalChanges()) {
25805 return false;
25806 }
25807 const currentMs = noteModifiedMs(this.note);
25808 const incomingMs = noteModifiedMs(note);
25809 if (currentMs > 0 && incomingMs > 0 && incomingMs <= currentMs && this.note.title === note.title && this.note.body === note.body) {
25810 return false;
25811 }
25812 return true;
25813 }
25814 setZIndex(zIndex) {
25815 this.element.style.zIndex = String(zIndex);
25816 }
25817 setVisible(visible) {
25818 this.element.style.display = visible ? "" : "none";
25819 }
25820 dispose() {
25821 this.disposed = true;
25822 if (this.saveTimer !== null) {
25823 window.clearTimeout(this.saveTimer);
25824 this.saveTimer = null;
25825 }
25826 if (this.geometryTimer !== null) {
25827 window.clearTimeout(this.geometryTimer);
25828 this.geometryTimer = null;
25829 }
25830 this.resizeObserver?.disconnect();
25831 this.resizeObserver = null;
25832 }
25833 paint() {
25834 this.element.innerHTML = "";
25835 this.element.style.minWidth = `${MIN_WIDTH}px`;
25836 this.element.style.minHeight = `${MIN_HEIGHT}px`;
25837 const header = document.createElement("div");
25838 header.className = "desktop-mode-sticky-note__header";
25839 const grip = document.createElement("span");
25840 grip.className = "desktop-mode-sticky-note__grip";
25841 grip.setAttribute("aria-hidden", "true");
25842 this.titleEl.className = "desktop-mode-sticky-note__title";
25843 this.titleEl.textContent = this.note.title;
25844 this.statusEl.setAttribute("mode", "icon");
25845 this.statusEl.setAttribute("phase", "idle");
25846 this.statusEl.className = "desktop-mode-sticky-note__status";
25847 this.openButton.setAttribute("icon", "detach");
25848 this.openButton.setAttribute("title", __("Open artifact"));
25849 this.openButton.className = "desktop-mode-sticky-note__open";
25850 this.openButton.addEventListener("wpd-button-activate", () => {
25851 this.layer.openNoteArtifact(this.note);
25852 });
25853 const close = document.createElement("wpd-window-button");
25854 close.setAttribute("icon", "close");
25855 close.setAttribute("danger", "");
25856 close.setAttribute("title", __("Hide sticky note"));
25857 close.className = "desktop-mode-sticky-note__close";
25858 close.addEventListener("wpd-button-activate", () => this.close());
25859 header.append(grip, this.titleEl, this.statusEl, this.openButton, close);
25860 header.addEventListener("pointerdown", (event) => this.startDrag(event));
25861 this.editor.className = "desktop-mode-sticky-note__editor";
25862 this.editor.setAttribute("aria-label", __("Sticky note text"));
25863 this.editor.setAttribute("rows", "8");
25864 this.editor.setAttribute("value", this.note.body);
25865 this.installEditorKeyboardGuard();
25866 this.editor.addEventListener("wpd-input-change", (event) => {
25867 const detail = event.detail;
25868 this.note.body = detail.value;
25869 this.note.title = titleForBody(detail.value);
25870 this.titleEl.textContent = this.note.title;
25871 this.setPhase("pending");
25872 this.scheduleSave();
25873 });
25874 this.editor.addEventListener("wpd-input-commit", () => this.flushSave());
25875 this.element.append(header, this.editor);
25876 this.refreshOpenButton();
25877 }
25878 installEditorKeyboardGuard() {
25879 ["keydown", "keypress", "keyup"].forEach((eventName) => {
25880 this.editor.addEventListener(eventName, (event) => {
25881 event.stopPropagation();
25882 });
25883 });
25884 }
25885 refreshOpenButton() {
25886 const disabled = this.note.guidelineId === null;
25887 this.openButton.classList.toggle("is-disabled", disabled);
25888 this.openButton.setAttribute("aria-disabled", disabled ? "true" : "false");
25889 }
25890 close() {
25891 if (this.note.guidelineId === null && this.note.body.trim().length === 0) {
25892 this.layer.forget(this);
25893 return;
25894 }
25895 this.flushSave();
25896 this.layer.forget(this);
25897 }
25898 scheduleSave() {
25899 if (this.note.guidelineId === null && this.note.body.trim().length === 0) {
25900 this.setPhase("idle");
25901 return;
25902 }
25903 if (this.saveTimer !== null) {
25904 window.clearTimeout(this.saveTimer);
25905 }
25906 this.saveTimer = window.setTimeout(() => {
25907 this.saveTimer = null;
25908 void this.save();
25909 }, SAVE_DEBOUNCE_MS);
25910 }
25911 flushSave() {
25912 if (this.saveTimer !== null) {
25913 window.clearTimeout(this.saveTimer);
25914 this.saveTimer = null;
25915 }
25916 if (this.note.guidelineId !== null || this.note.body.trim().length > 0) {
25917 void this.save();
25918 }
25919 }
25920 async save() {
25921 if (this.saving) {
25922 this.saveAgain = true;
25923 this.setPhase("pending");
25924 return;
25925 }
25926 this.saving = true;
25927 this.setPhase("saving");
25928 const bodyAtSave = this.note.body;
25929 try {
25930 const saved = await this.layer.save({
25931 ...this.note,
25932 body: bodyAtSave
25933 });
25934 if (this.disposed) {
25935 return;
25936 }
25937 const oldKey = noteKey(this.note);
25938 this.note.guidelineId = saved.guidelineId;
25939 this.note.modified = saved.modified;
25940 this.note.link = saved.link;
25941 this.note.termIds = saved.termIds.length > 0 ? saved.termIds : this.note.termIds;
25942 if (this.note.body === bodyAtSave) {
25943 this.note.title = saved.title;
25944 this.titleEl.textContent = saved.title;
25945 }
25946 if (oldKey !== noteKey(this.note)) {
25947 this.element.dataset.stickyNoteId = noteKey(this.note);
25948 this.layer.replaceControllerKey(oldKey, this);
25949 }
25950 this.layer.bumpHighWaterFromNote(this.note);
25951 this.refreshOpenButton();
25952 this.setPhase("saved");
25953 } catch (error) {
25954 if (this.disposed) {
25955 return;
25956 }
25957 const message = error instanceof Error ? error.message : __("Could not save sticky note.");
25958 this.setPhase("failed", message);
25959 this.layer.notifyError(message);
25960 } finally {
25961 this.saving = false;
25962 if (!this.disposed && this.saveAgain) {
25963 this.saveAgain = false;
25964 this.scheduleSave();
25965 }
25966 }
25967 }
25968 setPhase(phase, error) {
25969 this.statusEl.setAttribute("phase", phase);
25970 if (error) {
25971 this.statusEl.setAttribute("error", error);
25972 this.statusEl.setAttribute("title", error);
25973 } else {
25974 this.statusEl.removeAttribute("error");
25975 this.statusEl.removeAttribute("title");
25976 }
25977 }
25978 hasLocalChanges() {
25979 const phase = this.statusEl.getAttribute("phase");
25980 return this.saveTimer !== null || this.saving || this.saveAgain || phase === "pending" || phase === "failed";
25981 }
25982 startDrag(event) {
25983 if (event.button !== 0) {
25984 return;
25985 }
25986 const target2 = event.target;
25987 if (target2?.closest("wpd-window-button, wpd-save-status")) {
25988 return;
25989 }
25990 event.preventDefault();
25991 const startRect = this.element.getBoundingClientRect();
25992 const hostRect = this.layerHostRect();
25993 const startLeft = startRect.left - hostRect.left;
25994 const startTop = startRect.top - hostRect.top;
25995 const startX = event.clientX;
25996 const startY = event.clientY;
25997 this.element.classList.add("desktop-mode-sticky-note--dragging");
25998 this.element.setPointerCapture?.(event.pointerId);
25999 const move = (moveEvent) => {
26000 const width = this.element.offsetWidth;
26001 const height = this.element.offsetHeight;
26002 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26003 const left = clamp(
26004 startLeft + moveEvent.clientX - startX,
26005 EDGE_PADDING,
26006 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
26007 );
26008 const top = clamp(
26009 startTop + moveEvent.clientY - startY,
26010 EDGE_PADDING,
26011 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
26012 );
26013 this.element.style.left = `${left}px`;
26014 this.element.style.top = `${top}px`;
26015 };
26016 const up = (upEvent) => {
26017 this.element.classList.remove("desktop-mode-sticky-note--dragging");
26018 this.element.releasePointerCapture?.(upEvent.pointerId);
26019 document.removeEventListener("pointermove", move);
26020 document.removeEventListener("pointerup", up);
26021 this.persistGeometry();
26022 };
26023 document.addEventListener("pointermove", move);
26024 document.addEventListener("pointerup", up);
26025 }
26026 applyGeometry(geometry) {
26027 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26028 const width = clamp(geometry.width, MIN_WIDTH, hostWidth - EDGE_PADDING * 2);
26029 const height = clamp(geometry.height, MIN_HEIGHT, hostHeight - EDGE_PADDING * 2);
26030 const left = clamp(
26031 geometry.x * hostWidth,
26032 EDGE_PADDING,
26033 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
26034 );
26035 const top = clamp(
26036 geometry.y * hostHeight,
26037 EDGE_PADDING,
26038 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
26039 );
26040 this.element.style.left = `${left}px`;
26041 this.element.style.top = `${top}px`;
26042 this.element.style.width = `${width}px`;
26043 this.element.style.height = `${height}px`;
26044 }
26045 watchResize() {
26046 if (typeof ResizeObserver === "undefined") {
26047 return;
26048 }
26049 this.resizeObserver = new ResizeObserver(() => {
26050 if (this.geometryTimer !== null) {
26051 window.clearTimeout(this.geometryTimer);
26052 }
26053 this.geometryTimer = window.setTimeout(() => {
26054 this.geometryTimer = null;
26055 this.persistGeometry();
26056 }, 150);
26057 });
26058 this.resizeObserver.observe(this.element);
26059 }
26060 persistGeometry() {
26061 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26062 const left = parseFloat(this.element.style.left) || 0;
26063 const top = parseFloat(this.element.style.top) || 0;
26064 const existing = loadGeometry(noteKey(this.note));
26065 saveGeometry(noteKey(this.note), {
26066 ...existing ?? {},
26067 x: clamp(left / hostWidth, 0, 1),
26068 y: clamp(top / hostHeight, 0, 1),
26069 width: this.element.offsetWidth,
26070 height: this.element.offsetHeight
26071 });
26072 }
26073 layerHostRect() {
26074 const parent = this.element.parentElement?.parentElement;
26075 return (parent ?? document.body).getBoundingClientRect();
26076 }
26077 }
26078 function bootStickyNotes(options) {
26079 const layer = new StickyNotesLayer(options);
26080 void layer.boot();
26081 return layer;
26082 }
26083 function noteKey(note) {
26084 return note.guidelineId === null ? note.localId : `guideline:${note.guidelineId}`;
26085 }
26086 function noteModifiedMs(note) {
26087 if (typeof note.modifiedMs === "number" && Number.isFinite(note.modifiedMs)) {
26088 return note.modifiedMs;
26089 }
26090 if (!note.modified) {
26091 return 0;
26092 }
26093 const parsed = Date.parse(note.modified);
26094 return Number.isFinite(parsed) ? parsed : 0;
26095 }
26096 function sortNotesByModified(notes) {
26097 return [...notes].sort((a, b) => noteModifiedMs(a) - noteModifiedMs(b));
26098 }
26099 function loadGeometry(key) {
26100 const map = readGeometryMap();
26101 const value = map[key];
26102 if (!value || !Number.isFinite(value.x) || !Number.isFinite(value.y) || !Number.isFinite(value.width) || !Number.isFinite(value.height)) {
26103 return null;
26104 }
26105 return value;
26106 }
26107 function saveGeometry(key, geometry) {
26108 const map = readGeometryMap();
26109 map[key] = geometry;
26110 writeGeometryMap(map);
26111 }
26112 function moveStoredGeometry(oldKey, newKey) {
26113 if (oldKey === newKey) {
26114 return;
26115 }
26116 const map = readGeometryMap();
26117 if (map[oldKey]) {
26118 map[newKey] = map[oldKey];
26119 delete map[oldKey];
26120 writeGeometryMap(map);
26121 }
26122 }
26123 function readGeometryMap() {
26124 try {
26125 const raw = window.localStorage.getItem(GEOMETRY_KEY);
26126 return raw ? JSON.parse(raw) : {};
26127 } catch {
26128 return {};
26129 }
26130 }
26131 function writeGeometryMap(map) {
26132 try {
26133 window.localStorage.setItem(GEOMETRY_KEY, JSON.stringify(map));
26134 } catch {
26135 }
26136 }
26137 function clamp(value, min, max) {
26138 if (max < min) {
26139 return min;
26140 }
26141 return Math.min(max, Math.max(min, value));
26142 }
26143 const clock = {
26144 id: "clock",
26145 // Labels/descriptions on built-in defs stay string-literal at
26146 // module-eval time so the extract-pot pass picks them up. The
26147 // values are wrapped in `__()` so they translate at runtime.
26148 get label() {
26149 return __("Clock");
26150 },
26151 get description() {
26152 return __("Local time and date, refreshed every second.");
26153 },
26154 icon: "dashicons-clock",
26155 mount: (container) => {
26156 container.classList.add("desktop-mode-widget-clock");
26157 const time = document.createElement("div");
26158 time.className = "desktop-mode-widget-clock__time";
26159 container.appendChild(time);
26160 const date = document.createElement("div");
26161 date.className = "desktop-mode-widget-clock__date";
26162 container.appendChild(date);
26163 const render2 = () => {
26164 const now = /* @__PURE__ */ new Date();
26165 time.textContent = now.toLocaleTimeString(void 0, {
26166 hour: "2-digit",
26167 minute: "2-digit"
26168 });
26169 date.textContent = now.toLocaleDateString(void 0, {
26170 weekday: "long",
26171 month: "short",
26172 day: "numeric"
26173 });
26174 };
26175 render2();
26176 const msUntilNextSecond = 1e3 - Date.now() % 1e3;
26177 let interval = null;
26178 const kickoff = window.setTimeout(() => {
26179 render2();
26180 interval = window.setInterval(render2, 1e3);
26181 }, msUntilNextSecond);
26182 return () => {
26183 window.clearTimeout(kickoff);
26184 if (interval !== null) {
26185 window.clearInterval(interval);
26186 }
26187 };
26188 }
26189 };
26190 function registerBuiltInWidgets() {
26191 register(clock);
26192 }
26193 function createWidgetRegistrySync(deps2) {
26194 const { layer } = deps2;
26195 const registered = /* @__PURE__ */ new Set();
26196 const loadedScripts = /* @__PURE__ */ new Set();
26197 const ensureScript = async (entry) => {
26198 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
26199 return;
26200 }
26201 try {
26202 await loadVendorScript(entry.scriptUrl, {
26203 translations: entry.scriptTranslations,
26204 l10n: entry.scriptL10n,
26205 before: entry.scriptBefore,
26206 after: entry.scriptAfter
26207 });
26208 } catch (err) {
26209 doAction(HOOKS.SHELL_ERROR, {
26210 scope: "widget-script-load",
26211 id: entry.id,
26212 error: err
26213 });
26214 }
26215 loadedScripts.add(entry.scriptUrl);
26216 };
26217 const buildDefFromEntry = (entry) => {
26218 const globals = window.desktopModeWidgets || {};
26219 const mount = globals[entry.id];
26220 if (!mount) {
26221 doAction(HOOKS.SHELL_ERROR, {
26222 scope: "widget-missing-mount",
26223 id: entry.id,
26224 error: new Error(
26225 `[desktop-mode] No mount callback on window.desktopModeWidgets["${entry.id}"]. Plugin script loaded but didn't register. Check the plugin's enqueue + global assignment.`
26226 )
26227 });
26228 return null;
26229 }
26230 return {
26231 id: entry.id,
26232 label: entry.label,
26233 description: entry.description,
26234 icon: entry.icon,
26235 movable: entry.movable,
26236 resizable: entry.resizable,
26237 minWidth: entry.minWidth || void 0,
26238 minHeight: entry.minHeight || void 0,
26239 maxWidth: entry.maxWidth || void 0,
26240 maxHeight: entry.maxHeight || void 0,
26241 defaultWidth: entry.defaultWidth || void 0,
26242 defaultHeight: entry.defaultHeight || void 0,
26243 mount
26244 };
26245 };
26246 const registerEntry = async (entry) => {
26247 if (registered.has(entry.id)) {
26248 return;
26249 }
26250 await ensureScript(entry);
26251 const def = buildDefFromEntry(entry);
26252 if (!def) {
26253 return;
26254 }
26255 try {
26256 register(def);
26257 } catch (err) {
26258 doAction(HOOKS.SHELL_ERROR, {
26259 scope: "widget-register",
26260 id: entry.id,
26261 error: err
26262 });
26263 return;
26264 }
26265 registered.add(entry.id);
26266 refreshWidgetPicker();
26267 if (layer) {
26268 layer.mountIfEnabled(entry.id);
26269 }
26270 };
26271 const unregisterEntry = (id) => {
26272 if (!registered.has(id)) {
26273 return;
26274 }
26275 layer?.unmount(id);
26276 unregister(id);
26277 registered.delete(id);
26278 refreshWidgetPicker();
26279 };
26280 return async (list2) => {
26281 const incoming = /* @__PURE__ */ new Set();
26282 for (const entry of list2) {
26283 incoming.add(entry.id);
26284 }
26285 for (const id of Array.from(registered)) {
26286 if (!incoming.has(id)) {
26287 unregisterEntry(id);
26288 }
26289 }
26290 for (const entry of list2) {
26291 if (!registered.has(entry.id)) {
26292 await registerEntry(entry);
26293 }
26294 }
26295 };
26296 }
26297 const WPD_COMPONENT_TAGS = [
26298 "wpd-section",
26299 "wpd-button",
26300 "wpd-swatch",
26301 "wpd-swatch-grid",
26302 "wpd-segmented",
26303 "wpd-segment",
26304 "wpd-select",
26305 "wpd-option",
26306 "wpd-multiselect",
26307 "wpd-color-field",
26308 "wpd-range-field",
26309 "wpd-text-field",
26310 "wpd-number-field",
26311 "wpd-checkbox",
26312 "wpd-checkbox-label",
26313 "wpd-toast",
26314 "wpd-toast-container",
26315 "wpd-tabs",
26316 "wpd-tab",
26317 "wpd-tabpanel",
26318 "wpd-window-button",
26319 "wpd-menu",
26320 "wpd-menu-item",
26321 "wpd-context-menu",
26322 "wpd-context-menu-option",
26323 "wpd-confirm-dialog",
26324 "wpd-modal",
26325 "wpd-user-search",
26326 "wpd-role-picker",
26327 "wpd-flyout",
26328 "wpd-tab-chip",
26329 "wpd-stack",
26330 "wpd-cluster",
26331 "wpd-icon",
26332 "wpd-body",
26333 "wpd-panel",
26334 "wpd-row",
26335 "wpd-grid",
26336 "wpd-display",
26337 "wpd-empty-state",
26338 "wpd-key",
26339 "wpd-code",
26340 "wpd-badge",
26341 "wpd-log",
26342 "wpd-steps",
26343 "wpd-step",
26344 "wpd-table",
26345 "wpd-spinner",
26346 "wpd-relative-time",
26347 "wpd-avatar",
26348 "wpd-textarea",
26349 "wpd-chip",
26350 "wpd-tag-input",
26351 "wpd-form",
26352 "wpd-save-status",
26353 "wpd-category-picker",
26354 "wpd-crumb-chain",
26355 "wpd-card",
26356 "wpd-notice"
26357 ];
26358 const KNOWN = new Set(WPD_COMPONENT_TAGS);
26359 const WARN_GRACE_MS = 2e3;
26360 const warnedTags = /* @__PURE__ */ new Set();
26361 const observedRoots = /* @__PURE__ */ new WeakSet();
26362 let started$2 = false;
26363 function distance(a, b) {
26364 const m = a.length;
26365 const n = b.length;
26366 if (m === 0) {
26367 return n;
26368 }
26369 if (n === 0) {
26370 return m;
26371 }
26372 const dp = new Array(n + 1);
26373 for (let j = 0; j <= n; j++) {
26374 dp[j] = j;
26375 }
26376 for (let i = 1; i <= m; i++) {
26377 let prev = dp[0];
26378 dp[0] = i;
26379 for (let j = 1; j <= n; j++) {
26380 const tmp = dp[j];
26381 dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
26382 prev = tmp;
26383 }
26384 }
26385 return dp[n];
26386 }
26387 function suggest(tag) {
26388 let best = null;
26389 let bestD = Infinity;
26390 for (const known of KNOWN) {
26391 const d = distance(tag, known);
26392 if (d < bestD) {
26393 bestD = d;
26394 best = known;
26395 }
26396 }
26397 return bestD > 0 && bestD <= 3 ? best : null;
26398 }
26399 function folderFor(tag) {
26400 return tag.startsWith("wpd-") ? tag.slice(4) : tag;
26401 }
26402 function warnFor(tag, sample) {
26403 if (warnedTags.has(tag)) {
26404 return;
26405 }
26406 warnedTags.add(tag);
26407 const isKnown = KNOWN.has(tag);
26408 if (isKnown) {
26409 const folder = folderFor(tag);
26410 console.error(
26411 `[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.
26412
26413 Fix — side-effect-import the component module from wherever you render it:
26414
26415 import '<rel>/ui/components/${folder}/${folder}';
26416
26417 Or pull every wpd-* component in one go (heavier — only do this from an entry bundle):
26418
26419 import '<rel>/ui/components';
26420
26421 See docs/components-reference.md for the full list.`,
26422 "\nFirst offending element:",
26423 sample
26424 );
26425 return;
26426 }
26427 const guess = suggest(tag);
26428 if (guess) {
26429 console.error(
26430 `[wp.desktop] <${tag}> is not a registered wpd-* component. Did you mean <${guess}>?
26431
26432 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'.`,
26433 "\nFirst offending element:",
26434 sample
26435 );
26436 return;
26437 }
26438 console.error(
26439 `[wp.desktop] <${tag}> looks like a wpd-* tag but no component by that name exists.
26440
26441 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.`,
26442 "\nFirst offending element:",
26443 sample
26444 );
26445 }
26446 function checkElement(el) {
26447 const tag = el.tagName.toLowerCase();
26448 if (!tag.startsWith("wpd-")) {
26449 return;
26450 }
26451 if (warnedTags.has(tag)) {
26452 return;
26453 }
26454 if (customElements.get(tag)) {
26455 return;
26456 }
26457 let settled = false;
26458 customElements.whenDefined(tag).then(() => {
26459 settled = true;
26460 });
26461 setTimeout(() => {
26462 if (settled) {
26463 return;
26464 }
26465 if (customElements.get(tag)) {
26466 return;
26467 }
26468 warnFor(tag, el);
26469 }, WARN_GRACE_MS);
26470 }
26471 function walk(root) {
26472 if (root instanceof Element) {
26473 checkElement(root);
26474 if (root.shadowRoot) {
26475 observeRoot(root.shadowRoot);
26476 }
26477 }
26478 const all2 = root.querySelectorAll("*");
26479 for (let i = 0; i < all2.length; i++) {
26480 const el = all2[i];
26481 checkElement(el);
26482 if (el.shadowRoot) {
26483 observeRoot(el.shadowRoot);
26484 }
26485 }
26486 }
26487 function observeRoot(root) {
26488 if (observedRoots.has(root)) {
26489 return;
26490 }
26491 observedRoots.add(root);
26492 walk(root);
26493 const mo = new MutationObserver((records) => {
26494 for (let i = 0; i < records.length; i++) {
26495 const added = records[i].addedNodes;
26496 for (let j = 0; j < added.length; j++) {
26497 const node = added[j];
26498 if (node.nodeType === 1) {
26499 walk(node);
26500 }
26501 }
26502 }
26503 });
26504 mo.observe(root, { childList: true, subtree: true });
26505 }
26506 function patchAttachShadow() {
26507 const proto = Element.prototype;
26508 const original = proto.attachShadow;
26509 if (original.__wpdPatched) {
26510 return;
26511 }
26512 const patched = function(init2) {
26513 const root = original.call(this, init2);
26514 if (root.mode === "open") {
26515 observeRoot(root);
26516 }
26517 return root;
26518 };
26519 patched.__wpdPatched = true;
26520 proto.attachShadow = patched;
26521 }
26522 function startMissingImportWarner() {
26523 if (started$2) {
26524 return;
26525 }
26526 if (typeof document === "undefined") {
26527 return;
26528 }
26529 started$2 = true;
26530 patchAttachShadow();
26531 observeRoot(document);
26532 }
26533 const TRASH_DROP_ACTIVE_ATTR = "data-desktop-mode-trash-drop-active";
26534 const RECYCLE_BIN_WINDOW_ID = "desktop-mode-recycle-bin";
26535 const BIN_TILE_SELECTORS = [
26536 `.desktop-mode-file-tile[data-file-ref="${RECYCLE_BIN_WINDOW_ID}"]`,
26537 `[data-icon-id="${RECYCLE_BIN_WINDOW_ID}"]`,
26538 `[data-system-id="${RECYCLE_BIN_WINDOW_ID}"]`
26539 ];
26540 function findBinTile() {
26541 for (const sel of BIN_TILE_SELECTORS) {
26542 const el = document.querySelector(sel);
26543 if (el instanceof HTMLElement) {
26544 return el;
26545 }
26546 }
26547 return null;
26548 }
26549 let _installed = false;
26550 let _dockDeregister = null;
26551 let _windowDeregister = null;
26552 let _binMutationObserver = null;
26553 function isDesktopFilePayload(session) {
26554 return session.payload.type === "desktop-file";
26555 }
26556 function registerOn(dragManager, id, el) {
26557 return dragManager.registerDropTarget({
26558 id,
26559 element: el,
26560 // Reject the drop UP FRONT when the viewer can't trash the
26561 // payload's placement (e.g. an item inside a read-only
26562 // shared folder, or someone else's tile in a shared
26563 // namespace). `accept` flipping to `false` means the
26564 // drop-active highlight never lights up + onDrop never
26565 // fires + the drag manager surfaces a `rejected` outcome.
26566 // The user sees the icon snap back instead of attempting a
26567 // REST call that would 403 and only log to the console.
26568 accept: (payload) => {
26569 if (payload.type !== "desktop-file") {
26570 return false;
26571 }
26572 const data = payload.data;
26573 const placement = data?.placement;
26574 if (!placement) {
26575 return false;
26576 }
26577 if (placement.file?.ref === RECYCLE_BIN_WINDOW_ID) {
26578 return false;
26579 }
26580 return placement.canTrash !== false;
26581 },
26582 onEnter: () => {
26583 el.setAttribute(TRASH_DROP_ACTIVE_ATTR, "");
26584 },
26585 onLeave: () => {
26586 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
26587 },
26588 onDrop: (session) => {
26589 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
26590 if (!isDesktopFilePayload(session)) {
26591 return;
26592 }
26593 const placement = session.payload.data.placement;
26594 void trashByFileType(placement);
26595 }
26596 });
26597 }
26598 function installRecycleBinDropTargets(dragManager) {
26599 if (_installed) {
26600 return;
26601 }
26602 _installed = true;
26603 const reprobeTile = () => {
26604 const el = findBinTile();
26605 if (!el) {
26606 _dockDeregister?.();
26607 _dockDeregister = null;
26608 return;
26609 }
26610 if (_dockDeregister && getRegisteredElementId(dragManager) === el) {
26611 return;
26612 }
26613 _dockDeregister?.();
26614 _dockDeregister = registerOn(dragManager, "recycle-bin-dock", el);
26615 };
26616 reprobeTile();
26617 document.addEventListener("desktop-mode-files-changed", reprobeTile);
26618 document.addEventListener("desktop-mode-desktop-icons-rendered", reprobeTile);
26619 addAction(
26620 HOOKS.DOCK_AFTER_RENDER,
26621 "desktop-mode/files/recycle-bin-dock-target",
26622 reprobeTile
26623 );
26624 if (typeof MutationObserver !== "undefined") {
26625 _binMutationObserver = new MutationObserver(() => {
26626 reprobeTile();
26627 });
26628 const desktopArea = document.getElementById("desktop-mode-area") ?? document.body;
26629 _binMutationObserver.observe(desktopArea, {
26630 childList: true,
26631 subtree: true
26632 });
26633 }
26634 addAction(
26635 HOOKS.WINDOW_OPENED,
26636 "desktop-mode/files/recycle-bin-window-target",
26637 (detail) => {
26638 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
26639 return;
26640 }
26641 _windowDeregister?.();
26642 _windowDeregister = null;
26643 const el = document.querySelector(
26644 "[data-desktop-mode-recycle-bin-root]"
26645 );
26646 if (el instanceof HTMLElement) {
26647 _windowDeregister = registerOn(
26648 dragManager,
26649 "recycle-bin-window",
26650 el
26651 );
26652 }
26653 }
26654 );
26655 addAction(
26656 HOOKS.WINDOW_CLOSED,
26657 "desktop-mode/files/recycle-bin-window-cleanup",
26658 (detail) => {
26659 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
26660 return;
26661 }
26662 _windowDeregister?.();
26663 _windowDeregister = null;
26664 }
26665 );
26666 }
26667 function getRegisteredElementId(dragManager) {
26668 const t = dragManager.debug().listTargets().find((target2) => target2.id === "recycle-bin-dock");
26669 return t ? t.element : null;
26670 }
26671 let started$1 = false;
26672 let highWaterMs = 0;
26673 function startFilesHeartbeat() {
26674 if (started$1) {
26675 return;
26676 }
26677 started$1 = true;
26678 heartbeat.contribute("desktop_mode_files_subscribe", () => {
26679 const state2 = getFilesState();
26680 const folderVersions = {};
26681 for (const [id, folder] of state2.folders) {
26682 folderVersions[String(id)] = folder.updatedAtMs;
26683 }
26684 return {
26685 folderVersions,
26686 placementsVersion: highWaterMs,
26687 sharesVersion: sharesStore().state.sharesVersion
26688 };
26689 });
26690 heartbeat.subscribe("desktop_mode_files", (payload) => {
26691 applyDelta(payload);
26692 });
26693 }
26694 function applyDelta(payload) {
26695 const folders = payload.folders ?? [];
26696 for (const folder of folders) {
26697 upsertFolder(folder, "remote");
26698 if (folder.updatedAtMs > highWaterMs) {
26699 highWaterMs = folder.updatedAtMs;
26700 }
26701 }
26702 const placements = payload.placements ?? [];
26703 for (const placement of placements) {
26704 upsertPlacement(placement, "remote");
26705 if (placement.updatedAtMs > highWaterMs) {
26706 highWaterMs = placement.updatedAtMs;
26707 }
26708 }
26709 const removed = payload.removed ?? {};
26710 for (const id of removed.folders ?? []) {
26711 removeFolder(id, "remote");
26712 }
26713 for (const id of removed.placements ?? []) {
26714 removePlacement(id, "remote");
26715 }
26716 if (typeof payload.serverTimeMs === "number" && payload.serverTimeMs > highWaterMs) {
26717 highWaterMs = payload.serverTimeMs;
26718 }
26719 const pending2 = payload.shares?.pending;
26720 if (Array.isArray(pending2) && pending2.length > 0) {
26721 ingestPendingInvites(pending2);
26722 }
26723 if (payload.truncated) {
26724 const hydrated = Array.from(getFilesState().hydratedFolders);
26725 for (const folderId of hydrated) {
26726 void listPlacements(folderId).then((res) => {
26727 setFolderPlacements(folderId, res.placements);
26728 }).catch(() => {
26729 });
26730 }
26731 }
26732 }
26733 let started = false;
26734 const unsubscribers = [];
26735 function startFilesRestoreSync() {
26736 if (started) {
26737 return;
26738 }
26739 started = true;
26740 const onChange = (payload) => {
26741 const detail = payload;
26742 if (!detail || detail.action !== "untrashed") {
26743 return;
26744 }
26745 resyncFromServer();
26746 };
26747 unsubscribers.push(
26748 subscribe$2("desktop-mode.placement.changed", onChange),
26749 subscribe$2("desktop-mode.shortcut.changed", onChange),
26750 subscribe$2("desktop-mode.folder.changed", onChange)
26751 );
26752 }
26753 function resyncFromServer() {
26754 void listFolders().then((res) => {
26755 setFolders(res.folders);
26756 }).catch((err) => {
26757 console.error(
26758 "[desktop-mode] files restore-sync: listFolders failed",
26759 err
26760 );
26761 });
26762 const hydrated = Array.from(getFilesState().hydratedFolders);
26763 for (const folderId of hydrated) {
26764 void listPlacements(folderId).then((res) => {
26765 setFolderPlacements(folderId, res.placements);
26766 }).catch((err) => {
26767 console.error(
26768 "[desktop-mode] files restore-sync: listPlacements failed for",
26769 folderId,
26770 err
26771 );
26772 });
26773 }
26774 }
26775 const MENU_CLASS = "desktop-mode-wallpaper-menu";
26776 let activeMenu = null;
26777 function isWallpaperMenuOpen() {
26778 return activeMenu !== null;
26779 }
26780 let openGeneration = 0;
26781 function openWallpaperMenu(host, pos, items, options = {}) {
26782 closeWallpaperMenu();
26783 const myGen = ++openGeneration;
26784 openWithShellOverlays(
26785 () => myGen === openGeneration,
26786 () => openWallpaperMenuImmediate(host, pos, items, options)
26787 );
26788 }
26789 function openWallpaperMenuImmediate(host, pos, items, options = {}) {
26790 if (items.length === 0) {
26791 return;
26792 }
26793 items = items.slice().sort((a, b) => {
26794 const sa = typeof a.sort === "number" ? a.sort : 100;
26795 const sb = typeof b.sort === "number" ? b.sort : 100;
26796 if (sa !== sb) {
26797 return sa - sb;
26798 }
26799 return a.label.localeCompare(b.label);
26800 });
26801 const menu = document.createElement("wpd-context-menu");
26802 menu.setAttribute("open", "");
26803 menu.classList.add(MENU_CLASS);
26804 menu.style.left = `${pos.x}px`;
26805 menu.style.top = `${pos.y}px`;
26806 const itemById = /* @__PURE__ */ new Map();
26807 let activeFlyout2 = null;
26808 let activeFlyoutParent = null;
26809 const closeActiveFlyout = () => {
26810 if (activeFlyout2) {
26811 activeFlyout2.remove();
26812 activeFlyout2 = null;
26813 activeFlyoutParent = null;
26814 }
26815 };
26816 for (const item of items) {
26817 itemById.set(item.id, item);
26818 const opt = document.createElement("wpd-context-menu-option");
26819 opt.dataset.menuItemId = item.id;
26820 opt.setAttribute("value", item.id);
26821 if (item.heading) {
26822 opt.setAttribute("heading", "");
26823 }
26824 if (item.disabled) {
26825 opt.setAttribute("disabled", "");
26826 }
26827 if (item.icon) {
26828 opt.setAttribute("icon", sanitizeClass(item.icon));
26829 }
26830 const hasChildren2 = Array.isArray(item.children) && item.children.length > 0;
26831 if (hasChildren2) {
26832 opt.setAttribute("has-children", "");
26833 }
26834 opt.textContent = item.label;
26835 opt.addEventListener("mouseenter", () => {
26836 if (hasChildren2) {
26837 openFlyout2(item, opt);
26838 return;
26839 }
26840 closeActiveFlyout();
26841 });
26842 menu.appendChild(opt);
26843 }
26844 menu.addEventListener("wpd-context-menu-pick", (e) => {
26845 const detail = e.detail;
26846 const item = itemById.get(detail.id) ?? null;
26847 if (!item) {
26848 return;
26849 }
26850 if (Array.isArray(item.children) && item.children.length > 0) {
26851 e.stopPropagation();
26852 if (activeFlyoutParent && activeFlyoutParent.id === item.id) {
26853 closeActiveFlyout();
26854 return;
26855 }
26856 const anchor = menu.querySelector(
26857 `[data-menu-item-id="${item.id}"]`
26858 );
26859 if (anchor) {
26860 openFlyout2(item, anchor);
26861 }
26862 return;
26863 }
26864 closeWallpaperMenu();
26865 void item.onClick(new MouseEvent("click"));
26866 });
26867 function openFlyout2(parent, anchor) {
26868 closeActiveFlyout();
26869 const fly = document.createElement("wpd-context-menu");
26870 fly.setAttribute("open", "");
26871 fly.classList.add(MENU_CLASS, `${MENU_CLASS}--flyout`);
26872 fly.dataset.parentId = parent.id;
26873 const sortedKids = (parent.children ?? []).slice().sort((a, b) => {
26874 const sa = typeof a.sort === "number" ? a.sort : 100;
26875 const sb = typeof b.sort === "number" ? b.sort : 100;
26876 if (sa !== sb) {
26877 return sa - sb;
26878 }
26879 return a.label.localeCompare(b.label);
26880 });
26881 for (const child of sortedKids) {
26882 const kopt = document.createElement("wpd-context-menu-option");
26883 kopt.dataset.menuItemId = child.id;
26884 kopt.setAttribute("value", child.id);
26885 if (child.icon) {
26886 kopt.setAttribute("icon", sanitizeClass(child.icon));
26887 }
26888 if (child.disabled) {
26889 kopt.setAttribute("disabled", "");
26890 }
26891 if (child.checked) {
26892 kopt.setAttribute("checked", "");
26893 }
26894 kopt.textContent = child.label;
26895 kopt.addEventListener("wpd-context-menu-pick", (e) => {
26896 e.stopPropagation();
26897 closeWallpaperMenu();
26898 void child.onClick(new MouseEvent("click"));
26899 });
26900 fly.appendChild(kopt);
26901 }
26902 document.body.appendChild(fly);
26903 activeFlyout2 = fly;
26904 activeFlyoutParent = parent;
26905 positionFlyout2(fly, anchor);
26906 }
26907 function positionFlyout2(fly, anchor) {
26908 const ar = anchor.getBoundingClientRect();
26909 fly.style.position = "fixed";
26910 fly.style.left = `${ar.right}px`;
26911 fly.style.top = `${ar.top}px`;
26912 const fr = fly.getBoundingClientRect();
26913 if (fr.right > window.innerWidth) {
26914 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
26915 }
26916 if (fr.bottom > window.innerHeight) {
26917 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
26918 }
26919 }
26920 host.appendChild(menu);
26921 activeMenu = menu;
26922 const rect = menu.getBoundingClientRect();
26923 if (rect.right > window.innerWidth) {
26924 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
26925 }
26926 if (rect.bottom > window.innerHeight) {
26927 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
26928 }
26929 const detach = attachDismissable(menu, {
26930 close: () => closeWallpaperMenu(),
26931 siblingSelectors: [`.${MENU_CLASS}--flyout`],
26932 excludeOutsideTarget: options.excludeOutsideTarget
26933 });
26934 menu.addEventListener("wallpaper-menu-closed", detach);
26935 doAction("desktop-mode.wallpaper-menu.opened", { items: items.map((i) => i.id) });
26936 }
26937 function closeWallpaperMenu() {
26938 if (!activeMenu) {
26939 return;
26940 }
26941 document.querySelectorAll(`.${MENU_CLASS}--flyout`).forEach((el) => el.remove());
26942 activeMenu.dispatchEvent(new CustomEvent("wallpaper-menu-closed"));
26943 activeMenu.remove();
26944 activeMenu = null;
26945 doAction("desktop-mode.wallpaper-menu.closed", {});
26946 }
26947 function buildMenuItems(deps2) {
26948 const builtIn = [
26949 {
26950 id: "create-folder",
26951 label: deps2.labels.createFolder,
26952 icon: "dashicons-portfolio",
26953 sort: 10,
26954 onClick: () => deps2.createFolder()
26955 },
26956 {
26957 id: "new-url",
26958 label: deps2.labels.newUrl,
26959 icon: "dashicons-admin-links",
26960 sort: 12,
26961 onClick: () => deps2.createUrl()
26962 },
26963 {
26964 id: "sort-by",
26965 label: deps2.labels.sortHeading,
26966 icon: "dashicons-sort",
26967 sort: 16,
26968 onClick: () => void 0,
26969 children: [
26970 {
26971 id: "sort-name-asc",
26972 label: deps2.labels.sortNameAsc,
26973 sort: 10,
26974 checked: deps2.currentSortMode === "name-asc",
26975 onClick: () => deps2.sortIcons("name-asc")
26976 },
26977 {
26978 id: "sort-name-desc",
26979 label: deps2.labels.sortNameDesc,
26980 sort: 20,
26981 checked: deps2.currentSortMode === "name-desc",
26982 onClick: () => deps2.sortIcons("name-desc")
26983 },
26984 {
26985 id: "sort-date-desc",
26986 label: deps2.labels.sortDateDesc,
26987 sort: 30,
26988 checked: deps2.currentSortMode === "date-desc",
26989 onClick: () => deps2.sortIcons("date-desc")
26990 },
26991 {
26992 id: "sort-date-asc",
26993 label: deps2.labels.sortDateAsc,
26994 sort: 40,
26995 checked: deps2.currentSortMode === "date-asc",
26996 onClick: () => deps2.sortIcons("date-asc")
26997 }
26998 ]
26999 },
27000 ...deps2.includeShowDesktop === false ? [] : [
27001 {
27002 id: "show-desktop",
27003 label: deps2.labels.showDesktop,
27004 icon: "dashicons-desktop",
27005 sort: 20,
27006 onClick: () => deps2.toggleShowDesktop()
27007 }
27008 ],
27009 {
27010 id: "os-settings",
27011 label: deps2.labels.osSettings,
27012 icon: "dashicons-admin-generic",
27013 sort: 30,
27014 onClick: () => deps2.openOsSettings()
27015 }
27016 ];
27017 const serverItems = (deps2.serverItems ?? []).map(
27018 (s) => serverItemToMenuItem(s, deps2)
27019 );
27020 const merged = [...builtIn, ...serverItems];
27021 const filtered = applyFilters(
27022 "desktop-mode.wallpaper-context-menu",
27023 merged
27024 );
27025 return Array.isArray(filtered) ? filtered : merged;
27026 }
27027 function serverItemToMenuItem(server, deps2) {
27028 return {
27029 id: server.id,
27030 label: server.label,
27031 icon: server.icon,
27032 sort: server.sort,
27033 disabled: server.disabled,
27034 onClick: () => {
27035 if (server.callbackId) {
27036 const cb = deps2.serverCallbacks?.[server.callbackId];
27037 if (typeof cb === "function") {
27038 return cb();
27039 }
27040 }
27041 doAction("desktop-mode.wallpaper-context-menu.activated", {
27042 id: server.id,
27043 callbackId: server.callbackId ?? ""
27044 });
27045 }
27046 };
27047 }
27048 function sanitizeClass(raw) {
27049 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
27050 }
27051 const ROOT_CLASS = "desktop-mode-url-dialog";
27052 let active = null;
27053 function closeUrlDialog() {
27054 if (!active) {
27055 return;
27056 }
27057 active.dispatchEvent(new CustomEvent("url-dialog-closed"));
27058 active.remove();
27059 active = null;
27060 doAction("desktop-mode.files.url-dialog.closed", {});
27061 }
27062 function openUrlDialog(options) {
27063 closeUrlDialog();
27064 const decision = applyFilters(
27065 "desktop-mode.files.url-dialog",
27066 null,
27067 options
27068 );
27069 if (decision === false) {
27070 return;
27071 }
27072 const overlay = document.createElement("div");
27073 overlay.className = `${ROOT_CLASS}__overlay desktop-mode-create-folder-dialog__overlay`;
27074 overlay.setAttribute("role", "presentation");
27075 const dialog2 = document.createElement("div");
27076 dialog2.className = `${ROOT_CLASS} desktop-mode-create-folder-dialog`;
27077 dialog2.setAttribute("role", "dialog");
27078 dialog2.setAttribute("aria-modal", "true");
27079 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS}-title`);
27080 const title = document.createElement("h2");
27081 title.id = `${ROOT_CLASS}-title`;
27082 title.className = "desktop-mode-create-folder-dialog__title";
27083 title.textContent = options.title;
27084 dialog2.appendChild(title);
27085 if (options.description) {
27086 const desc = document.createElement("p");
27087 desc.className = `${ROOT_CLASS}__description`;
27088 desc.textContent = options.description;
27089 dialog2.appendChild(desc);
27090 }
27091 const nameField = document.createElement("wpd-text-field");
27092 nameField.setAttribute("label", options.nameLabel ?? "Name");
27093 nameField.setAttribute("value", options.initialName ?? "");
27094 nameField.setAttribute("placeholder", "My web app");
27095 nameField.setAttribute("autocomplete", "off");
27096 dialog2.appendChild(nameField);
27097 const urlField = document.createElement("wpd-text-field");
27098 urlField.setAttribute("label", options.urlLabel ?? "URL");
27099 urlField.setAttribute("value", options.initialUrl ?? "https://");
27100 urlField.setAttribute("placeholder", "https://example.com");
27101 urlField.setAttribute("type", "url");
27102 urlField.setAttribute("autocomplete", "off");
27103 dialog2.appendChild(urlField);
27104 const error = document.createElement("p");
27105 error.className = "desktop-mode-create-folder-dialog__error";
27106 error.hidden = true;
27107 error.setAttribute("role", "alert");
27108 dialog2.appendChild(error);
27109 const actions = document.createElement("div");
27110 actions.className = "desktop-mode-create-folder-dialog__actions";
27111 const cancel = document.createElement("button");
27112 cancel.type = "button";
27113 cancel.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--secondary";
27114 cancel.textContent = "Cancel";
27115 const submit = document.createElement("button");
27116 submit.type = "button";
27117 submit.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--primary";
27118 submit.textContent = options.submitLabel ?? "Create";
27119 actions.appendChild(cancel);
27120 actions.appendChild(submit);
27121 dialog2.appendChild(actions);
27122 overlay.appendChild(dialog2);
27123 document.body.appendChild(overlay);
27124 active = overlay;
27125 queueMicrotask(() => {
27126 const input = nameField.shadowRoot?.querySelector("input");
27127 input?.focus();
27128 input?.select();
27129 });
27130 doAction("desktop-mode.files.url-dialog.opened", {});
27131 const readValue = (field) => {
27132 const v = field.value;
27133 if (typeof v === "string") {
27134 return v;
27135 }
27136 return field.shadowRoot?.querySelector("input")?.value ?? "";
27137 };
27138 const setBusy = (busy) => {
27139 nameField.disabled = busy;
27140 urlField.disabled = busy;
27141 cancel.disabled = busy;
27142 submit.disabled = busy;
27143 dialog2.classList.toggle("desktop-mode-create-folder-dialog--busy", busy);
27144 };
27145 const showError = (msg) => {
27146 error.textContent = msg;
27147 error.hidden = false;
27148 };
27149 const doCancel = () => {
27150 closeUrlDialog();
27151 options.onCancel?.();
27152 };
27153 const doSubmit = async () => {
27154 const url = readValue(urlField).trim();
27155 if (!url) {
27156 showError("Please enter a URL.");
27157 return;
27158 }
27159 const finalUrl = /^[a-z][a-z0-9+\-.]*:/i.test(url) ? url : `https://${url}`;
27160 try {
27161 new URL(finalUrl);
27162 } catch {
27163 showError("That doesn't look like a valid URL.");
27164 return;
27165 }
27166 const name = readValue(nameField).trim();
27167 error.hidden = true;
27168 setBusy(true);
27169 try {
27170 await options.onSubmit({ name, url: finalUrl });
27171 closeUrlDialog();
27172 } catch (err) {
27173 setBusy(false);
27174 showError(err instanceof Error ? err.message : "Could not save.");
27175 }
27176 };
27177 cancel.addEventListener("click", () => doCancel());
27178 submit.addEventListener("click", () => void doSubmit());
27179 overlay.addEventListener("click", (e) => {
27180 if (e.target === overlay) {
27181 doCancel();
27182 }
27183 });
27184 const onKey = (e) => {
27185 if (e.key === "Escape") {
27186 e.preventDefault();
27187 doCancel();
27188 } else if (e.key === "Enter" && !e.isComposing) {
27189 e.preventDefault();
27190 void doSubmit();
27191 }
27192 };
27193 dialog2.addEventListener("keydown", onKey);
27194 overlay.addEventListener("url-dialog-closed", () => {
27195 dialog2.removeEventListener("keydown", onKey);
27196 });
27197 }
27198 const _earlyReadyQueue = [];
27199 let _earlyReady = false;
27200 (function installEarlyDesktopShim() {
27201 const w = window;
27202 if (!w.wp) {
27203 w.wp = {};
27204 }
27205 if (w.wp.desktop) {
27206 return;
27207 }
27208 const shim = {
27209 whenReady(cb) {
27210 if (typeof cb !== "function") {
27211 return;
27212 }
27213 if (_earlyReady) {
27214 Promise.resolve().then(cb);
27215 return;
27216 }
27217 _earlyReadyQueue.push(cb);
27218 },
27219 ready(cb) {
27220 shim.whenReady(cb);
27221 },
27222 isReady() {
27223 return _earlyReady;
27224 }
27225 };
27226 w.wp.desktop = shim;
27227 })();
27228 const OS_SETTINGS_WINDOW_ID = "desktop-mode-os-settings";
27229 function init() {
27230 const config = window.desktopModeConfig;
27231 if (!config) {
27232 return;
27233 }
27234 const desktopArea = document.getElementById("desktop-mode-area");
27235 if (!desktopArea) {
27236 return;
27237 }
27238 const manager = new WindowManager(desktopArea);
27239 const wallpaperEl = document.getElementById("desktop-mode-wallpaper");
27240 const pluginUrl = config.pluginUrl || "";
27241 let wallpaperLayer = null;
27242 if (wallpaperEl) {
27243 wallpaperLayer = new WallpaperLayer(wallpaperEl, pluginUrl);
27244 }
27245 const widgetsEl = document.getElementById("desktop-mode-widgets");
27246 let widgetLayer = null;
27247 registerBuiltInWidgets();
27248 installDefaultDockRailRenderer();
27249 if (widgetsEl) {
27250 widgetLayer = new WidgetLayer(widgetsEl, pluginUrl);
27251 }
27252 registerModule({
27253 id: "pixijs",
27254 url: `${pluginUrl}/assets/vendor/pixi.min.js`,
27255 isReady: () => typeof window.PIXI !== "undefined"
27256 });
27257 const osSettings = new OsSettings(
27258 {
27259 mediaUrl: config.mediaUrl,
27260 restNonce: config.restNonce,
27261 canUpload: !!config.canUpload,
27262 isAdmin: !!config.currentUserIsAdmin,
27263 aiPlatformSettings: config.aiPlatformSettings ?? null,
27264 aiPlatformSettingsUrl: config.aiPlatformSettingsUrl ?? "",
27265 extendedOptions: config.extendedOptions ?? null,
27266 extendedOptionsUrl: config.extendedOptionsUrl ?? "",
27267 osSettingsPanelBundleUrl: config.osSettingsPanelBundleUrl ?? ""
27268 },
27269 wallpaperLayer ?? new WallpaperLayer(document.createElement("div"), pluginUrl)
27270 );
27271 osSettings.apply();
27272 const aiAssistant = new AiAssistantStub(
27273 {
27274 aiSearchUrl: config.aiSearchUrl ?? "",
27275 aiSearchStreamUrl: config.aiSearchStreamUrl ?? "",
27276 restNonce: config.restNonce,
27277 // Transport picker lives in OS Settings → AI Settings. Read
27278 // live (not captured at construction) so a change applies on
27279 // the next search without a page reload.
27280 getTransport: () => osSettings.getOsSettingsSnapshot().ai.transport
27281 },
27282 config.aiAssistantBundleUrl ?? ""
27283 );
27284 aiAssistant.attachAsk(
27285 createAsk({
27286 config: () => config,
27287 fallbackContext: () => ({
27288 close: () => aiAssistant.close(),
27289 openInWindow: (url, title, icon) => {
27290 manager.open({
27291 url,
27292 title,
27293 icon: icon ?? "dashicons-admin-generic"
27294 });
27295 },
27296 confirm: (msg) => wpdConfirm({ message: msg })
27297 })
27298 })
27299 );
27300 const dragBridge = new DragBridge();
27301 const dragManager = new DragManager();
27302 document.addEventListener(DRAG_EVENTS.START, (e) => {
27303 const detail = e.detail;
27304 const payload = detail?.payload;
27305 if (!payload) {
27306 return;
27307 }
27308 if (payload.type !== "shortcut" && payload.type !== "desktop-file") {
27309 return;
27310 }
27311 const bridgePayload = payload.data?.bridgePayload;
27312 if (bridgePayload) {
27313 dragBridge.start(bridgePayload);
27314 }
27315 });
27316 document.addEventListener(DRAG_EVENTS.END, () => {
27317 dragBridge.end();
27318 });
27319 installIframeDropTargets(dragManager);
27320 window.addEventListener("message", (e) => {
27321 if (e.origin !== window.location.origin) {
27322 return;
27323 }
27324 const data = e.data;
27325 if (!data || data.type !== "desktop-mode-drop-failed") {
27326 return;
27327 }
27328 showToast({
27329 message: "Could not insert into the editor."
27330 });
27331 });
27332 registerPalette({
27333 id: "desktop-mode-ai-assistant",
27334 label: "AI Assistant",
27335 open: () => aiAssistant.open(),
27336 close: () => aiAssistant.close(),
27337 isOpen: () => aiAssistant.isOpen
27338 });
27339 installPaletteShortcut();
27340 installWindowSwitcherShortcut(manager);
27341 installDesktopArrowShortcuts(manager);
27342 new IframeCommandBridge({
27343 manager,
27344 adminUrl: config.adminUrl
27345 }).install();
27346 new ShellCommandHarvester({
27347 manager,
27348 adminUrl: config.adminUrl
27349 }).install();
27350 document.addEventListener("desktop-mode-open-ai", () => {
27351 openPaletteOnly("desktop-mode-ai-assistant");
27352 });
27353 const bottomDockEl = document.getElementById("desktop-mode-dock");
27354 const shellEl = document.getElementById("desktop-mode-shell");
27355 const shellBody = shellEl?.querySelector(
27356 ".desktop-mode-shell__body"
27357 );
27358 let layoutDispatcher = null;
27359 const nativeWindows = createNativeWindowSync({
27360 manager,
27361 appendSystemTile: (item) => layoutDispatcher?.appendSystemTile(item),
27362 removeSystemTile: (id) => layoutDispatcher?.removeSystemTile(id)
27363 });
27364 const syncNativeWindows = nativeWindows.sync;
27365 bindNativeUrlRemap({
27366 getSnapshot: () => osSettings.getOsSettingsSnapshot(),
27367 openById: (id) => nativeWindows.openById(id),
27368 adminUrl: config.adminUrl
27369 });
27370 const findDockEntryForUrl2 = (url) => {
27371 const targetSlug = deriveWindowId(url, config.adminUrl);
27372 const items = layoutDispatcher ? layoutDispatcher.getMenuItems() : config.dockItems ?? [];
27373 for (const item of items) {
27374 if (deriveWindowId(item.url, config.adminUrl) === targetSlug) {
27375 return {
27376 title: item.title,
27377 icon: item.icon,
27378 url: item.url,
27379 submenu: item.submenu,
27380 multi: item.multi
27381 };
27382 }
27383 for (const sub of item.submenu ?? []) {
27384 if (deriveWindowId(sub.url, config.adminUrl) === targetSlug) {
27385 return {
27386 title: sub.title,
27387 // Sub-menu entries inherit the parent tile's
27388 // icon — that's the dock's own convention and
27389 // avoids painting a generic glyph on a window
27390 // the user knows by its parent's identity.
27391 icon: item.icon,
27392 // `url` holds the PARENT tile's landing page, so
27393 // the new window's synthetic "back to parent"
27394 // tab links to the dock URL (themes.php) rather
27395 // than to the sub-page itself.
27396 url: item.url,
27397 multi: item.multi
27398 };
27399 }
27400 }
27401 }
27402 return null;
27403 };
27404 bindAdminLinkDispatch({
27405 adminUrl: config.adminUrl,
27406 deriveSlug: (url) => deriveWindowId(url, config.adminUrl),
27407 openWindow: (windowConfig) => {
27408 void manager.open(windowConfig);
27409 },
27410 findDockEntry: findDockEntryForUrl2
27411 });
27412 registerNativeUrlRemap({
27413 id: "desktop-mode-posts",
27414 nativeWindowId: "desktop-mode-posts",
27415 matches: (_url, parsed) => {
27416 if (!parsed.pathname.endsWith("/edit.php")) {
27417 return false;
27418 }
27419 const postType = parsed.searchParams.get("post_type");
27420 return !postType || postType === "post";
27421 },
27422 enabled: (snapshot) => snapshot.nativePostsEnabled === true
27423 });
27424 registerNativeUrlRemap({
27425 id: "desktop-mode-pages",
27426 nativeWindowId: "desktop-mode-pages",
27427 matches: (_url, parsed) => {
27428 if (!parsed.pathname.endsWith("/edit.php")) {
27429 return false;
27430 }
27431 return parsed.searchParams.get("post_type") === "page";
27432 },
27433 enabled: (snapshot) => snapshot.nativePagesEnabled === true
27434 });
27435 registerNativeUrlRemap({
27436 id: "desktop-mode-users",
27437 nativeWindowId: "desktop-mode-users",
27438 matches: (_url, parsed) => parsed.pathname.endsWith("/users.php"),
27439 enabled: (snapshot) => snapshot.nativeUsersEnabled === true
27440 });
27441 registerNativeUrlRemap({
27442 id: "desktop-mode-user-edit",
27443 nativeWindowId: "desktop-mode-user-edit",
27444 matches: (_url, parsed) => {
27445 const path = parsed.pathname;
27446 if (path.endsWith("/profile.php")) {
27447 return true;
27448 }
27449 if (path.endsWith("/user-edit.php")) {
27450 return parsed.searchParams.has("user_id");
27451 }
27452 return false;
27453 },
27454 enabled: (snapshot) => snapshot.nativeUsersEnabled === true,
27455 onMatch: (_url, parsed) => {
27456 const userId = parseInt(
27457 parsed.searchParams.get("user_id") ?? "0",
27458 10
27459 );
27460 if (userId > 0) {
27461 setUserEditTarget(userId);
27462 }
27463 }
27464 });
27465 registerNativeUrlRemap({
27466 id: "desktop-mode-comments",
27467 nativeWindowId: "desktop-mode-comments",
27468 matches: (_url, parsed) => parsed.pathname.endsWith("/edit-comments.php"),
27469 enabled: (snapshot) => snapshot.nativeCommentsEnabled === true
27470 });
27471 registerNativeUrlRemap({
27472 id: "desktop-mode-plugins",
27473 nativeWindowId: "desktop-mode-plugins",
27474 matches: (_url, parsed) => {
27475 const path = parsed.pathname;
27476 return path.endsWith("/plugins.php") || path.endsWith("/plugin-install.php");
27477 },
27478 enabled: (snapshot) => snapshot.nativePluginsEnabled === true,
27479 onMatch: (_url, parsed) => {
27480 const tab = parsed.pathname.endsWith("/plugin-install.php") ? "browse" : "installed";
27481 void Promise.resolve().then(() => tabTarget).then((m) => {
27482 m.setPluginsWindowTab(tab);
27483 });
27484 }
27485 });
27486 if (bottomDockEl && shellEl && shellBody && config.dockItems) {
27487 desktopArea.classList.add("desktop-mode-area--with-dock");
27488 const initialLayout = osSettings.getOsSettingsSnapshot().desktopLayout;
27489 const renderIcons2 = (icons) => {
27490 renderDesktopIcons(desktopArea, icons, {
27491 openWindow: nativeWindows.openById,
27492 manager
27493 });
27494 };
27495 layoutDispatcher = createLayoutDispatcher(
27496 {
27497 shellRoot: shellEl,
27498 shellBody,
27499 bottomDockEl,
27500 desktopArea,
27501 windowManager: manager,
27502 adminUrl: config.adminUrl,
27503 renderIcons: renderIcons2,
27504 getSettings: () => {
27505 const snap = osSettings.getOsSettingsSnapshot();
27506 return {
27507 itemVisibility: snap.itemVisibility,
27508 dockOrder: snap.dockOrder
27509 };
27510 }
27511 },
27512 initialLayout,
27513 config.dockItems,
27514 config.desktopIcons
27515 );
27516 layoutDispatcher.appendSystemTile(
27517 {
27518 id: OS_SETTINGS_WINDOW_ID,
27519 title: "OS Settings",
27520 icon: "dashicons-desktop",
27521 // "Open" for the dock dot means "open on the currently
27522 // active desktop." OS Settings on another desktop
27523 // shouldn't paint the dot on the active view.
27524 isOpen: () => {
27525 const win = manager.getById(OS_SETTINGS_WINDOW_ID);
27526 if (!win) {
27527 return false;
27528 }
27529 return (win.config.desktopId || manager.getActiveDesktopId()) === manager.getActiveDesktopId();
27530 },
27531 onOpen: openOsSettings
27532 },
27533 "core"
27534 );
27535 if (!isStandaloneDisplay()) {
27536 layoutDispatcher.appendSystemTile(
27537 getInstallTileDef(
27538 config.pwa?.appName || "WordPress",
27539 showToast
27540 ),
27541 "core"
27542 );
27543 }
27544 window.matchMedia("(display-mode: standalone)").addEventListener("change", (e) => {
27545 if (e.matches) {
27546 layoutDispatcher?.removeSystemTile(
27547 "desktop-mode-pwa-install"
27548 );
27549 }
27550 });
27551 void isLikelyInstalled().then((installed2) => {
27552 if (installed2) {
27553 layoutDispatcher?.removeSystemTile(
27554 "desktop-mode-pwa-install"
27555 );
27556 }
27557 });
27558 }
27559 function openOsSettings() {
27560 void manager.open({
27561 id: OS_SETTINGS_WINDOW_ID,
27562 baseId: OS_SETTINGS_WINDOW_ID,
27563 url: "#os-settings",
27564 title: "OS Settings",
27565 icon: "dashicons-desktop",
27566 native: true,
27567 render: (body) => osSettings.renderPanel(body),
27568 width: 820,
27569 height: 720,
27570 minWidth: 560,
27571 minHeight: 480
27572 });
27573 }
27574 function openBugReport() {
27575 void manager.open({
27576 id: BUG_REPORT_WINDOW_ID,
27577 baseId: BUG_REPORT_WINDOW_ID,
27578 url: `#${BUG_REPORT_WINDOW_ID}`,
27579 title: "Report a bug",
27580 icon: "dashicons-buddicons-replies",
27581 native: true,
27582 render: (body) => renderBugReport(body),
27583 width: 560,
27584 height: 620,
27585 minWidth: 420,
27586 minHeight: 480
27587 });
27588 }
27589 document.addEventListener("desktop-mode-open-bug-report", () => {
27590 openBugReport();
27591 });
27592 if (layoutDispatcher) {
27593 layoutDispatcher.appendSystemTile(
27594 {
27595 id: BUG_REPORT_WINDOW_ID,
27596 title: "Report a bug",
27597 icon: "dashicons-buddicons-replies",
27598 isOpen: () => {
27599 const win = manager.getById(BUG_REPORT_WINDOW_ID);
27600 if (!win) {
27601 return false;
27602 }
27603 return (win.config.desktopId || manager.getActiveDesktopId()) === manager.getActiveDesktopId();
27604 },
27605 onOpen: openBugReport
27606 },
27607 "core"
27608 );
27609 layoutDispatcher.appendSystemTile(
27610 getExitDesktopModeTileDef(),
27611 "core"
27612 );
27613 }
27614 const dock = layoutDispatcher?.getPrimary() ?? null;
27615 void syncNativeWindows(
27616 Array.isArray(config.nativeWindows) ? config.nativeWindows : []
27617 );
27618 const hasSession = hasRestorableSession(config.session);
27619 const sessionRestore = hasSession ? restoreSession(manager, config, desktopArea).catch((err) => {
27620 if (typeof console !== "undefined") {
27621 console.error("[desktop-mode] session restore failed:", err);
27622 }
27623 }) : Promise.resolve();
27624 const defaultEnabled = config.defaultWindow?.enabled !== false;
27625 const defaultUrlEarly = config.defaultWindow?.url ?? "";
27626 const isNativeDefault = typeof defaultUrlEarly === "string" && defaultUrlEarly.startsWith("native:");
27627 if (shouldAutoOpenCurrentPage({
27628 fromPortal: config.fromPortal,
27629 fromPortalIntent: config.fromPortalIntent,
27630 hasSession,
27631 defaultEnabled,
27632 isNativeDefault
27633 })) {
27634 void sessionRestore.then(
27635 () => openCurrentPage(manager, config).catch((err) => {
27636 if (typeof console !== "undefined") {
27637 console.error("[desktop-mode] openCurrentPage failed:", err);
27638 }
27639 })
27640 );
27641 }
27642 const saveSession = createSessionSaver(manager, config);
27643 wireSessionEvents(saveSession);
27644 const setDefaultWindow = async (url) => {
27645 try {
27646 const response = await trackedFetch(
27647 manager,
27648 config.defaultWindowUrl,
27649 {
27650 method: "POST",
27651 credentials: "same-origin",
27652 headers: {
27653 "Content-Type": "application/json",
27654 "X-WP-Nonce": config.restNonce
27655 },
27656 body: JSON.stringify({ url })
27657 },
27658 { source: "desktop-mode/default-window" }
27659 );
27660 if (!response.ok) {
27661 throw new Error(`HTTP ${response.status}`);
27662 }
27663 const data = await response.json();
27664 config.defaultWindow = data;
27665 document.dispatchEvent(
27666 new CustomEvent("desktop-mode-default-window-changed", {
27667 detail: data
27668 })
27669 );
27670 } catch (err) {
27671 doAction(HOOKS.SHELL_ERROR, { scope: "default-window-save", error: err });
27672 if (typeof console !== "undefined") {
27673 console.error(
27674 "[desktop-mode] Failed to save default window:",
27675 err
27676 );
27677 }
27678 }
27679 };
27680 manager.onToggleStartupRequested = (win) => {
27681 const currentPref = config.defaultWindow;
27682 const isNative = !!win.config.native;
27683 const winValue = isNative ? `native:${win.id}` : win.getCurrentUrl();
27684 const matchesCurrent = isNative ? currentPref?.url === winValue : urlMatchKey(currentPref?.url ?? "") === urlMatchKey(winValue);
27685 const alreadyDefault = !!currentPref?.enabled && matchesCurrent;
27686 void setDefaultWindow(alreadyDefault ? null : winValue);
27687 };
27688 if (config.defaultWindow?.enabled && config.fromPortal && !config.fromPortalIntent && !hasSession && isNativeDefault) {
27689 const nativeId = defaultUrlEarly.slice("native:".length);
27690 queueMicrotask(() => {
27691 if (nativeId === OS_SETTINGS_WINDOW_ID) {
27692 openOsSettings();
27693 return;
27694 }
27695 void nativeWindows.openById(nativeId);
27696 });
27697 }
27698 const placeSystemTile = (item) => {
27699 layoutDispatcher?.appendSystemTile(item);
27700 };
27701 const syncServerWidgets = createWidgetRegistrySync({
27702 layer: widgetLayer
27703 });
27704 void syncServerWidgets(
27705 Array.isArray(config.serverWidgets) ? config.serverWidgets : []
27706 );
27707 const syncServerWallpapers = createWallpaperRegistrySync({
27708 osSettings
27709 });
27710 void syncServerWallpapers(
27711 Array.isArray(config.serverWallpapers) ? config.serverWallpapers : []
27712 );
27713 const syncServerCommands = createCommandRegistrySync();
27714 void syncServerCommands(
27715 Array.isArray(config.serverCommandScripts) ? config.serverCommandScripts : [],
27716 Array.isArray(config.serverCommands) ? config.serverCommands : []
27717 );
27718 const syncServerSettingsTabs = createSettingsTabRegistrySync();
27719 void syncServerSettingsTabs(
27720 Array.isArray(config.serverSettingsTabScripts) ? config.serverSettingsTabScripts : [],
27721 Array.isArray(config.serverSettingsTabs) ? config.serverSettingsTabs : []
27722 );
27723 const syncServerTitleBarButtons = createTitleBarButtonRegistrySync();
27724 void syncServerTitleBarButtons(
27725 Array.isArray(config.serverTitleBarButtonScripts) ? config.serverTitleBarButtonScripts : []
27726 );
27727 const syncServerDockRailRenderers = createDockRailRendererSync();
27728 void syncServerDockRailRenderers(
27729 Array.isArray(config.serverDockRailRendererScripts) ? config.serverDockRailRendererScripts : []
27730 );
27731 const syncServerWindowThemes = createWindowThemeRegistrySync();
27732 void syncServerWindowThemes(
27733 Array.isArray(config.serverWindowThemeScripts) ? config.serverWindowThemeScripts : [],
27734 Array.isArray(config.serverWindowThemes) ? config.serverWindowThemes : []
27735 );
27736 registerBuiltInControls();
27737 const syncServerWindowControls = createWindowControlRegistrySync();
27738 void syncServerWindowControls(
27739 Array.isArray(config.serverWindowControlScripts) ? config.serverWindowControlScripts : [],
27740 Array.isArray(config.serverWindowControls) ? config.serverWindowControls : []
27741 );
27742 const syncServerWindowSlots = createWindowSlotRegistrySync();
27743 void syncServerWindowSlots(
27744 Array.isArray(config.serverWindowSlotScripts) ? config.serverWindowSlotScripts : [],
27745 Array.isArray(config.serverWindowSlots) ? config.serverWindowSlots : []
27746 );
27747 applyServerWindowNotices(
27748 Array.isArray(config.serverWindowNotices) ? config.serverWindowNotices : []
27749 );
27750 const syncServerWindowChromes = createWindowChromeRegistrySync();
27751 void syncServerWindowChromes(
27752 Array.isArray(config.serverWindowChromeScripts) ? config.serverWindowChromeScripts : [],
27753 Array.isArray(config.serverWindowChromes) ? config.serverWindowChromes : []
27754 );
27755 const connectionBridge = createConnectionBridge(manager);
27756 attachBroadcastBus(manager);
27757 installBroadcastReceiver();
27758 installWindowLoadingTransitions();
27759 addAction(
27760 "desktop-mode.shell.toast",
27761 "desktop-mode/shell-toast",
27762 (payload) => {
27763 if (!payload || typeof payload.message !== "string") {
27764 return;
27765 }
27766 showToast({
27767 message: payload.message,
27768 action: payload.action,
27769 duration: payload.duration
27770 });
27771 }
27772 );
27773 const cfgWithBin = config;
27774 const cfgCountRaw = cfgWithBin.recycleBinCount;
27775 startRecycleBinBadge(
27776 Number(cfgCountRaw) || 0,
27777 typeof cfgWithBin.recycleBinCountUrl === "string" ? cfgWithBin.recycleBinCountUrl : ""
27778 );
27779 registerBuiltInPeekRenderers({
27780 getRecycleBinCount: _currentRecycleBinBadge
27781 });
27782 window.__desktopModeConnectionBridge = connectionBridge;
27783 addAction(HOOKS.WINDOW_CLOSED, "desktop-mode/connection-cleanup", (e) => {
27784 if (e?.windowId) {
27785 connectionBridge.onWindowClosed(e.windowId);
27786 }
27787 });
27788 addAction(HOOKS.IFRAME_READY, "desktop-mode/connection-rearm", (e) => {
27789 if (e?.windowId) {
27790 connectionBridge.onIframeReady(e.windowId);
27791 }
27792 });
27793 const registerWindow = createRegisterWindow(manager);
27794 const renderIcons = (icons) => {
27795 if (layoutDispatcher) {
27796 layoutDispatcher.applyDesktopIcons(icons);
27797 return;
27798 }
27799 renderDesktopIcons(desktopArea, icons, {
27800 openWindow: nativeWindows.openById,
27801 manager
27802 });
27803 };
27804 const refreshMenu = bindMenuRefresh({
27805 layoutDispatcher,
27806 config,
27807 syncNativeWindows,
27808 syncServerWidgets,
27809 syncServerWallpapers,
27810 syncServerCommands,
27811 syncServerSettingsTabs,
27812 syncServerTitleBarButtons,
27813 syncServerDockRailRenderers,
27814 renderIcons
27815 });
27816 osSettings.subscribeOsSettings((snapshot) => {
27817 if (!layoutDispatcher) {
27818 return;
27819 }
27820 const prevLayout = layoutDispatcher.getLayout();
27821 layoutDispatcher.setLayout(snapshot.desktopLayout);
27822 desktopApi.dock = layoutDispatcher.getPrimary();
27823 desktopApi.sideDock = layoutDispatcher.getSide();
27824 desktopApi.desktopLayout = snapshot.desktopLayout;
27825 if (prevLayout === snapshot.desktopLayout) {
27826 layoutDispatcher.refresh();
27827 }
27828 syncShortcutsWithVisibility(
27829 snapshot.itemVisibility,
27830 snapshot.dockPromotedPositions
27831 );
27832 setCurrentLayout(snapshot.desktopLayout);
27833 });
27834 installShortcutsSync(
27835 () => osSettings.getOsSettingsSnapshot().itemVisibility,
27836 () => osSettings.getOsSettingsSnapshot().dockPromotedPositions
27837 );
27838 setCurrentLayout(osSettings.getOsSettingsSnapshot().desktopLayout);
27839 const desktopApi = buildPublicApi({
27840 manager,
27841 dock,
27842 layoutDispatcher,
27843 osSettings,
27844 iconsApi,
27845 filesApi,
27846 saveSession,
27847 widgetLayer,
27848 registerWindow,
27849 openWindowById: nativeWindows.openById,
27850 openNewWindowById: nativeWindows.openNewById,
27851 placeSystemTile,
27852 setDefaultWindow,
27853 refreshMenu,
27854 openOsSettings,
27855 aiAssistant,
27856 dragBridge,
27857 dragManager,
27858 connect: connectionBridge.connect,
27859 getConnection: connectionBridge.getConnection,
27860 config
27861 });
27862 installPublicApi(desktopApi);
27863 installRecycleBinDropTargets(dragManager);
27864 bootHeartbeatBus();
27865 bootNonceRefresh();
27866 bootStickyNotes({
27867 host: desktopArea,
27868 config,
27869 getActiveDesktopId: () => manager.getActiveDesktopId(),
27870 openArtifact: (url, title) => {
27871 const id = deriveWindowId(url, config.adminUrl);
27872 void manager.open({
27873 id,
27874 baseId: id,
27875 url,
27876 title,
27877 icon: "dashicons-edit-page"
27878 });
27879 },
27880 onError: (message) => {
27881 showToast({ message });
27882 }
27883 });
27884 installOpenDeps({
27885 openUrl: ({ id, url, title, icon }) => {
27886 if (tryNativeUrlRemap(url)) {
27887 return true;
27888 }
27889 void manager.open({ id, baseId: id, url, title, icon });
27890 return true;
27891 },
27892 openNativeWindow: (id) => nativeWindows.openById(id),
27893 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
27894 });
27895 setUserAssociations(
27896 config.userFileAssociations ?? {}
27897 );
27898 if (typeof config.filesUrl === "string" && config.filesUrl) {
27899 installRestDeps({
27900 baseUrl: config.filesUrl,
27901 nonce: config.restNonce
27902 });
27903 const rootHost = document.getElementById("desktop-mode-area");
27904 if (rootHost) {
27905 const layerHandle = mountFilesLayer(rootHost, 0);
27906 const reveal = () => {
27907 if (!desktopArea.classList.contains("desktop-mode-area--booting")) {
27908 return;
27909 }
27910 requestAnimationFrame(() => {
27911 desktopArea.classList.remove("desktop-mode-area--booting");
27912 });
27913 };
27914 const safetyTimer = setTimeout(reveal, 2e3);
27915 void layerHandle.hydrated.then(() => {
27916 clearTimeout(safetyTimer);
27917 reveal();
27918 });
27919 }
27920 }
27921 startFilesHeartbeat();
27922 startFilesRestoreSync();
27923 bootPresenceProbe();
27924 doAction(HOOKS.COMPONENTS_REGISTERED, { tags: [...WPD_COMPONENT_TAGS] });
27925 registerBuiltInCommands();
27926 bootstrapPwa(config, showToast);
27927 const overlayPreload = () => {
27928 preloadShellOverlays(config.shellOverlaysBundleUrl ?? "");
27929 preloadWindowSystem(config.windowSystemBundleUrl ?? "");
27930 };
27931 if (typeof window.requestIdleCallback === "function") {
27932 window.requestIdleCallback(overlayPreload, { timeout: 1500 });
27933 } else {
27934 window.setTimeout(overlayPreload, 0);
27935 }
27936 doAction(HOOKS.INIT, { config });
27937 _earlyReady = true;
27938 const queued = _earlyReadyQueue.splice(0);
27939 for (const cb of queued) {
27940 try {
27941 cb();
27942 } catch (err) {
27943 doAction(HOOKS.SHELL_ERROR, {
27944 scope: "when-ready-cb",
27945 error: err
27946 });
27947 if (typeof console !== "undefined") {
27948 console.error("[desktop-mode] whenReady cb threw:", err);
27949 }
27950 }
27951 }
27952 osSettings.apply();
27953 widgetLayer?.hydrate();
27954 window.addEventListener("pagehide", () => {
27955 wallpaperLayer?.teardownActive();
27956 widgetLayer?.disposeAll();
27957 });
27958 bindShellLifecycle();
27959 bindTopWindowLinkInterceptor(manager, config);
27960 const relayoutRoot = (transform, persist2 = true) => {
27961 const root = filesApi.store.getState().placementsByFolder.get(0) ?? [];
27962 const ordered = transform(root);
27963 const rowsPerCol = Math.max(
27964 1,
27965 Math.floor((desktopArea.clientHeight - 16) / 110)
27966 );
27967 const occupied = /* @__PURE__ */ new Set();
27968 let i = 0;
27969 for (const p of ordered) {
27970 const cell = snapToEmptyCell(
27971 16 + Math.floor(i / rowsPerCol) * 96,
27972 16 + i % rowsPerCol * 110,
27973 occupied,
27974 desktopArea
27975 );
27976 occupied.add(`${cell.col},${cell.row}`);
27977 i++;
27978 if (p.x === cell.x && p.y === cell.y) {
27979 continue;
27980 }
27981 filesApi.store.upsertPlacement({
27982 ...p,
27983 x: cell.x,
27984 y: cell.y,
27985 sortOrder: i
27986 });
27987 if (!persist2) {
27988 continue;
27989 }
27990 void updatePlacement(p.id, {
27991 x: cell.x,
27992 y: cell.y,
27993 sortOrder: i
27994 }).catch((err) => {
27995 console.error("[desktop-mode] relayout persist failed", err);
27996 });
27997 }
27998 };
27999 const rootSortTransform = (mode) => (arr) => {
28000 const sorted = arr.slice();
28001 switch (mode) {
28002 case "name-asc":
28003 sorted.sort(
28004 (a, b) => a.file.title.localeCompare(b.file.title)
28005 );
28006 break;
28007 case "name-desc":
28008 sorted.sort(
28009 (a, b) => b.file.title.localeCompare(a.file.title)
28010 );
28011 break;
28012 case "date-asc":
28013 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
28014 break;
28015 case "date-desc":
28016 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
28017 break;
28018 }
28019 return sorted;
28020 };
28021 const ROOT_SORT_MODE_KEY = "desktop-mode:root-sort-mode";
28022 const isRootSortMode = (v) => v === "name-asc" || v === "name-desc" || v === "date-asc" || v === "date-desc";
28023 let rootSortMode = (() => {
28024 try {
28025 const raw = window.localStorage.getItem(ROOT_SORT_MODE_KEY);
28026 return isRootSortMode(raw) ? raw : null;
28027 } catch {
28028 return null;
28029 }
28030 })();
28031 const setRootSortMode = (mode) => {
28032 rootSortMode = mode;
28033 try {
28034 if (mode) {
28035 window.localStorage.setItem(ROOT_SORT_MODE_KEY, mode);
28036 } else {
28037 window.localStorage.removeItem(ROOT_SORT_MODE_KEY);
28038 }
28039 } catch {
28040 }
28041 };
28042 addAction(
28043 "desktop-mode.files.tile-manually-placed",
28044 "desktop-mode/root-sort-clear",
28045 (payload) => {
28046 const folderId = payload?.folderId;
28047 if (folderId === 0) {
28048 setRootSortMode(null);
28049 }
28050 }
28051 );
28052 if (typeof ResizeObserver !== "undefined") {
28053 let lastW = desktopArea.clientWidth;
28054 let lastH = desktopArea.clientHeight;
28055 const ro = new ResizeObserver(() => {
28056 if (!rootSortMode) {
28057 return;
28058 }
28059 const w = desktopArea.clientWidth;
28060 const h = desktopArea.clientHeight;
28061 if (w === lastW && h === lastH) {
28062 return;
28063 }
28064 lastW = w;
28065 lastH = h;
28066 relayoutRoot(rootSortTransform(rootSortMode), false);
28067 });
28068 ro.observe(desktopArea);
28069 }
28070 desktopArea.addEventListener("click", (e) => {
28071 if (!osSettings.state.showDesktopOnWallpaperClick) {
28072 return;
28073 }
28074 if (e.target !== desktopArea) {
28075 return;
28076 }
28077 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
28078 return;
28079 }
28080 if (isWallpaperMenuOpen()) {
28081 return;
28082 }
28083 if (dragManager.recentlyEndedDrag()) {
28084 return;
28085 }
28086 manager.toggleShowDesktop();
28087 });
28088 desktopArea.addEventListener("contextmenu", (e) => {
28089 if (e.target !== desktopArea) {
28090 return;
28091 }
28092 e.preventDefault();
28093 const clientX = e.clientX;
28094 const clientY = e.clientY;
28095 (() => {
28096 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
28097 return;
28098 }
28099 if (isWallpaperMenuOpen()) {
28100 closeWallpaperMenu();
28101 return;
28102 }
28103 const dropClient = { x: clientX, y: clientY };
28104 const cellAtClick = () => {
28105 const rect = desktopArea.getBoundingClientRect();
28106 const rawX = Math.max(0, dropClient.x - rect.left);
28107 const rawY = Math.max(0, dropClient.y - rect.top);
28108 const occupied = buildOccupiedSet(
28109 filesApi.store.getState().placementsByFolder.get(0) ?? []
28110 );
28111 return snapToEmptyCell(rawX, rawY, occupied, desktopArea);
28112 };
28113 const createUrlPlacement = (dialogTitle, description) => {
28114 openUrlDialog({
28115 title: dialogTitle,
28116 description,
28117 nameLabel: "Name",
28118 urlLabel: "URL",
28119 submitLabel: "Create",
28120 onSubmit: async ({ name, url }) => {
28121 const cell = cellAtClick();
28122 const placement = await createPlacement({
28123 type: "link",
28124 ref: url,
28125 parentId: 0,
28126 x: cell.x,
28127 y: cell.y,
28128 meta: name ? { name } : void 0
28129 });
28130 filesApi.store.upsertPlacement(placement);
28131 }
28132 });
28133 };
28134 const items = buildMenuItems({
28135 createFolder: () => {
28136 openCreateFolderDialog({
28137 onSubmit: async (name) => {
28138 const folder = await createFolder({ name });
28139 const cell = cellAtClick();
28140 const placement = await createPlacement({
28141 type: "folder",
28142 ref: String(folder.id),
28143 parentId: 0,
28144 x: cell.x,
28145 y: cell.y
28146 });
28147 filesApi.store.upsertFolder(folder);
28148 filesApi.store.upsertPlacement(placement);
28149 }
28150 });
28151 },
28152 createUrl: () => createUrlPlacement(
28153 "New URL",
28154 "Opens the URL in a new browser tab."
28155 ),
28156 toggleShowDesktop: () => manager.toggleShowDesktop(),
28157 openOsSettings: () => openOsSettings(),
28158 sortIcons: (mode) => {
28159 setRootSortMode(mode);
28160 relayoutRoot(rootSortTransform(mode));
28161 },
28162 currentSortMode: rootSortMode,
28163 includeShowDesktop: !osSettings.state.showDesktopOnWallpaperClick,
28164 labels: {
28165 createFolder: "New folder",
28166 showDesktop: "Show desktop",
28167 osSettings: "OS Settings",
28168 sortHeading: "Sort by",
28169 sortNameAsc: "Name (A → Z)",
28170 sortNameDesc: "Name (Z → A)",
28171 sortDateAsc: "Date (oldest first)",
28172 sortDateDesc: "Date (newest first)",
28173 newUrl: "New URL"
28174 },
28175 serverItems: config.serverWallpaperMenuItems ?? []
28176 });
28177 openWallpaperMenu(
28178 document.body,
28179 { x: clientX, y: clientY },
28180 items
28181 );
28182 })();
28183 });
28184 void Promise.resolve().then(() => index).then((mod) => {
28185 mod.bootOsFileDrop({
28186 config: config.dropConfig,
28187 mediaUrl: config.mediaUrl,
28188 restNonce: config.restNonce
28189 });
28190 });
28191 document.dispatchEvent(
28192 new CustomEvent("desktop-mode-init", {
28193 detail: { config, restored: hasSession }
28194 })
28195 );
28196 }
28197 startMissingImportWarner();
28198 if (document.readyState === "loading") {
28199 document.addEventListener("DOMContentLoaded", init);
28200 } else {
28201 init();
28202 }
28203 const _initial = {
28204 tab: null,
28205 requestedAt: 0
28206 };
28207 let _store = null;
28208 function getStore() {
28209 if (_store) {
28210 return _store;
28211 }
28212 const w = window;
28213 const factory = w.wp?.desktop?.createSharedStore;
28214 if (typeof factory !== "function") {
28215 return null;
28216 }
28217 _store = factory(
28218 "desktop-mode/plugins-window/tab-target",
28219 () => ({ ..._initial })
28220 );
28221 return _store;
28222 }
28223 function setPluginsWindowTab(tab) {
28224 const store2 = getStore();
28225 if (store2) {
28226 store2.state.tab = tab;
28227 store2.state.requestedAt = Date.now();
28228 store2.notify();
28229 return;
28230 }
28231 const w = window;
28232 w._wpdPluginsWindowTab = { tab, requestedAt: Date.now() };
28233 }
28234 function consumePluginsWindowTab() {
28235 const store2 = getStore();
28236 if (store2) {
28237 const tab = store2.state.tab;
28238 if (tab !== null) {
28239 store2.state.tab = null;
28240 store2.state.requestedAt = 0;
28241 store2.notify();
28242 }
28243 return tab;
28244 }
28245 const w = window;
28246 const prev = w._wpdPluginsWindowTab;
28247 if (prev) {
28248 w._wpdPluginsWindowTab = { tab: null, requestedAt: 0 };
28249 return prev.tab;
28250 }
28251 return null;
28252 }
28253 function subscribePluginsWindowTab(cb) {
28254 const store2 = getStore();
28255 if (!store2) {
28256 return () => {
28257 };
28258 }
28259 return store2.subscribe((state2) => cb({ ...state2 }));
28260 }
28261 const tabTarget = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
28262 __proto__: null,
28263 consumePluginsWindowTab,
28264 setPluginsWindowTab,
28265 subscribePluginsWindowTab
28266 }, Symbol.toStringTag, { value: "Module" }));
28267 const FILE_DROP_HOOKS = {
28268 /**
28269 * Filter — fires once per drop, after the manager has parsed
28270 * the OS `DataTransfer` into `File[]` and BEFORE the mime /
28271 * size filter runs.
28272 *
28273 * Signature: `(files: File[], ctx: DropContext) => File[]`.
28274 * Return an empty array to abort the drop silently.
28275 */
28276 FILES_DETECTED: "desktop-mode.drop.files-detected",
28277 /**
28278 * Action — fires after the mime / size filter has rejected
28279 * one or more files. Payload: `{ rejections: DropRejection[],
28280 * context: DropContext }`. The shell toasts a default message;
28281 * subscribers can surface a custom UX (a side panel with the
28282 * list, an analytics call).
28283 */
28284 FILES_REJECTED: "desktop-mode.drop.files-rejected",
28285 /**
28286 * Filter — fires per file before the upload dialog renders.
28287 * Receives `DropFileEntry` (the underlying file + the
28288 * manager's default `fields`). Mutate `fields` (or return a
28289 * new object) to change what the user sees in the form.
28290 *
28291 * Signature: `(entry: DropFileEntry, ctx: DropContext)
28292 * => DropFileEntry`.
28293 */
28294 DIALOG_FIELDS: "desktop-mode.drop.dialog-fields",
28295 /**
28296 * Filter — last call before the manager `POST`s to
28297 * `wp/v2/media`. Receives `{ file: File, fields:
28298 * DropDialogFields, mime: string }`. Return `null` to cancel
28299 * the upload entirely (e.g. a plugin handled it via a
28300 * different endpoint).
28301 *
28302 * Signature: `(payload, ctx: DropContext) => payload | null`.
28303 */
28304 BEFORE_UPLOAD: "desktop-mode.drop.before-upload",
28305 /**
28306 * Action — fires once `BEFORE_UPLOAD` has cleared and the XHR
28307 * is `open()`ed, immediately before `send()`. Payload:
28308 * `{ file: File, fields: DropDialogFields, context: DropContext,
28309 * abort: () => void }`. The `abort` handle aborts the in-flight
28310 * request; the manager rejects with `UploadAbortedError` and
28311 * fires `UPLOAD_FAILED` with that error.
28312 *
28313 * Pair with `UPLOAD_PROGRESS` to drive a progress UI; pair with
28314 * `AFTER_UPLOAD` / `UPLOAD_FAILED` to know when the upload ends.
28315 *
28316 * @since 0.31.0
28317 */
28318 UPLOAD_STARTED: "desktop-mode.drop.upload-started",
28319 /**
28320 * Action — fires for every `XMLHttpRequestUpload.progress` event.
28321 * Payload: `{ file: File, fields: DropDialogFields, context:
28322 * DropContext, loaded: number, total: number, indeterminate:
28323 * boolean }`. `total` is `0` and `indeterminate` is `true` when
28324 * the request body length isn't known (rare for multipart, but
28325 * possible on transcoding proxies); subscribers should treat
28326 * that as an indeterminate state.
28327 *
28328 * A synthetic 100%-loaded event is dispatched once the `upload`
28329 * stream emits `load` so a HUD can show a definite "wrapping up"
28330 * state while the server finishes the response.
28331 *
28332 * @since 0.31.0
28333 */
28334 UPLOAD_PROGRESS: "desktop-mode.drop.upload-progress",
28335 /**
28336 * Action — fires after a successful upload. Payload:
28337 * `{ file: File, result: DropUploadResult, fields:
28338 * DropDialogFields, context: DropContext }`.
28339 *
28340 * The `file` field carries the same `File` reference that
28341 * `UPLOAD_STARTED` / `UPLOAD_PROGRESS` exposed (i.e. the
28342 * payload returned by the `BEFORE_UPLOAD` filter, in case a
28343 * plugin swapped the file). Subscribers tracking per-file
28344 * state — progress HUDs, sequence counters — should match on
28345 * this identity rather than the filename: two drops of
28346 * `photo.jpg` from different folders would otherwise route
28347 * each other's success event to the wrong row.
28348 *
28349 * @since 0.31.0 the `file` field was added; pre-0.31.0 code
28350 * that destructured `{ result, fields, context }` keeps working.
28351 */
28352 AFTER_UPLOAD: "desktop-mode.drop.after-upload",
28353 /**
28354 * Action — fires after an upload fails. Payload:
28355 * `{ file: File, error: Error, context: DropContext }`.
28356 * `error` is an `UploadAbortedError` when the failure came
28357 * from the caller invoking the `abort()` handle on
28358 * `UPLOAD_STARTED`.
28359 *
28360 * `file` carries the same identity as `UPLOAD_STARTED` /
28361 * `UPLOAD_PROGRESS` / `AFTER_UPLOAD` — the post-`BEFORE_UPLOAD`
28362 * `File`, in case a plugin swapped it. Match by reference, not
28363 * filename: a HUD that keys its row map on the started-File
28364 * needs the same key here, otherwise the row stays stuck in
28365 * "running" after a failure when a `BEFORE_UPLOAD` filter
28366 * replaced the file.
28367 */
28368 UPLOAD_FAILED: "desktop-mode.drop.upload-failed"
28369 };
28370 const IFRAME_PASSTHROUGH_SELECTORS = [
28371 ".components-drop-zone",
28372 "[data-drop-zone]",
28373 ".uploader-window",
28374 ".media-frame-content"
28375 ];
28376 function dragHasFiles(ev) {
28377 const types = ev.dataTransfer?.types;
28378 if (!types) {
28379 return false;
28380 }
28381 const list2 = types;
28382 if (typeof list2.includes === "function") {
28383 return list2.includes("Files");
28384 }
28385 if (typeof list2.contains === "function") {
28386 return list2.contains("Files");
28387 }
28388 for (let i = 0; i < list2.length; i++) {
28389 if (list2[i] === "Files") {
28390 return true;
28391 }
28392 }
28393 return false;
28394 }
28395 function resolveWindowIdFromSource(source) {
28396 if (!source) {
28397 return void 0;
28398 }
28399 const iframes = document.querySelectorAll("iframe");
28400 for (const f of Array.from(iframes)) {
28401 if (f.contentWindow === source) {
28402 const host = f.closest("[data-window-id]");
28403 return host?.getAttribute("data-window-id") || void 0;
28404 }
28405 }
28406 return void 0;
28407 }
28408 function mountOsFileDropManager(opts) {
28409 const host = window;
28410 if (host.__desktopModeOsFileDropMounted) {
28411 return host.__desktopModeOsFileDropMounted;
28412 }
28413 if (!opts.config.enabled) {
28414 return mountNoOp();
28415 }
28416 const overlayEl = ensureDropOverlay();
28417 let dragDepth = 0;
28418 let dragWatchdog = null;
28419 const resetOverlay = () => {
28420 dragDepth = 0;
28421 overlayEl.classList.remove("is-active");
28422 if (dragWatchdog !== null) {
28423 clearTimeout(dragWatchdog);
28424 dragWatchdog = null;
28425 }
28426 };
28427 const bumpWatchdog = () => {
28428 if (dragWatchdog !== null) {
28429 clearTimeout(dragWatchdog);
28430 }
28431 dragWatchdog = setTimeout(resetOverlay, 250);
28432 };
28433 const onDragEnter = (ev) => {
28434 if (!dragHasFiles(ev)) {
28435 return;
28436 }
28437 ev.preventDefault();
28438 dragDepth++;
28439 overlayEl.classList.add("is-active");
28440 bumpWatchdog();
28441 };
28442 const onDragOver = (ev) => {
28443 if (!dragHasFiles(ev)) {
28444 return;
28445 }
28446 if (ev.defaultPrevented) {
28447 resetOverlay();
28448 return;
28449 }
28450 ev.preventDefault();
28451 if (ev.dataTransfer) {
28452 ev.dataTransfer.dropEffect = "copy";
28453 }
28454 bumpWatchdog();
28455 };
28456 const onDragLeave = () => {
28457 dragDepth = Math.max(0, dragDepth - 1);
28458 if (dragDepth === 0) {
28459 overlayEl.classList.remove("is-active");
28460 }
28461 };
28462 const onDrop = (ev) => {
28463 if (!dragHasFiles(ev)) {
28464 return;
28465 }
28466 if (ev.defaultPrevented) {
28467 resetOverlay();
28468 return;
28469 }
28470 ev.preventDefault();
28471 resetOverlay();
28472 const files = ev.dataTransfer?.files ? Array.from(ev.dataTransfer.files) : [];
28473 if (files.length === 0) {
28474 return;
28475 }
28476 const ctx = classifyDropTarget(ev);
28477 void handleFiles(files, ctx, opts);
28478 };
28479 const onDragEnd2 = () => resetOverlay();
28480 const onVisibilityChange = () => {
28481 if (document.visibilityState === "hidden") {
28482 resetOverlay();
28483 }
28484 };
28485 const onIframeMessage = (ev) => {
28486 if (ev.origin !== window.location.origin) {
28487 return;
28488 }
28489 const data = ev.data;
28490 if (!data || data.type !== "desktop-mode-os-file-drop") {
28491 return;
28492 }
28493 if (!Array.isArray(data.files) || data.files.length === 0) {
28494 return;
28495 }
28496 const files = data.files.filter((f) => f instanceof File);
28497 if (files.length === 0) {
28498 return;
28499 }
28500 const windowId = resolveWindowIdFromSource(ev.source);
28501 if (!windowId) {
28502 return;
28503 }
28504 const ctx = {
28505 surface: "iframe",
28506 windowId,
28507 x: typeof data.x === "number" ? data.x : 0,
28508 y: typeof data.y === "number" ? data.y : 0
28509 };
28510 dragDepth = 0;
28511 overlayEl.classList.remove("is-active");
28512 void handleFiles(files, ctx, opts);
28513 };
28514 window.addEventListener("dragenter", onDragEnter);
28515 window.addEventListener("dragover", onDragOver);
28516 window.addEventListener("dragleave", onDragLeave);
28517 window.addEventListener("drop", onDrop);
28518 window.addEventListener("dragend", onDragEnd2);
28519 document.addEventListener("visibilitychange", onVisibilityChange);
28520 window.addEventListener("blur", onDragEnd2);
28521 window.addEventListener("message", onIframeMessage);
28522 const manager = {
28523 dispose: () => {
28524 window.removeEventListener("dragenter", onDragEnter);
28525 window.removeEventListener("dragover", onDragOver);
28526 window.removeEventListener("dragleave", onDragLeave);
28527 window.removeEventListener("drop", onDrop);
28528 window.removeEventListener("dragend", onDragEnd2);
28529 document.removeEventListener(
28530 "visibilitychange",
28531 onVisibilityChange
28532 );
28533 window.removeEventListener("blur", onDragEnd2);
28534 window.removeEventListener("message", onIframeMessage);
28535 overlayEl.remove();
28536 delete window.__desktopModeOsFileDropMounted;
28537 }
28538 };
28539 host.__desktopModeOsFileDropMounted = manager;
28540 return manager;
28541 }
28542 function ensureDropOverlay() {
28543 const existing = document.querySelector(".desktop-mode-os-drop-overlay");
28544 if (existing) {
28545 return existing;
28546 }
28547 const el = document.createElement("div");
28548 el.className = "desktop-mode-os-drop-overlay";
28549 el.setAttribute("aria-hidden", "true");
28550 el.style.cssText = [
28551 "position:fixed",
28552 "inset:0",
28553 "pointer-events:none",
28554 "z-index:200",
28555 "opacity:0",
28556 "transition:opacity 120ms ease",
28557 "background:radial-gradient(circle at center, rgba(34,113,177,0.18) 0%, rgba(34,113,177,0.06) 60%, transparent 100%)",
28558 "box-shadow:inset 0 0 0 3px rgba(34,113,177,0.55)"
28559 ].join(";");
28560 const label = document.createElement("div");
28561 label.style.cssText = [
28562 "position:absolute",
28563 "top:50%",
28564 "left:50%",
28565 "transform:translate(-50%,-50%)",
28566 "padding:14px 22px",
28567 "border-radius:12px",
28568 "background:rgba(20,20,24,0.78)",
28569 "color:#fff",
28570 "font:600 14px/1.2 -apple-system,BlinkMacSystemFont,sans-serif",
28571 "letter-spacing:0.02em"
28572 ].join(";");
28573 label.textContent = "Drop to upload";
28574 el.appendChild(label);
28575 document.body.appendChild(el);
28576 const style = document.createElement("style");
28577 style.textContent = ".desktop-mode-os-drop-overlay.is-active{opacity:1!important;}";
28578 document.head.appendChild(style);
28579 return el;
28580 }
28581 function mountNoOp() {
28582 const cancel = (ev) => {
28583 if (!dragHasFiles(ev)) {
28584 return;
28585 }
28586 const target2 = ev.target;
28587 if (target2?.closest && IFRAME_PASSTHROUGH_SELECTORS.some((s) => target2.closest(s))) {
28588 return;
28589 }
28590 ev.preventDefault();
28591 };
28592 window.addEventListener("dragover", cancel);
28593 window.addEventListener("drop", cancel);
28594 const host = window;
28595 const manager = {
28596 dispose: () => {
28597 window.removeEventListener("dragover", cancel);
28598 window.removeEventListener("drop", cancel);
28599 delete host.__desktopModeOsFileDropMounted;
28600 }
28601 };
28602 host.__desktopModeOsFileDropMounted = manager;
28603 return manager;
28604 }
28605 function classifyDropTarget(ev) {
28606 const x = ev.clientX;
28607 const y = ev.clientY;
28608 let node = ev.target;
28609 while (node && node !== document.body) {
28610 if (node.tagName === "IFRAME") {
28611 const id = node.closest(
28612 "[data-window-id]"
28613 );
28614 return {
28615 surface: "iframe",
28616 windowId: id?.getAttribute("data-window-id") || void 0,
28617 x,
28618 y
28619 };
28620 }
28621 if (node.hasAttribute("data-window-id")) {
28622 return {
28623 surface: "window",
28624 windowId: node.getAttribute("data-window-id") || void 0,
28625 x,
28626 y
28627 };
28628 }
28629 if (node.classList.contains("desktop-mode-folder-grid")) {
28630 return { surface: "folder", x, y };
28631 }
28632 if (node.id === "desktop-mode-wallpaper" || node.classList.contains("desktop-mode-wallpaper") || node.classList.contains("desktop-mode-desktop")) {
28633 return { surface: "wallpaper", x, y };
28634 }
28635 node = node.parentElement;
28636 }
28637 return { surface: "unknown", x, y };
28638 }
28639 async function handleFiles(rawFiles, ctx, opts) {
28640 const detected = applyFilters(
28641 FILE_DROP_HOOKS.FILES_DETECTED,
28642 rawFiles,
28643 ctx
28644 );
28645 if (!Array.isArray(detected) || detected.length === 0) {
28646 return;
28647 }
28648 const { accepted, rejected } = partitionByPolicy(
28649 detected,
28650 opts.config
28651 );
28652 if (rejected.length > 0) {
28653 doAction(FILE_DROP_HOOKS.FILES_REJECTED, {
28654 rejections: rejected,
28655 context: ctx
28656 });
28657 showToast({
28658 message: rejected.length === 1 ? rejected[0].message : `${rejected.length} files couldn't be uploaded.`
28659 });
28660 }
28661 if (accepted.length === 0) {
28662 return;
28663 }
28664 const entries = accepted.map(({ file, mime }) => {
28665 const base = {
28666 file,
28667 mime,
28668 fields: defaultFields(file, mime)
28669 };
28670 const filtered = applyFilters(
28671 FILE_DROP_HOOKS.DIALOG_FIELDS,
28672 base,
28673 ctx
28674 );
28675 if (!filtered || typeof filtered !== "object" || !("fields" in filtered) || typeof filtered.fields !== "object") {
28676 return base;
28677 }
28678 return filtered;
28679 });
28680 await opts.openDialog(entries, ctx);
28681 }
28682 function partitionByPolicy(files, config) {
28683 const accepted = [];
28684 const rejected = [];
28685 for (const file of files) {
28686 if (file.size === 0) {
28687 rejected.push({
28688 file,
28689 reason: "empty",
28690 message: `“${file.name}” is empty.`
28691 });
28692 continue;
28693 }
28694 if (config.maxSize > 0 && file.size > config.maxSize) {
28695 rejected.push({
28696 file,
28697 reason: "size",
28698 message: `“${file.name}” exceeds the ${formatBytes$1(
28699 config.maxSize
28700 )} upload limit.`
28701 });
28702 continue;
28703 }
28704 const mime = resolveAllowedMime(
28705 file,
28706 config.allowedMimes,
28707 config.extToMime
28708 );
28709 if (!mime) {
28710 rejected.push({
28711 file,
28712 reason: "mime",
28713 message: `“${file.name}” is not an allowed file type.`
28714 });
28715 continue;
28716 }
28717 accepted.push({ file, mime });
28718 }
28719 return { accepted, rejected };
28720 }
28721 function resolveAllowedMime(file, allowedMimes, extToMime) {
28722 if (allowedMimes.length === 0) {
28723 return null;
28724 }
28725 const lower = file.type.toLowerCase();
28726 if (lower && allowedMimes.includes(lower)) {
28727 return lower;
28728 }
28729 const ext = extensionOf(file.name);
28730 if (!ext) {
28731 return null;
28732 }
28733 if (extToMime) {
28734 for (const [key, mime] of Object.entries(extToMime)) {
28735 if (key.split("|").includes(ext) && allowedMimes.includes(mime)) {
28736 return mime;
28737 }
28738 }
28739 return null;
28740 }
28741 const guess = EXTENSION_GUESSES[ext];
28742 if (guess && allowedMimes.includes(guess)) {
28743 return guess;
28744 }
28745 return null;
28746 }
28747 const EXTENSION_GUESSES = {
28748 jpg: "image/jpeg",
28749 jpeg: "image/jpeg",
28750 png: "image/png",
28751 gif: "image/gif",
28752 webp: "image/webp",
28753 avif: "image/avif",
28754 heic: "image/heic",
28755 heif: "image/heif",
28756 svg: "image/svg+xml",
28757 mp4: "video/mp4",
28758 mov: "video/quicktime",
28759 webm: "video/webm",
28760 mp3: "audio/mpeg",
28761 wav: "audio/wav",
28762 pdf: "application/pdf"
28763 };
28764 function extensionOf(name) {
28765 const dot = name.lastIndexOf(".");
28766 if (dot < 0) {
28767 return "";
28768 }
28769 return name.slice(dot + 1).toLowerCase();
28770 }
28771 function defaultFields(file, mime) {
28772 const safeName = sanitizeFilename(file.name);
28773 const ext = extensionOf(safeName);
28774 const stem = ext ? safeName.slice(0, safeName.length - ext.length - 1) : safeName;
28775 const title = humanize(stem);
28776 return {
28777 title,
28778 altText: mime.startsWith("image/") ? title : "",
28779 caption: "",
28780 description: "",
28781 filename: safeName
28782 };
28783 }
28784 function sanitizeFilename(name) {
28785 const cleaned = name.replace(/[\\/]/g, "-").replace(/[\x00-\x1f\x7f]/g, "").replace(/\s+/g, " ").replace(/ *- */g, "-").replace(/-+/g, "-").trim().replace(/^[-.]+|[-.]+$/g, "");
28786 return cleaned || "upload";
28787 }
28788 function humanize(stem) {
28789 const spaced = stem.replace(/[-_]+/g, " ").trim();
28790 if (!spaced) {
28791 return "Upload";
28792 }
28793 return spaced.charAt(0).toUpperCase() + spaced.slice(1);
28794 }
28795 function formatBytes$1(bytes) {
28796 if (bytes >= 1024 * 1024) {
28797 return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
28798 }
28799 if (bytes >= 1024) {
28800 return `${(bytes / 1024).toFixed(0)} KB`;
28801 }
28802 return `${bytes} B`;
28803 }
28804 function formatBytes(bytes) {
28805 if (!Number.isFinite(bytes) || bytes <= 0) {
28806 return "0 B";
28807 }
28808 const units = ["B", "KB", "MB", "GB", "TB"];
28809 let v = bytes;
28810 let i = 0;
28811 while (v >= 1024 && i < units.length - 1) {
28812 v /= 1024;
28813 i++;
28814 }
28815 const decimals = v >= 100 || i === 0 ? 0 : 1;
28816 return `${v.toFixed(decimals)} ${units[i]}`;
28817 }
28818 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}}`;
28819 const _WpdProgressBar = class _WpdProgressBar extends Component {
28820 constructor() {
28821 super(...arguments);
28822 this._ownedAriaLabel = null;
28823 }
28824 render() {
28825 return html`<div class="root" part="root">
28826 <div class="header" part="header" hidden>
28827 <span class="label" part="label"></span>
28828 <span class="percent" part="percent"></span>
28829 </div>
28830 <div class="track" part="track">
28831 <div class="fill" part="fill"></div>
28832 </div>
28833 </div>`;
28834 }
28835 requestUpdate() {
28836 super.requestUpdate();
28837 queueMicrotask(() => this._paint());
28838 }
28839 connectedCallback() {
28840 super.connectedCallback();
28841 queueMicrotask(() => this._paint());
28842 }
28843 _paint() {
28844 const root = this.shadowRoot;
28845 if (!root) {
28846 return;
28847 }
28848 const max = this._readMax();
28849 const indeterminate = this.hasAttribute("indeterminate") || max <= 0;
28850 const value = indeterminate ? 0 : this._readValue(max);
28851 const ratio = indeterminate ? 0 : value / max;
28852 const percent = Math.round(ratio * 100);
28853 const label = this.getAttribute("label") ?? "";
28854 const showPercent = this.hasAttribute("show-percent");
28855 const fill = root.querySelector(".fill");
28856 if (fill && !indeterminate) {
28857 fill.style.width = `${(ratio * 100).toFixed(2)}%`;
28858 } else if (fill && indeterminate) {
28859 fill.style.removeProperty("width");
28860 }
28861 const header = root.querySelector(".header");
28862 const labelEl = root.querySelector(".label");
28863 const percentEl = root.querySelector(".percent");
28864 if (header && labelEl && percentEl) {
28865 const visible = label || showPercent && !indeterminate;
28866 header.hidden = !visible;
28867 labelEl.textContent = label;
28868 percentEl.hidden = !(showPercent && !indeterminate);
28869 percentEl.textContent = `${percent}%`;
28870 }
28871 this._syncAria(max, value, indeterminate, label);
28872 const track = root.querySelector(".track");
28873 if (track) {
28874 track.setAttribute("role", "progressbar");
28875 track.setAttribute("aria-valuemin", "0");
28876 if (indeterminate) {
28877 track.removeAttribute("aria-valuenow");
28878 track.removeAttribute("aria-valuemax");
28879 } else {
28880 track.setAttribute("aria-valuemax", String(max));
28881 track.setAttribute("aria-valuenow", String(value));
28882 }
28883 if (label) {
28884 track.setAttribute("aria-label", label);
28885 } else {
28886 track.removeAttribute("aria-label");
28887 }
28888 }
28889 }
28890 _syncAria(max, value, indeterminate, label) {
28891 this.setAttribute("role", "progressbar");
28892 this.setAttribute("aria-valuemin", "0");
28893 if (indeterminate) {
28894 this.removeAttribute("aria-valuenow");
28895 this.removeAttribute("aria-valuemax");
28896 } else {
28897 this.setAttribute("aria-valuemax", String(max));
28898 this.setAttribute("aria-valuenow", String(value));
28899 }
28900 const existing = this.getAttribute("aria-label");
28901 if (label) {
28902 if (existing === null || existing === this._ownedAriaLabel) {
28903 this.setAttribute("aria-label", label);
28904 this._ownedAriaLabel = label;
28905 }
28906 } else if (existing !== null && existing === this._ownedAriaLabel) {
28907 this.removeAttribute("aria-label");
28908 this._ownedAriaLabel = null;
28909 }
28910 }
28911 _readMax() {
28912 const attr = this.getAttribute("max");
28913 if (attr === null) {
28914 return 100;
28915 }
28916 const raw = parseFloat(attr);
28917 return Number.isFinite(raw) ? raw : 100;
28918 }
28919 _readValue(max) {
28920 const raw = parseFloat(this.getAttribute("value") ?? "0");
28921 if (!Number.isFinite(raw)) {
28922 return 0;
28923 }
28924 if (raw < 0) {
28925 return 0;
28926 }
28927 if (raw > max) {
28928 return max;
28929 }
28930 return raw;
28931 }
28932 };
28933 _WpdProgressBar.props = [
28934 "value",
28935 "max",
28936 "indeterminate",
28937 "tone",
28938 "label",
28939 "showPercent"
28940 ];
28941 _WpdProgressBar.styles = [styles];
28942 _WpdProgressBar.help = {
28943 title: "Progress bar",
28944 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.",
28945 status: "experimental",
28946 since: "0.31.0",
28947 props: [
28948 {
28949 name: "value",
28950 type: "number",
28951 default: "0",
28952 description: "Current progress. Clamped to `[0, max]`."
28953 },
28954 {
28955 name: "max",
28956 type: "number",
28957 default: "100",
28958 description: "Maximum value. Setting `max <= 0` forces indeterminate."
28959 },
28960 {
28961 name: "indeterminate",
28962 type: "boolean",
28963 description: "Show the sweeping indeterminate animation instead of a value-driven fill. The `value` attribute is ignored while this is set."
28964 },
28965 {
28966 name: "tone",
28967 type: '"default" | "success" | "warning" | "danger"',
28968 default: "default",
28969 description: "Tints the fill from the shared status palette."
28970 },
28971 {
28972 name: "label",
28973 type: "string",
28974 description: "Optional inline label rendered above the track. Also wired into `aria-label` when set."
28975 },
28976 {
28977 name: "show-percent",
28978 type: "boolean",
28979 description: "Render a right-aligned percent readout next to the label. Only meaningful in determinate mode."
28980 }
28981 ],
28982 cssProps: [
28983 {
28984 name: "--wpd-progress-track-bg",
28985 default: "var(--desktop-mode-control-bg, rgba(0,0,0,0.08))"
28986 },
28987 {
28988 name: "--wpd-progress-fill",
28989 default: "var(--wp-admin-theme-color, #2271b1)"
28990 },
28991 { name: "--wpd-progress-height", default: "6px" },
28992 { name: "--wpd-progress-radius", default: "999px" },
28993 { name: "--wpd-progress-label-color", default: "inherit" },
28994 { name: "--wpd-progress-label-size", default: "12px" },
28995 { name: "--wpd-progress-label-gap", default: "4px" }
28996 ],
28997 example: html`<wpd-progress-bar
28998 value="42"
28999 label="Uploading hero.jpg"
29000 show-percent
29001 ></wpd-progress-bar>`
29002 };
29003 let WpdProgressBar = _WpdProgressBar;
29004 defineComponent("wpd-progress-bar", WpdProgressBar);
29005 const ROWS = /* @__PURE__ */ new Map();
29006 let panel = null;
29007 function mountUploadProgressHud() {
29008 if (document.body.hasAttribute("data-desktop-mode-suppress-upload-hud")) {
29009 return;
29010 }
29011 if (window.__wpdUploadHud) {
29012 return;
29013 }
29014 window.__wpdUploadHud = true;
29015 const ns = "desktop-mode/os-file-drop-hud";
29016 addAction(
29017 FILE_DROP_HOOKS.UPLOAD_STARTED,
29018 ns,
29019 (payload) => onStarted(payload.file, payload.fields, payload.abort)
29020 );
29021 addAction(
29022 FILE_DROP_HOOKS.UPLOAD_PROGRESS,
29023 ns,
29024 (payload) => onProgress(
29025 payload.file,
29026 payload.loaded,
29027 payload.total,
29028 payload.indeterminate
29029 )
29030 );
29031 addAction(
29032 FILE_DROP_HOOKS.AFTER_UPLOAD,
29033 ns,
29034 (payload) => onComplete(payload.file, payload.fields, payload.result)
29035 );
29036 addAction(
29037 FILE_DROP_HOOKS.UPLOAD_FAILED,
29038 ns,
29039 (payload) => onFailed(payload.file, payload.error)
29040 );
29041 }
29042 function onStarted(file, fields, abort) {
29043 const p = ensurePanel();
29044 const row = document.createElement("div");
29045 row.className = "desktop-mode-upload-hud__row";
29046 const meta = document.createElement("div");
29047 meta.className = "desktop-mode-upload-hud__meta";
29048 const name = document.createElement("div");
29049 name.className = "desktop-mode-upload-hud__name";
29050 name.textContent = fields.filename || file.name;
29051 name.title = fields.filename || file.name;
29052 const statusEl = document.createElement("div");
29053 statusEl.className = "desktop-mode-upload-hud__status";
29054 statusEl.textContent = "Uploading…";
29055 meta.append(name, statusEl);
29056 const bar = document.createElement("wpd-progress-bar");
29057 bar.setAttribute("indeterminate", "");
29058 bar.setAttribute("show-percent", "");
29059 const actions = document.createElement("div");
29060 actions.className = "desktop-mode-upload-hud__actions";
29061 const cancelBtn = document.createElement("wpd-button");
29062 cancelBtn.setAttribute("variant", "tertiary");
29063 cancelBtn.setAttribute("size", "small");
29064 cancelBtn.textContent = "Cancel";
29065 cancelBtn.addEventListener("click", () => {
29066 const r = ROWS.get(file);
29067 if (!r) {
29068 return;
29069 }
29070 if (r.state === "running") {
29071 r.statusEl.textContent = "Cancelling…";
29072 r.cancelBtn.disabled = true;
29073 r.abort();
29074 } else {
29075 dismissRow(r);
29076 }
29077 });
29078 actions.appendChild(cancelBtn);
29079 row.append(meta, bar, actions);
29080 p.querySelector(".desktop-mode-upload-hud__list").appendChild(row);
29081 ROWS.set(file, {
29082 file,
29083 abort,
29084 root: row,
29085 bar,
29086 statusEl,
29087 cancelBtn,
29088 state: "running",
29089 lingerTimer: null
29090 });
29091 updateHeader();
29092 }
29093 function onProgress(file, loaded, total, indeterminate) {
29094 const r = ROWS.get(file);
29095 if (!r || r.state !== "running") {
29096 return;
29097 }
29098 if (indeterminate || total <= 0) {
29099 r.bar.setAttribute("indeterminate", "");
29100 r.statusEl.textContent = `${formatBytes(loaded)} sent`;
29101 } else {
29102 r.bar.removeAttribute("indeterminate");
29103 r.bar.setAttribute("max", String(total));
29104 r.bar.setAttribute("value", String(loaded));
29105 r.statusEl.textContent = `${formatBytes(loaded)} / ${formatBytes(total)}`;
29106 }
29107 }
29108 function onComplete(file, fields, result) {
29109 const r = ROWS.get(file);
29110 if (!r) {
29111 return;
29112 }
29113 r.state = "success";
29114 r.bar.removeAttribute("indeterminate");
29115 r.bar.setAttribute("value", "100");
29116 r.bar.setAttribute("max", "100");
29117 r.bar.setAttribute("tone", "success");
29118 r.statusEl.textContent = "Uploaded";
29119 r.cancelBtn.textContent = "Dismiss";
29120 r.lingerTimer = setTimeout(() => dismissRow(r), 2500);
29121 updateHeader();
29122 activity.publish("desktop-mode/upload-hud-complete", {
29123 filename: fields.filename || result.filename,
29124 attachmentId: result.id
29125 });
29126 }
29127 function onFailed(file, error) {
29128 const r = ROWS.get(file);
29129 if (!r) {
29130 return;
29131 }
29132 r.bar.removeAttribute("indeterminate");
29133 r.bar.setAttribute("tone", "danger");
29134 r.cancelBtn.textContent = "Dismiss";
29135 r.cancelBtn.disabled = false;
29136 if (error.name === "UploadAbortedError") {
29137 r.state = "aborted";
29138 r.statusEl.textContent = "Cancelled";
29139 } else {
29140 r.state = "failed";
29141 r.statusEl.textContent = error.message || "Upload failed";
29142 }
29143 updateHeader();
29144 }
29145 function dismissRow(r) {
29146 if (r.lingerTimer) {
29147 clearTimeout(r.lingerTimer);
29148 }
29149 ROWS.delete(r.file);
29150 r.root.remove();
29151 updateHeader();
29152 if (ROWS.size === 0 && panel) {
29153 panel.hidden = true;
29154 }
29155 }
29156 function ensurePanel() {
29157 if (panel && panel.isConnected) {
29158 panel.hidden = false;
29159 return panel;
29160 }
29161 const p = document.createElement("div");
29162 p.className = "desktop-mode-upload-hud";
29163 p.setAttribute("role", "region");
29164 p.setAttribute("aria-label", "Uploads");
29165 const header = document.createElement("div");
29166 header.className = "desktop-mode-upload-hud__header";
29167 const title = document.createElement("div");
29168 title.className = "desktop-mode-upload-hud__title";
29169 title.textContent = "Uploads";
29170 const closeBtn = document.createElement("button");
29171 closeBtn.type = "button";
29172 closeBtn.className = "desktop-mode-upload-hud__close";
29173 closeBtn.setAttribute("aria-label", "Hide upload panel");
29174 closeBtn.textContent = "×";
29175 closeBtn.addEventListener("click", () => {
29176 for (const r of [...ROWS.values()]) {
29177 if (r.state !== "running") {
29178 dismissRow(r);
29179 }
29180 }
29181 if (ROWS.size === 0) {
29182 p.hidden = true;
29183 }
29184 });
29185 header.append(title, closeBtn);
29186 const list2 = document.createElement("div");
29187 list2.className = "desktop-mode-upload-hud__list";
29188 p.append(header, list2);
29189 document.body.appendChild(p);
29190 panel = p;
29191 return p;
29192 }
29193 function updateHeader() {
29194 if (!panel) {
29195 return;
29196 }
29197 const title = panel.querySelector(
29198 ".desktop-mode-upload-hud__title"
29199 );
29200 if (!title) {
29201 return;
29202 }
29203 const total = ROWS.size;
29204 const running = [...ROWS.values()].filter((r) => r.state === "running").length;
29205 if (running > 0) {
29206 title.textContent = running === total ? `Uploading ${running} file${running === 1 ? "" : "s"}…` : `${running} of ${total} uploading…`;
29207 } else if (total > 0) {
29208 title.textContent = `Uploads (${total})`;
29209 } else {
29210 title.textContent = "Uploads";
29211 }
29212 }
29213 function mountMediaLibraryRefresher() {
29214 if (document.body.hasAttribute(
29215 "data-desktop-mode-suppress-media-library-refresh"
29216 )) {
29217 return;
29218 }
29219 const sentinel = window;
29220 if (sentinel.__wpdMediaLibraryRefresher) {
29221 return;
29222 }
29223 sentinel.__wpdMediaLibraryRefresher = true;
29224 addAction(
29225 FILE_DROP_HOOKS.AFTER_UPLOAD,
29226 "desktop-mode/os-file-drop-library-refresh",
29227 () => refreshOpenLibraries()
29228 );
29229 }
29230 function refreshOpenLibraries() {
29231 const iframes = document.querySelectorAll("iframe");
29232 for (const frame of Array.from(iframes)) {
29233 if (!isMediaLibraryUrl(resolveIframeUrl(frame))) {
29234 continue;
29235 }
29236 try {
29237 frame.contentWindow?.location.reload();
29238 } catch {
29239 const reloadHref = resolveIframeUrl(frame);
29240 if (reloadHref) {
29241 frame.setAttribute("src", reloadHref);
29242 }
29243 }
29244 }
29245 }
29246 function resolveIframeUrl(frame) {
29247 try {
29248 return frame.contentWindow?.location.href ?? frame.src ?? "";
29249 } catch {
29250 return frame.src ?? "";
29251 }
29252 }
29253 function isMediaLibraryUrl(url) {
29254 if (!url) {
29255 return false;
29256 }
29257 return /\/wp-admin\/upload\.php(?:[?#]|$)/.test(url);
29258 }
29259 function bootOsFileDrop(args) {
29260 const config = args.config || {
29261 enabled: false,
29262 allowedMimes: [],
29263 maxSize: 0
29264 };
29265 mountUploadProgressHud();
29266 mountMediaLibraryRefresher();
29267 mountOsFileDropManager({
29268 config,
29269 mediaUrl: args.mediaUrl,
29270 restNonce: args.restNonce,
29271 openDialog: async (entries, ctx) => {
29272 const { openUploadDialog: openUploadDialog2 } = await Promise.resolve().then(() => dialog);
29273 await openUploadDialog2({
29274 entries,
29275 context: ctx,
29276 mediaUrl: args.mediaUrl,
29277 restNonce: args.restNonce
29278 });
29279 }
29280 });
29281 }
29282 const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
29283 __proto__: null,
29284 FILE_DROP_HOOKS,
29285 bootOsFileDrop
29286 }, Symbol.toStringTag, { value: "Module" }));
29287 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}`;
29288 const _WpdTextField = class _WpdTextField extends Component {
29289 constructor() {
29290 super(...arguments);
29291 this._revealed = false;
29292 }
29293 connectedCallback() {
29294 super.connectedCallback();
29295 ensureAutoId(this);
29296 }
29297 render() {
29298 const label = this.label || "";
29299 const value = this.value ?? "";
29300 const placeholder = this.placeholder || "";
29301 const disabled = this.disabled !== null;
29302 const readonly = this.readonly !== null;
29303 const declaredAutocomplete = this.autocomplete;
29304 const declaredType = this.type || "text";
29305 const isPassword = declaredType === "password";
29306 let autocomplete = declaredAutocomplete || "off";
29307 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
29308 autocomplete = "new-password";
29309 }
29310 const maxLength = this.maxlength;
29311 const minLength = this.minlength;
29312 const pattern = this.pattern || "";
29313 const name = this.name || "";
29314 const suffix = this.suffix || "";
29315 const invalid = this.invalid !== null;
29316 const reveal = this.reveal !== null;
29317 const isPasswordIntent = declaredType === "password";
29318 const isMasked = isPasswordIntent && !(reveal && this._revealed);
29319 let effectiveType;
29320 if (isPasswordIntent) {
29321 effectiveType = "text";
29322 } else if (reveal && this._revealed) {
29323 effectiveType = "text";
29324 } else {
29325 effectiveType = declaredType;
29326 }
29327 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
29328 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
29329 const hostId = this.id || "wpd-unnamed";
29330 const inputId = `${hostId}__input`;
29331 return html`
29332 ${label ? html`<label
29333 class="wpd-text-field__label"
29334 for=${inputId}
29335 >${label}</label>` : html``}
29336 <span class=${rowClass}>
29337 <input
29338 id=${inputId}
29339 class=${inputClass}
29340 type=${effectiveType}
29341 .value=${value}
29342 placeholder=${placeholder}
29343 ?disabled=${disabled}
29344 ?readonly=${readonly}
29345 autocomplete=${autocomplete}
29346 maxlength=${maxLength ?? ""}
29347 minlength=${minLength ?? ""}
29348 pattern=${pattern}
29349 name=${name}
29350 aria-invalid=${invalid ? "true" : "false"}
29351 aria-label=${label || ""}
29352 @input=${(e) => this._onInput(e)}
29353 @change=${(e) => this._onChange(e)}
29354 @keydown=${(e) => this._onKeyDown(e)}
29355 />
29356 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
29357 ${reveal ? this._renderRevealButton(disabled) : html``}
29358 </span>
29359 `;
29360 }
29361 _renderRevealButton(disabled) {
29362 const label = this._revealed ? "Hide" : "Show";
29363 return html`
29364 <button
29365 type="button"
29366 class="wpd-text-field__reveal"
29367 aria-label=${label}
29368 aria-pressed=${this._revealed ? "true" : "false"}
29369 ?disabled=${disabled}
29370 tabindex="0"
29371 @click=${() => this._onToggleReveal()}
29372 >
29373 ${this._revealed ? _iconEyeOff() : _iconEye()}
29374 </button>
29375 `;
29376 }
29377 _onToggleReveal() {
29378 this._revealed = !this._revealed;
29379 this.requestUpdate();
29380 }
29381 _onInput(e) {
29382 const input = e.target;
29383 this.value = input.value;
29384 this.emit("wpd-input-change", { value: input.value });
29385 }
29386 _onChange(e) {
29387 const input = e.target;
29388 this.emit("wpd-input-commit", { value: input.value });
29389 }
29390 _onKeyDown(e) {
29391 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
29392 const input = e.target;
29393 this.emit("wpd-submit", { value: input.value });
29394 }
29395 }
29396 };
29397 _WpdTextField.props = [
29398 "label",
29399 "value",
29400 "placeholder",
29401 "disabled",
29402 "readonly",
29403 "autocomplete",
29404 "type",
29405 "maxlength",
29406 "minlength",
29407 "pattern",
29408 "name",
29409 "suffix",
29410 "invalid",
29411 "reveal"
29412 ];
29413 _WpdTextField.styles = [textFieldStyles];
29414 _WpdTextField.help = {
29415 title: "Text field",
29416 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.",
29417 status: "stable",
29418 since: "0.11.0",
29419 props: [
29420 { name: "label", type: "string", description: "Visible label above the input." },
29421 { name: "value", type: "string", description: "Current input value; reflected two-way." },
29422 { name: "placeholder", type: "string", description: "Native placeholder string." },
29423 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
29424 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
29425 {
29426 name: "autocomplete",
29427 type: "string",
29428 default: "off",
29429 description: "Forwarded to the native input autocomplete attribute."
29430 },
29431 {
29432 name: "type",
29433 type: "string",
29434 default: "text",
29435 description: "Native input type (text, password, email, search, tel, url)."
29436 },
29437 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
29438 { name: "minlength", type: "integer (string)", description: "Native minlength." },
29439 { name: "pattern", type: "regex string", description: "Native validation pattern." },
29440 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
29441 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
29442 {
29443 name: "invalid",
29444 type: "boolean attribute",
29445 description: "Marks the field aria-invalid and applies the error style."
29446 },
29447 {
29448 name: "reveal",
29449 type: "boolean attribute",
29450 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
29451 }
29452 ],
29453 events: [
29454 {
29455 name: "wpd-input-change",
29456 description: "Fires on every input keystroke.",
29457 detail: "{ value: string }"
29458 },
29459 {
29460 name: "wpd-input-commit",
29461 description: "Fires on the native change event (blur / Enter).",
29462 detail: "{ value: string }"
29463 },
29464 {
29465 name: "wpd-submit",
29466 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
29467 detail: "{ value: string }"
29468 }
29469 ],
29470 cssProps: [
29471 { name: "--desktop-mode-text", description: "Text colour." },
29472 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
29473 { name: "--desktop-mode-border", description: "Input outline." },
29474 { name: "--desktop-mode-window-bg", description: "Input background." }
29475 ],
29476 example: html`
29477 <wpd-stack gap="8">
29478 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
29479 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
29480 </wpd-stack>
29481 `
29482 };
29483 let WpdTextField = _WpdTextField;
29484 defineComponent("wpd-text-field", WpdTextField);
29485 function _iconEye() {
29486 return html`
29487 <svg
29488 viewBox="0 0 16 16"
29489 width="14"
29490 height="14"
29491 fill="none"
29492 stroke="currentColor"
29493 stroke-width="1.5"
29494 stroke-linecap="round"
29495 stroke-linejoin="round"
29496 aria-hidden="true"
29497 focusable="false"
29498 >
29499 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
29500 <circle cx="8" cy="8" r="2" />
29501 </svg>
29502 `;
29503 }
29504 function _iconEyeOff() {
29505 return html`
29506 <svg
29507 viewBox="0 0 16 16"
29508 width="14"
29509 height="14"
29510 fill="none"
29511 stroke="currentColor"
29512 stroke-width="1.5"
29513 stroke-linecap="round"
29514 stroke-linejoin="round"
29515 aria-hidden="true"
29516 focusable="false"
29517 >
29518 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
29519 <circle cx="8" cy="8" r="2" />
29520 <line x1="2" y1="2" x2="14" y2="14" />
29521 </svg>
29522 `;
29523 }
29524 async function uploadFile(args) {
29525 const initial = {
29526 file: args.file,
29527 mime: args.mime,
29528 fields: args.fields
29529 };
29530 const filtered = applyFilters(
29531 FILE_DROP_HOOKS.BEFORE_UPLOAD,
29532 initial,
29533 args.context
29534 );
29535 if (!filtered) {
29536 throw new UploadCancelledError();
29537 }
29538 const body = new FormData();
29539 const renamed = filtered.fields.filename !== filtered.file.name ? new File([filtered.file], filtered.fields.filename, {
29540 type: filtered.mime || filtered.file.type
29541 }) : filtered.file;
29542 body.append("file", renamed);
29543 body.append("title", filtered.fields.title);
29544 body.append("alt_text", filtered.fields.altText);
29545 body.append("caption", filtered.fields.caption);
29546 body.append("description", filtered.fields.description);
29547 return new Promise((resolve2, reject) => {
29548 const xhr = new XMLHttpRequest();
29549 xhr.open("POST", args.mediaUrl, true);
29550 xhr.withCredentials = true;
29551 xhr.setRequestHeader("X-WP-Nonce", args.restNonce);
29552 xhr.responseType = "text";
29553 let aborted = false;
29554 let bodyFullySent = false;
29555 let cancelRequested = false;
29556 const abort = () => {
29557 cancelRequested = true;
29558 if (bodyFullySent) {
29559 return;
29560 }
29561 aborted = true;
29562 try {
29563 xhr.abort();
29564 } catch {
29565 }
29566 };
29567 doAction(FILE_DROP_HOOKS.UPLOAD_STARTED, {
29568 file: filtered.file,
29569 fields: filtered.fields,
29570 context: args.context,
29571 abort
29572 });
29573 xhr.upload.addEventListener("progress", (e) => {
29574 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
29575 file: filtered.file,
29576 fields: filtered.fields,
29577 context: args.context,
29578 loaded: e.loaded,
29579 total: e.lengthComputable ? e.total : 0,
29580 indeterminate: !e.lengthComputable
29581 });
29582 });
29583 xhr.upload.addEventListener("load", () => {
29584 bodyFullySent = true;
29585 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
29586 file: filtered.file,
29587 fields: filtered.fields,
29588 context: args.context,
29589 loaded: filtered.file.size,
29590 total: filtered.file.size,
29591 indeterminate: false
29592 });
29593 });
29594 xhr.addEventListener("error", () => {
29595 if (aborted) {
29596 return;
29597 }
29598 const error = new Error("Network error during upload.");
29599 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29600 // `filtered.file` — same identity as UPLOAD_STARTED /
29601 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
29602 // that swapped the File would otherwise route this
29603 // failure to a row keyed by the original (pre-swap)
29604 // File, leaving the HUD row stuck in "running".
29605 file: filtered.file,
29606 error,
29607 context: args.context
29608 });
29609 reject(error);
29610 });
29611 xhr.addEventListener("abort", () => {
29612 const error = new UploadAbortedError();
29613 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29614 // `filtered.file` — same identity as UPLOAD_STARTED /
29615 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
29616 // that swapped the File would otherwise route this
29617 // failure to a row keyed by the original (pre-swap)
29618 // File, leaving the HUD row stuck in "running".
29619 file: filtered.file,
29620 error,
29621 context: args.context
29622 });
29623 reject(error);
29624 });
29625 xhr.addEventListener("load", () => {
29626 if (aborted) {
29627 return;
29628 }
29629 if (xhr.status < 200 || xhr.status >= 300) {
29630 const message = extractXhrMessage(xhr);
29631 const error = new Error(message);
29632 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29633 file: filtered.file,
29634 error,
29635 context: args.context
29636 });
29637 reject(error);
29638 return;
29639 }
29640 let data;
29641 try {
29642 data = JSON.parse(xhr.responseText);
29643 } catch (err) {
29644 const error = err instanceof Error ? err : new Error("Could not parse server response.");
29645 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29646 file: filtered.file,
29647 error,
29648 context: args.context
29649 });
29650 reject(error);
29651 return;
29652 }
29653 if (cancelRequested && data.id) {
29654 void deleteAttachment(
29655 args.mediaUrl,
29656 args.restNonce,
29657 data.id
29658 );
29659 const error = new UploadAbortedError();
29660 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29661 file: filtered.file,
29662 error,
29663 context: args.context
29664 });
29665 reject(error);
29666 return;
29667 }
29668 const result = {
29669 id: data.id,
29670 url: data.source_url,
29671 mime: data.mime_type || filtered.mime,
29672 title: data.title?.rendered || filtered.fields.title,
29673 filename: data.media_details?.file || filtered.fields.filename
29674 };
29675 doAction(FILE_DROP_HOOKS.AFTER_UPLOAD, {
29676 file: filtered.file,
29677 result,
29678 fields: filtered.fields,
29679 context: args.context
29680 });
29681 resolve2(result);
29682 });
29683 xhr.send(body);
29684 });
29685 }
29686 class UploadCancelledError extends Error {
29687 constructor() {
29688 super("Upload cancelled by desktop-mode.drop.before-upload filter.");
29689 this.name = "UploadCancelledError";
29690 }
29691 }
29692 class UploadAbortedError extends Error {
29693 constructor() {
29694 super("Upload aborted by the caller.");
29695 this.name = "UploadAbortedError";
29696 }
29697 }
29698 function deleteAttachment(mediaUrl, restNonce, id) {
29699 const url = `${mediaUrl.replace(/\/$/, "")}/${id}?force=true`;
29700 const cleanup = new XMLHttpRequest();
29701 cleanup.open("DELETE", url, true);
29702 cleanup.withCredentials = true;
29703 cleanup.setRequestHeader("X-WP-Nonce", restNonce);
29704 return new Promise((resolve2) => {
29705 cleanup.addEventListener("loadend", () => {
29706 if (cleanup.status < 200 || cleanup.status >= 300) {
29707 console.warn(
29708 `[os-file-drop] late-cancel cleanup failed for attachment ${id} (HTTP ${cleanup.status}). The attachment remains in the Media Library; delete it manually.`
29709 );
29710 }
29711 resolve2();
29712 });
29713 cleanup.addEventListener("error", () => {
29714 console.warn(
29715 `[os-file-drop] late-cancel cleanup network error for attachment ${id}. The attachment remains in the Media Library; delete it manually.`
29716 );
29717 resolve2();
29718 });
29719 try {
29720 cleanup.send();
29721 } catch (err) {
29722 console.warn(
29723 `[os-file-drop] late-cancel cleanup could not be dispatched for attachment ${id}:`,
29724 err
29725 );
29726 resolve2();
29727 }
29728 });
29729 }
29730 function extractXhrMessage(xhr) {
29731 const fallback = `Upload failed (HTTP ${xhr.status}).`;
29732 const text = xhr.responseText;
29733 if (!text) {
29734 return fallback;
29735 }
29736 try {
29737 const data = JSON.parse(text);
29738 if (data && typeof data.message === "string") {
29739 return data.message;
29740 }
29741 } catch {
29742 }
29743 return fallback;
29744 }
29745 async function openUploadDialog(args) {
29746 if (args.entries.length === 0) {
29747 return;
29748 }
29749 const modal = document.createElement("wpd-modal");
29750 modal.setAttribute("open", "");
29751 modal.setAttribute("size", "md");
29752 modal.setAttribute(
29753 "title",
29754 args.entries.length === 1 ? "Upload to Media Library" : `Upload ${args.entries.length} files to Media Library`
29755 );
29756 document.body.appendChild(modal);
29757 const draft = args.entries.map((entry) => ({
29758 ...entry.fields
29759 }));
29760 const renderBody = () => {
29761 modal.innerHTML = "";
29762 const list2 = document.createElement("div");
29763 list2.style.cssText = "display:flex;flex-direction:column;gap:18px;max-height:60vh;overflow:auto;padding-right:6px;";
29764 args.entries.forEach((entry, i) => {
29765 list2.appendChild(renderEntry(entry, draft[i], i + 1));
29766 });
29767 modal.appendChild(list2);
29768 const footer = document.createElement("div");
29769 footer.setAttribute("slot", "footer");
29770 footer.style.cssText = "display:flex;gap:8px;justify-content:flex-end;";
29771 const cancel = document.createElement("wpd-button");
29772 cancel.setAttribute("variant", "secondary");
29773 cancel.textContent = "Cancel";
29774 cancel.addEventListener("click", () => {
29775 modal.remove();
29776 });
29777 const upload = document.createElement("wpd-button");
29778 upload.setAttribute("variant", "primary");
29779 upload.textContent = args.entries.length === 1 ? "Upload" : `Upload ${args.entries.length} files`;
29780 upload.addEventListener("click", () => {
29781 void runUploads(upload, cancel);
29782 });
29783 footer.appendChild(cancel);
29784 footer.appendChild(upload);
29785 modal.appendChild(footer);
29786 };
29787 const renderEntry = (entry, fields, index2) => {
29788 const wrap = document.createElement("div");
29789 wrap.style.cssText = "display:flex;flex-direction:column;gap:8px;border:1px solid rgba(255,255,255,0.08);border-radius:10px;padding:14px;";
29790 const heading = document.createElement("div");
29791 heading.style.cssText = "display:flex;gap:10px;align-items:center;font-weight:600;";
29792 const tag = document.createElement("span");
29793 tag.textContent = args.entries.length === 1 ? "" : `#${index2} · `;
29794 tag.style.opacity = "0.6";
29795 const fname = document.createElement("span");
29796 fname.textContent = entry.file.name;
29797 fname.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
29798 const size = document.createElement("span");
29799 size.textContent = `${entry.mime || "unknown"} · ${formatBytes(
29800 entry.file.size
29801 )}`;
29802 size.style.cssText = "opacity:0.6;font-size:12px;";
29803 heading.appendChild(tag);
29804 heading.appendChild(fname);
29805 heading.appendChild(size);
29806 wrap.appendChild(heading);
29807 wrap.appendChild(textField("Title", fields.title, (v) => fields.title = v));
29808 wrap.appendChild(textField("Filename", fields.filename, (v) => fields.filename = v));
29809 if (entry.mime.startsWith("image/")) {
29810 wrap.appendChild(
29811 textField("Alt text", fields.altText, (v) => fields.altText = v)
29812 );
29813 }
29814 wrap.appendChild(textField("Caption", fields.caption, (v) => fields.caption = v));
29815 wrap.appendChild(
29816 textareaField("Description", fields.description, (v) => fields.description = v)
29817 );
29818 return wrap;
29819 };
29820 const runUploads = async (uploadBtn, cancelBtn) => {
29821 uploadBtn.disabled = true;
29822 cancelBtn.disabled = true;
29823 uploadBtn.textContent = "Uploading…";
29824 const total = args.entries.length;
29825 let successes = 0;
29826 let failures = 0;
29827 let cancelled = 0;
29828 const failureDetails = [];
29829 for (let i = 0; i < total; i++) {
29830 const entry = args.entries[i];
29831 try {
29832 await uploadFile({
29833 file: entry.file,
29834 mime: entry.mime,
29835 fields: draft[i],
29836 context: args.context,
29837 mediaUrl: args.mediaUrl,
29838 restNonce: args.restNonce
29839 });
29840 successes++;
29841 } catch (err) {
29842 if (err instanceof UploadCancelledError) {
29843 cancelled++;
29844 continue;
29845 }
29846 if (err instanceof UploadAbortedError) {
29847 cancelled++;
29848 continue;
29849 }
29850 failures++;
29851 const message = err instanceof Error ? err.message : "Upload failed.";
29852 failureDetails.push(`“${entry.file.name}” — ${message}`);
29853 }
29854 }
29855 modal.remove();
29856 showBatchSummaryToast({
29857 total,
29858 successes,
29859 failures,
29860 cancelled,
29861 failureDetails
29862 });
29863 };
29864 renderBody();
29865 await new Promise((resolve2) => {
29866 modal.addEventListener("wpd-modal-cancel", () => {
29867 modal.remove();
29868 resolve2();
29869 });
29870 const observer = new MutationObserver(() => {
29871 if (!modal.isConnected) {
29872 observer.disconnect();
29873 resolve2();
29874 }
29875 });
29876 observer.observe(document.body, { childList: true, subtree: true });
29877 });
29878 }
29879 function textField(label, value, onChange) {
29880 const el = document.createElement("wpd-text-field");
29881 el.setAttribute("label", label);
29882 el.setAttribute("value", value);
29883 el.addEventListener("input", () => {
29884 const v = el.value;
29885 if (typeof v === "string") {
29886 onChange(v);
29887 }
29888 });
29889 return el;
29890 }
29891 function textareaField(label, value, onChange) {
29892 const el = document.createElement("wpd-textarea");
29893 el.setAttribute("label", label);
29894 el.setAttribute("value", value);
29895 el.setAttribute("rows", "3");
29896 el.addEventListener("input", () => {
29897 const v = el.value;
29898 if (typeof v === "string") {
29899 onChange(v);
29900 }
29901 });
29902 return el;
29903 }
29904 function showBatchSummaryToast(args) {
29905 const { total, successes, failures, cancelled, failureDetails } = args;
29906 if (total === 0) {
29907 return;
29908 }
29909 if (total === 1) {
29910 if (successes === 1) {
29911 showToast({ message: "Uploaded to Media Library." });
29912 } else if (failures === 1 && failureDetails[0]) {
29913 showToast({ message: failureDetails[0] });
29914 } else if (cancelled === 1) {
29915 showToast({ message: "Upload cancelled." });
29916 }
29917 return;
29918 }
29919 if (successes === total) {
29920 showToast({
29921 message: `Uploaded ${successes} files to Media Library.`
29922 });
29923 return;
29924 }
29925 if (cancelled === total) {
29926 showToast({ message: "All uploads cancelled." });
29927 return;
29928 }
29929 if (failures === total) {
29930 showToast({
29931 message: failures === 1 && failureDetails[0] ? failureDetails[0] : `${failures} uploads failed.`
29932 });
29933 return;
29934 }
29935 const parts = [];
29936 if (successes > 0) {
29937 parts.push(
29938 `Uploaded ${successes} file${successes === 1 ? "" : "s"}.`
29939 );
29940 }
29941 if (cancelled > 0) {
29942 parts.push(`Cancelled ${cancelled}.`);
29943 }
29944 if (failures > 0) {
29945 parts.push(`Failed ${failures}.`);
29946 }
29947 showToast({ message: parts.join(" ") });
29948 }
29949 const dialog = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
29950 __proto__: null,
29951 openUploadDialog
29952 }, Symbol.toStringTag, { value: "Module" }));
29953 exports.clampGeometryToViewport = clampGeometryToViewport;
29954 Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
29955 return exports;
29956 }({});
29957