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

30,165 lines 985.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 /**
345 * Action, fires when a window is minimized. Payload:
346 * `{ windowId: string, element: HTMLElement }`.
347 *
348 * The element ride-along matches {@link WINDOW_CLOSING}'s shape so
349 * wallpaper plugins anchored to window tops (snow, leaves, rain
350 * splash) can match stuck particles by element identity and run
351 * their teardown — minimized windows render at `opacity: 0` so
352 * `offsetParent === null` checks miss them.
353 */
354 WINDOW_MINIMIZED: "desktop-mode.window.minimized",
355 /**
356 * Action, fires when a window is restored from minimized. Payload:
357 * `{ windowId: string, element: HTMLElement }`.
358 */
359 WINDOW_RESTORED: "desktop-mode.window.restored",
360 /**
361 * Action, fires when a window is maximized (fills desktop area).
362 * Payload: `{ windowId: string, element: HTMLElement }`.
363 */
364 WINDOW_MAXIMIZED: "desktop-mode.window.maximized",
365 /**
366 * Action, fires when a window exits maximized state. Payload:
367 * `{ windowId: string, element: HTMLElement }`.
368 */
369 WINDOW_UNMAXIMIZED: "desktop-mode.window.unmaximized",
370 /**
371 * Action, fires when a window enters fullscreen / focus mode.
372 * Payload: `{ windowId: string, element: HTMLElement }`.
373 */
374 WINDOW_FULLSCREEN_ENTERED: "desktop-mode.window.fullscreen-entered",
375 /**
376 * Action, fires when a window exits fullscreen / focus mode.
377 * Payload: `{ windowId: string, element: HTMLElement }`.
378 */
379 WINDOW_FULLSCREEN_EXITED: "desktop-mode.window.fullscreen-exited",
380 /**
381 * Filter, decides whether a fullscreen ("focus mode") window
382 * should auto-exit when focus moves to a different window.
383 *
384 * Default is `true` so a newly-focused window is never silently
385 * occluded by a fullscreen one (its `z-index` sits above all
386 * other windows). Plugins whose fullscreen surface is meant to
387 * persist across focus changes — slideshows, video players,
388 * immersive games — can return `false` to keep their window
389 * fullscreen.
390 *
391 * Signature:
392 *
393 * ( shouldExit: boolean, ctx: {
394 * windowId: string, // the fullscreen window
395 * focusedTo: string, // the window gaining focus
396 * } ) => boolean
397 *
398 * @since 0.8.6
399 */
400 WINDOW_AUTO_EXIT_FULLSCREEN: "desktop-mode.window.auto-exit-fullscreen",
401 /**
402 * Action, fires at most once per animation frame during an
403 * active drag or resize with the live geometry. Payload: `{
404 * windowId: string, x: number, y: number, width: number,
405 * height: number, state: WindowState, phase: 'drag' | 'resize' }`.
406 *
407 * Intended for per-frame collision-aware wallpapers (snow piling
408 * on window tops, rain splash on edges) that would otherwise
409 * poll `getBoundingClientRect` every rAF. Coalesced via
410 * `requestAnimationFrame` so a pointermove storm collapses to
411 * one fire per paint — matches the cadence a wallpaper's own
412 * ticker runs at.
413 *
414 * NOT fired at drag/resize end — `WINDOW_DRAG_END` /
415 * `WINDOW_RESIZE_END` handle the settled geometry. Subscribers
416 * that only want the final position should listen to those
417 * instead.
418 */
419 WINDOW_BOUNDS_CHANGED: "desktop-mode.window.bounds-changed",
420 /** Action, fires at drag-end with the final `{ x, y }` position. */
421 WINDOW_MOVED: "desktop-mode.window.moved",
422 /** Action, fires at resize-end with the final `{ width, height }`. */
423 WINDOW_RESIZED: "desktop-mode.window.resized",
424 /** Action, fires when title-bar drag begins. */
425 WINDOW_DRAG_START: "desktop-mode.window.drag-start",
426 /** Action, fires when title-bar drag ends. Payload mirrors WINDOW_MOVED. */
427 WINDOW_DRAG_END: "desktop-mode.window.drag-end",
428 /** Action, fires when the resize handle is first pressed. */
429 WINDOW_RESIZE_START: "desktop-mode.window.resize-start",
430 /** Action, fires when resize completes. Payload mirrors WINDOW_RESIZED. */
431 WINDOW_RESIZE_END: "desktop-mode.window.resize-end",
432 /** Action, fires when the user "detaches" a window to a classic tab. */
433 WINDOW_DETACHED: "desktop-mode.window.detached",
434 /**
435 * Action, fires when the user clicks the title-bar reload button
436 * on an iframe-backed window. Payload: `{ windowId: string, url:
437 * string }` where `url` is the URL being reloaded (the active
438 * primary or external sub-tab). Subscribers can use this to
439 * invalidate their own cache, force a save before navigation,
440 * track usage as a UX signal, or sync state across companion
441 * surfaces. Native windows do not fire this — they own their
442 * DOM directly and the reload button doesn't apply.
443 */
444 WINDOW_RELOADED: "desktop-mode.window.reloaded",
445 /** Action, fires when iframe title updates change the window title. */
446 WINDOW_TITLE_CHANGED: "desktop-mode.window.title-changed",
447 /**
448 * Action, fires when a window's `setHighlight()` mode changes.
449 * Payload: `{ windowId: string, mode: 'preview' | 'persistent' | null,
450 * color?: string }`. Lets onboarding / guidance / drag-bridge
451 * plugins react when another module flagged one of their
452 * windows as the focus of a multi-step interaction without
453 * having to observe DOM mutations.
454 *
455 * @since 0.24.0
456 */
457 WINDOW_HIGHLIGHT_CHANGED: "desktop-mode.window.highlight-changed",
458 /**
459 * Action, fires when a window's body element's dimensions
460 * change — mount, user resize, viewport reflow. Payload: `{
461 * windowId: string, width: number, height: number }`. Body
462 * dimensions exclude the title bar + tab strip, matching what a
463 * canvas or layout engine inside the body would measure.
464 */
465 WINDOW_BODY_RESIZED: "desktop-mode.window.body-resized",
466 // ------------------------------------------------------------------
467 // Native-window lifecycle. These fire ONLY for windows constructed
468 // with `native: true` — iframe windows have no render phase to
469 // intercept. Use them to wrap / instrument / cancel the paint of
470 // plugin-contributed native windows (the Calculator, Jorvy, custom
471 // native launchers).
472 // ------------------------------------------------------------------
473 /**
474 * Filter, applied to the body element a native window will render
475 * into, just BEFORE the user's `render( body )` callback runs.
476 * Payload: the `HTMLElement`; context: `{ windowId, config }`.
477 *
478 * Return the same element (or a wrapper) to intercept. Subscribers
479 * commonly use this to inject a consistent shell (padding,
480 * background, decorative chrome) around every native window
481 * without every plugin re-implementing the pattern.
482 */
483 NATIVE_WINDOW_BEFORE_RENDER: "desktop-mode.native-window.before-render",
484 /**
485 * Action, fires AFTER a native window's `render( body )` callback
486 * returns. Payload: `{ windowId, body, config }`. Observability
487 * hook — analytics / auto-focus / post-render measurement.
488 */
489 NATIVE_WINDOW_AFTER_RENDER: "desktop-mode.native-window.after-render",
490 /**
491 * Filter, applied when a native window is about to start its
492 * close animation. Return `false` to CANCEL the close — the
493 * window stays open. Payload: `true`; context: `{ windowId,
494 * config }`. Any non-`false` return (including `undefined`) lets
495 * the close proceed.
496 *
497 * Intended for "unsaved changes" guards: a calculator with a
498 * pending operation can prompt the user and abort the close
499 * mid-flight. Does NOT apply to iframe windows — their close is
500 * driven by browser navigation patterns the shell doesn't own.
501 */
502 NATIVE_WINDOW_BEFORE_CLOSE: "desktop-mode.native-window.before-close",
503 // ------------------------------------------------------------------
504 // Window-chrome customization framework. Plugins drive per-window
505 // appearance (theme, controls, slots, full chrome render) through
506 // the `wp.desktop.registerWindow*` registries; these hooks expose
507 // every resolution step so plugins can mutate or observe the
508 // chrome pipeline without owning a registration.
509 //
510 // Layers 1-3 (theme, controls, slots) are Stable. Layer 4 (chrome
511 // render) is Experimental — `WINDOW_CHROME_RENDER` may change.
512 // ------------------------------------------------------------------
513 /**
514 * Filter, applied to the resolved CSS-variable map for a window.
515 * Receives `Record< string, string >`; context: `{ windowId,
516 * config }`. Plugins return a mutated map to override or augment
517 * the per-window theme tokens — e.g. tint every Gutenberg
518 * window's title bar to brand colour.
519 *
520 * Stable since 0.6.0.
521 */
522 WINDOW_CHROME_THEME: "desktop-mode.window.chrome.theme",
523 /**
524 * Filter, applied to the resolved control list for a window.
525 * Receives `WindowControlDef[]`; context: `{ windowId, config,
526 * placement: 'left' | 'right' | 'controls' }`. Plugins return a
527 * mutated array to reorder, hide, or inject controls per-window.
528 *
529 * Stable since 0.6.0.
530 */
531 WINDOW_CHROME_CONTROLS: "desktop-mode.window.chrome.controls",
532 /**
533 * Filter, applied per slot when the chrome paints. Receives the
534 * slot host element; context: `{ windowId, slot, config }`.
535 * Plugins can mutate `host` (append decorative children, set
536 * inline styles) without owning a `WindowSlotDef` registration.
537 * The shell never reads the return value — this is an action-
538 * shaped filter so existing `addFilter` plumbing applies.
539 *
540 * Stable since 0.6.0.
541 */
542 WINDOW_CHROME_SLOT: "desktop-mode.window.chrome.slot",
543 /**
544 * Filter, applied to the chrome id selected for a window.
545 * Receives the resolved id (defaults to `'core/standard'`);
546 * context: `{ windowId, config }`. Returning a different id
547 * swaps the chrome registration. **Experimental** — chrome
548 * render contract may change.
549 *
550 * @since 0.6.0
551 */
552 WINDOW_CHROME_RENDER: "desktop-mode.window.chrome.render",
553 /**
554 * Action, fires after a window's chrome has been mounted /
555 * remounted. Payload: `{ windowId, chromeId }`. Subscribers can
556 * post-decorate the chrome (attach observers, anchor pickers).
557 *
558 * @since 0.6.0
559 */
560 WINDOW_CHROME_APPLIED: "desktop-mode.window.chrome.applied",
561 /**
562 * Action, fires after a window's theme tokens are applied to its
563 * outer element. Payload: `{ windowId, themeId, tokens }`. Lets
564 * plugins react to theme changes without diffing CSS variables.
565 *
566 * @since 0.6.0
567 */
568 WINDOW_CHROME_THEME_CHANGED: "desktop-mode.window.chrome.theme-changed",
569 /**
570 * Action, fires when a user clicks a desktop icon (a shortcut
571 * tile registered server-side via `desktop_mode_register_icon()`
572 * and rendered on the wallpaper). Payload: `{ id: string,
573 * target: 'window' | 'url' }`. Fires BEFORE the default open
574 * action — plugins cannot cancel the open from this hook, but
575 * can use it to track click-throughs or augment behaviour (e.g.
576 * play a sound, surface a confirmation toast).
577 *
578 * @since 0.11.0
579 */
580 DESKTOP_ICON_CLICKED: "desktop-mode.desktop-icon.clicked",
581 /**
582 * Action, fires after the wallpaper icon grid is rendered or
583 * re-rendered. Payload:
584 *
585 * {
586 * ids: string[]; // paint order
587 * container: HTMLElement; // <div class="desktop-mode-icons">
588 * tiles: ReadonlyMap<string, HTMLElement>; // id → tile <button>
589 * }
590 *
591 * Plugins that decorate icons with surfaces the framework doesn't
592 * natively expose (drag handles, status dots, cursor adornments)
593 * subscribe here so their decorations survive a live menu refresh
594 * that legitimately rebuilds the grid. The `container` and
595 * `tiles` map mirror the {@link DOCK_AFTER_RENDER}
596 * `tileElements` contract — reach into them directly instead of
597 * re-`querySelector`ing the rendered DOM.
598 *
599 * Notification badges have a first-class API since 0.24.0 —
600 * use `wp.desktop.icons.setBadge( id, count )` (and subscribe
601 * to {@link ICON_BADGE_CHANGED}) instead of decorating from
602 * here. The framework persists badge state across rebuilds, so
603 * a plugin that uses the API doesn't need to re-decorate on
604 * every render.
605 *
606 * Suppressed entirely when the rendered DOM is unchanged from
607 * the previous call (the fingerprint short-circuit upstream
608 * skips both the rebuild and this signal). When the icon list
609 * is empty the hook does not fire at all — the previous
610 * container is removed and no new one is appended.
611 *
612 * @since 0.21.0
613 * @since 0.25.0 — `container` + `tiles` added to the payload
614 * (`ids` retained for back-compat).
615 */
616 DESKTOP_ICONS_RENDERED: "desktop-mode.desktop-icons.rendered",
617 /**
618 * Action, fires whenever the badge count on a desktop icon
619 * changes. Payload: `{ iconId: string, count: number,
620 * previousCount: number }`. Symmetric to {@link DOCK_ITEM_APPENDED}
621 * and the dock/taskbar `wpd-dock-item-badge-changed` CustomEvent
622 * — the icon rail's lifecycle hook for badge transitions.
623 *
624 * Mirrors `desktop-mode/badge-changed` on the activity bus with
625 * `rail: 'icon'`. Subscribe to whichever surface fits — the
626 * activity channel composes across rails for global widgets,
627 * this hook fires only for icon-rail badges with the previous
628 * count carried alongside for delta-aware consumers.
629 *
630 * @since 0.24.0
631 */
632 ICON_BADGE_CHANGED: "desktop-mode.icon.badge-changed",
633 // ------------------------------------------------------------------
634 // Cross-plugin composition.
635 // ------------------------------------------------------------------
636 /**
637 * Action, fires ONCE after every shell-shipped `<wpd-*>` custom
638 * element has registered with `customElements`. Payload: `{
639 * tags: string[] }` — the list of registered tag names. Plugins
640 * that need to defer work until the component registry is
641 * complete (e.g. hydrate user content that uses these tags)
642 * subscribe here instead of polling `customElements.get()`.
643 */
644 COMPONENTS_REGISTERED: "desktop-mode.components.registered",
645 /**
646 * Action, fires after `wp.desktop.registerSystemTile()` inserts
647 * a tile into the unified dock. Payload: `{ id: string }`. Useful
648 * for plugins that want to decorate tiles they didn't register
649 * themselves — analytics, theming, per-tile badges.
650 */
651 DOCK_ITEM_APPENDED: "desktop-mode.dock.item-appended",
652 /**
653 * Action, fires after a system tile is removed from a rail
654 * via `Dock.removeSystemItem()` (typically the server-driven
655 * native-window-sync path on plugin deactivation). Payload:
656 * `{ id: string, placement: 'dock' | 'taskbar' }`. Symmetric
657 * to {@link DOCK_ITEM_APPENDED}; lets analytics / decorators /
658 * cleanup hooks see the full lifecycle without polling the DOM.
659 *
660 * @since 0.24.0
661 */
662 DOCK_ITEM_REMOVED: "desktop-mode.dock.item-removed",
663 // ------------------------------------------------------------------
664 // Dock decoration hooks — render-pipeline filters and actions the
665 // default `Dock` renderer fires while painting tiles. Plugins
666 // compose decoration (animations, classNames, wrappers, tooltips)
667 // without forking the renderer. Custom rail renderers SHOULD fire
668 // the same hooks for ecosystem compatibility — see
669 // `docs/examples/dock-decoration-hooks.md` for the contract.
670 //
671 // Every detail object carries `{ rail, orientation, dockId,
672 // container }` so a single subscriber can disambiguate when two
673 // rails coexist (Classic layout's left side bar + bottom dock).
674 // `dockId` matches the host element's `id` (e.g. `'desktop-mode-dock'`
675 // or `'desktop-mode-side-dock'`) and is the stable
676 // disambiguator — `rail` and `orientation` are convenience
677 // projections of where the renderer is painting.
678 // ------------------------------------------------------------------
679 /**
680 * Action, fires at the start of every dock paint pass — both the
681 * initial mount and every `replaceItems()` that follows on the
682 * live menu-refresh path. Payload `DockRenderContext`. Use this
683 * to invalidate cached per-render decoration state before the
684 * tiles repopulate.
685 *
686 * @since 0.18.0
687 */
688 DOCK_BEFORE_RENDER: "desktop-mode.dock.before-render",
689 /**
690 * Action, fires once every menu and system tile has landed in
691 * the DOM for a paint pass. Payload `DockRenderContext` plus a
692 * frozen `tileElements: ReadonlyMap<string, HTMLElement>` so a
693 * plugin can decorate every tile in one sweep. Symmetric to
694 * {@link DOCK_BEFORE_RENDER}.
695 *
696 * @since 0.18.0
697 */
698 DOCK_AFTER_RENDER: "desktop-mode.dock.after-render",
699 /**
700 * Filter, runs once per tile while the renderer is composing the
701 * className list. Plugins may add, remove, or reorder classes.
702 * Signature: `( classes: string[], detail: DockTileContext ) =>
703 * string[]`. Order is preserved.
704 *
705 * @since 0.18.0
706 */
707 DOCK_TILE_CLASS: "desktop-mode.dock.tile-class",
708 /**
709 * Filter, runs once per tile after the renderer finishes building
710 * the element but before it lands in the DOM. Return the same
711 * element with mutations, or replace with a wrapper — the shell
712 * inserts whatever you return. Signature:
713 * `( el: HTMLElement, detail: DockTileContext ) => HTMLElement`.
714 *
715 * Returning a different node still has to expose a stable
716 * `[data-menu-slug="<id>"]` (or `[data-system-id="<id>"]`)
717 * descendant for active-state / badge updates to find the tile;
718 * wrap, don't replace.
719 *
720 * @since 0.18.0
721 */
722 DOCK_TILE_ELEMENT: "desktop-mode.dock.tile-element",
723 /**
724 * Action, fires once per tile after it has been inserted into
725 * the DOM. Payload `DockTileContext` plus the resolved `el`. Use
726 * for post-insertion decoration where computed layout matters
727 * (measurements, IntersectionObserver bindings, etc.).
728 *
729 * @since 0.18.0
730 */
731 DOCK_TILE_RENDERED: "desktop-mode.dock.tile-rendered",
732 /**
733 * Filter, resolves the tooltip text for a tile. Runs once at
734 * bind time so the dock doesn't re-filter on every pointerenter.
735 * Signature: `( label: string, detail: DockTileContext ) =>
736 * string`. Return an empty string to suppress the tooltip.
737 *
738 * @since 0.18.0
739 */
740 DOCK_TILE_TOOLTIP: "desktop-mode.dock.tile-tooltip",
741 /**
742 * Filter, resolves the body content of a single hover-peek card.
743 * Runs once per card build (i.e., on every show of the peek for
744 * a multi-instance dock tile that has ≥1 open window). Lets a
745 * plugin render a custom thumbnail, status block, or any other
746 * markup inside the card in place of (or alongside) the default
747 * mini-window styling.
748 *
749 * Signature:
750 * ( body: HTMLElement, detail: DockPeekCardContext ) => HTMLElement
751 *
752 * Where `body` is the `<span class="desktop-mode-dock-peek__card-body">`
753 * element that the peek would otherwise populate with ghosted
754 * content lines. The filter may:
755 * - Mutate `body` in place (e.g., append a custom child) and
756 * return it.
757 * - Empty `body` and append plugin-owned children.
758 * - Return an entirely different element to replace `body`.
759 *
760 * `detail.window` is the live `Window` instance the card represents
761 * — plugins can read `window.config`, call `window.getCurrentUrl()`,
762 * subscribe to lifecycle events, etc. `detail.item` is the dock
763 * item descriptor (id / title / icon / url).
764 *
765 * The filter is invoked under the `applyFilters` namespace
766 * `desktop-mode.dock.peek-card-content`.
767 *
768 * @since 0.6.2
769 */
770 DOCK_PEEK_CARD_CONTENT: "desktop-mode.dock.peek-card-content",
771 /**
772 * Filter, runs once per peek card right before it's appended to
773 * the popover. Receives the fully-built default card (with its
774 * mini-window chrome already populated) and can return either
775 * the same node, a mutated version, or an entirely different
776 * element to replace the card outright. Use this when the
777 * `peek-card-content` body filter isn't enough — e.g., when a
778 * plugin wants to swap the whole card chrome (custom titlebar,
779 * different shape) or wrap the card in a third-party component.
780 *
781 * Signature:
782 * ( card: HTMLElement, detail: DockPeekCardContext ) => HTMLElement
783 *
784 * If a plugin returns a brand-new node, it is responsible for
785 * preserving anything the peek relies on:
786 * - The `desktop-mode-dock-peek__card` class (used by the
787 * fan-out animation timing + hover styles).
788 * - A `click` handler if the card should still focus the
789 * window. The default click handler lives on the original
790 * node — replacing the node loses it.
791 *
792 * @since 0.6.2
793 */
794 DOCK_PEEK_CARD_ELEMENT: "desktop-mode.dock.peek-card-element",
795 // ------------------------------------------------------------------
796 // Overview / Arrange lifecycle actions.
797 //
798 // The "Arrange" admin-bar menu drives two layout algorithms —
799 // Cascade (instantly reposition every window in a staggered
800 // stack) and Overview (zoom-out grid view with click-to-focus).
801 // These hooks surface the state transitions so plugins can
802 // instrument analytics, apply custom transitions, override
803 // thumbnail decorations, etc. All actions; a filter for
804 // mutating the overview layout may be added later if plugins
805 // want to reorder or group thumbnails.
806 // ------------------------------------------------------------------
807 /** Action, fires before the overview enter animation starts. */
808 OVERVIEW_ENTERING: "desktop-mode.overview.entering",
809 /** Action, fires once the overview enter animation has completed. */
810 OVERVIEW_ENTERED: "desktop-mode.overview.entered",
811 /**
812 * Action, fires at the start of the overview-exit animation.
813 * Payload: `{ windowId?: string, reason: 'select' | 'cancel' }` —
814 * `windowId` set when the user clicked a thumbnail (reason
815 * 'select'); omitted when the user pressed Escape or clicked
816 * the backdrop (reason 'cancel').
817 */
818 OVERVIEW_EXITING: "desktop-mode.overview.exiting",
819 /** Action, fires once the overview-exit animation has settled. */
820 OVERVIEW_EXITED: "desktop-mode.overview.exited",
821 /** Action, fires when the cursor enters a thumbnail. Payload `{ windowId }`. */
822 OVERVIEW_WINDOW_HOVER: "desktop-mode.overview.window-hover",
823 /** Action, fires when the cursor leaves a thumbnail. Payload `{ windowId }`. */
824 OVERVIEW_WINDOW_UNHOVER: "desktop-mode.overview.window-unhover",
825 /** Action, fires the instant a thumbnail click is registered (before exit + maximize kick in). Payload `{ windowId }`. */
826 OVERVIEW_WINDOW_CLICK: "desktop-mode.overview.window-click",
827 /** Action, fires before cascade computes + applies new positions. Payload `{ windowCount }`. */
828 ARRANGE_CASCADE_STARTING: "desktop-mode.arrange.cascade.starting",
829 /** Action, fires after cascade has positioned every window. Payload `{ windowCount }`. */
830 ARRANGE_CASCADE_APPLIED: "desktop-mode.arrange.cascade.applied",
831 /** Action, fires before tile computes + applies new positions. Payload `{ windowCount, cols, rows }`. */
832 ARRANGE_TILE_STARTING: "desktop-mode.arrange.tile.starting",
833 /** Action, fires after tile has positioned every window. Payload `{ windowCount, cols, rows }`. */
834 ARRANGE_TILE_APPLIED: "desktop-mode.arrange.tile.applied",
835 /**
836 * Filter on the tile-grid dimensions chosen by the built-in
837 * algorithm. Receives `{ cols, rows }` plus a context arg
838 * `{ windowCount, areaWidth, areaHeight }`. Plugins can return
839 * a different `{ cols, rows }` to enforce a custom layout
840 * (fixed-column newsroom, golden-ratio cells, etc.). Returned
841 * values are validated — non-positive integers, or a product
842 * smaller than `windowCount`, fall back to the original.
843 */
844 ARRANGE_TILE_DIMENSIONS: "desktop-mode.arrange.tile.dimensions",
845 /** Action, fires when snap-to-grid is toggled. Payload `{ enabled }`. */
846 ARRANGE_SNAP_CHANGED: "desktop-mode.arrange.snap.changed",
847 /**
848 * Filter on the snap-grid cell size. Receives
849 * `{ cellWidth, cellHeight }` plus a context arg
850 * `{ areaWidth, areaHeight }`. Plugins can return different
851 * dimensions to enforce a Tetris-style fixed grid, a musical
852 * staff aspect, etc. Non-positive returns fall back to the
853 * original.
854 */
855 ARRANGE_SNAP_CELL_SIZE: "desktop-mode.arrange.snap.cell-size",
856 /**
857 * Action, fires when the user clicks a plugin-registered entry in
858 * the Arrange admin-bar submenu (items added via the
859 * `desktop_mode_arrange_menu_items` PHP filter). Payload `{ id }`
860 * where `id` is the item's `id` field as registered. Plugins
861 * subscribe here to run their custom arrangement logic.
862 */
863 ARRANGE_CUSTOM_ACTION: "desktop-mode.arrange.custom-action",
864 // ------------------------------------------------------------------
865 // Snap-zones — Windows-style edge snapping with a split-overview
866 // picker to fill the opposite half after commit.
867 // ------------------------------------------------------------------
868 /**
869 * Action, fires when the drag cursor enters a snap zone and the
870 * shell shows the target-position preview. Payload
871 * `{ windowId, zone: 'left' | 'right' }`.
872 */
873 SNAP_ZONE_PENDING: "desktop-mode.snap.zone-pending",
874 /**
875 * Action, fires when the drag cursor leaves the snap zone without
876 * releasing — the preview disappears. Payload `{ windowId }`.
877 */
878 SNAP_ZONE_CANCELED: "desktop-mode.snap.zone-canceled",
879 /**
880 * Action, fires once the window has animated into its snapped
881 * bounds. Payload `{ windowId, zone: 'left' | 'right' }`.
882 */
883 SNAP_ZONE_COMMITTED: "desktop-mode.snap.zone-committed",
884 /**
885 * Action, fires when a user picks a thumbnail from the split
886 * overview to fill the opposite half. Payload
887 * `{ windowId, zone: 'left' | 'right' }`.
888 */
889 SNAP_SPLIT_FILLED: "desktop-mode.snap.split-filled",
890 // ------------------------------------------------------------------
891 // Widgets — the right-side column. Widgets paint above the
892 // wallpaper but beneath windows. Lifecycle mirrors canvas
893 // wallpapers: register via filter, mount/unmount actions bracket
894 // each paint, mount-failed fires on sync throws / async rejects.
895 // ------------------------------------------------------------------
896 /** Filter, receives the widget registry array. */
897 WIDGETS: "desktop-mode.widgets",
898 /** Action before a widget mounts. Payload `{ id, container, ctx }`. */
899 WIDGET_MOUNTING: "desktop-mode.widget.mounting",
900 /** Action after a widget mounts successfully. Payload `{ id, container, ctx }`. */
901 WIDGET_MOUNTED: "desktop-mode.widget.mounted",
902 /** Action before a widget tears down. Payload `{ id }`. */
903 WIDGET_UNMOUNTING: "desktop-mode.widget.unmounting",
904 /** Action when a widget's mount throws / rejects. Payload `{ id, error }`. */
905 WIDGET_MOUNT_FAILED: "desktop-mode.widget.mount-failed",
906 /** Action when the user adds a widget via the picker. Payload `{ id }`. */
907 WIDGET_ADDED: "desktop-mode.widget.added",
908 /** Action when the user removes a widget via the card's × button. Payload `{ id }`. */
909 WIDGET_REMOVED: "desktop-mode.widget.removed",
910 // ------------------------------------------------------------------
911 // Virtual-desktop ("Spaces") lifecycle actions.
912 //
913 // Spaces let users group windows into separate workspaces and flip
914 // between them from the overview top bar. These hooks expose every
915 // state change so plugins can persist per-space state, sync custom
916 // indicators, or react to the user's workspace context.
917 // ------------------------------------------------------------------
918 /** Action, fires when a new desktop is created. Payload `{ desktopId }`. */
919 DESKTOP_CREATED: "desktop-mode.desktop.created",
920 /** Action, fires when a desktop is closed. Payload `{ desktopId, migratedTo }`. */
921 DESKTOP_CLOSED: "desktop-mode.desktop.closed",
922 /** Action, fires when the active desktop changes. Payload `{ from, to }`. */
923 DESKTOP_SWITCHED: "desktop-mode.desktop.switched",
924 /**
925 * Filter. Returns the id of the "primary" desktop — the one the
926 * shell treats as canonical for batch operations. Receives the
927 * default (first desktop's id) and the full `Desktop[]` list.
928 * @since 0.14.0
929 */
930 PRIMARY_DESKTOP_ID: "desktop-mode.primary-desktop-id",
931 // ------------------------------------------------------------------
932 // Batch window operations.
933 // ------------------------------------------------------------------
934 /**
935 * Action, fires before {@link WindowManager.closeAll} starts
936 * iterating. Payload `{ candidates: Window[] }` — every window the
937 * shell is about to close (after `exceptIds` was applied).
938 * @since 0.14.0
939 */
940 WINDOWS_BEFORE_CLOSE_ALL: "desktop-mode.windows.before-close-all",
941 /**
942 * Filter, runs inside {@link WindowManager.closeAll}. Receives the
943 * candidate `Window[]` list and returns the (possibly trimmed) list
944 * that will actually be closed. Plugins use this to PROTECT specific
945 * windows from a bulk close — e.g. keep the active draft open.
946 * Returning an empty array cancels the close entirely.
947 * @since 0.14.0
948 */
949 WINDOWS_CLOSE_ALL: "desktop-mode.windows.close-all",
950 /**
951 * Action, fires after {@link WindowManager.closeAll} has finished.
952 * Payload `{ closed: number, skipped: Window[] }`.
953 * @since 0.14.0
954 */
955 WINDOWS_AFTER_CLOSE_ALL: "desktop-mode.windows.after-close-all",
956 // ------------------------------------------------------------------
957 // Slash-command lifecycle.
958 // ------------------------------------------------------------------
959 /**
960 * Filter. Runs immediately before a command's `run()` is invoked.
961 * Receives `{ proceed: true, slug, args, command }` and may return
962 * the same shape with `proceed: false` to cancel the run.
963 * @since 0.14.0
964 */
965 COMMAND_BEFORE_RUN: "desktop-mode.command.before-run",
966 /**
967 * Action, fires after a command's `run()` resolves successfully.
968 * Payload `{ slug, args, command, result }`.
969 * @since 0.14.0
970 */
971 COMMAND_AFTER_RUN: "desktop-mode.command.after-run",
972 /**
973 * Action, fires when a command's `run()` throws. Payload
974 * `{ slug, args, command, error }`.
975 * @since 0.14.0
976 */
977 COMMAND_ERROR: "desktop-mode.command.error",
978 // ------------------------------------------------------------------
979 // Shell-level lifecycle actions.
980 // ------------------------------------------------------------------
981 /**
982 * Action, fires (debounced) after the browser viewport stops
983 * resizing. Payload `{ width, height }` describes the shell's
984 * bounding rect — plugins that render canvas-driven UIs hook here
985 * to adjust their render surface.
986 */
987 SHELL_RESIZED: "desktop-mode.shell.resized",
988 /**
989 * Action mirroring `document.visibilitychange` for the shell as a
990 * whole. Payload `{ state: 'visible' | 'hidden' }`. Different from
991 * the wallpaper-specific visibility action in that it fires
992 * regardless of which wallpaper (if any) is active.
993 */
994 SHELL_VISIBILITY: "desktop-mode.shell.visibility",
995 /**
996 * Action — fires when a `wp.desktop.connect()` connection
997 * completes its iframe handshake. Payload:
998 * `{ connectionId, targetWindowId, topics }`.
999 *
1000 * @since 0.17.0
1001 */
1002 CONNECTION_OPENED: "desktop-mode.connection.opened",
1003 /**
1004 * Action — fires when a connection tears down. Payload:
1005 * `{ connectionId, reason: 'disconnect' | 'window-closed' | 'navigated' }`.
1006 *
1007 * @since 0.17.0
1008 */
1009 CONNECTION_CLOSED: "desktop-mode.connection.closed",
1010 /**
1011 * Action — fires for every message routed through a connection.
1012 * Payload: `{ connectionId, topic, direction: 'in' | 'out' }`.
1013 * Used for debug consoles + traffic auditing; high-volume topics
1014 * fire this many times per second, so subscribers should be
1015 * cheap.
1016 *
1017 * @since 0.17.0
1018 */
1019 CONNECTION_MESSAGE: "desktop-mode.connection.message",
1020 /**
1021 * Filter — fires when an iframe calls
1022 * `wp.desktop.iframe.requestConnection()`. Default value is
1023 * `true` (accept). Return `false` to reject, or an object
1024 * `{ topics: string[] }` to accept while narrowing the topic
1025 * list. `$context` carries `{ windowId, requestId, topics }`.
1026 *
1027 * @since 0.18.0
1028 */
1029 IFRAME_CONNECTION_REQUEST: "desktop-mode.iframe.connection-request",
1030 // ------------------------------------------------------------------
1031 // OS-file drop manager (since 0.30.0). Catches files dragged from
1032 // the user's host OS (Finder / Explorer / Nautilus) onto any
1033 // desktop-mode surface and routes them through a confirmation
1034 // dialog before uploading to the Media Library. Authoritative
1035 // constants live in `src/os-file-drop/hooks.ts`; mirrored here so
1036 // every hook the shell fires is reachable from a single `HOOKS`
1037 // import. See `docs/examples/os-file-drop.md`.
1038 // ------------------------------------------------------------------
1039 /** Filter — `(files: File[], ctx) => File[]`, before mime/size check. */
1040 FILE_DROP_FILES_DETECTED: "desktop-mode.drop.files-detected",
1041 /** Action — `{ rejections, context }` for files that failed policy. */
1042 FILE_DROP_FILES_REJECTED: "desktop-mode.drop.files-rejected",
1043 /** Filter — `(entry, ctx) => entry`, per-file dialog defaults. */
1044 FILE_DROP_DIALOG_FIELDS: "desktop-mode.drop.dialog-fields",
1045 /** Filter — `(payload, ctx) => payload | null`, last call before POST. */
1046 FILE_DROP_BEFORE_UPLOAD: "desktop-mode.drop.before-upload",
1047 /** Action — `{ file, fields, context, abort }` once XHR is open and about to send. @since 0.31.0 */
1048 FILE_DROP_UPLOAD_STARTED: "desktop-mode.drop.upload-started",
1049 /** Action — `{ file, fields, context, loaded, total, indeterminate }` per progress tick. @since 0.31.0 */
1050 FILE_DROP_UPLOAD_PROGRESS: "desktop-mode.drop.upload-progress",
1051 /** Action — `{ file, result, fields, context }` after successful upload. `file` since 0.31.0. */
1052 FILE_DROP_AFTER_UPLOAD: "desktop-mode.drop.after-upload",
1053 /** Action — `{ file, error, context }` on upload failure. */
1054 FILE_DROP_UPLOAD_FAILED: "desktop-mode.drop.upload-failed"
1055 };
1056 let _whenReadySeq = 0;
1057 function whenReady(cb) {
1058 if (didAction(HOOKS.INIT) > 0) {
1059 Promise.resolve().then(cb);
1060 return;
1061 }
1062 const ns = `desktop-mode/when-ready-${++_whenReadySeq}`;
1063 addAction(HOOKS.INIT, ns, cb);
1064 }
1065 function isReady() {
1066 return didAction(HOOKS.INIT) > 0;
1067 }
1068 let inflight$1 = null;
1069 function isLoaded$1() {
1070 return !!window.desktopModeWindowSystem;
1071 }
1072 function injectScript$1(scriptUrl) {
1073 return new Promise((resolve2, reject) => {
1074 const existing = document.querySelector(
1075 'script[data-desktop-mode-window-system="1"]'
1076 );
1077 const finish = () => {
1078 if (isLoaded$1()) {
1079 resolve2();
1080 return;
1081 }
1082 reject(
1083 new Error(
1084 "[desktop-mode] window-system bundle loaded but did not register `window.desktopModeWindowSystem`."
1085 )
1086 );
1087 };
1088 if (existing) {
1089 if (isLoaded$1()) {
1090 finish();
1091 } else {
1092 existing.addEventListener("load", finish);
1093 existing.addEventListener(
1094 "error",
1095 () => reject(new Error("failed to load window-system bundle"))
1096 );
1097 }
1098 return;
1099 }
1100 const s = document.createElement("script");
1101 s.src = scriptUrl;
1102 s.async = true;
1103 s.dataset.desktopModeWindowSystem = "1";
1104 s.addEventListener("load", finish);
1105 s.addEventListener(
1106 "error",
1107 () => reject(new Error("failed to load window-system bundle"))
1108 );
1109 document.head.appendChild(s);
1110 });
1111 }
1112 function windowSystemBundleUrl() {
1113 const cfg = window.desktopModeConfig;
1114 return cfg?.windowSystemBundleUrl ?? "";
1115 }
1116 function preloadWindowSystem(scriptUrl) {
1117 if (!scriptUrl || isLoaded$1() || inflight$1) {
1118 return;
1119 }
1120 inflight$1 = injectScript$1(scriptUrl).catch((err) => {
1121 inflight$1 = null;
1122 if (typeof console !== "undefined") {
1123 console.warn(
1124 "[desktop-mode] window-system preload failed; will retry on first open():",
1125 err
1126 );
1127 }
1128 });
1129 }
1130 async function ensureWindowSystemLoaded(scriptUrl) {
1131 if (isLoaded$1()) {
1132 return window.desktopModeWindowSystem;
1133 }
1134 if (!scriptUrl) {
1135 const fn = window.desktopModeWindowSystem;
1136 if (fn) {
1137 return fn;
1138 }
1139 throw new Error(
1140 "[desktop-mode] ensureWindowSystemLoaded(): no bundle URL configured and `window.desktopModeWindowSystem` is not pre-registered."
1141 );
1142 }
1143 if (!inflight$1) {
1144 inflight$1 = injectScript$1(scriptUrl);
1145 }
1146 await inflight$1;
1147 return window.desktopModeWindowSystem;
1148 }
1149 const CANARY_TAG = "wpd-confirm-dialog";
1150 let inflight = null;
1151 function isLoaded() {
1152 return typeof window.customElements !== "undefined" && !!window.customElements.get(CANARY_TAG);
1153 }
1154 function injectScript(scriptUrl) {
1155 return new Promise((resolve2, reject) => {
1156 const existing = document.querySelector(
1157 'script[data-desktop-mode-shell-overlays="1"]'
1158 );
1159 const finish = () => {
1160 if (isLoaded()) {
1161 resolve2();
1162 return;
1163 }
1164 reject(
1165 new Error(
1166 "[desktop-mode] shell-overlays bundle loaded but did not register the overlay components."
1167 )
1168 );
1169 };
1170 if (existing) {
1171 if (isLoaded()) {
1172 finish();
1173 } else {
1174 existing.addEventListener("load", finish);
1175 existing.addEventListener(
1176 "error",
1177 () => reject(new Error("failed to load shell-overlays bundle"))
1178 );
1179 }
1180 return;
1181 }
1182 const s = document.createElement("script");
1183 s.src = scriptUrl;
1184 s.async = true;
1185 s.dataset.desktopModeShellOverlays = "1";
1186 s.addEventListener("load", finish);
1187 s.addEventListener(
1188 "error",
1189 () => reject(new Error("failed to load shell-overlays bundle"))
1190 );
1191 document.head.appendChild(s);
1192 });
1193 }
1194 function preloadShellOverlays(scriptUrl) {
1195 if (!scriptUrl || isLoaded() || inflight) {
1196 return;
1197 }
1198 inflight = injectScript(scriptUrl).catch((err) => {
1199 inflight = null;
1200 if (typeof console !== "undefined") {
1201 console.warn(
1202 "[desktop-mode] shell-overlays preload failed; will retry on first overlay use:",
1203 err
1204 );
1205 }
1206 });
1207 }
1208 function ensureShellOverlaysLoaded(scriptUrl) {
1209 if (isLoaded()) {
1210 return Promise.resolve();
1211 }
1212 if (!scriptUrl) {
1213 return Promise.resolve();
1214 }
1215 if (!inflight) {
1216 inflight = injectScript(scriptUrl);
1217 }
1218 return inflight;
1219 }
1220 function shellOverlaysBundleUrl() {
1221 const cfg = window.desktopModeConfig;
1222 return cfg?.shellOverlaysBundleUrl ?? "";
1223 }
1224 function openWithShellOverlays(isStillCurrent, fn) {
1225 const url = shellOverlaysBundleUrl();
1226 if (isLoaded() || !url) {
1227 fn();
1228 return;
1229 }
1230 void ensureShellOverlaysLoaded(url).then(() => {
1231 if (!isStillCurrent()) {
1232 return;
1233 }
1234 fn();
1235 }).catch((err) => {
1236 if (typeof console !== "undefined") {
1237 console.warn(
1238 "[desktop-mode] shell-overlays failed to load; menu/dialog suppressed:",
1239 err
1240 );
1241 }
1242 });
1243 }
1244 const TEXT_DOMAIN = "desktop-mode";
1245 function i18n() {
1246 return window.wp?.i18n;
1247 }
1248 function __(text, domain = TEXT_DOMAIN) {
1249 return i18n()?.__(text, domain) ?? text;
1250 }
1251 function _n(single, plural, number, domain = TEXT_DOMAIN) {
1252 return i18n()?._n(single, plural, number, domain) ?? (number === 1 ? single : plural);
1253 }
1254 function sprintf(format, ...args) {
1255 const impl = i18n()?.sprintf;
1256 if (impl) {
1257 return impl(format, ...args);
1258 }
1259 let i = 0;
1260 return format.replace(/%[sd]/g, () => String(args[i++] ?? ""));
1261 }
1262 function isValidGrid(candidate, windowCount) {
1263 if (!candidate || typeof candidate !== "object") {
1264 return false;
1265 }
1266 const c = candidate.cols;
1267 const r = candidate.rows;
1268 if (typeof c !== "number" || typeof r !== "number") {
1269 return false;
1270 }
1271 if (!Number.isFinite(c) || !Number.isFinite(r)) {
1272 return false;
1273 }
1274 if (c < 1 || r < 1) {
1275 return false;
1276 }
1277 return Math.floor(c) * Math.floor(r) >= windowCount;
1278 }
1279 function isValidCellSize(candidate) {
1280 if (!candidate || typeof candidate !== "object") {
1281 return false;
1282 }
1283 const w = candidate.cellWidth;
1284 const h = candidate.cellHeight;
1285 if (typeof w !== "number" || typeof h !== "number") {
1286 return false;
1287 }
1288 if (!Number.isFinite(w) || !Number.isFinite(h)) {
1289 return false;
1290 }
1291 return w > 0 && h > 0;
1292 }
1293 function pickGridDimensions(n, width, height) {
1294 if (n <= 1) {
1295 return { cols: 1, rows: 1 };
1296 }
1297 const areaAspect = width / Math.max(1, height);
1298 const max = 6;
1299 let best = { cols: n, rows: 1, score: Infinity };
1300 for (let cols = 1; cols <= Math.min(max, n); cols++) {
1301 const rows = Math.min(max, Math.ceil(n / cols));
1302 if (cols * rows < n) {
1303 continue;
1304 }
1305 const cellAspect = width / cols / Math.max(1, height / rows);
1306 const aspectDelta = Math.abs(cellAspect - areaAspect);
1307 const emptyCells = cols * rows - n;
1308 const score = aspectDelta + emptyCells * 0.05;
1309 if (score < best.score) {
1310 best = { cols, rows, score };
1311 }
1312 }
1313 return { cols: best.cols, rows: best.rows };
1314 }
1315 function computeOverviewLayout(windows, rect, topInset = 0) {
1316 const n = windows.length;
1317 if (n === 0) {
1318 return [];
1319 }
1320 const cols = Math.ceil(Math.sqrt(n));
1321 const rows = Math.ceil(n / cols);
1322 const padding = 40;
1323 const gap = 24;
1324 const labelReserve = 34;
1325 const cellWidth = (rect.width - padding * 2 - gap * (cols - 1)) / cols;
1326 const cellHeight = (rect.height - padding * 2 - topInset - gap * (rows - 1)) / rows;
1327 const thumbCellHeight = Math.max(40, cellHeight - labelReserve);
1328 return windows.map((win, i) => {
1329 const col = i % cols;
1330 const row = Math.floor(i / cols);
1331 const cellX = rect.left + padding + col * (cellWidth + gap);
1332 const cellY = rect.top + topInset + padding + row * (cellHeight + gap) + labelReserve;
1333 const sourceW = win.element.offsetWidth;
1334 const sourceH = win.element.offsetHeight;
1335 const scale = Math.min(
1336 cellWidth / sourceW,
1337 thumbCellHeight / sourceH
1338 );
1339 const scaledW = sourceW * scale;
1340 const scaledH = sourceH * scale;
1341 return {
1342 win,
1343 x: cellX + (cellWidth - scaledW) / 2,
1344 y: cellY + (thumbCellHeight - scaledH) / 2,
1345 scale
1346 };
1347 });
1348 }
1349 const OVERVIEW_TOP_BAR_RESERVE = 120;
1350 function enterOverview(mgr) {
1351 if (mgr._overviewActive) {
1352 return;
1353 }
1354 const onActive = mgr._stack.filter(
1355 (w) => w.config.desktopId === mgr._activeDesktopId
1356 );
1357 if (onActive.length > 0 && onActive.every((w) => w.state === "minimized")) {
1358 for (const w of onActive) {
1359 try {
1360 w.restore();
1361 } catch (err) {
1362 if (typeof console !== "undefined") {
1363 console.error(
1364 "[desktop-mode] enterOverview: window.restore() threw for",
1365 w.id,
1366 err
1367 );
1368 }
1369 }
1370 }
1371 }
1372 const eligible = mgr._stack.filter(
1373 (w) => w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
1374 );
1375 mgr._overviewActive = true;
1376 doAction(HOOKS.OVERVIEW_ENTERING, {});
1377 mgr._overviewSnapshot.clear();
1378 for (const w of eligible) {
1379 mgr._overviewSnapshot.set(w.id, {
1380 transform: w.element.style.transform || "",
1381 transition: w.element.style.transition || ""
1382 });
1383 }
1384 for (const w of eligible) {
1385 if (w.state === "fullscreen") {
1386 w.toggleFullscreen();
1387 }
1388 }
1389 const currentRect = mgr._desktop.getBoundingClientRect();
1390 const docks = Array.from(
1391 document.querySelectorAll(".desktop-mode-dock")
1392 );
1393 let reclaimedWidth = 0;
1394 for (const d of docks) {
1395 const r = d.getBoundingClientRect();
1396 const verticallyOverlaps = r.bottom > currentRect.top && r.top < currentRect.bottom;
1397 const isHorizontalRail = r.height > r.width;
1398 if (verticallyOverlaps && isHorizontalRail) {
1399 reclaimedWidth += r.width;
1400 }
1401 }
1402 const targetRect = new DOMRect(
1403 0,
1404 0,
1405 currentRect.width + reclaimedWidth,
1406 currentRect.height
1407 );
1408 mgr._desktop.classList.add("desktop-mode-area--overview");
1409 const shell = document.getElementById("desktop-mode-shell");
1410 shell?.classList.add("desktop-mode-shell--overview");
1411 mgr._overviewTopBar = buildOverviewTopBar(mgr);
1412 mgr._desktop.appendChild(mgr._overviewTopBar);
1413 const layout = computeOverviewLayout(
1414 eligible,
1415 targetRect,
1416 OVERVIEW_TOP_BAR_RESERVE
1417 );
1418 mgr._overviewLabels.clear();
1419 for (const item of layout) {
1420 const el = item.win.element;
1421 el.classList.add("desktop-mode-window--overview");
1422 const dx = item.x - el.offsetLeft;
1423 const dy = item.y - el.offsetTop;
1424 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
1425 const label = createOverviewLabel(item);
1426 el.insertAdjacentElement("afterend", label);
1427 mgr._overviewLabels.set(item.win.id, label);
1428 }
1429 const pressTargetForEvent = (e) => {
1430 const target2 = e.target;
1431 const winEl = target2?.closest(
1432 ".desktop-mode-window--overview"
1433 );
1434 if (winEl) {
1435 return {
1436 id: winEl.id.replace(/^wp-window-/, ""),
1437 element: winEl
1438 };
1439 }
1440 if (target2 === mgr._desktop) {
1441 return { id: "backdrop", element: mgr._desktop };
1442 }
1443 return null;
1444 };
1445 mgr._overviewPointerDownHandler = (e) => {
1446 if (e.button !== 0) {
1447 mgr._overviewPressTarget = null;
1448 return;
1449 }
1450 mgr._overviewPressTarget = pressTargetForEvent(e);
1451 if (mgr._overviewPressTarget) {
1452 e.preventDefault();
1453 e.stopPropagation();
1454 }
1455 };
1456 mgr._overviewPointerUpHandler = (e) => {
1457 if (e.button !== 0) {
1458 return;
1459 }
1460 const pressed = mgr._overviewPressTarget;
1461 mgr._overviewPressTarget = null;
1462 if (!pressed) {
1463 return;
1464 }
1465 const rect = pressed.element.getBoundingClientRect();
1466 const inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
1467 if (!inside) {
1468 return;
1469 }
1470 e.preventDefault();
1471 e.stopPropagation();
1472 if (pressed.id === "backdrop") {
1473 exitOverview(mgr);
1474 return;
1475 }
1476 const selected = mgr.getById(pressed.id);
1477 doAction(HOOKS.OVERVIEW_WINDOW_CLICK, { windowId: pressed.id });
1478 exitOverview(mgr, selected, true);
1479 };
1480 mgr._overviewKeyHandler = (e) => {
1481 if (e.key === "Escape") {
1482 exitOverview(mgr);
1483 return;
1484 }
1485 if (e.key === "Enter") {
1486 e.preventDefault();
1487 if (mgr._overviewAddTileFocused) {
1488 commitAddTile(mgr);
1489 return;
1490 }
1491 exitOverview(mgr);
1492 }
1493 };
1494 mgr._desktop.addEventListener(
1495 "pointerdown",
1496 mgr._overviewPointerDownHandler,
1497 true
1498 );
1499 mgr._desktop.addEventListener(
1500 "pointerup",
1501 mgr._overviewPointerUpHandler,
1502 true
1503 );
1504 mgr._overviewClickBlocker = (e) => {
1505 const target2 = e.target;
1506 if (target2?.closest(".desktop-mode-overview-top-bar")) {
1507 return;
1508 }
1509 e.stopPropagation();
1510 e.preventDefault();
1511 };
1512 mgr._desktop.addEventListener(
1513 "click",
1514 mgr._overviewClickBlocker,
1515 true
1516 );
1517 document.addEventListener("keydown", mgr._overviewKeyHandler);
1518 mgr._lastOverviewHoverId = null;
1519 mgr._overviewMouseHandler = (e) => {
1520 const target2 = e.target;
1521 const winEl = target2?.closest(
1522 ".desktop-mode-window--overview"
1523 );
1524 const newId = winEl ? winEl.id.replace(/^wp-window-/, "") : null;
1525 if (newId === mgr._lastOverviewHoverId) {
1526 return;
1527 }
1528 if (mgr._lastOverviewHoverId) {
1529 doAction(HOOKS.OVERVIEW_WINDOW_UNHOVER, {
1530 windowId: mgr._lastOverviewHoverId
1531 });
1532 }
1533 if (newId) {
1534 doAction(HOOKS.OVERVIEW_WINDOW_HOVER, { windowId: newId });
1535 }
1536 mgr._lastOverviewHoverId = newId;
1537 };
1538 mgr._desktop.addEventListener("mouseover", mgr._overviewMouseHandler);
1539 window.setTimeout(() => {
1540 if (mgr._overviewActive) {
1541 doAction(HOOKS.OVERVIEW_ENTERED, {});
1542 }
1543 }, 300);
1544 }
1545 function buildOverviewTopBar(mgr) {
1546 const bar = document.createElement("div");
1547 bar.className = "desktop-mode-overview-top-bar";
1548 const list2 = document.createElement("div");
1549 list2.className = "desktop-mode-overview-top-bar__list";
1550 bar.appendChild(list2);
1551 for (const d of mgr._desktops) {
1552 list2.appendChild(buildDesktopTile(mgr, d));
1553 }
1554 const addTile = document.createElement("button");
1555 addTile.type = "button";
1556 addTile.className = "desktop-mode-overview-top-bar__tile desktop-mode-overview-top-bar__tile--add";
1557 if (mgr._overviewAddTileFocused) {
1558 addTile.classList.add(
1559 "desktop-mode-overview-top-bar__tile--cursor"
1560 );
1561 }
1562 addTile.setAttribute("aria-label", __("Add new desktop"));
1563 addTile.innerHTML = '<span class="desktop-mode-overview-top-bar__tile-plus" aria-hidden="true">+</span>';
1564 addTile.addEventListener("click", (e) => {
1565 e.preventDefault();
1566 e.stopPropagation();
1567 commitAddTile(mgr);
1568 });
1569 list2.appendChild(addTile);
1570 return bar;
1571 }
1572 function commitAddTile(mgr) {
1573 const created = createDesktop(mgr);
1574 mgr._overviewAddTileFocused = false;
1575 exitOverviewToDesktop(mgr, created.id);
1576 }
1577 function buildDesktopTile(mgr, d) {
1578 const tile2 = document.createElement("button");
1579 tile2.type = "button";
1580 tile2.className = "desktop-mode-overview-top-bar__tile";
1581 tile2.dataset.desktopId = d.id;
1582 if (d.id === mgr._activeDesktopId && !mgr._overviewAddTileFocused) {
1583 tile2.classList.add("desktop-mode-overview-top-bar__tile--active");
1584 }
1585 tile2.setAttribute("aria-label", sprintf(__("Switch to %s"), d.label));
1586 const preview = document.createElement("span");
1587 preview.className = "desktop-mode-overview-top-bar__tile-preview";
1588 const count = mgr._stack.filter(
1589 (w) => w.config.desktopId === d.id
1590 ).length;
1591 if (count > 0) {
1592 const badge = document.createElement("span");
1593 badge.className = "desktop-mode-overview-top-bar__tile-count";
1594 badge.textContent = String(count);
1595 preview.appendChild(badge);
1596 }
1597 tile2.appendChild(preview);
1598 const label = document.createElement("span");
1599 label.className = "desktop-mode-overview-top-bar__tile-label";
1600 label.textContent = d.label;
1601 tile2.appendChild(label);
1602 const closeBtn = document.createElement("span");
1603 closeBtn.className = "desktop-mode-overview-top-bar__tile-close";
1604 closeBtn.setAttribute("role", "button");
1605 closeBtn.setAttribute("tabindex", "0");
1606 closeBtn.setAttribute("aria-label", sprintf(__("Close %s"), d.label));
1607 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>';
1608 closeBtn.addEventListener("click", (e) => {
1609 e.preventDefault();
1610 e.stopPropagation();
1611 closeDesktop(mgr, d.id);
1612 refreshOverviewTopBar(mgr);
1613 });
1614 tile2.appendChild(closeBtn);
1615 tile2.addEventListener("click", (e) => {
1616 e.preventDefault();
1617 e.stopPropagation();
1618 exitOverviewToDesktop(mgr, d.id);
1619 });
1620 return tile2;
1621 }
1622 function refreshOverviewTopBar(mgr) {
1623 if (!mgr._overviewTopBar) {
1624 return;
1625 }
1626 const fresh = buildOverviewTopBar(mgr);
1627 mgr._overviewTopBar.replaceWith(fresh);
1628 mgr._overviewTopBar = fresh;
1629 }
1630 function exitOverviewToDesktop(mgr, desktopId) {
1631 switchDesktop(mgr, desktopId);
1632 exitOverview(mgr);
1633 }
1634 function createOverviewLabel(item) {
1635 const label = document.createElement("div");
1636 label.className = "desktop-mode-overview-label";
1637 label.dataset.windowId = item.win.id;
1638 const thumbW = item.win.element.offsetWidth * item.scale;
1639 label.style.left = `${item.x}px`;
1640 label.style.top = `${item.y - 34}px`;
1641 label.style.width = `${thumbW}px`;
1642 const iconClass = item.win.config.icon || "dashicons-admin-generic";
1643 const icon = document.createElement("span");
1644 icon.className = `desktop-mode-overview-label__icon dashicons ${iconClass}`;
1645 icon.setAttribute("aria-hidden", "true");
1646 label.appendChild(icon);
1647 const title = document.createElement("span");
1648 title.className = "desktop-mode-overview-label__title";
1649 title.textContent = item.win.config.title;
1650 label.appendChild(title);
1651 const tabCount = item.win.getExternalTabCount();
1652 if (tabCount > 0) {
1653 const meta = document.createElement("span");
1654 meta.className = "desktop-mode-overview-label__meta";
1655 meta.textContent = sprintf(
1656 // translators: %d is the number of external sub-tabs open on this window.
1657 _n("· %d open tab", "· %d open tabs", tabCount),
1658 tabCount
1659 );
1660 label.appendChild(meta);
1661 }
1662 return label;
1663 }
1664 function exitOverview(mgr, selected, maximize = false) {
1665 if (!mgr._overviewActive) {
1666 return;
1667 }
1668 mgr._overviewActive = false;
1669 mgr._overviewAddTileFocused = false;
1670 doAction(HOOKS.OVERVIEW_EXITING, {
1671 windowId: selected && maximize ? selected.id : void 0,
1672 reason: selected && maximize ? "select" : "cancel"
1673 });
1674 mgr._desktop.classList.remove("desktop-mode-area--overview");
1675 const shell = document.getElementById("desktop-mode-shell");
1676 shell?.classList.remove("desktop-mode-shell--overview");
1677 for (const [id, snap] of mgr._overviewSnapshot) {
1678 const w = mgr.getById(id);
1679 if (!w) {
1680 continue;
1681 }
1682 w.element.style.transform = snap.transform;
1683 }
1684 if (selected && maximize) {
1685 mgr.focus(selected);
1686 selected.maximize();
1687 }
1688 for (const label of mgr._overviewLabels.values()) {
1689 label.classList.add("desktop-mode-overview-label--out");
1690 }
1691 if (mgr._overviewTopBar) {
1692 mgr._overviewTopBar.classList.add(
1693 "desktop-mode-overview-top-bar--out"
1694 );
1695 }
1696 const ANIMATION_MS = 280;
1697 window.setTimeout(() => {
1698 for (const w of mgr._stack) {
1699 w.element.classList.remove("desktop-mode-window--overview");
1700 }
1701 for (const label of mgr._overviewLabels.values()) {
1702 label.remove();
1703 }
1704 mgr._overviewLabels.clear();
1705 mgr._overviewSnapshot.clear();
1706 if (mgr._overviewTopBar) {
1707 mgr._overviewTopBar.remove();
1708 mgr._overviewTopBar = null;
1709 }
1710 if (mgr._overviewClickBlocker) {
1711 mgr._desktop.removeEventListener(
1712 "click",
1713 mgr._overviewClickBlocker,
1714 true
1715 );
1716 mgr._overviewClickBlocker = null;
1717 }
1718 doAction(HOOKS.OVERVIEW_EXITED, {
1719 windowId: selected && maximize ? selected.id : void 0,
1720 reason: selected && maximize ? "select" : "cancel"
1721 });
1722 }, ANIMATION_MS);
1723 if (mgr._overviewPointerDownHandler) {
1724 mgr._desktop.removeEventListener(
1725 "pointerdown",
1726 mgr._overviewPointerDownHandler,
1727 true
1728 );
1729 mgr._overviewPointerDownHandler = null;
1730 }
1731 if (mgr._overviewPointerUpHandler) {
1732 mgr._desktop.removeEventListener(
1733 "pointerup",
1734 mgr._overviewPointerUpHandler,
1735 true
1736 );
1737 mgr._overviewPointerUpHandler = null;
1738 }
1739 mgr._overviewPressTarget = null;
1740 if (mgr._overviewKeyHandler) {
1741 document.removeEventListener("keydown", mgr._overviewKeyHandler);
1742 mgr._overviewKeyHandler = null;
1743 }
1744 if (mgr._overviewMouseHandler) {
1745 mgr._desktop.removeEventListener(
1746 "mouseover",
1747 mgr._overviewMouseHandler
1748 );
1749 mgr._overviewMouseHandler = null;
1750 }
1751 if (mgr._lastOverviewHoverId) {
1752 doAction(HOOKS.OVERVIEW_WINDOW_UNHOVER, {
1753 windowId: mgr._lastOverviewHoverId
1754 });
1755 mgr._lastOverviewHoverId = null;
1756 }
1757 }
1758 function getDesktops(mgr) {
1759 return [...mgr._desktops];
1760 }
1761 function getActiveDesktop(mgr) {
1762 const found = mgr._desktops.find((d) => d.id === mgr._activeDesktopId);
1763 return found ?? mgr._desktops[0];
1764 }
1765 function getActiveDesktopId(mgr) {
1766 return getActiveDesktop(mgr).id;
1767 }
1768 function applyDesktopVisibility(mgr, win) {
1769 const visible = win.config.desktopId === mgr._activeDesktopId;
1770 win.element.style.display = visible ? "" : "none";
1771 }
1772 function refreshDesktopVisibility(mgr) {
1773 for (const w of mgr._stack) {
1774 applyDesktopVisibility(mgr, w);
1775 }
1776 }
1777 function createDesktop(mgr) {
1778 mgr._desktopSeq++;
1779 const desktop = {
1780 id: `desktop-${mgr._desktopSeq}`,
1781 // translators: %d is the desktop number (e.g., "Desktop 2")
1782 label: sprintf(__("Desktop %d"), mgr._desktopSeq)
1783 };
1784 mgr._desktops.push(desktop);
1785 doAction(HOOKS.DESKTOP_CREATED, { desktopId: desktop.id });
1786 return desktop;
1787 }
1788 function switchDesktop(mgr, id, opts) {
1789 if (id === mgr._activeDesktopId) {
1790 return;
1791 }
1792 if (!mgr._desktops.some((d) => d.id === id)) {
1793 return;
1794 }
1795 const previousId = mgr._activeDesktopId;
1796 mgr._activeDesktopId = id;
1797 if (mgr._overviewActive) {
1798 relayoutOverviewForActiveDesktop(mgr);
1799 refreshOverviewTopBar(mgr);
1800 } else {
1801 refreshDesktopVisibility(mgr);
1802 if (opts?.direction) {
1803 animateDesktopSwitch(mgr, opts.direction);
1804 }
1805 const topOnNew = [...mgr._stack].reverse().find(
1806 (w) => w.config.desktopId === id && w.state !== "minimized"
1807 );
1808 if (topOnNew) {
1809 mgr.focus(topOnNew);
1810 }
1811 }
1812 doAction(HOOKS.DESKTOP_SWITCHED, {
1813 from: previousId,
1814 to: id
1815 });
1816 }
1817 function animateDesktopSwitch(mgr, direction) {
1818 const el = mgr._desktop;
1819 const cls = direction === "next" ? "desktop-mode-area--sliding-from-right" : "desktop-mode-area--sliding-from-left";
1820 el.classList.remove(
1821 "desktop-mode-area--sliding-from-right",
1822 "desktop-mode-area--sliding-from-left"
1823 );
1824 void el.offsetWidth;
1825 el.classList.add(cls);
1826 const onEnd = (e) => {
1827 if (!e.animationName.startsWith("desktop-mode-area-slide-from-")) {
1828 return;
1829 }
1830 el.classList.remove(cls);
1831 el.removeEventListener("animationend", onEnd);
1832 };
1833 el.addEventListener("animationend", onEnd);
1834 }
1835 function closeDesktop(mgr, id) {
1836 if (mgr._desktops.length <= 1) {
1837 return;
1838 }
1839 const idx = mgr._desktops.findIndex((d) => d.id === id);
1840 if (idx === -1) {
1841 return;
1842 }
1843 const survivorIdx = idx > 0 ? idx - 1 : 1;
1844 const survivor = mgr._desktops[survivorIdx];
1845 for (const w of mgr._stack) {
1846 if (w.config.desktopId === id) {
1847 w.config.desktopId = survivor.id;
1848 }
1849 }
1850 mgr._desktops.splice(idx, 1);
1851 const wasActive = mgr._activeDesktopId === id;
1852 if (wasActive) {
1853 mgr._activeDesktopId = survivor.id;
1854 }
1855 if (mgr._overviewActive) {
1856 relayoutOverviewForActiveDesktop(mgr);
1857 } else {
1858 refreshDesktopVisibility(mgr);
1859 }
1860 doAction(HOOKS.DESKTOP_CLOSED, {
1861 desktopId: id,
1862 migratedTo: survivor.id
1863 });
1864 }
1865 function relayoutOverviewForActiveDesktop(mgr) {
1866 for (const [winId, snap] of mgr._overviewSnapshot) {
1867 const w = mgr.getById(winId);
1868 if (w) {
1869 w.element.style.transform = snap.transform;
1870 w.element.style.transition = snap.transition;
1871 w.element.classList.remove("desktop-mode-window--overview");
1872 }
1873 }
1874 for (const label of mgr._overviewLabels.values()) {
1875 label.remove();
1876 }
1877 mgr._overviewLabels.clear();
1878 mgr._overviewSnapshot.clear();
1879 refreshDesktopVisibility(mgr);
1880 const eligible = mgr._stack.filter(
1881 (w) => w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
1882 );
1883 if (eligible.length === 0) {
1884 return;
1885 }
1886 for (const w of eligible) {
1887 mgr._overviewSnapshot.set(w.id, {
1888 transform: w.element.style.transform || "",
1889 transition: w.element.style.transition || ""
1890 });
1891 }
1892 const live = mgr._desktop.getBoundingClientRect();
1893 const targetRect = new DOMRect(0, 0, live.width, live.height);
1894 const layout = computeOverviewLayout(
1895 eligible,
1896 targetRect,
1897 OVERVIEW_TOP_BAR_RESERVE
1898 );
1899 for (const item of layout) {
1900 const el = item.win.element;
1901 el.classList.add("desktop-mode-window--overview");
1902 const dx = item.x - el.offsetLeft;
1903 const dy = item.y - el.offsetTop;
1904 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
1905 const label = createOverviewLabel(item);
1906 el.insertAdjacentElement("afterend", label);
1907 mgr._overviewLabels.set(item.win.id, label);
1908 }
1909 }
1910 function seedDesktops(mgr, desktops, activeDesktopId) {
1911 if (desktops.length === 0) {
1912 return;
1913 }
1914 mgr._desktops = desktops.map((d) => ({ ...d }));
1915 mgr._activeDesktopId = desktops.some((d) => d.id === activeDesktopId) ? activeDesktopId : desktops[0].id;
1916 let highest = 0;
1917 for (const d of desktops) {
1918 const match = d.id.match(/^desktop-(\d+)$/);
1919 if (match) {
1920 const n = parseInt(match[1], 10);
1921 if (Number.isFinite(n) && n > highest) {
1922 highest = n;
1923 }
1924 }
1925 }
1926 mgr._desktopSeq = Math.max(mgr._desktopSeq, highest);
1927 }
1928 function cascade(mgr) {
1929 const eligible = mgr._stack.filter(
1930 (w) => w.config.desktopId === mgr._activeDesktopId
1931 );
1932 if (eligible.length === 0) {
1933 return;
1934 }
1935 doAction(HOOKS.ARRANGE_CASCADE_STARTING, {
1936 windowCount: eligible.length
1937 });
1938 for (const w of eligible) {
1939 if (w.state === "minimized") {
1940 w.restore();
1941 }
1942 if (w.state === "fullscreen") {
1943 w.toggleFullscreen();
1944 }
1945 if (w.state === "maximized") {
1946 w.toggleMaximize();
1947 }
1948 }
1949 const rect = mgr._desktop.getBoundingClientRect();
1950 const padding = 30;
1951 const offset = 30;
1952 const targetWidth = Math.min(Math.round(rect.width * 0.7), 1100);
1953 const targetHeight = Math.min(Math.round(rect.height * 0.75), 750);
1954 const maxStepsX = Math.max(
1955 1,
1956 Math.floor((rect.width - targetWidth - padding) / offset)
1957 );
1958 const maxStepsY = Math.max(
1959 1,
1960 Math.floor((rect.height - targetHeight - padding) / offset)
1961 );
1962 const maxSteps = Math.min(maxStepsX, maxStepsY);
1963 eligible.forEach((w, i) => {
1964 const step = i % Math.max(1, maxSteps);
1965 w.element.style.left = `${padding + step * offset}px`;
1966 w.element.style.top = `${padding + step * offset}px`;
1967 w.element.style.width = `${targetWidth}px`;
1968 w.element.style.height = `${targetHeight}px`;
1969 });
1970 const focused = mgr.getFocused();
1971 if (focused) {
1972 mgr.focus(focused);
1973 }
1974 document.dispatchEvent(
1975 new CustomEvent("desktop-mode-window-changed", {
1976 detail: { reason: "cascade" }
1977 })
1978 );
1979 doAction(HOOKS.ARRANGE_CASCADE_APPLIED, {
1980 windowCount: eligible.length
1981 });
1982 }
1983 function tile(mgr) {
1984 const eligible = mgr._stack.filter(
1985 (w) => w.config.desktopId === mgr._activeDesktopId
1986 );
1987 if (eligible.length === 0) {
1988 return;
1989 }
1990 for (const w of eligible) {
1991 if (w.state === "minimized") {
1992 w.restore();
1993 }
1994 if (w.state === "fullscreen") {
1995 w.toggleFullscreen();
1996 }
1997 if (w.state === "maximized") {
1998 w.toggleMaximize();
1999 }
2000 }
2001 const rect = mgr._desktop.getBoundingClientRect();
2002 const auto = pickGridDimensions(
2003 eligible.length,
2004 rect.width,
2005 rect.height
2006 );
2007 const filtered = applyFilters(
2008 HOOKS.ARRANGE_TILE_DIMENSIONS,
2009 auto,
2010 {
2011 windowCount: eligible.length,
2012 areaWidth: rect.width,
2013 areaHeight: rect.height
2014 }
2015 );
2016 const { cols, rows } = isValidGrid(filtered, eligible.length) ? { cols: Math.floor(filtered.cols), rows: Math.floor(filtered.rows) } : auto;
2017 doAction(HOOKS.ARRANGE_TILE_STARTING, {
2018 windowCount: eligible.length,
2019 cols,
2020 rows
2021 });
2022 const padding = 16;
2023 const gap = 12;
2024 const cellWidth = Math.floor(
2025 (rect.width - padding * 2 - gap * (cols - 1)) / cols
2026 );
2027 const cellHeight = Math.floor(
2028 (rect.height - padding * 2 - gap * (rows - 1)) / rows
2029 );
2030 eligible.forEach((w, i) => {
2031 const col = i % cols;
2032 const row = Math.floor(i / cols);
2033 w.element.style.left = `${padding + col * (cellWidth + gap)}px`;
2034 w.element.style.top = `${padding + row * (cellHeight + gap)}px`;
2035 w.element.style.width = `${cellWidth}px`;
2036 w.element.style.height = `${cellHeight}px`;
2037 });
2038 const focused = mgr.getFocused();
2039 if (focused) {
2040 mgr.focus(focused);
2041 }
2042 document.dispatchEvent(
2043 new CustomEvent("desktop-mode-window-changed", {
2044 detail: { reason: "tile" }
2045 })
2046 );
2047 doAction(HOOKS.ARRANGE_TILE_APPLIED, {
2048 windowCount: eligible.length,
2049 cols,
2050 rows
2051 });
2052 }
2053 const SNAP_STORAGE_KEY = "desktop-mode-snap-to-grid";
2054 function loadSnapEnabled() {
2055 try {
2056 return window.localStorage.getItem(SNAP_STORAGE_KEY) === "1";
2057 } catch {
2058 return false;
2059 }
2060 }
2061 function setSnapEnabled(mgr, enabled) {
2062 if (mgr._snapEnabled === enabled) {
2063 return;
2064 }
2065 mgr._snapEnabled = enabled;
2066 try {
2067 window.localStorage.setItem(SNAP_STORAGE_KEY, enabled ? "1" : "0");
2068 } catch {
2069 }
2070 doAction(HOOKS.ARRANGE_SNAP_CHANGED, { enabled });
2071 }
2072 function getSnapConfig(mgr) {
2073 if (!mgr._snapEnabled) {
2074 return { enabled: false, cellWidth: 0, cellHeight: 0 };
2075 }
2076 const rect = mgr._desktop.getBoundingClientRect();
2077 const targetCols = rect.width >= rect.height ? 12 : 8;
2078 const auto = {
2079 cellWidth: Math.max(40, Math.round(rect.width / targetCols)),
2080 cellHeight: Math.max(
2081 40,
2082 Math.round(rect.height / Math.round(targetCols * 0.66))
2083 )
2084 };
2085 const filtered = applyFilters(
2086 HOOKS.ARRANGE_SNAP_CELL_SIZE,
2087 auto,
2088 { areaWidth: rect.width, areaHeight: rect.height }
2089 );
2090 const { cellWidth, cellHeight } = isValidCellSize(filtered) ? filtered : auto;
2091 return { enabled: true, cellWidth, cellHeight };
2092 }
2093 function enterSplitOverview(mgr, anchor, zone) {
2094 if (mgr._splitOverviewActive) {
2095 return;
2096 }
2097 mgr._splitOverviewActive = true;
2098 mgr._splitOverviewAnchor = anchor;
2099 mgr._splitOverviewZone = zone;
2100 const eligible = mgr._stack.filter(
2101 (w) => w !== anchor && w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
2102 );
2103 if (eligible.length === 0) {
2104 cleanupSplitOverviewState(mgr);
2105 return;
2106 }
2107 mgr._splitOverviewSnapshot.clear();
2108 for (const w of eligible) {
2109 mgr._splitOverviewSnapshot.set(w.id, {
2110 transform: w.element.style.transform || "",
2111 transition: w.element.style.transition || ""
2112 });
2113 }
2114 mgr._desktop.classList.add("desktop-mode-area--split-overview");
2115 const rect = oppositeHalfRect(mgr, zone);
2116 const layout = computeOverviewLayout(eligible, rect, 0);
2117 mgr._splitOverviewLabels.clear();
2118 for (const item of layout) {
2119 const el = item.win.element;
2120 el.classList.add("desktop-mode-window--overview");
2121 const dx = item.x - el.offsetLeft;
2122 const dy = item.y - el.offsetTop;
2123 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
2124 const label = createOverviewLabel(item);
2125 el.insertAdjacentElement("afterend", label);
2126 mgr._splitOverviewLabels.set(item.win.id, label);
2127 }
2128 const pressTargetForEvent = (e) => {
2129 const target2 = e.target;
2130 const winEl = target2?.closest(
2131 ".desktop-mode-window--overview"
2132 );
2133 if (winEl) {
2134 return {
2135 id: winEl.id.replace(/^wp-window-/, ""),
2136 element: winEl
2137 };
2138 }
2139 if (target2) {
2140 return { id: "dismiss", element: mgr._desktop };
2141 }
2142 return null;
2143 };
2144 mgr._splitOverviewPointerDown = (e) => {
2145 if (e.button !== 0) {
2146 mgr._splitOverviewPressTarget = null;
2147 return;
2148 }
2149 mgr._splitOverviewPressTarget = pressTargetForEvent(e);
2150 if (mgr._splitOverviewPressTarget) {
2151 e.preventDefault();
2152 e.stopPropagation();
2153 }
2154 };
2155 mgr._splitOverviewPointerUp = (e) => {
2156 if (e.button !== 0) {
2157 return;
2158 }
2159 const pressed = mgr._splitOverviewPressTarget;
2160 mgr._splitOverviewPressTarget = null;
2161 if (!pressed) {
2162 return;
2163 }
2164 const r = pressed.element.getBoundingClientRect();
2165 const inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
2166 if (!inside) {
2167 return;
2168 }
2169 e.preventDefault();
2170 e.stopPropagation();
2171 if (pressed.id === "dismiss") {
2172 exitSplitOverview(mgr);
2173 return;
2174 }
2175 const selected = mgr.getById(pressed.id);
2176 if (!selected) {
2177 exitSplitOverview(mgr);
2178 return;
2179 }
2180 fillOppositeHalfAndExit(mgr, selected);
2181 };
2182 mgr._splitOverviewKey = (e) => {
2183 if (e.key === "Escape") {
2184 exitSplitOverview(mgr);
2185 }
2186 };
2187 mgr._splitOverviewClickBlocker = (e) => {
2188 e.stopPropagation();
2189 e.preventDefault();
2190 };
2191 mgr._desktop.addEventListener(
2192 "pointerdown",
2193 mgr._splitOverviewPointerDown,
2194 true
2195 );
2196 mgr._desktop.addEventListener(
2197 "pointerup",
2198 mgr._splitOverviewPointerUp,
2199 true
2200 );
2201 mgr._desktop.addEventListener(
2202 "click",
2203 mgr._splitOverviewClickBlocker,
2204 true
2205 );
2206 document.addEventListener("keydown", mgr._splitOverviewKey);
2207 }
2208 function fillOppositeHalfAndExit(mgr, selected) {
2209 const anchorZone = mgr._splitOverviewZone;
2210 if (!anchorZone) {
2211 exitSplitOverview(mgr);
2212 return;
2213 }
2214 const partnerZone = anchorZone === "left" ? "right" : "left";
2215 selected.element.style.transform = "";
2216 selected.element.classList.remove("desktop-mode-window--overview");
2217 selected.applySnap(partnerZone);
2218 mgr._splitOverviewSnapshot.delete(selected.id);
2219 mgr.focus(selected);
2220 doAction(HOOKS.SNAP_SPLIT_FILLED, {
2221 windowId: selected.id,
2222 zone: partnerZone
2223 });
2224 exitSplitOverview(mgr);
2225 }
2226 function exitSplitOverview(mgr) {
2227 if (!mgr._splitOverviewActive) {
2228 return;
2229 }
2230 mgr._splitOverviewActive = false;
2231 for (const [id, snap] of mgr._splitOverviewSnapshot) {
2232 const w = mgr.getById(id);
2233 if (!w) {
2234 continue;
2235 }
2236 w.element.style.transform = snap.transform;
2237 }
2238 for (const label of mgr._splitOverviewLabels.values()) {
2239 label.classList.add("desktop-mode-overview-label--out");
2240 }
2241 mgr._desktop.classList.remove("desktop-mode-area--split-overview");
2242 const ANIMATION_MS = 260;
2243 window.setTimeout(() => {
2244 for (const w of mgr._stack) {
2245 if (mgr._splitOverviewSnapshot.has(w.id)) {
2246 w.element.classList.remove("desktop-mode-window--overview");
2247 }
2248 }
2249 for (const label of mgr._splitOverviewLabels.values()) {
2250 label.remove();
2251 }
2252 cleanupSplitOverviewState(mgr);
2253 }, ANIMATION_MS);
2254 if (mgr._splitOverviewPointerDown) {
2255 mgr._desktop.removeEventListener(
2256 "pointerdown",
2257 mgr._splitOverviewPointerDown,
2258 true
2259 );
2260 mgr._splitOverviewPointerDown = null;
2261 }
2262 if (mgr._splitOverviewPointerUp) {
2263 mgr._desktop.removeEventListener(
2264 "pointerup",
2265 mgr._splitOverviewPointerUp,
2266 true
2267 );
2268 mgr._splitOverviewPointerUp = null;
2269 }
2270 if (mgr._splitOverviewClickBlocker) {
2271 mgr._desktop.removeEventListener(
2272 "click",
2273 mgr._splitOverviewClickBlocker,
2274 true
2275 );
2276 mgr._splitOverviewClickBlocker = null;
2277 }
2278 if (mgr._splitOverviewKey) {
2279 document.removeEventListener("keydown", mgr._splitOverviewKey);
2280 mgr._splitOverviewKey = null;
2281 }
2282 mgr._splitOverviewPressTarget = null;
2283 }
2284 function cleanupSplitOverviewState(mgr) {
2285 mgr._splitOverviewSnapshot.clear();
2286 mgr._splitOverviewLabels.clear();
2287 mgr._splitOverviewAnchor = null;
2288 mgr._splitOverviewZone = null;
2289 mgr._splitOverviewActive = false;
2290 }
2291 const SNAP_EDGE_THRESHOLD = 30;
2292 const SNAP_COMMIT_MS = 260;
2293 function detectSnapZone(clientX, desktopRect) {
2294 if (clientX <= desktopRect.left + SNAP_EDGE_THRESHOLD) {
2295 return "left";
2296 }
2297 if (clientX >= desktopRect.right - SNAP_EDGE_THRESHOLD) {
2298 return "right";
2299 }
2300 return null;
2301 }
2302 function snapZoneBounds(mgr, zone) {
2303 const rect = mgr._desktop.getBoundingClientRect();
2304 const halfW = Math.floor(rect.width / 2);
2305 const height = Math.floor(rect.height);
2306 return {
2307 x: zone === "left" ? 0 : rect.width - halfW,
2308 y: 0,
2309 width: halfW,
2310 height
2311 };
2312 }
2313 function oppositeHalfRect(mgr, zone) {
2314 const rect = mgr._desktop.getBoundingClientRect();
2315 const halfW = Math.floor(rect.width / 2);
2316 const height = Math.floor(rect.height);
2317 if (zone === "left") {
2318 return new DOMRect(halfW, 0, halfW, height);
2319 }
2320 return new DOMRect(0, 0, halfW, height);
2321 }
2322 function showSnapPreview(mgr, zone) {
2323 if (mgr._snapPendingZone === zone && mgr._snapPreviewEl) {
2324 return;
2325 }
2326 mgr._snapPendingZone = zone;
2327 if (!mgr._snapPreviewEl) {
2328 const el = document.createElement("div");
2329 el.className = "desktop-mode-snap-preview";
2330 el.setAttribute("aria-hidden", "true");
2331 mgr._desktop.appendChild(el);
2332 mgr._snapPreviewEl = el;
2333 Promise.resolve().then(() => {
2334 el.classList.add("desktop-mode-snap-preview--visible");
2335 });
2336 }
2337 const b = snapZoneBounds(mgr, zone);
2338 mgr._snapPreviewEl.style.left = `${b.x}px`;
2339 mgr._snapPreviewEl.style.top = `${b.y}px`;
2340 mgr._snapPreviewEl.style.width = `${b.width}px`;
2341 mgr._snapPreviewEl.style.height = `${b.height}px`;
2342 mgr._snapPreviewEl.dataset.zone = zone;
2343 }
2344 function hideSnapPreview(mgr) {
2345 if (!mgr._snapPreviewEl) {
2346 mgr._snapPendingZone = null;
2347 return;
2348 }
2349 const el = mgr._snapPreviewEl;
2350 mgr._snapPreviewEl = null;
2351 mgr._snapPendingZone = null;
2352 el.classList.remove("desktop-mode-snap-preview--visible");
2353 window.setTimeout(() => {
2354 el.remove();
2355 }, SNAP_COMMIT_MS);
2356 }
2357 function updateSnapZoneForDrag(mgr, win, clientX) {
2358 if (mgr._splitOverviewActive) {
2359 return;
2360 }
2361 const rect = mgr._desktop.getBoundingClientRect();
2362 const zone = detectSnapZone(clientX, rect);
2363 const previous = mgr._snapPendingZone;
2364 if (zone) {
2365 showSnapPreview(mgr, zone);
2366 if (previous !== zone) {
2367 doAction(HOOKS.SNAP_ZONE_PENDING, {
2368 windowId: win.id,
2369 zone
2370 });
2371 }
2372 } else if (previous) {
2373 hideSnapPreview(mgr);
2374 doAction(HOOKS.SNAP_ZONE_CANCELED, { windowId: win.id });
2375 }
2376 }
2377 function commitSnapIfPending(mgr, win) {
2378 const zone = mgr._snapPendingZone;
2379 if (!zone) {
2380 return false;
2381 }
2382 hideSnapPreview(mgr);
2383 if (win.state === "normal") {
2384 win._savedGeometry = {
2385 x: win.element.offsetLeft,
2386 y: win.element.offsetTop,
2387 width: win.element.offsetWidth,
2388 height: win.element.offsetHeight
2389 };
2390 }
2391 win.applySnap(zone);
2392 doAction(HOOKS.SNAP_ZONE_COMMITTED, {
2393 windowId: win.id,
2394 zone
2395 });
2396 window.requestAnimationFrame(() => {
2397 enterSplitOverview(mgr, win, zone);
2398 });
2399 return true;
2400 }
2401 function abortSnapIfPending(mgr) {
2402 if (mgr._snapPendingZone) {
2403 hideSnapPreview(mgr);
2404 }
2405 }
2406 const NATIVE_GEOMETRY_STORAGE_KEY = "desktop-mode-native-window-geometry";
2407 const MAX_ENTRIES = 64;
2408 const MAX_DIMENSION = 8192;
2409 function readMap$1() {
2410 try {
2411 const raw = window.localStorage.getItem(NATIVE_GEOMETRY_STORAGE_KEY);
2412 if (!raw) {
2413 return {};
2414 }
2415 const parsed = JSON.parse(raw);
2416 if (!parsed || typeof parsed !== "object") {
2417 return {};
2418 }
2419 return parsed;
2420 } catch {
2421 return {};
2422 }
2423 }
2424 function writeMap$1(map) {
2425 try {
2426 window.localStorage.setItem(
2427 NATIVE_GEOMETRY_STORAGE_KEY,
2428 JSON.stringify(map)
2429 );
2430 } catch {
2431 }
2432 }
2433 function loadNativeWindowGeometry(baseId) {
2434 if (!baseId) {
2435 return null;
2436 }
2437 const map = readMap$1();
2438 const entry = map[baseId];
2439 if (!entry) {
2440 return null;
2441 }
2442 const width = Number(entry.width);
2443 const height = Number(entry.height);
2444 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2445 return null;
2446 }
2447 const state2 = entry.state === "maximized" ? "maximized" : void 0;
2448 const x = Number(entry.x);
2449 const y = Number(entry.y);
2450 const hasPosition = Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0 && x <= MAX_DIMENSION && y <= MAX_DIMENSION;
2451 return {
2452 width: Math.round(width),
2453 height: Math.round(height),
2454 ...hasPosition ? { x: Math.round(x), y: Math.round(y) } : {},
2455 ...state2 ? { state: state2 } : {}
2456 };
2457 }
2458 function saveNativeWindowGeometry(baseId, geometry) {
2459 if (!baseId) {
2460 return;
2461 }
2462 const width = Math.round(Number(geometry.width));
2463 const height = Math.round(Number(geometry.height));
2464 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2465 return;
2466 }
2467 const map = readMap$1();
2468 const prev = map[baseId];
2469 const state2 = prev && prev.state === "maximized" ? "maximized" : void 0;
2470 const carriedX = typeof prev?.x === "number" ? prev.x : void 0;
2471 const carriedY = typeof prev?.y === "number" ? prev.y : void 0;
2472 if (prev && prev.width === width && prev.height === height && prev.state === state2 && prev.x === carriedX && prev.y === carriedY) {
2473 return;
2474 }
2475 upsertEntry(map, baseId, {
2476 width,
2477 height,
2478 ...typeof carriedX === "number" && typeof carriedY === "number" ? { x: carriedX, y: carriedY } : {},
2479 ...state2 ? { state: state2 } : {}
2480 });
2481 writeMapTrimmed(map);
2482 }
2483 function saveNativeWindowPosition(baseId, position) {
2484 if (!baseId) {
2485 return;
2486 }
2487 const x = Math.round(Number(position.x));
2488 const y = Math.round(Number(position.y));
2489 if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || y < 0 || x > MAX_DIMENSION || y > MAX_DIMENSION) {
2490 return;
2491 }
2492 const map = readMap$1();
2493 const prev = map[baseId];
2494 if (!prev) {
2495 return;
2496 }
2497 if (prev.x === x && prev.y === y) {
2498 return;
2499 }
2500 upsertEntry(map, baseId, {
2501 ...prev,
2502 x,
2503 y
2504 });
2505 writeMapTrimmed(map);
2506 }
2507 function setNativeWindowSavedState(baseId, state2, defaults) {
2508 if (!baseId) {
2509 return;
2510 }
2511 const map = readMap$1();
2512 const prev = map[baseId];
2513 if (!prev) {
2514 if (state2 === null || !defaults) {
2515 return;
2516 }
2517 const width = Math.round(Number(defaults.width));
2518 const height = Math.round(Number(defaults.height));
2519 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2520 return;
2521 }
2522 upsertEntry(map, baseId, { width, height, state: state2 });
2523 writeMapTrimmed(map);
2524 return;
2525 }
2526 if (state2 === null) {
2527 if (!prev.state) {
2528 return;
2529 }
2530 const { state: _state2, ...rest } = prev;
2531 upsertEntry(map, baseId, rest);
2532 writeMapTrimmed(map);
2533 return;
2534 }
2535 if (prev.state === state2) {
2536 return;
2537 }
2538 upsertEntry(map, baseId, {
2539 ...prev,
2540 state: state2
2541 });
2542 writeMapTrimmed(map);
2543 }
2544 function upsertEntry(map, baseId, entry) {
2545 delete map[baseId];
2546 map[baseId] = entry;
2547 }
2548 function writeMapTrimmed(map) {
2549 const keys = Object.keys(map);
2550 if (keys.length > MAX_ENTRIES) {
2551 const trimmed = {};
2552 for (const key of keys.slice(-MAX_ENTRIES)) {
2553 trimmed[key] = map[key];
2554 }
2555 writeMap$1(trimmed);
2556 return;
2557 }
2558 writeMap$1(map);
2559 }
2560 const BASE_Z_INDEX = 100;
2561 const CASCADE_OFFSET = 30;
2562 class WindowManager {
2563 constructor(desktop) {
2564 this._stack = [];
2565 this.cascadeIndex = 0;
2566 this._desktops = [
2567 // translators: default desktop name — "Desktop 1"
2568 { id: "desktop-1", label: "Desktop 1" }
2569 ];
2570 this._activeDesktopId = "desktop-1";
2571 this._desktopSeq = 1;
2572 this.onToggleStartupRequested = null;
2573 this.desktopResizeObserver = null;
2574 this._reflowRestoreTimer = null;
2575 this._snapEnabled = loadSnapEnabled();
2576 this._overviewActive = false;
2577 this._overviewSnapshot = /* @__PURE__ */ new Map();
2578 this._overviewLabels = /* @__PURE__ */ new Map();
2579 this._overviewPointerDownHandler = null;
2580 this._overviewPointerUpHandler = null;
2581 this._overviewKeyHandler = null;
2582 this._overviewPressTarget = null;
2583 this._overviewClickBlocker = null;
2584 this._overviewTopBar = null;
2585 this._overviewMouseHandler = null;
2586 this._lastOverviewHoverId = null;
2587 this._overviewAddTileFocused = false;
2588 this._snapPendingZone = null;
2589 this._snapPreviewEl = null;
2590 this._splitOverviewActive = false;
2591 this._splitOverviewAnchor = null;
2592 this._splitOverviewZone = null;
2593 this._splitOverviewSnapshot = /* @__PURE__ */ new Map();
2594 this._splitOverviewLabels = /* @__PURE__ */ new Map();
2595 this._splitOverviewPointerDown = null;
2596 this._splitOverviewPointerUp = null;
2597 this._splitOverviewPressTarget = null;
2598 this._splitOverviewClickBlocker = null;
2599 this._splitOverviewKey = null;
2600 this._desktop = desktop;
2601 if (typeof ResizeObserver !== "undefined") {
2602 this.desktopResizeObserver = new ResizeObserver(
2603 () => this.reflowStatefulWindows()
2604 );
2605 this.desktopResizeObserver.observe(desktop);
2606 }
2607 this.installIframeFocusBridge();
2608 }
2609 /**
2610 * Clicks inside an iframe don't cross the browsing-context
2611 * boundary — pointerdown / focusin in the iframe's document never
2612 * reach the parent. BUT the parent `window` does lose focus,
2613 * because focus moves to the iframe's content window.
2614 *
2615 * We use that signal: listen for `window.blur` on the parent,
2616 * check `document.activeElement` — if it's an iframe, walk up to
2617 * its owning `.desktop-mode-window`, find the matching Window in
2618 * our stack, and focus it. Covers clicks on the primary iframe
2619 * AND any external-tab sub-iframes mounted as descendants of the
2620 * window element.
2621 */
2622 installIframeFocusBridge() {
2623 window.addEventListener("blur", () => {
2624 window.setTimeout(() => {
2625 const active2 = this._desktop.ownerDocument?.activeElement ?? null;
2626 if (!active2 || active2.tagName !== "IFRAME") {
2627 return;
2628 }
2629 const winEl = active2.closest(
2630 ".desktop-mode-window"
2631 );
2632 if (!winEl) {
2633 return;
2634 }
2635 const id = winEl.id.replace(/^wp-window-/, "");
2636 const win = this.getById(id);
2637 if (!win) {
2638 return;
2639 }
2640 if (this._overviewActive) {
2641 return;
2642 }
2643 if (this.getFocused() === win) {
2644 return;
2645 }
2646 this.focus(win);
2647 }, 0);
2648 });
2649 }
2650 /**
2651 * Re-apply state-driven bounds to any window whose geometry is
2652 * derived from the desktop area's dimensions: maximized (full
2653 * area) and snapped-left / snapped-right (half area). Called from
2654 * the desktop-area ResizeObserver so shrinking the browser window
2655 * drags the stateful windows along with it.
2656 *
2657 * Inlines the geometry writes instead of calling `applySnap` —
2658 * that method emits `_emitChange('state')` which would spam the
2659 * session saver on every resize tick. Viewport resize is an
2660 * INCOMING shape change (the shell reshaped us), not an outgoing
2661 * user action worth persisting.
2662 *
2663 * Also toggles `desktop-mode-window--reflowing` so the base
2664 * left/top/width/height transition doesn't interpolate between
2665 * every ResizeObserver tick — without that, the windows would
2666 * always lag ~250 ms behind a browser edge-drag.
2667 *
2668 * Skipped while overview is active — windows are mid-transform
2669 * and touching their inline geometry would desync the live
2670 * transform math; overview exit re-applies state correctly via
2671 * its own path.
2672 */
2673 reflowStatefulWindows() {
2674 if (this._overviewActive) {
2675 return;
2676 }
2677 for (const w of this._stack) {
2678 const parent = w.element.parentElement;
2679 if (!parent) {
2680 continue;
2681 }
2682 if (w.state === "maximized") {
2683 w.element.classList.add("desktop-mode-window--reflowing");
2684 w.element.style.width = `${parent.clientWidth}px`;
2685 w.element.style.height = `${parent.clientHeight}px`;
2686 } else if (w.state === "snapped-left" || w.state === "snapped-right") {
2687 w.element.classList.add("desktop-mode-window--reflowing");
2688 const halfW = Math.floor(parent.clientWidth / 2);
2689 const height = parent.clientHeight;
2690 const left = w.state === "snapped-left" ? 0 : halfW;
2691 w.element.style.left = `${left}px`;
2692 w.element.style.top = "0px";
2693 w.element.style.width = `${halfW}px`;
2694 w.element.style.height = `${height}px`;
2695 }
2696 }
2697 if (this._reflowRestoreTimer !== null) {
2698 window.clearTimeout(this._reflowRestoreTimer);
2699 }
2700 this._reflowRestoreTimer = window.setTimeout(() => {
2701 this._reflowRestoreTimer = null;
2702 for (const w of this._stack) {
2703 w.element.classList.remove("desktop-mode-window--reflowing");
2704 }
2705 }, 140);
2706 }
2707 /**
2708 * Open a new window — or focus an existing one — for the given
2709 * page.
2710 *
2711 * Matches any existing window sharing the same `baseId`
2712 * (defaulting to the config's `id`). For singleton pages
2713 * (Settings, Dashboard, …) `baseId === id`, so this behaves
2714 * exactly like strict id matching. For multi pages, clicking the
2715 * dock icon while a window is already open focuses the
2716 * most-recent instance rather than creating a twin.
2717 *
2718 * To force a brand-new instance alongside an existing one, use
2719 * {@link openNew}.
2720 */
2721 async open(config) {
2722 if (!config || typeof config !== "object") {
2723 throw new TypeError(
2724 "windowManager.open() requires a config object with at least { id, url, title }; received " + (config === null ? "null" : typeof config)
2725 );
2726 }
2727 if (typeof config.id !== "string" || config.id === "") {
2728 throw new TypeError(
2729 "windowManager.open(): config.id must be a non-empty string."
2730 );
2731 }
2732 if (typeof config.url !== "string" || config.url === "") {
2733 throw new TypeError(
2734 '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.'
2735 );
2736 }
2737 if (typeof config.title !== "string") {
2738 throw new TypeError(
2739 "windowManager.open(): config.title must be a string."
2740 );
2741 }
2742 const baseId = config.baseId || config.id;
2743 const existing = this.getByBaseIdOnActiveDesktop(baseId);
2744 if (existing) {
2745 const wasMinimized = existing.state === "minimized";
2746 this.focus(existing);
2747 if (wasMinimized) {
2748 existing.restore();
2749 }
2750 const reopenedDetail = {
2751 windowId: existing.id,
2752 baseId,
2753 wasMinimized
2754 };
2755 document.dispatchEvent(
2756 new CustomEvent("desktop-mode-window-reopened", { detail: reopenedDetail })
2757 );
2758 doAction(HOOKS.WINDOW_REOPENED, reopenedDetail);
2759 return existing;
2760 }
2761 const id = this.getByBaseId(baseId) ? this.nextInstanceId(baseId) : config.id;
2762 return this.createWindow({ ...config, id, baseId });
2763 }
2764 /**
2765 * Open a brand-new window even if one is already open for this
2766 * page. Only makes sense for pages flagged `multi`.
2767 *
2768 * Duplicates always open in the floating ('normal') state and at
2769 * a fresh cascade slot — the per-baseId saved size / state /
2770 * position preferences apply to the primary instance only.
2771 * Spawning a maximized twin alongside the maximized primary
2772 * would hide the primary; landing a twin on top of the primary's
2773 * remembered position would hide it too. Callers can override
2774 * either default by passing `initialState` / `x` / `y` explicitly.
2775 */
2776 async openNew(config) {
2777 const baseId = config.baseId || config.id;
2778 const nextId2 = this.nextInstanceId(baseId);
2779 const cascadeX = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2780 const cascadeY = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2781 return this.createWindow({
2782 initialState: "normal",
2783 x: cascadeX,
2784 y: cascadeY,
2785 ...config,
2786 id: nextId2,
2787 baseId
2788 });
2789 }
2790 /**
2791 * Build and mount a window element. Common tail shared by
2792 * `open()` and `openNew()`.
2793 */
2794 async createWindow(config) {
2795 const desktopRect = this._desktop.getBoundingClientRect();
2796 const defaultWidth = Math.min(Math.round(desktopRect.width * 0.8), 1200);
2797 const defaultHeight = Math.min(Math.round(desktopRect.height * 0.8), 800);
2798 const cascadeX = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2799 const cascadeY = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2800 const resolvedBaseId = config.baseId || config.id;
2801 const minWidth = config.minWidth ?? 320;
2802 const minHeight = config.minHeight ?? 200;
2803 const hasExplicitWidth = typeof config.width === "number";
2804 const hasExplicitHeight = typeof config.height === "number";
2805 const hasExplicitX = typeof config.x === "number";
2806 const hasExplicitY = typeof config.y === "number";
2807 const hasExplicitState = typeof config.initialState === "string";
2808 const saved = !hasExplicitWidth || !hasExplicitHeight || !hasExplicitState || !hasExplicitX || !hasExplicitY ? loadNativeWindowGeometry(resolvedBaseId) : null;
2809 const resolvedWidth = config.width ?? (saved ? Math.max(saved.width, minWidth) : defaultWidth);
2810 const resolvedHeight = config.height ?? (saved ? Math.max(saved.height, minHeight) : defaultHeight);
2811 const resolvedState = config.initialState ?? (saved?.state === "maximized" ? "maximized" : void 0);
2812 let clampedSavedX;
2813 let clampedSavedY;
2814 if (saved && typeof saved.x === "number" && typeof saved.y === "number") {
2815 const margin = 12;
2816 const maxX = Math.max(
2817 0,
2818 desktopRect.width - resolvedWidth - margin
2819 );
2820 const maxY = Math.max(
2821 0,
2822 desktopRect.height - resolvedHeight - margin
2823 );
2824 clampedSavedX = Math.max(margin, Math.min(saved.x, maxX));
2825 clampedSavedY = Math.max(margin, Math.min(saved.y, maxY));
2826 }
2827 const resolvedX = config.x ?? clampedSavedX ?? cascadeX;
2828 const resolvedY = config.y ?? clampedSavedY ?? cascadeY;
2829 const callerPinned = hasExplicitWidth || hasExplicitHeight || hasExplicitX || hasExplicitY || hasExplicitState;
2830 const hasSavedGeometry = !!saved;
2831 const preFilterGeometry = {
2832 x: resolvedX,
2833 y: resolvedY,
2834 width: resolvedWidth,
2835 height: resolvedHeight,
2836 state: resolvedState
2837 };
2838 let filtered;
2839 try {
2840 filtered = applyFilters(
2841 HOOKS.WINDOW_GEOMETRY,
2842 preFilterGeometry,
2843 {
2844 windowId: config.id,
2845 baseId: resolvedBaseId,
2846 hasSavedGeometry,
2847 callerPinned,
2848 desktopRect: {
2849 width: desktopRect.width,
2850 height: desktopRect.height
2851 }
2852 }
2853 );
2854 } catch (err) {
2855 doAction(HOOKS.SHELL_ERROR, {
2856 scope: "window-geometry-filter",
2857 windowId: config.id,
2858 error: err
2859 });
2860 if (typeof console !== "undefined") {
2861 console.error(
2862 `[desktop-mode] WINDOW_GEOMETRY filter threw for "${config.id}":`,
2863 err
2864 );
2865 }
2866 filtered = preFilterGeometry;
2867 }
2868 const coalesce = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback;
2869 const safeFiltered = filtered && typeof filtered === "object" ? filtered : preFilterGeometry;
2870 const finalWidth = Math.max(
2871 coalesce(safeFiltered.width, resolvedWidth),
2872 minWidth
2873 );
2874 const finalHeight = Math.max(
2875 coalesce(safeFiltered.height, resolvedHeight),
2876 minHeight
2877 );
2878 const finalX = coalesce(safeFiltered.x, resolvedX);
2879 const finalY = coalesce(safeFiltered.y, resolvedY);
2880 const finalState = safeFiltered.state ?? resolvedState;
2881 const fullConfig = {
2882 icon: config.icon || "dashicons-admin-generic",
2883 ...config,
2884 // Spread `config` first so callers can pass through any
2885 // extras (render, ownerHandle, parentUrl, …), then pin the
2886 // dimensions + state we resolved above. The pin has to
2887 // follow the spread because an explicit `width: undefined`
2888 // from the caller would otherwise blow away the default.
2889 x: finalX,
2890 y: finalY,
2891 width: finalWidth,
2892 height: finalHeight,
2893 minWidth,
2894 minHeight,
2895 ...finalState ? { initialState: finalState } : {},
2896 baseId: resolvedBaseId,
2897 // New windows always join the active desktop. A caller can
2898 // pre-seed `desktopId` (e.g. session restore) by passing it
2899 // in `config`, which the spread above preserves.
2900 desktopId: config.desktopId || this._activeDesktopId
2901 };
2902 this.cascadeIndex++;
2903 const [system] = await Promise.all([
2904 ensureWindowSystemLoaded(windowSystemBundleUrl()),
2905 ensureShellOverlaysLoaded(shellOverlaysBundleUrl())
2906 ]);
2907 const win = system.createWindow(fullConfig);
2908 win.onFocusRequest = (w) => this.focus(w);
2909 win.onClose = (w) => this.remove(w);
2910 win.onMinimize = () => {
2911 const visible = this._stack.filter((w) => w.state !== "minimized");
2912 if (visible.length > 0) {
2913 this.focus(visible[visible.length - 1]);
2914 }
2915 };
2916 win.onOpenAnother = (w) => {
2917 const baseId = w.config.baseId || w.id;
2918 if (w.config.native) {
2919 const api = window.wp?.desktop;
2920 if (api?.openNewWindow?.(baseId, { source: "open-another" })) {
2921 return;
2922 }
2923 }
2924 void this.openNew({
2925 id: baseId,
2926 baseId,
2927 url: w.config.url || "",
2928 title: w.config.title,
2929 icon: w.config.icon,
2930 submenu: w.config.submenu,
2931 multi: true
2932 });
2933 };
2934 win.onOpenInNewWindow = (w) => {
2935 const baseId = w.config.baseId || w.id;
2936 if (w.config.native) {
2937 const api = window.wp?.desktop;
2938 if (api?.openNewWindow?.(baseId, { source: "open-in-new-window" })) {
2939 return;
2940 }
2941 }
2942 const currentUrl = w.getCurrentUrl();
2943 void this.openNew({
2944 id: baseId,
2945 baseId,
2946 url: currentUrl || w.config.url || "",
2947 title: w.config.title,
2948 icon: w.config.icon,
2949 submenu: w.config.submenu,
2950 multi: true
2951 });
2952 };
2953 win.onToggleStartup = (w) => {
2954 this.onToggleStartupRequested?.(w);
2955 };
2956 win.snapConfigProvider = () => this.getSnapConfig();
2957 win.onDragMove = (w, clientX) => {
2958 updateSnapZoneForDrag(this, w, clientX);
2959 };
2960 win.onDragEnd = (w) => {
2961 if (this._snapPendingZone) {
2962 return commitSnapIfPending(this, w);
2963 }
2964 abortSnapIfPending(this);
2965 return false;
2966 };
2967 this._stack.push(win);
2968 this._desktop.appendChild(win.element);
2969 applyDesktopVisibility(this, win);
2970 win.hydrateNative();
2971 this.focus(win);
2972 const openedDetail = {
2973 windowId: win.id,
2974 page: config.url,
2975 title: config.title,
2976 url: config.url
2977 };
2978 document.dispatchEvent(
2979 new CustomEvent("desktop-mode-window-opened", { detail: openedDetail })
2980 );
2981 doAction(HOOKS.WINDOW_OPENED, openedDetail);
2982 return win;
2983 }
2984 /**
2985 * Find the next unused suffixed id for a given baseId. Prefers
2986 * the bare baseId itself if free (user closed the original), then
2987 * walks `-2`, `-3`, … until it lands on one not currently in the
2988 * stack.
2989 */
2990 nextInstanceId(baseId) {
2991 const taken = new Set(this._stack.map((w) => w.id));
2992 if (!taken.has(baseId)) {
2993 return baseId;
2994 }
2995 let n = 2;
2996 while (taken.has(`${baseId}-${n}`)) {
2997 n++;
2998 }
2999 return `${baseId}-${n}`;
3000 }
3001 /** Focus a window: bring it to top of z-stack. */
3002 focus(win) {
3003 const previouslyFocused = this._stack.length > 0 ? this._stack[this._stack.length - 1] : null;
3004 const priorFullscreen = this._stack.find(
3005 (w) => w !== win && w.isFocused() && w.isFullscreen()
3006 );
3007 if (priorFullscreen) {
3008 const shouldExit = applyFilters(
3009 HOOKS.WINDOW_AUTO_EXIT_FULLSCREEN,
3010 true,
3011 { windowId: priorFullscreen.id, focusedTo: win.id }
3012 );
3013 if (shouldExit) {
3014 priorFullscreen.toggleFullscreen();
3015 }
3016 }
3017 const idx = this._stack.indexOf(win);
3018 if (idx > -1) {
3019 this._stack.splice(idx, 1);
3020 }
3021 this._stack.push(win);
3022 this._stack.forEach((w, i) => {
3023 w.setZIndex(BASE_Z_INDEX + i);
3024 w.setFocused(i === this._stack.length - 1);
3025 });
3026 if (previouslyFocused && previouslyFocused !== win && previouslyFocused.id !== win.id) {
3027 const blurredDetail = {
3028 windowId: previouslyFocused.id,
3029 focusedTo: win.id
3030 };
3031 document.dispatchEvent(
3032 new CustomEvent("desktop-mode-window-blurred", { detail: blurredDetail })
3033 );
3034 doAction(HOOKS.WINDOW_BLURRED, blurredDetail);
3035 }
3036 const focusedDetail = { windowId: win.id };
3037 document.dispatchEvent(
3038 new CustomEvent("desktop-mode-window-focused", { detail: focusedDetail })
3039 );
3040 doAction(HOOKS.WINDOW_FOCUSED, focusedDetail);
3041 }
3042 /** Remove a window from the stack and DOM. */
3043 remove(win) {
3044 const idx = this._stack.indexOf(win);
3045 if (idx > -1) {
3046 this._stack.splice(idx, 1);
3047 }
3048 if (this._stack.length > 0) {
3049 this.focus(this._stack[this._stack.length - 1]);
3050 }
3051 const closingDetail = { windowId: win.id, element: win.element };
3052 document.dispatchEvent(
3053 new CustomEvent("desktop-mode-window-closing", { detail: closingDetail })
3054 );
3055 doAction(HOOKS.WINDOW_CLOSING, closingDetail);
3056 const closedDetail = { windowId: win.id };
3057 document.dispatchEvent(
3058 new CustomEvent("desktop-mode-window-closed", { detail: closedDetail })
3059 );
3060 doAction(HOOKS.WINDOW_CLOSED, closedDetail);
3061 }
3062 /** Get a window by its ID. */
3063 getById(id) {
3064 return this._stack.find((w) => w.id === id);
3065 }
3066 /**
3067 * Get the most-recently-focused window for a given baseId.
3068 *
3069 * Multi-instance windows share a baseId; the stack is ordered
3070 * bottom to top by focus, so iterating from the end finds the
3071 * best candidate to bring forward when the user re-clicks the
3072 * dock icon.
3073 */
3074 getByBaseId(baseId) {
3075 for (let i = this._stack.length - 1; i >= 0; i--) {
3076 const w = this._stack[i];
3077 if ((w.config.baseId || w.id) === baseId) {
3078 return w;
3079 }
3080 }
3081 return void 0;
3082 }
3083 /**
3084 * Like {@link getByBaseId} but only considers windows on the
3085 * currently-active virtual desktop. The dock's "open or focus"
3086 * path uses this — a Plugins instance that lives on Desktop 2 is
3087 * invisible from Desktop 1's dock click, so clicking Plugins on
3088 * Desktop 1 should open a fresh instance there instead of trying
3089 * to focus the far-off sibling (which would silently do nothing
3090 * because the other desktop's windows are display: none here).
3091 */
3092 getByBaseIdOnActiveDesktop(baseId) {
3093 for (let i = this._stack.length - 1; i >= 0; i--) {
3094 const w = this._stack[i];
3095 if ((w.config.baseId || w.id) !== baseId) {
3096 continue;
3097 }
3098 const winDesktop = w.config.desktopId || this._activeDesktopId;
3099 if (winDesktop === this._activeDesktopId) {
3100 return w;
3101 }
3102 }
3103 return void 0;
3104 }
3105 /**
3106 * Get every open window sharing the given baseId, ordered by
3107 * instance slot (bare baseId first, then `-2`, `-3`, …) rather
3108 * than z-order — so the dock's instance rail keeps a stable
3109 * left-to-right order even as the user focuses between windows.
3110 */
3111 getAllByBaseId(baseId) {
3112 const instanceSlot = (id) => {
3113 if (id === baseId) {
3114 return 1;
3115 }
3116 const prefix = `${baseId}-`;
3117 if (id.startsWith(prefix)) {
3118 const n = parseInt(id.slice(prefix.length), 10);
3119 return Number.isFinite(n) ? n : 999;
3120 }
3121 return 999;
3122 };
3123 return this._stack.filter((w) => (w.config.baseId || w.id) === baseId).sort((a, b) => instanceSlot(a.id) - instanceSlot(b.id));
3124 }
3125 /** Get all open windows. */
3126 getAll() {
3127 return [...this._stack];
3128 }
3129 /**
3130 * Find the window whose iframe's contentWindow matches the given
3131 * message source. Used by cross-frame bridges to attribute inbound
3132 * `postMessage` events to the originating window without reaching
3133 * into `_stack`.
3134 */
3135 findByIframeSource(source) {
3136 if (!source) {
3137 return void 0;
3138 }
3139 return this._stack.find(
3140 (w) => w.iframe !== null && w.iframe.contentWindow === source
3141 );
3142 }
3143 /** Get the currently focused (topmost) window. */
3144 getFocused() {
3145 return this._stack.length > 0 ? this._stack[this._stack.length - 1] : void 0;
3146 }
3147 /**
3148 * "Is the window with this id currently in front of the user?"
3149 *
3150 * Returns true when the window exists in the manager AND it
3151 * isn't minimized AND it's the currently focused (topmost)
3152 * window. False otherwise — including for unknown ids, closed
3153 * windows, minimized windows, or windows that exist but aren't
3154 * on top.
3155 *
3156 * The canonical query for plugins implementing the "show
3157 * something *only when the user can't already see my
3158 * window*" pattern (badge counts, attention pulses, sounds,
3159 * toasts). Plugins that previously hand-rolled
3160 * `getById(id) && state !== 'minimized' && focused` can
3161 * collapse to this.
3162 *
3163 * @since 0.5.5
3164 *
3165 * @param id Window id to query.
3166 * @return True when the user is actively looking at this window.
3167 */
3168 isActive(id) {
3169 const win = this.getById(id);
3170 if (!win) {
3171 return false;
3172 }
3173 if (win.state === "minimized") {
3174 return false;
3175 }
3176 const focused = this.getFocused();
3177 return !!focused && focused.id === id;
3178 }
3179 // ---- Virtual desktop delegations ----
3180 getDesktops() {
3181 return getDesktops(this);
3182 }
3183 getActiveDesktop() {
3184 return getActiveDesktop(this);
3185 }
3186 getActiveDesktopId() {
3187 return getActiveDesktopId(this);
3188 }
3189 createDesktop() {
3190 return createDesktop(this);
3191 }
3192 switchDesktop(id, opts) {
3193 switchDesktop(this, id, opts);
3194 }
3195 closeDesktop(id) {
3196 closeDesktop(this, id);
3197 }
3198 /**
3199 * Returns the "primary" desktop id — the one new sessions land on
3200 * and that batch operations like {@link closeAll} treat as the
3201 * survivor when an `onlyOnPrimary` mode is requested.
3202 *
3203 * Default: the first desktop in `getDesktops()`. Filterable via
3204 * `desktop-mode.primary-desktop-id` so downstream code that wants a
3205 * different convention (e.g. a pinned "Inbox" desktop) can override
3206 * without having to fork the manager.
3207 *
3208 * @since 0.14.0
3209 */
3210 getPrimaryDesktopId() {
3211 const all2 = this.getDesktops();
3212 const fallback = all2.length > 0 ? all2[0].id : "desktop-1";
3213 const filtered = applyFilters(
3214 HOOKS.PRIMARY_DESKTOP_ID,
3215 fallback,
3216 all2
3217 );
3218 if (typeof filtered !== "string" || filtered === "") {
3219 return fallback;
3220 }
3221 const exists = all2.some((d) => d.id === filtered);
3222 return exists ? filtered : fallback;
3223 }
3224 /**
3225 * Close every open window in batch.
3226 *
3227 * Hook chain:
3228 *
3229 * 1. `desktop-mode.windows.before-close-all` — action. Subscribers
3230 * can prepare for the wipe (cancel pending saves, dismiss
3231 * menus, etc.). Detail: `{ candidates: Window[] }`.
3232 *
3233 * 2. `desktop-mode.windows.close-all` — filter. Receives the
3234 * candidate Window list and returns the (possibly smaller) list
3235 * that will actually be closed. Plugins use this to PROTECT
3236 * specific windows — e.g. keep a draft post window open during
3237 * a "Close all" operation. Returning an empty array cancels
3238 * the close entirely.
3239 *
3240 * 3. Each surviving window's `close()` is called.
3241 *
3242 * 4. `desktop-mode.windows.after-close-all` — action. Detail:
3243 * `{ closed: number, skipped: Window[] }`.
3244 *
3245 * @since 0.14.0
3246 *
3247 * @param options Close options.
3248 * @param options.exceptIds Window ids to skip even before the filter runs.
3249 * @return Number of windows actually closed.
3250 */
3251 closeAll(options) {
3252 const exceptSet = new Set(options?.exceptIds ?? []);
3253 const initialCandidates = this._stack.filter(
3254 (w) => !exceptSet.has(w.id)
3255 );
3256 doAction(HOOKS.WINDOWS_BEFORE_CLOSE_ALL, { candidates: initialCandidates });
3257 const filtered = applyFilters(
3258 HOOKS.WINDOWS_CLOSE_ALL,
3259 initialCandidates
3260 );
3261 const finalList = Array.isArray(filtered) ? filtered : initialCandidates;
3262 const skipped = initialCandidates.filter((w) => !finalList.includes(w));
3263 let closed = 0;
3264 for (const win of finalList.slice()) {
3265 try {
3266 win.close();
3267 closed++;
3268 } catch (err) {
3269 if (typeof console !== "undefined") {
3270 console.error(
3271 "[desktop-mode] closeAll: window.close() threw for",
3272 win.id,
3273 err
3274 );
3275 }
3276 }
3277 }
3278 doAction(HOOKS.WINDOWS_AFTER_CLOSE_ALL, { closed, skipped });
3279 return closed;
3280 }
3281 /**
3282 * Minimize every currently-non-minimized window. Returns the
3283 * exact set that was minimized — i.e., excludes windows already
3284 * in the `'minimized'` state — so callers can pair the call with
3285 * a later {@link restoreFrom} that touches only the windows
3286 * they minimized.
3287 *
3288 * The "Show Desktop" gesture (clicking the wallpaper) routes
3289 * through this method (and {@link restoreFrom} on the second
3290 * click); plugin authors building expand/collapse UIs that
3291 * mimic the gesture should use these primitives instead of
3292 * rolling the loop themselves.
3293 *
3294 * @public
3295 * @since 0.18.0
3296 */
3297 minimizeAll() {
3298 const minimized = [];
3299 for (const win of this._stack.slice()) {
3300 if (win.state === "minimized") {
3301 continue;
3302 }
3303 try {
3304 win.minimize();
3305 minimized.push(win);
3306 } catch (err) {
3307 if (typeof console !== "undefined") {
3308 console.error(
3309 "[desktop-mode] minimizeAll: window.minimize() threw for",
3310 win.id,
3311 err
3312 );
3313 }
3314 }
3315 }
3316 return minimized;
3317 }
3318 /**
3319 * Restore the given window list — the symmetric counterpart to
3320 * {@link minimizeAll}. Skips windows that have since been
3321 * closed and windows the user manually un-minimized between
3322 * the minimize and the restore.
3323 *
3324 * Pass the array {@link minimizeAll} returned to restore
3325 * exactly what you minimized; pass any subset to restore
3326 * selectively.
3327 *
3328 * @public
3329 * @since 0.18.0
3330 */
3331 restoreFrom(windows) {
3332 if (!Array.isArray(windows)) {
3333 return;
3334 }
3335 const live = new Set(this._stack);
3336 for (const win of windows) {
3337 if (!live.has(win)) {
3338 continue;
3339 }
3340 if (win.state !== "minimized") {
3341 continue;
3342 }
3343 try {
3344 win.restore();
3345 } catch (err) {
3346 if (typeof console !== "undefined") {
3347 console.error(
3348 "[desktop-mode] restoreFrom: window.restore() threw for",
3349 win.id,
3350 err
3351 );
3352 }
3353 }
3354 }
3355 }
3356 /**
3357 * Toggle the "Show Desktop" state — if every live window is
3358 * already minimized, restore them all; otherwise minimize the
3359 * non-minimized cohort. Returns `true` when the new state is
3360 * "showing the desktop" (everything minimized after the call),
3361 * `false` when windows have just been restored.
3362 *
3363 * Mirrors the wallpaper-click gesture exactly, in one call.
3364 *
3365 * @public
3366 * @since 0.18.0
3367 */
3368 toggleShowDesktop() {
3369 const all2 = this._stack.slice();
3370 if (all2.length === 0) {
3371 return false;
3372 }
3373 const allMinimized = all2.every((w) => w.state === "minimized");
3374 if (allMinimized) {
3375 for (const win of all2) {
3376 try {
3377 win.restore();
3378 } catch {
3379 }
3380 }
3381 return false;
3382 }
3383 this.minimizeAll();
3384 return true;
3385 }
3386 // ---- Arrange + snap delegations ----
3387 cascade() {
3388 cascade(this);
3389 }
3390 tile() {
3391 tile(this);
3392 }
3393 isSnapEnabled() {
3394 return this._snapEnabled;
3395 }
3396 setSnapEnabled(enabled) {
3397 setSnapEnabled(this, enabled);
3398 }
3399 getSnapConfig() {
3400 return getSnapConfig(this);
3401 }
3402 // ---- Overview delegations ----
3403 enterOverview() {
3404 enterOverview(this);
3405 }
3406 exitOverview(selected, maximize = false) {
3407 exitOverview(this, selected, maximize);
3408 }
3409 /**
3410 * Snapshot every open window's current geometry + state.
3411 *
3412 * Returns a plain array of `{ windowId, rect, state, element }`
3413 * entries — one per window in the stack, regardless of which
3414 * virtual desktop owns it. Rect coordinates are in desktop-area
3415 * space (the same coordinate space the windows themselves use
3416 * inline-style left/top); `state` is the live `WindowState`, and
3417 * `element` is the window's outer DOM node.
3418 *
3419 * Intended for wallpaper / overlay plugins that used to scrape
3420 * `document.querySelectorAll('.desktop-mode-window')` + read the
3421 * `--minimized` / `--maximized` modifier classes by name. The
3422 * accessor decouples plugin code from the shell's CSS class
3423 * naming, so a future refactor of modifier prefixes is not an
3424 * ecosystem break.
3425 *
3426 * The array contains every window in the stack — callers filter
3427 * on `state` if they want only "actually visible" (typically
3428 * `state !== 'minimized'`). Minimized windows are included so
3429 * plugins that care about the "will be restored to X geometry"
3430 * case still have the data; filtering them out would be a
3431 * subtraction the caller can do but the provider can't reverse.
3432 *
3433 * Order matches the internal z-stack: earliest-opened first,
3434 * focused window last.
3435 */
3436 getVisibleRects() {
3437 return this._stack.map((w) => {
3438 const snap = w.getSnapshot();
3439 return {
3440 windowId: w.id,
3441 rect: {
3442 x: snap.x,
3443 y: snap.y,
3444 width: snap.width,
3445 height: snap.height
3446 },
3447 state: snap.state,
3448 element: w.element
3449 };
3450 });
3451 }
3452 /**
3453 * Serialize the current window stack for session persistence.
3454 *
3455 * Order in the returned `windows` array mirrors z-order (earliest
3456 * opened / lowest-z first, focused last) so restoring preserves
3457 * the stacking the user left behind.
3458 */
3459 snapshot() {
3460 const focused = this.getFocused();
3461 const persistable = this._stack.filter((w) => !w.config.native);
3462 const windows = persistable.map((w) => {
3463 const snap = w.getSnapshot();
3464 const externalTabs = w.getExternalTabsSnapshot();
3465 return {
3466 id: w.id,
3467 baseId: w.config.baseId || w.id,
3468 desktopId: w.config.desktopId || this._activeDesktopId,
3469 url: w.getCurrentUrl(),
3470 title: w.config.title,
3471 icon: w.config.icon,
3472 state: snap.state,
3473 x: snap.x,
3474 y: snap.y,
3475 width: snap.width,
3476 height: snap.height,
3477 ...externalTabs.length > 0 ? { externalTabs } : {}
3478 };
3479 });
3480 const focusedId = focused && !focused.config.native ? focused.id : "";
3481 return {
3482 windows,
3483 desktops: this.getDesktops(),
3484 activeDesktop: this._activeDesktopId,
3485 focused: focusedId,
3486 updated: Math.floor(Date.now() / 1e3)
3487 };
3488 }
3489 seedDesktops(desktops, activeDesktopId) {
3490 seedDesktops(this, desktops, activeDesktopId);
3491 }
3492 }
3493 function cycleableWindows(mgr) {
3494 const activeDesktopId = mgr.getActiveDesktopId();
3495 const domOrder = Array.from(mgr._desktop.children);
3496 return mgr.getAll().filter((w) => {
3497 const winDesktop = w.config.desktopId || activeDesktopId;
3498 return winDesktop === activeDesktopId;
3499 }).sort(
3500 (a, b) => domOrder.indexOf(a.element) - domOrder.indexOf(b.element)
3501 );
3502 }
3503 function cycleFocus(mgr, direction) {
3504 if (mgr._overviewActive) {
3505 return;
3506 }
3507 const list2 = cycleableWindows(mgr);
3508 if (list2.length < 2) {
3509 return;
3510 }
3511 const focused = mgr.getFocused();
3512 const currentIdx = focused ? list2.indexOf(focused) : -1;
3513 const step = direction === "next" ? 1 : -1;
3514 const nextIdx = (currentIdx + step + list2.length) % list2.length;
3515 const target2 = list2[nextIdx];
3516 if (target2.state === "minimized") {
3517 target2.restore();
3518 } else {
3519 mgr.focus(target2);
3520 }
3521 }
3522 let installed$3 = false;
3523 function isTextEntryFocus(doc) {
3524 let el = doc.activeElement;
3525 while (el && el.shadowRoot && el.shadowRoot.activeElement) {
3526 el = el.shadowRoot.activeElement;
3527 }
3528 if (!el) {
3529 return false;
3530 }
3531 if (el instanceof HTMLIFrameElement) {
3532 return true;
3533 }
3534 if (el instanceof HTMLTextAreaElement) {
3535 return true;
3536 }
3537 if (el instanceof HTMLInputElement) {
3538 const textTypes = /* @__PURE__ */ new Set([
3539 "text",
3540 "search",
3541 "url",
3542 "email",
3543 "password",
3544 "tel",
3545 "number",
3546 "date",
3547 "datetime-local",
3548 "month",
3549 "week",
3550 "time"
3551 ]);
3552 return textTypes.has(el.type);
3553 }
3554 if (el instanceof HTMLElement && el.isContentEditable === true) {
3555 return true;
3556 }
3557 const ce = el.getAttribute("contenteditable");
3558 return ce !== null && ce !== "false";
3559 }
3560 function installWindowSwitcherShortcut(mgr) {
3561 if (installed$3) {
3562 return;
3563 }
3564 installed$3 = true;
3565 document.addEventListener(
3566 "keydown",
3567 (e) => {
3568 if (e.ctrlKey || e.metaKey || e.altKey) {
3569 return;
3570 }
3571 if (e.code !== "Backquote") {
3572 return;
3573 }
3574 if (isTextEntryFocus(document)) {
3575 return;
3576 }
3577 e.preventDefault();
3578 cycleFocus(mgr, e.shiftKey ? "prev" : "next");
3579 },
3580 true
3581 );
3582 const origin = window.location.origin;
3583 window.addEventListener("message", (e) => {
3584 if (e.origin !== origin) {
3585 return;
3586 }
3587 const data = e.data;
3588 if (!data || data.type !== "desktop-mode-window-switch") {
3589 return;
3590 }
3591 cycleFocus(mgr, data.direction === "prev" ? "prev" : "next");
3592 });
3593 }
3594 function switchToAdjacentDesktop(mgr, direction) {
3595 const desktops = mgr.getDesktops();
3596 if (desktops.length < 2) {
3597 return false;
3598 }
3599 const activeId = mgr.getActiveDesktopId();
3600 const idx = desktops.findIndex((d) => d.id === activeId);
3601 if (idx === -1) {
3602 return false;
3603 }
3604 const step = direction === "next" ? 1 : -1;
3605 const targetIdx = (idx + step + desktops.length) % desktops.length;
3606 if (targetIdx === idx) {
3607 return false;
3608 }
3609 mgr.switchDesktop(desktops[targetIdx].id, { direction });
3610 return true;
3611 }
3612 function cycleOverviewCursor(mgr, direction) {
3613 if (!mgr._overviewActive) {
3614 return false;
3615 }
3616 const desktops = mgr.getDesktops();
3617 const cycleLength = desktops.length + 1;
3618 const ADD_INDEX = desktops.length;
3619 const currentIdx = mgr._overviewAddTileFocused ? ADD_INDEX : desktops.findIndex((d) => d.id === mgr.getActiveDesktopId());
3620 if (currentIdx === -1) {
3621 return false;
3622 }
3623 const step = direction === "next" ? 1 : -1;
3624 const targetIdx = (currentIdx + step + cycleLength) % cycleLength;
3625 if (targetIdx === currentIdx) {
3626 return false;
3627 }
3628 if (targetIdx === ADD_INDEX) {
3629 mgr._overviewAddTileFocused = true;
3630 refreshOverviewTopBar(mgr);
3631 return true;
3632 }
3633 mgr._overviewAddTileFocused = false;
3634 mgr.switchDesktop(desktops[targetIdx].id, { direction });
3635 return true;
3636 }
3637 function toggleOverview(mgr) {
3638 if (mgr._overviewActive) {
3639 mgr.exitOverview();
3640 } else {
3641 mgr.enterOverview();
3642 }
3643 return true;
3644 }
3645 function toggleShowDesktop(mgr) {
3646 if (mgr._overviewActive) {
3647 return false;
3648 }
3649 if (mgr.getAll().length === 0) {
3650 return false;
3651 }
3652 mgr.toggleShowDesktop();
3653 return true;
3654 }
3655 function exitOverviewIfActive(mgr) {
3656 if (!mgr._overviewActive) {
3657 return false;
3658 }
3659 mgr.exitOverview();
3660 return true;
3661 }
3662 function isShowDesktopActive(mgr) {
3663 const all2 = mgr.getAll();
3664 if (all2.length === 0) {
3665 return false;
3666 }
3667 return all2.every((w) => w.state === "minimized");
3668 }
3669 function exitShowDesktopIfActive(mgr) {
3670 if (!isShowDesktopActive(mgr)) {
3671 return false;
3672 }
3673 mgr.toggleShowDesktop();
3674 return true;
3675 }
3676 let installed$2 = false;
3677 function installDesktopArrowShortcuts(mgr) {
3678 if (installed$2) {
3679 return;
3680 }
3681 installed$2 = true;
3682 document.addEventListener(
3683 "keydown",
3684 (e) => {
3685 if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) {
3686 return;
3687 }
3688 if (e.code !== "ArrowLeft" && e.code !== "ArrowRight" && e.code !== "ArrowUp" && e.code !== "ArrowDown") {
3689 return;
3690 }
3691 if (isTextEntryFocus(document)) {
3692 return;
3693 }
3694 let handled = false;
3695 switch (e.code) {
3696 case "ArrowLeft":
3697 handled = mgr._overviewActive ? cycleOverviewCursor(mgr, "prev") : switchToAdjacentDesktop(mgr, "prev");
3698 break;
3699 case "ArrowRight":
3700 handled = mgr._overviewActive ? cycleOverviewCursor(mgr, "next") : switchToAdjacentDesktop(mgr, "next");
3701 break;
3702 case "ArrowUp":
3703 handled = exitOverviewIfActive(mgr) || exitShowDesktopIfActive(mgr) || toggleOverview(mgr);
3704 break;
3705 case "ArrowDown":
3706 handled = exitOverviewIfActive(mgr) || toggleShowDesktop(mgr);
3707 break;
3708 }
3709 if (handled) {
3710 e.preventDefault();
3711 }
3712 },
3713 true
3714 );
3715 }
3716 const IDENTITY_PARAMS = [
3717 "post_type",
3718 "page",
3719 "taxonomy",
3720 // WooCommerce (and other React-app-style plugins) register
3721 // SEPARATE top-level admin menus that all share `?page=wc-admin`
3722 // and only differ by `path` (e.g. `path=/analytics/overview`,
3723 // `path=/marketing`). Without `path` in the identity set, every
3724 // such menu collapses to the same window id — opening any one of
3725 // them lights up the dock indicator for ALL of them. WC's
3726 // /admin/path query is the most prominent example today; future
3727 // plugins that route inside `admin.php?page=` via a custom param
3728 // can either piggyback on `path` or grow this list.
3729 "path",
3730 // The post ID on `post.php?post=X&action=edit`. Without this, every
3731 // individual post edit URL collapses to `post-php`, so clicking a
3732 // second row in the Posts window just refocuses the first post's
3733 // window instead of opening the new one.
3734 "post",
3735 // Site-editor entity path: `site-editor.php?p=/wp_template_part/
3736 // twentytwentyfive//footer-columns`. Each template / template
3737 // part / pattern / navigation entity is a distinct "page" from
3738 // the user's perspective — picking "Header" after "Footer column"
3739 // should open a new window, not refocus the existing footer one.
3740 // Without `p` in identity, every site-editor URL collapses to
3741 // `site-editor-php` and the second pick is a no-op.
3742 "p"
3743 ];
3744 function slugify$1(path) {
3745 return path.replace(/\.php/g, "-php").replace(/[?&=]/g, "-").replace(/[^a-zA-Z0-9_-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "") || "index";
3746 }
3747 function deriveWindowId(url, adminUrl) {
3748 let parsed = null;
3749 try {
3750 parsed = new URL(url, adminUrl);
3751 } catch (err) {
3752 parsed = null;
3753 }
3754 if (parsed) {
3755 const basePath = new URL(adminUrl).pathname;
3756 const filename = parsed.pathname.replace(basePath, "").replace(/^\/+/, "");
3757 const significant = new URLSearchParams();
3758 for (const key of IDENTITY_PARAMS) {
3759 const value = parsed.searchParams.get(key);
3760 if (value) {
3761 significant.set(key, value);
3762 }
3763 }
3764 const query = significant.toString();
3765 return slugify$1(query ? `${filename}?${query}` : filename);
3766 }
3767 let path = url.replace(adminUrl, "");
3768 if (path.startsWith("/")) {
3769 path = path.substring(1);
3770 }
3771 return slugify$1(path);
3772 }
3773 function sanitizeClassName(value) {
3774 return value.replace(/[^a-zA-Z0-9_-]/g, "");
3775 }
3776 function applyTileEntryStagger(tile2) {
3777 tile2.style.setProperty(
3778 "--desktop-mode-file-tile-enter-delay",
3779 `${(Math.random() * 0.25).toFixed(3)}s`
3780 );
3781 tile2.style.setProperty(
3782 "--desktop-mode-file-tile-enter-duration",
3783 `${(0.3 + Math.random() * 0.25).toFixed(3)}s`
3784 );
3785 }
3786 function urlMatchKey(url) {
3787 try {
3788 const parsed = new URL(url, window.location.origin);
3789 parsed.searchParams.delete("desktop_mode_chromeless");
3790 parsed.searchParams.delete("desktop_mode_portal");
3791 return parsed.pathname.replace(/\/+$/, "") + "?" + parsed.searchParams.toString();
3792 } catch {
3793 return url;
3794 }
3795 }
3796 function sanitizeIconSvg(svg) {
3797 if (typeof svg !== "string" || svg === "") {
3798 return "";
3799 }
3800 if (typeof DOMParser === "undefined") {
3801 return "";
3802 }
3803 let doc;
3804 try {
3805 doc = new DOMParser().parseFromString(svg, "image/svg+xml");
3806 } catch {
3807 return "";
3808 }
3809 const root = doc.documentElement;
3810 if (!root || root.nodeName.toLowerCase() !== "svg") {
3811 return "";
3812 }
3813 if (doc.getElementsByTagName("parsererror").length > 0) {
3814 return "";
3815 }
3816 const BANNED_TAGS = /* @__PURE__ */ new Set(["script", "style", "foreignobject", "iframe", "object", "embed"]);
3817 const walk2 = (el) => {
3818 const children = Array.from(el.children);
3819 for (const child of children) {
3820 if (BANNED_TAGS.has(child.nodeName.toLowerCase())) {
3821 child.remove();
3822 continue;
3823 }
3824 for (const attr of Array.from(child.attributes)) {
3825 const name = attr.name.toLowerCase();
3826 const value = attr.value.trim().toLowerCase();
3827 if (name.startsWith("on")) {
3828 child.removeAttribute(attr.name);
3829 continue;
3830 }
3831 if (value.startsWith("javascript:")) {
3832 child.removeAttribute(attr.name);
3833 }
3834 }
3835 walk2(child);
3836 }
3837 };
3838 walk2(root);
3839 for (const attr of Array.from(root.attributes)) {
3840 const name = attr.name.toLowerCase();
3841 const value = attr.value.trim().toLowerCase();
3842 if (name.startsWith("on") || value.startsWith("javascript:")) {
3843 root.removeAttribute(attr.name);
3844 }
3845 }
3846 return root.outerHTML;
3847 }
3848 const _parentSubs = /* @__PURE__ */ new Map();
3849 const _nativeSubs = /* @__PURE__ */ new Map();
3850 function bucket(root, windowId, channel, create) {
3851 let perWindow = root.get(windowId);
3852 if (!perWindow) {
3853 if (!create) {
3854 return void 0;
3855 }
3856 perWindow = /* @__PURE__ */ new Map();
3857 root.set(windowId, perWindow);
3858 }
3859 let bucketSet = perWindow.get(channel);
3860 if (!bucketSet) {
3861 if (!create) {
3862 return void 0;
3863 }
3864 bucketSet = /* @__PURE__ */ new Set();
3865 perWindow.set(channel, bucketSet);
3866 }
3867 return bucketSet;
3868 }
3869 function dispatch(root, windowId, channel, payload) {
3870 const meta = { channel, windowId };
3871 const exact = bucket(root, windowId, channel, false);
3872 if (exact) {
3873 for (const cb of Array.from(exact)) {
3874 try {
3875 cb(payload, meta);
3876 } catch (err) {
3877 if (typeof console !== "undefined") {
3878 console.error(
3879 `[desktop-mode] window-channel subscriber for "${channel}" threw:`,
3880 err
3881 );
3882 }
3883 }
3884 }
3885 }
3886 const wildcard = bucket(root, windowId, "*", false);
3887 if (wildcard) {
3888 for (const cb of Array.from(wildcard)) {
3889 try {
3890 cb(payload, meta);
3891 } catch (err) {
3892 if (typeof console !== "undefined") {
3893 console.error(
3894 `[desktop-mode] window-channel wildcard subscriber for "${windowId}" threw:`,
3895 err
3896 );
3897 }
3898 }
3899 }
3900 }
3901 }
3902 function addParentSubscriber(windowId, channel, cb) {
3903 const set = bucket(_parentSubs, windowId, channel, true);
3904 set.add(cb);
3905 let removed = false;
3906 return () => {
3907 if (removed) {
3908 return;
3909 }
3910 removed = true;
3911 set.delete(cb);
3912 };
3913 }
3914 function dispatchFromWindow(windowId, channel, payload) {
3915 dispatch(_parentSubs, windowId, channel, payload);
3916 }
3917 function dispatchToNative(windowId, channel, payload) {
3918 dispatch(_nativeSubs, windowId, channel, payload);
3919 }
3920 const _readyWindows = /* @__PURE__ */ new Set();
3921 const _loadingWindows = /* @__PURE__ */ new Set();
3922 const _pendingSends = /* @__PURE__ */ new Map();
3923 function markWindowContentReady(windowId) {
3924 if (!_readyWindows.has(windowId)) {
3925 _readyWindows.add(windowId);
3926 const queued = _pendingSends.get(windowId);
3927 if (queued) {
3928 _pendingSends.delete(windowId);
3929 for (const m of queued) {
3930 try {
3931 m.flush();
3932 } catch (err) {
3933 if (typeof console !== "undefined") {
3934 console.error(
3935 `[desktop-mode] flushing queued window-send for "${m.channel}" threw:`,
3936 err
3937 );
3938 }
3939 }
3940 }
3941 }
3942 }
3943 if (_loadingWindows.delete(windowId)) {
3944 doAction(HOOKS.WINDOW_CONTENT_LOADED, { windowId });
3945 if (typeof document !== "undefined") {
3946 document.dispatchEvent(
3947 new CustomEvent("desktop-mode-window-content-loaded", {
3948 detail: { windowId }
3949 })
3950 );
3951 }
3952 }
3953 }
3954 const WINDOW_CONFIG_KEY = Symbol.for("desktop-mode/window-config");
3955 function getWindowConfigFromElement(el) {
3956 return el[WINDOW_CONFIG_KEY];
3957 }
3958 function buildDefaultLoadingOverlay() {
3959 const overlay = document.createElement("div");
3960 overlay.className = "desktop-mode-window__loading";
3961 overlay.setAttribute("aria-hidden", "true");
3962 const spinner = document.createElement("wpd-spinner");
3963 spinner.setAttribute("preset", "classic");
3964 spinner.setAttribute("size", "clamp(96px, 14vw, 192px)");
3965 spinner.setAttribute("label", __("Loading window content"));
3966 overlay.appendChild(spinner);
3967 return overlay;
3968 }
3969 function createLoadingOverlay(config) {
3970 let overlay = buildDefaultLoadingOverlay();
3971 const ctx = { windowId: config.id, config };
3972 if (typeof config.loading?.render === "function") {
3973 try {
3974 config.loading.render(overlay, ctx);
3975 } catch (err) {
3976 if (typeof console !== "undefined") {
3977 console.error(
3978 `[desktop-mode] loading.render threw for "${config.id}":`,
3979 err
3980 );
3981 }
3982 }
3983 }
3984 try {
3985 const filtered = applyFilters(
3986 HOOKS.WINDOW_LOADING_OVERLAY,
3987 overlay,
3988 ctx
3989 );
3990 if (filtered instanceof HTMLElement) {
3991 overlay = filtered;
3992 }
3993 } catch (err) {
3994 if (typeof console !== "undefined") {
3995 console.error(
3996 `[desktop-mode] WINDOW_LOADING_OVERLAY filter threw for "${config.id}":`,
3997 err
3998 );
3999 }
4000 }
4001 if (overlay && !overlay.classList.contains("desktop-mode-window__loading")) {
4002 overlay.classList.add("desktop-mode-window__loading");
4003 }
4004 return overlay;
4005 }
4006 function removeLoadingOverlay(windowEl) {
4007 const overlay = windowEl.querySelector(":scope .desktop-mode-window__loading");
4008 overlay?.remove();
4009 }
4010 function ensureLoadingOverlay(windowEl) {
4011 const body = windowEl.querySelector(
4012 ":scope .desktop-mode-window__body"
4013 );
4014 if (!body) {
4015 return;
4016 }
4017 const existing = body.querySelector(":scope .desktop-mode-window__loading");
4018 if (existing) {
4019 return;
4020 }
4021 const config = getWindowConfigFromElement(windowEl);
4022 body.appendChild(config ? createLoadingOverlay(config) : buildDefaultLoadingOverlay());
4023 }
4024 const FADE_OUT_MS$1 = 250;
4025 let _installed$3 = false;
4026 function findWindowElement(windowId) {
4027 if (!windowId) {
4028 return null;
4029 }
4030 return document.getElementById(`wp-window-${windowId}`);
4031 }
4032 function installWindowLoadingTransitions() {
4033 if (_installed$3) {
4034 return;
4035 }
4036 _installed$3 = true;
4037 _installSubscriptions();
4038 }
4039 function _installSubscriptions() {
4040 addAction(
4041 HOOKS.WINDOW_CONTENT_LOADING,
4042 "desktop-mode/window-loading-enter",
4043 (e) => {
4044 const el = findWindowElement(e?.windowId ?? "");
4045 if (!el) {
4046 return;
4047 }
4048 const body = el.querySelector(
4049 ":scope .desktop-mode-window__body"
4050 );
4051 if (!body) {
4052 return;
4053 }
4054 body.classList.add("desktop-mode-window__body--loading");
4055 ensureLoadingOverlay(el);
4056 }
4057 );
4058 addAction(
4059 HOOKS.WINDOW_CONTENT_LOADED,
4060 "desktop-mode/window-loading-exit",
4061 (e) => {
4062 const el = findWindowElement(e?.windowId ?? "");
4063 if (!el) {
4064 return;
4065 }
4066 const body = el.querySelector(
4067 ":scope .desktop-mode-window__body"
4068 );
4069 if (!body) {
4070 return;
4071 }
4072 body.classList.remove("desktop-mode-window__body--loading");
4073 window.setTimeout(() => {
4074 if (!body.classList.contains("desktop-mode-window__body--loading")) {
4075 removeLoadingOverlay(el);
4076 }
4077 }, FADE_OUT_MS$1);
4078 }
4079 );
4080 addAction(
4081 HOOKS.INIT,
4082 "desktop-mode/loading-overlay-init-sweep",
4083 () => {
4084 queueMicrotask(() => repaintLoadingOverlays());
4085 }
4086 );
4087 }
4088 function repaintLoadingOverlays() {
4089 const bodies = document.querySelectorAll(
4090 ".desktop-mode-window__body--loading"
4091 );
4092 bodies.forEach((body) => {
4093 const windowEl = body.closest(".desktop-mode-window");
4094 if (!windowEl) {
4095 return;
4096 }
4097 body.querySelector(":scope .desktop-mode-window__loading")?.remove();
4098 ensureLoadingOverlay(windowEl);
4099 });
4100 }
4101 const SHARED_STORES_SLOT = "__desktopModeSharedStores";
4102 function resolveSlot() {
4103 const w = window;
4104 let slot = w[SHARED_STORES_SLOT];
4105 if (!slot) {
4106 slot = /* @__PURE__ */ new Map();
4107 w[SHARED_STORES_SLOT] = slot;
4108 }
4109 return slot;
4110 }
4111 function createSharedStore(key, initialState) {
4112 const slot = resolveSlot();
4113 let record = slot.get(key);
4114 if (!record) {
4115 record = {
4116 state: initialState(),
4117 listeners: /* @__PURE__ */ new Set(),
4118 rebuild: initialState
4119 };
4120 slot.set(key, record);
4121 }
4122 const handle = {
4123 // `record.state` is the live reference. The getter on the
4124 // `state` field reads the latest value even if `reset()`
4125 // reassigned it to a fresh object.
4126 get state() {
4127 return record.state;
4128 },
4129 set state(next) {
4130 record.state = next;
4131 },
4132 getState() {
4133 return record.state;
4134 },
4135 notify() {
4136 for (const cb of Array.from(record.listeners)) {
4137 try {
4138 cb(record.state);
4139 } catch (err) {
4140 console.error(
4141 `[desktop-mode/shared-store:${key}] subscriber threw:`,
4142 err
4143 );
4144 }
4145 }
4146 },
4147 subscribe(cb) {
4148 record.listeners.add(cb);
4149 return () => {
4150 record.listeners.delete(cb);
4151 };
4152 },
4153 setState(patch) {
4154 const cur = record.state;
4155 if (typeof cur !== "object" || cur === null) {
4156 console.warn(
4157 `[desktop-mode/shared-store:${key}] setState called on a primitive store; use the state setter instead.`
4158 );
4159 return;
4160 }
4161 Object.assign(cur, patch);
4162 handle.notify();
4163 },
4164 reset() {
4165 const fresh = record.rebuild();
4166 const cur = record.state;
4167 if (typeof cur === "object" && cur !== null && typeof fresh === "object" && fresh !== null) {
4168 const target2 = cur;
4169 for (const k of Object.keys(target2)) {
4170 delete target2[k];
4171 }
4172 Object.assign(target2, fresh);
4173 } else {
4174 record.state = fresh;
4175 }
4176 record.listeners.clear();
4177 }
4178 };
4179 return handle;
4180 }
4181 const remapStore = createSharedStore(
4182 "desktop-mode/native-url-remap",
4183 () => ({ remaps: [], deps: null })
4184 );
4185 function bindNativeUrlRemap(bound) {
4186 remapStore.state.deps = bound;
4187 }
4188 function registerNativeUrlRemap(entry) {
4189 if (!entry || typeof entry.id !== "string" || entry.id.trim() === "") {
4190 return () => {
4191 };
4192 }
4193 if (typeof entry.nativeWindowId !== "string" || entry.nativeWindowId === "") {
4194 return () => {
4195 };
4196 }
4197 if (typeof entry.matches !== "function") {
4198 return () => {
4199 };
4200 }
4201 const remaps = remapStore.state.remaps;
4202 const existingIdx = remaps.findIndex((r) => r.id === entry.id);
4203 if (existingIdx >= 0) {
4204 remaps.splice(existingIdx, 1);
4205 }
4206 remaps.push(entry);
4207 return () => unregisterNativeUrlRemap(entry.id);
4208 }
4209 function unregisterNativeUrlRemap(id) {
4210 const remaps = remapStore.state.remaps;
4211 const i = remaps.findIndex((r) => r.id === id);
4212 if (i >= 0) {
4213 remaps.splice(i, 1);
4214 }
4215 }
4216 function resolveNativeUrlRemap(url) {
4217 const { deps: deps2, remaps } = remapStore.state;
4218 if (!deps2 || !url) {
4219 return null;
4220 }
4221 let parsed;
4222 try {
4223 parsed = new URL(url, deps2.adminUrl);
4224 } catch {
4225 return null;
4226 }
4227 const snapshot = deps2.getSnapshot();
4228 for (const entry of remaps) {
4229 if (!entry.matches(url, parsed)) {
4230 continue;
4231 }
4232 if (entry.enabled && !entry.enabled(snapshot)) {
4233 continue;
4234 }
4235 return entry.nativeWindowId;
4236 }
4237 return null;
4238 }
4239 function tryNativeUrlRemap(url) {
4240 const { deps: deps2, remaps } = remapStore.state;
4241 if (!deps2 || !url) {
4242 return false;
4243 }
4244 let parsed;
4245 try {
4246 parsed = new URL(url, deps2.adminUrl);
4247 } catch {
4248 return false;
4249 }
4250 const snapshot = deps2.getSnapshot();
4251 for (const entry of remaps) {
4252 if (!entry.matches(url, parsed)) {
4253 continue;
4254 }
4255 if (entry.enabled && !entry.enabled(snapshot)) {
4256 continue;
4257 }
4258 if (entry.onMatch) {
4259 try {
4260 entry.onMatch(url, parsed);
4261 } catch (err) {
4262 console.warn(
4263 `[desktop-mode] URL remap onMatch hook threw for "${entry.id}":`,
4264 err
4265 );
4266 }
4267 }
4268 if (deps2.openById(entry.nativeWindowId)) {
4269 return true;
4270 }
4271 }
4272 return false;
4273 }
4274 const HOOK_PREFIX = "desktop-mode.activity.";
4275 function hookName(channel) {
4276 return `${HOOK_PREFIX}${String(channel)}`;
4277 }
4278 let subscribeSeq = 0;
4279 const activity = {
4280 publish(channel, payload) {
4281 doAction(hookName(channel), payload);
4282 },
4283 subscribe(channel, cb) {
4284 const ns = `desktop-mode/activity-sub/${++subscribeSeq}`;
4285 const hook = hookName(channel);
4286 addAction(
4287 hook,
4288 ns,
4289 (payload) => cb(payload)
4290 );
4291 let removed = false;
4292 return () => {
4293 if (removed) {
4294 return;
4295 }
4296 removed = true;
4297 removeAction(hook, ns);
4298 };
4299 },
4300 filter(channel, value, ...args) {
4301 return applyFilters(hookName(channel), value, ...args);
4302 }
4303 };
4304 const DEFAULT_DURATION_MS = 4e3;
4305 const FADE_OUT_MS = 200;
4306 function showToast(options) {
4307 const intent = activity.filter(
4308 "desktop-mode/toast-requested",
4309 { ...options }
4310 );
4311 if (!intent || intent.cancel === true) {
4312 return () => void 0;
4313 }
4314 let dismissRequested = false;
4315 let realDismiss = null;
4316 openWithShellOverlays(
4317 () => !dismissRequested,
4318 () => {
4319 realDismiss = renderToast(intent);
4320 }
4321 );
4322 return () => {
4323 dismissRequested = true;
4324 if (realDismiss) {
4325 realDismiss();
4326 }
4327 };
4328 }
4329 function renderToast(intent) {
4330 const container = ensureContainer();
4331 const toast = document.createElement("wpd-toast");
4332 toast.textContent = intent.message;
4333 if (intent.action) {
4334 toast.setAttribute("action", intent.action.label);
4335 toast.addEventListener("wpd-toast-action", () => {
4336 intent.action?.onClick();
4337 dismiss();
4338 });
4339 }
4340 container.appendChild(toast);
4341 let dismissed = false;
4342 let dismissTimer = null;
4343 const dismiss = () => {
4344 if (dismissed) {
4345 return;
4346 }
4347 dismissed = true;
4348 if (dismissTimer !== null) {
4349 window.clearTimeout(dismissTimer);
4350 dismissTimer = null;
4351 }
4352 toast.setAttribute("state", "out");
4353 window.setTimeout(() => {
4354 toast.remove();
4355 }, FADE_OUT_MS);
4356 };
4357 requestAnimationFrame(() => {
4358 toast.setAttribute("state", "in");
4359 });
4360 dismissTimer = window.setTimeout(
4361 dismiss,
4362 intent.duration ?? DEFAULT_DURATION_MS
4363 );
4364 activity.publish("desktop-mode/toast-shown", { ...intent });
4365 return dismiss;
4366 }
4367 function ensureContainer() {
4368 const existing = document.querySelector(
4369 "wpd-toast-container"
4370 );
4371 if (existing) {
4372 return existing;
4373 }
4374 const el = document.createElement("wpd-toast-container");
4375 document.body.appendChild(el);
4376 return el;
4377 }
4378 const store$d = createSharedStore(
4379 "desktop-mode/destructive-admin-actions",
4380 () => ({ entries: [] })
4381 );
4382 function registerDestructiveAdminAction(entry) {
4383 if (!entry || typeof entry.id !== "string" || entry.id.trim() === "") {
4384 return () => {
4385 };
4386 }
4387 if (typeof entry.matches !== "function") {
4388 return () => {
4389 };
4390 }
4391 const entries = store$d.state.entries;
4392 const idx = entries.findIndex((e) => e.id === entry.id);
4393 if (idx >= 0) {
4394 entries.splice(idx, 1);
4395 }
4396 entries.push(entry);
4397 return () => unregisterDestructiveAdminAction(entry.id);
4398 }
4399 function unregisterDestructiveAdminAction(id) {
4400 const entries = store$d.state.entries;
4401 const idx = entries.findIndex((e) => e.id === id);
4402 if (idx >= 0) {
4403 entries.splice(idx, 1);
4404 }
4405 }
4406 function listDestructiveAdminActions() {
4407 return store$d.state.entries.slice();
4408 }
4409 const adminLinkDepsStore = createSharedStore(
4410 "desktop-mode/admin-link-deps",
4411 () => ({ deps: null })
4412 );
4413 function bindAdminLinkDispatch(deps2) {
4414 adminLinkDepsStore.state.deps = deps2;
4415 }
4416 function collectRegistrationErrors(def, checks) {
4417 if (!def || typeof def !== "object") {
4418 return ["def (not an object)"];
4419 }
4420 const d = def;
4421 const errors = [];
4422 for (const check of checks) {
4423 if (!check.valid(d)) {
4424 errors.push(`${check.field} (${check.message})`);
4425 }
4426 }
4427 return errors;
4428 }
4429 class RegistrationError extends Error {
4430 constructor(kind, errors, def) {
4431 super(
4432 `[desktop-mode] ${kind} registration rejected — fields: ` + errors.join(", ") + "."
4433 );
4434 this.name = "RegistrationError";
4435 this.kind = kind;
4436 this.errors = errors;
4437 this.def = def;
4438 }
4439 }
4440 function throwOnRegistrationErrors(kind, errors, def) {
4441 if (errors.length === 0) {
4442 return;
4443 }
4444 throw new RegistrationError(kind, errors, def);
4445 }
4446 const store$c = createSharedStore(
4447 "desktop-mode/wallpaper-registry",
4448 () => ({
4449 seed: [],
4450 listeners: /* @__PURE__ */ new Set()
4451 })
4452 );
4453 const seed$3 = store$c.state.seed;
4454 const listeners$b = store$c.state.listeners;
4455 function register$2(def) {
4456 throwOnRegistrationErrors(
4457 "Wallpaper",
4458 collectRegistrationErrors(def, WALLPAPER_CHECKS),
4459 def
4460 );
4461 const idx = seed$3.findIndex((w) => w.id === def.id);
4462 if (idx >= 0) {
4463 seed$3[idx] = def;
4464 } else {
4465 seed$3.push(def);
4466 }
4467 notify$d();
4468 }
4469 function unregister$2(id) {
4470 const idx = seed$3.findIndex((w) => w.id === id);
4471 if (idx >= 0) {
4472 seed$3.splice(idx, 1);
4473 notify$d();
4474 }
4475 }
4476 function notify$d() {
4477 const snapshot = Array.from(listeners$b);
4478 for (const cb of snapshot) {
4479 try {
4480 cb();
4481 } catch (err) {
4482 if (typeof console !== "undefined") {
4483 console.error(
4484 "[desktop-mode] wallpaper registry listener threw:",
4485 err
4486 );
4487 }
4488 }
4489 }
4490 }
4491 function all$1() {
4492 const copy = seed$3.slice();
4493 const filtered = applyFilters(HOOKS.WALLPAPERS, copy);
4494 if (!Array.isArray(filtered)) {
4495 if (typeof console !== "undefined") {
4496 console.warn(
4497 "[desktop-mode] `desktop-mode.wallpapers` filter returned a non-array; falling back to seed list."
4498 );
4499 }
4500 return copy;
4501 }
4502 return filtered.filter(isValidDef$1);
4503 }
4504 function get$1(id) {
4505 return all$1().find((w) => w.id === id);
4506 }
4507 const WALLPAPER_CHECKS = [
4508 {
4509 field: "id",
4510 message: "missing or not a non-empty string",
4511 valid: (d) => typeof d.id === "string" && d.id !== ""
4512 },
4513 {
4514 field: "label",
4515 message: "missing or not a non-empty string",
4516 valid: (d) => typeof d.label === "string" && d.label !== ""
4517 },
4518 {
4519 field: "preview",
4520 message: "missing or not a non-empty string",
4521 valid: (d) => typeof d.preview === "string" && d.preview !== ""
4522 },
4523 {
4524 field: "type",
4525 message: 'must be "css" or "canvas"',
4526 valid: (d) => d.type === "css" || d.type === "canvas"
4527 },
4528 {
4529 field: "value/resolveValue/mount",
4530 message: "css types need `value` or `resolveValue`; canvas types need `mount`",
4531 valid: (d) => {
4532 if (d.type === "css") {
4533 return typeof d.value === "string" || typeof d.resolveValue === "function";
4534 }
4535 if (d.type === "canvas") {
4536 return typeof d.mount === "function";
4537 }
4538 return true;
4539 }
4540 }
4541 ];
4542 function isValidDef$1(def) {
4543 return collectRegistrationErrors(def, WALLPAPER_CHECKS).length === 0;
4544 }
4545 const STORAGE_KEY = "desktop-mode-os-settings";
4546 const CUSTOM_GRADIENT_ID = "custom-gradient";
4547 const CUSTOM_IMAGE_ID = "custom-image";
4548 const DEFAULT_WALLPAPER_ID = "dark";
4549 const DEFAULT_ACCENTS = [
4550 { id: "wp-blue", label: "WordPress Blue", value: "#2271b1" },
4551 { id: "indigo", label: "Indigo", value: "#3858e9" },
4552 { id: "teal", label: "Teal", value: "#04a4cc" },
4553 { id: "emerald", label: "Emerald", value: "#059669" },
4554 { id: "amber", label: "Amber", value: "#d97706" },
4555 { id: "rose", label: "Rose", value: "#e11d48" }
4556 ];
4557 function getAccents() {
4558 const config = window.wp?.desktop?.config;
4559 const raw = config?.accentColors;
4560 if (!Array.isArray(raw) || raw.length === 0) {
4561 return DEFAULT_ACCENTS;
4562 }
4563 const clean = [];
4564 for (const entry of raw) {
4565 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)) {
4566 clean.push({ id: entry.id, label: entry.label, value: entry.value });
4567 }
4568 }
4569 return clean.length > 0 ? clean : DEFAULT_ACCENTS;
4570 }
4571 function getDefaultWallpaperId() {
4572 const config = window.wp?.desktop?.config;
4573 const raw = config?.defaultWallpaper;
4574 if (typeof raw === "string" && raw !== "") {
4575 return raw;
4576 }
4577 return DEFAULT_WALLPAPER_ID;
4578 }
4579 const DOCK_SIZES = [
4580 { id: "compact", label: "Compact", width: 48, icon: 18 },
4581 { id: "default", label: "Default", width: 56, icon: 20 },
4582 { id: "large", label: "Large", width: 72, icon: 26 }
4583 ];
4584 const DESKTOP_LAYOUTS = [
4585 { id: "classic", label: "Classic" },
4586 { id: "unified", label: "Unified" },
4587 { id: "spatial", label: "Spatial" }
4588 ];
4589 const DEFAULTS = {
4590 wallpaper: DEFAULT_WALLPAPER_ID,
4591 accent: "wp-blue",
4592 dockSize: "default",
4593 desktopLayout: "classic",
4594 dockRailRenderer: "default",
4595 customGradient: {
4596 from: "#2271b1",
4597 to: "#7c3aed",
4598 angle: 135
4599 },
4600 customImage: null,
4601 libraryHdOnly: true,
4602 ai: {
4603 enabled: false,
4604 provider: "openai",
4605 apiKey: "",
4606 apiKeys: {},
4607 transport: "off"
4608 },
4609 // Opt-out as of 0.8.0. Fresh installs land on the native Posts
4610 // window — same screen the rest of desktop mode is built for. A
4611 // user can still flip this off to fall back to the chromeless
4612 // `edit.php` iframe, but the new default is "use the native UI."
4613 heartbeatRate: 60,
4614 nativePostsEnabled: true,
4615 nativePostsHiddenColumns: [],
4616 // Same opt-out posture as Posts — fresh installs land on the
4617 // native Pages window, users can flip back to the iframe.
4618 nativePagesEnabled: true,
4619 // Native Users window — same opt-out posture. Capability-gated
4620 // server-side (the window is only registered for users with
4621 // `list_users`), so flipping this off only affects the small set
4622 // of users who can see the Users tile in the first place.
4623 nativeUsersEnabled: true,
4624 // Native Plugins window — replaces `plugins.php` and
4625 // `plugin-install.php`. Same opt-out posture; cap-gated on
4626 // `activate_plugins` server-side, so flipping this off only
4627 // affects users who could see the Plugins tile anyway.
4628 nativePluginsEnabled: true,
4629 // Native Comments window — replaces `edit-comments.php`. Same
4630 // opt-out posture; cap-gated on `edit_posts` server-side.
4631 nativeCommentsEnabled: true,
4632 showDesktopOnWallpaperClick: false,
4633 showPostStatusRibbons: true,
4634 foldersSharingEnabled: true,
4635 itemVisibility: {},
4636 dockOrder: [],
4637 dockPromotedPositions: {}
4638 };
4639 const AI_TRANSPORTS = [
4640 { id: "off", label: "Off" },
4641 { id: "sse", label: "Streaming (SSE)" }
4642 ];
4643 const AI_PROVIDERS = [
4644 {
4645 id: "openai",
4646 label: "OpenAI",
4647 apiKeyLabel: "OpenAI API key",
4648 apiKeyLink: "https://platform.openai.com/api-keys"
4649 }
4650 ];
4651 function getAiProviders() {
4652 const cfg = window.desktopModeConfig;
4653 const list2 = cfg?.aiProviders;
4654 if (!Array.isArray(list2) || list2.length === 0) {
4655 return AI_PROVIDERS;
4656 }
4657 return list2.map((p) => ({
4658 id: p.id,
4659 label: p.label,
4660 description: p.description,
4661 apiKeyLabel: p.api_key_label,
4662 apiKeyLink: p.api_key_link
4663 }));
4664 }
4665 function isHexColor(value) {
4666 return typeof value === "string" && /^#[0-9a-f]{3,8}$/i.test(value);
4667 }
4668 const NONCE_HEADER = "X-WP-Nonce";
4669 function injectRestNonce(input, init2) {
4670 const nonce = readRestNonce$3();
4671 if (!nonce) {
4672 return init2;
4673 }
4674 const url = resolveUrl(input);
4675 if (!url || !isSameOriginRestUrl(url)) {
4676 return init2;
4677 }
4678 const baseHeaders = init2?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
4679 const headers = new Headers(baseHeaders ?? {});
4680 if (headers.has(NONCE_HEADER)) {
4681 return init2;
4682 }
4683 headers.set(NONCE_HEADER, nonce);
4684 return { ...init2 ?? {}, headers };
4685 }
4686 function readRestNonce$3() {
4687 if (typeof window === "undefined") {
4688 return void 0;
4689 }
4690 const cfg = window.desktopModeConfig;
4691 const value = cfg?.restNonce;
4692 return typeof value === "string" && value.length > 0 ? value : void 0;
4693 }
4694 function resolveUrl(input) {
4695 try {
4696 const base = typeof window !== "undefined" && window.location ? window.location.href : void 0;
4697 if (typeof input === "string") {
4698 return new URL(input, base);
4699 }
4700 if (input instanceof URL) {
4701 return input;
4702 }
4703 if (typeof Request !== "undefined" && input instanceof Request) {
4704 return new URL(input.url, base);
4705 }
4706 return null;
4707 } catch {
4708 return null;
4709 }
4710 }
4711 function isSameOriginRestUrl(url) {
4712 if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) {
4713 return false;
4714 }
4715 if (url.pathname.includes("/wp-json/")) {
4716 return true;
4717 }
4718 if (url.searchParams.has("rest_route")) {
4719 return true;
4720 }
4721 return false;
4722 }
4723 function trackedFetch$1(input, init2, opts = {}) {
4724 const fn = window.wp?.desktop?.fetch;
4725 if (typeof fn === "function") {
4726 return fn(input, init2, opts);
4727 }
4728 const finalInit = injectRestNonce(input, init2);
4729 return fetch(input, finalInit);
4730 }
4731 function loadState() {
4732 const serverRaw = _readServerSettings();
4733 if (serverRaw) {
4734 const state2 = _parseRaw(serverRaw);
4735 _writeLocalStorage(state2);
4736 return state2;
4737 }
4738 try {
4739 const cached = window.localStorage.getItem(STORAGE_KEY);
4740 if (cached) {
4741 return _parseRaw(JSON.parse(cached));
4742 }
4743 } catch {
4744 }
4745 return structuredDefaults();
4746 }
4747 function _readServerSettings() {
4748 const config = window.desktopModeConfig;
4749 const raw = config?.osSettings;
4750 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
4751 return null;
4752 }
4753 return raw;
4754 }
4755 function _parseRaw(parsed) {
4756 const accents = getAccents();
4757 return {
4758 wallpaper: typeof parsed.wallpaper === "string" && parsed.wallpaper !== "" ? parsed.wallpaper : getDefaultWallpaperId(),
4759 accent: accents.some((a) => a.id === parsed.accent) ? parsed.accent : DEFAULTS.accent,
4760 dockSize: DOCK_SIZES.some((d) => d.id === parsed.dockSize) ? parsed.dockSize : DEFAULTS.dockSize,
4761 desktopLayout: DESKTOP_LAYOUTS.some(
4762 (l) => l.id === parsed.desktopLayout
4763 ) ? parsed.desktopLayout : DEFAULTS.desktopLayout,
4764 // Dock rail renderer — any sanitize_key()-clean string
4765 // survives; the registry resolves at use time and falls back
4766 // to `'default'` when the picked renderer isn't registered.
4767 dockRailRenderer: typeof parsed.dockRailRenderer === "string" && /^[a-z0-9_-]+$/.test(parsed.dockRailRenderer) ? parsed.dockRailRenderer : DEFAULTS.dockRailRenderer,
4768 customGradient: sanitizeCustomGradient(parsed.customGradient),
4769 customImage: sanitizeCustomImage(parsed.customImage),
4770 libraryHdOnly: typeof parsed.libraryHdOnly === "boolean" ? parsed.libraryHdOnly : DEFAULTS.libraryHdOnly,
4771 ai: sanitizeAi(parsed.ai),
4772 heartbeatRate: parsed.heartbeatRate === 15 || parsed.heartbeatRate === 30 || parsed.heartbeatRate === 45 || parsed.heartbeatRate === 60 ? parsed.heartbeatRate : DEFAULTS.heartbeatRate,
4773 nativePostsEnabled: typeof parsed.nativePostsEnabled === "boolean" ? parsed.nativePostsEnabled : DEFAULTS.nativePostsEnabled,
4774 nativePostsHiddenColumns: Array.isArray(parsed.nativePostsHiddenColumns) ? parsed.nativePostsHiddenColumns.filter((v) => typeof v === "string" && v !== "").slice(0, 32) : DEFAULTS.nativePostsHiddenColumns.slice(),
4775 nativePagesEnabled: typeof parsed.nativePagesEnabled === "boolean" ? parsed.nativePagesEnabled : DEFAULTS.nativePagesEnabled,
4776 nativeUsersEnabled: typeof parsed.nativeUsersEnabled === "boolean" ? parsed.nativeUsersEnabled : DEFAULTS.nativeUsersEnabled,
4777 nativePluginsEnabled: typeof parsed.nativePluginsEnabled === "boolean" ? parsed.nativePluginsEnabled : DEFAULTS.nativePluginsEnabled,
4778 nativeCommentsEnabled: typeof parsed.nativeCommentsEnabled === "boolean" ? parsed.nativeCommentsEnabled : DEFAULTS.nativeCommentsEnabled,
4779 showDesktopOnWallpaperClick: typeof parsed.showDesktopOnWallpaperClick === "boolean" ? parsed.showDesktopOnWallpaperClick : DEFAULTS.showDesktopOnWallpaperClick,
4780 showPostStatusRibbons: typeof parsed.showPostStatusRibbons === "boolean" ? parsed.showPostStatusRibbons : DEFAULTS.showPostStatusRibbons,
4781 foldersSharingEnabled: typeof parsed.foldersSharingEnabled === "boolean" ? parsed.foldersSharingEnabled : DEFAULTS.foldersSharingEnabled,
4782 itemVisibility: sanitizeItemVisibility(parsed.itemVisibility),
4783 dockOrder: sanitizeDockOrder(parsed.dockOrder),
4784 dockPromotedPositions: sanitizeDockPromotedPositions(
4785 parsed.dockPromotedPositions
4786 )
4787 };
4788 }
4789 function sanitizeItemVisibility(raw) {
4790 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
4791 return {};
4792 }
4793 const allowed = [
4794 "both",
4795 "dock",
4796 "desktop",
4797 "hidden"
4798 ];
4799 const out = {};
4800 let count = 0;
4801 for (const [k, v] of Object.entries(raw)) {
4802 if (count >= 256) {
4803 break;
4804 }
4805 if (typeof k !== "string" || k === "") {
4806 continue;
4807 }
4808 if (typeof v !== "string") {
4809 continue;
4810 }
4811 const placement = v;
4812 if (!allowed.includes(placement)) {
4813 continue;
4814 }
4815 out[k] = placement;
4816 count++;
4817 }
4818 return out;
4819 }
4820 function sanitizeDockOrder(raw) {
4821 if (!Array.isArray(raw)) {
4822 return [];
4823 }
4824 const out = [];
4825 const seen = /* @__PURE__ */ new Set();
4826 for (const id of raw) {
4827 if (typeof id !== "string" || id === "" || seen.has(id)) {
4828 continue;
4829 }
4830 seen.add(id);
4831 out.push(id);
4832 if (out.length >= 256) {
4833 break;
4834 }
4835 }
4836 return out;
4837 }
4838 function sanitizeDockPromotedPositions(raw) {
4839 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
4840 return {};
4841 }
4842 const out = {};
4843 let count = 0;
4844 const MAX_COORD = 1e5;
4845 for (const [k, v] of Object.entries(raw)) {
4846 if (count >= 256) {
4847 break;
4848 }
4849 if (typeof k !== "string" || k === "") {
4850 continue;
4851 }
4852 if (!v || typeof v !== "object" || Array.isArray(v)) {
4853 continue;
4854 }
4855 const pos = v;
4856 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) {
4857 continue;
4858 }
4859 out[k] = { x: pos.x, y: pos.y };
4860 count++;
4861 }
4862 return out;
4863 }
4864 let _syncTimer = null;
4865 const SYNC_DEBOUNCE_MS = 250;
4866 let _lastConfirmedState = null;
4867 function setLastConfirmedState(state2) {
4868 _lastConfirmedState = _cloneState(state2);
4869 }
4870 function _cloneState(state2) {
4871 return {
4872 ...state2,
4873 customGradient: { ...state2.customGradient },
4874 customImage: state2.customImage ? { ...state2.customImage } : null,
4875 ai: { ...state2.ai, apiKeys: { ...state2.ai.apiKeys } },
4876 nativePostsHiddenColumns: state2.nativePostsHiddenColumns.slice(),
4877 itemVisibility: { ...state2.itemVisibility },
4878 dockOrder: state2.dockOrder.slice(),
4879 dockPromotedPositions: Object.fromEntries(
4880 Object.entries(state2.dockPromotedPositions).map(([k, v]) => [
4881 k,
4882 { ...v }
4883 ])
4884 )
4885 };
4886 }
4887 function saveState(state2, opts = {}) {
4888 _writeLocalStorage(state2);
4889 _scheduleSyncToServer(state2, opts.windowId);
4890 }
4891 function _writeLocalStorage(state2) {
4892 try {
4893 window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state2));
4894 } catch {
4895 }
4896 }
4897 function _scheduleSyncToServer(state2, windowId) {
4898 if (_syncTimer !== null) {
4899 clearTimeout(_syncTimer);
4900 }
4901 if (windowId) {
4902 _pendingActivityWindowId = windowId;
4903 }
4904 _emitSaveLifecycle("pending");
4905 _syncTimer = setTimeout(() => {
4906 _syncTimer = null;
4907 const id = _pendingActivityWindowId;
4908 _pendingActivityWindowId = null;
4909 _postToServer(state2, id);
4910 }, SYNC_DEBOUNCE_MS);
4911 }
4912 let _pendingActivityWindowId = null;
4913 function _postToServer(state2, windowId) {
4914 const config = window.desktopModeConfig;
4915 const url = config?.osSettingsUrl;
4916 const nonce = config?.restNonce;
4917 if (!url || !nonce) {
4918 _emitSaveLifecycle("saved");
4919 return;
4920 }
4921 _emitSaveLifecycle("saving");
4922 const attributedWindowId = windowId || "desktop-mode-os-settings";
4923 trackedFetch$1(
4924 url,
4925 {
4926 method: "POST",
4927 headers: {
4928 "Content-Type": "application/json",
4929 "X-WP-Nonce": nonce
4930 },
4931 body: JSON.stringify({ settings: state2 })
4932 },
4933 { windowId: attributedWindowId }
4934 ).then((res) => {
4935 if (!res.ok) {
4936 throw new Error(`${res.status} ${res.statusText}`);
4937 }
4938 _lastConfirmedState = _cloneState(state2);
4939 _emitSaveLifecycle("saved");
4940 }).catch((err) => {
4941 if (_lastConfirmedState) {
4942 _writeLocalStorage(_lastConfirmedState);
4943 _emitSaveLifecycle(
4944 "failed",
4945 err instanceof Error ? err.message : String(err),
4946 _cloneState(_lastConfirmedState)
4947 );
4948 } else {
4949 _emitSaveLifecycle(
4950 "failed",
4951 err instanceof Error ? err.message : String(err)
4952 );
4953 }
4954 });
4955 }
4956 function _emitSaveLifecycle(phase, error, rolledBackTo) {
4957 const detail = { phase };
4958 if (error) {
4959 detail.error = error;
4960 }
4961 if (rolledBackTo) {
4962 detail.rolledBackTo = rolledBackTo;
4963 }
4964 document.dispatchEvent(
4965 new CustomEvent("desktop-mode-os-settings-save-lifecycle", { detail })
4966 );
4967 }
4968 function structuredDefaults() {
4969 return {
4970 ...DEFAULTS,
4971 customGradient: { ...DEFAULTS.customGradient },
4972 customImage: null,
4973 ai: { ...DEFAULTS.ai }
4974 };
4975 }
4976 function sanitizeAi(raw) {
4977 if (!raw || typeof raw !== "object") {
4978 return { ...DEFAULTS.ai, apiKeys: {} };
4979 }
4980 const { enabled, provider, apiKey, apiKeys, transport } = raw;
4981 const known = getAiProviders();
4982 const validProvider = typeof provider === "string" && known.some((p) => p.id === provider) ? provider : DEFAULTS.ai.provider;
4983 const cleanKeys = {};
4984 if (apiKeys && typeof apiKeys === "object") {
4985 for (const [pid, val] of Object.entries(apiKeys)) {
4986 if (typeof val === "string") {
4987 cleanKeys[pid] = val.slice(0, 512);
4988 }
4989 }
4990 }
4991 const validTransport = typeof transport === "string" && AI_TRANSPORTS.some((t) => t.id === transport) ? transport : DEFAULTS.ai.transport;
4992 return {
4993 enabled: typeof enabled === "boolean" ? enabled : DEFAULTS.ai.enabled,
4994 provider: validProvider,
4995 apiKey: typeof apiKey === "string" ? apiKey : DEFAULTS.ai.apiKey,
4996 apiKeys: cleanKeys,
4997 transport: validTransport
4998 };
4999 }
5000 function sanitizeCustomGradient(raw) {
5001 if (!raw || typeof raw !== "object") {
5002 return { ...DEFAULTS.customGradient };
5003 }
5004 const { from, to, angle } = raw;
5005 return {
5006 from: isHexColor(from) ? from : DEFAULTS.customGradient.from,
5007 to: isHexColor(to) ? to : DEFAULTS.customGradient.to,
5008 angle: typeof angle === "number" && Number.isFinite(angle) && angle >= 0 && angle <= 360 ? angle : DEFAULTS.customGradient.angle
5009 };
5010 }
5011 function sanitizeCustomImage(raw) {
5012 if (!raw || typeof raw !== "object") {
5013 return null;
5014 }
5015 const { id, url } = raw;
5016 if (typeof id !== "number" || !Number.isFinite(id) || id <= 0) {
5017 return null;
5018 }
5019 if (typeof url !== "string" || !/^https?:\/\//i.test(url)) {
5020 return null;
5021 }
5022 return { id, url };
5023 }
5024 const store$b = createSharedStore(
5025 "desktop-mode/dock-rail-registry",
5026 () => ({
5027 registry: /* @__PURE__ */ new Map(),
5028 listeners: /* @__PURE__ */ new Set(),
5029 activeId: "default"
5030 })
5031 );
5032 const registry$8 = store$b.state.registry;
5033 const listeners$a = store$b.state.listeners;
5034 const ID_RE = /^[a-z0-9_-]+$/;
5035 function register$1(renderer) {
5036 if (!renderer || typeof renderer !== "object") {
5037 throw new TypeError(
5038 "[desktop-mode] registerDockRailRenderer: renderer must be an object."
5039 );
5040 }
5041 if (typeof renderer.id !== "string" || !ID_RE.test(renderer.id)) {
5042 throw new TypeError(
5043 `[desktop-mode] registerDockRailRenderer: id must match /^[a-z0-9_-]+$/, got: ${String(renderer.id)}`
5044 );
5045 }
5046 if (typeof renderer.label !== "string" || renderer.label === "") {
5047 throw new TypeError(
5048 "[desktop-mode] registerDockRailRenderer: label must be a non-empty string."
5049 );
5050 }
5051 if (typeof renderer.mount !== "function") {
5052 throw new TypeError(
5053 "[desktop-mode] registerDockRailRenderer: mount must be a function."
5054 );
5055 }
5056 if (renderer.apiVersion !== void 0 && renderer.apiVersion !== 1) {
5057 throw new TypeError(
5058 `[desktop-mode] registerDockRailRenderer: unsupported apiVersion ${renderer.apiVersion} (this shell speaks v1).`
5059 );
5060 }
5061 registry$8.set(renderer.id, renderer);
5062 notify$c();
5063 }
5064 function unregister$1(id) {
5065 if (registry$8.delete(id)) {
5066 notify$c();
5067 }
5068 }
5069 function unregisterByOwner$1(owner) {
5070 if (!owner) {
5071 return 0;
5072 }
5073 let removed = 0;
5074 for (const [id, renderer] of Array.from(registry$8.entries())) {
5075 if (renderer.owner === owner) {
5076 registry$8.delete(id);
5077 removed++;
5078 }
5079 }
5080 if (removed > 0) {
5081 notify$c();
5082 }
5083 return removed;
5084 }
5085 function list() {
5086 return Array.from(registry$8.values());
5087 }
5088 function subscribe$3(cb) {
5089 listeners$a.add(cb);
5090 return () => {
5091 listeners$a.delete(cb);
5092 };
5093 }
5094 function setActiveRenderer(id) {
5095 if (store$b.state.activeId === id) {
5096 return;
5097 }
5098 store$b.state.activeId = id;
5099 notify$c();
5100 }
5101 function resolveActive() {
5102 return registry$8.get(store$b.state.activeId) ?? registry$8.get("default") ?? registry$8.values().next().value;
5103 }
5104 function notify$c() {
5105 const snapshot = Array.from(listeners$a);
5106 for (const cb of snapshot) {
5107 try {
5108 cb();
5109 } catch (err) {
5110 if (typeof console !== "undefined") {
5111 console.error(
5112 "[desktop-mode] dock-rail-renderer listener threw:",
5113 err
5114 );
5115 }
5116 }
5117 }
5118 }
5119 function hashTitleToHue(input) {
5120 if (!input) {
5121 return 214;
5122 }
5123 let hash2 = 5381;
5124 for (let i = 0; i < input.length; i++) {
5125 hash2 = Math.imul(hash2, 33) + input.charCodeAt(i);
5126 }
5127 return (hash2 % 360 + 360) % 360;
5128 }
5129 const SHOW_DELAY_MS = 180;
5130 const HIDE_DELAY_MS = 220;
5131 const STAGGER_MS = 32;
5132 function attachDockPeek(deps2) {
5133 const { tile: tile2 } = deps2;
5134 let popover = null;
5135 let showTimer = null;
5136 let hideTimer = null;
5137 let inside = false;
5138 const cancelShow = () => {
5139 if (showTimer !== null) {
5140 window.clearTimeout(showTimer);
5141 showTimer = null;
5142 }
5143 };
5144 const cancelHide = () => {
5145 if (hideTimer !== null) {
5146 window.clearTimeout(hideTimer);
5147 hideTimer = null;
5148 }
5149 };
5150 const tearDown = () => {
5151 cancelShow();
5152 cancelHide();
5153 if (popover) {
5154 popover.remove();
5155 popover = null;
5156 }
5157 deps2.suppressTooltip(false);
5158 };
5159 const onPointerEnterTile = (e) => {
5160 if (e.pointerType !== "mouse") {
5161 return;
5162 }
5163 if (!shouldShowPeek(deps2)) {
5164 return;
5165 }
5166 inside = true;
5167 cancelHide();
5168 if (popover) {
5169 return;
5170 }
5171 showTimer = window.setTimeout(() => {
5172 showTimer = null;
5173 if (!inside) {
5174 return;
5175 }
5176 showPeek();
5177 }, SHOW_DELAY_MS);
5178 };
5179 const onPointerLeaveTile = (e) => {
5180 if (popover && e.relatedTarget instanceof Node && popover.contains(e.relatedTarget)) {
5181 return;
5182 }
5183 inside = false;
5184 cancelShow();
5185 scheduleHide();
5186 };
5187 const scheduleHide = () => {
5188 cancelHide();
5189 hideTimer = window.setTimeout(() => {
5190 hideTimer = null;
5191 if (inside) {
5192 return;
5193 }
5194 tearDown();
5195 }, HIDE_DELAY_MS);
5196 };
5197 const showPeek = () => {
5198 deps2.suppressTooltip(true);
5199 popover = buildPopover(deps2, () => tearDown());
5200 document.body.appendChild(popover);
5201 inheritShellSchemeVars(popover);
5202 positionPopover(popover, tile2, deps2.getOrientation());
5203 requestAnimationFrame(() => {
5204 popover?.classList.add("desktop-mode-dock-peek--open");
5205 });
5206 popover.addEventListener("pointerenter", () => {
5207 inside = true;
5208 cancelHide();
5209 });
5210 popover.addEventListener("pointerleave", (e) => {
5211 if (e.relatedTarget instanceof Node && tile2.contains(e.relatedTarget)) {
5212 return;
5213 }
5214 inside = false;
5215 scheduleHide();
5216 });
5217 };
5218 tile2.addEventListener("pointerenter", onPointerEnterTile);
5219 tile2.addEventListener("pointerleave", onPointerLeaveTile);
5220 return () => {
5221 tile2.removeEventListener("pointerenter", onPointerEnterTile);
5222 tile2.removeEventListener("pointerleave", onPointerLeaveTile);
5223 tearDown();
5224 };
5225 }
5226 function shouldShowPeek(deps2) {
5227 return deps2.getInstances().length >= 1;
5228 }
5229 function buildPopover(deps2, dismiss) {
5230 const root = document.createElement("div");
5231 root.className = "desktop-mode-dock-peek";
5232 root.setAttribute("role", "menu");
5233 root.setAttribute("aria-label", sprintf(
5234 // translators: %s is the dock item's admin-page title (e.g., "Posts")
5235 __("%s — open windows"),
5236 deps2.item.title
5237 ));
5238 const cards = document.createElement("div");
5239 cards.className = "desktop-mode-dock-peek__cards";
5240 root.appendChild(cards);
5241 const instances = deps2.getInstances();
5242 let cardIndex = 0;
5243 for (const win of instances) {
5244 const card = buildInstanceCard(win, deps2, cardIndex++, dismiss);
5245 cards.appendChild(card);
5246 }
5247 if (deps2.enableGhost !== false) {
5248 const ghost = buildGhostCard(deps2, cardIndex, dismiss);
5249 cards.appendChild(ghost);
5250 }
5251 return root;
5252 }
5253 function buildInstanceCard(win, deps2, index2, dismiss) {
5254 const card = document.createElement("button");
5255 card.type = "button";
5256 card.setAttribute("role", "menuitem");
5257 card.className = "desktop-mode-dock-peek__card desktop-mode-dock-peek__card--instance";
5258 card.style.setProperty("--peek-card-index", String(index2));
5259 card.style.setProperty(
5260 "--peek-card-delay",
5261 `${index2 * STAGGER_MS}ms`
5262 );
5263 const title = win.config.title || deps2.item.title;
5264 card.style.setProperty(
5265 "--peek-card-hue",
5266 `${hashTitleToHue(win.id || title)}`
5267 );
5268 card.style.setProperty(
5269 "--peek-card-vt-name",
5270 `desktop-mode-peek-card-${win.id}`
5271 );
5272 const titlebar = document.createElement("span");
5273 titlebar.className = "desktop-mode-dock-peek__card-titlebar";
5274 const dots = document.createElement("span");
5275 dots.className = "desktop-mode-dock-peek__card-dots";
5276 dots.setAttribute("aria-hidden", "true");
5277 for (let i = 0; i < 3; i++) {
5278 dots.appendChild(document.createElement("i"));
5279 }
5280 titlebar.appendChild(dots);
5281 const iconHost = document.createElement("span");
5282 iconHost.className = "desktop-mode-dock-peek__card-icon";
5283 iconHost.setAttribute("aria-hidden", "true");
5284 const iconCls = win.config.icon || deps2.item.icon;
5285 if (iconCls.startsWith("dashicons-")) {
5286 iconHost.classList.add("dashicons", sanitizeClassName(iconCls));
5287 } else {
5288 iconHost.classList.add("dashicons", "dashicons-admin-generic");
5289 }
5290 titlebar.appendChild(iconHost);
5291 const label = document.createElement("span");
5292 label.className = "desktop-mode-dock-peek__card-label";
5293 label.textContent = title;
5294 titlebar.appendChild(label);
5295 card.appendChild(titlebar);
5296 const defaultBody = document.createElement("span");
5297 defaultBody.className = "desktop-mode-dock-peek__card-body";
5298 defaultBody.setAttribute("aria-hidden", "true");
5299 for (let i = 0; i < 3; i++) {
5300 const line = document.createElement("span");
5301 line.className = "desktop-mode-dock-peek__card-line";
5302 defaultBody.appendChild(line);
5303 }
5304 const ctx = { window: win, item: deps2.item };
5305 const body = applyFilters(
5306 HOOKS.DOCK_PEEK_CARD_CONTENT,
5307 defaultBody,
5308 ctx
5309 );
5310 if (body !== defaultBody) {
5311 body.classList.add("desktop-mode-dock-peek__card-body--custom");
5312 }
5313 card.appendChild(body);
5314 card.addEventListener("click", () => {
5315 spawnFocusViewTransition(deps2, win, card, dismiss);
5316 });
5317 card.addEventListener("pointerenter", () => {
5318 if (deps2.windowManager.getFocused() === win) {
5319 return;
5320 }
5321 deps2.windowManager.focus(win);
5322 });
5323 const finalCard = applyFilters(
5324 HOOKS.DOCK_PEEK_CARD_ELEMENT,
5325 card,
5326 ctx
5327 );
5328 return finalCard;
5329 }
5330 function spawnFocusViewTransition(deps2, win, card, dismiss) {
5331 const doc = document;
5332 const vtName = `desktop-mode-peek-card-${win.id}`;
5333 const focus = () => {
5334 dismiss();
5335 deps2.windowManager.focus(win);
5336 };
5337 if (typeof doc.startViewTransition !== "function") {
5338 focus();
5339 return;
5340 }
5341 const targetEl = win.element;
5342 card.style.setProperty("view-transition-name", vtName);
5343 targetEl.style.setProperty("view-transition-name", vtName);
5344 const transition = doc.startViewTransition(focus);
5345 const cleanup = () => {
5346 card.style.removeProperty("view-transition-name");
5347 targetEl.style.removeProperty("view-transition-name");
5348 };
5349 const t = transition;
5350 if (t.finished && typeof t.finished.then === "function") {
5351 t.finished.then(cleanup, cleanup);
5352 } else {
5353 Promise.resolve().then(cleanup);
5354 }
5355 }
5356 function buildGhostCard(deps2, index2, dismiss) {
5357 const card = document.createElement("button");
5358 card.type = "button";
5359 card.setAttribute("role", "menuitem");
5360 card.className = "desktop-mode-dock-peek__card desktop-mode-dock-peek__card--ghost";
5361 card.style.setProperty("--peek-card-index", String(index2));
5362 card.style.setProperty(
5363 "--peek-card-delay",
5364 `${index2 * STAGGER_MS}ms`
5365 );
5366 const plus = document.createElement("span");
5367 plus.className = "desktop-mode-dock-peek__card-plus";
5368 plus.setAttribute("aria-hidden", "true");
5369 plus.textContent = "+";
5370 card.appendChild(plus);
5371 const label = document.createElement("span");
5372 label.className = "desktop-mode-dock-peek__card-label";
5373 label.textContent = sprintf(
5374 // translators: %s is the admin-page title (e.g., "Posts")
5375 __("New %s"),
5376 deps2.item.title
5377 );
5378 card.appendChild(label);
5379 card.addEventListener("click", () => {
5380 spawnWithViewTransition(deps2, dismiss);
5381 });
5382 return card;
5383 }
5384 function spawnWithViewTransition(deps2, dismiss) {
5385 const doc = document;
5386 const spawn = () => {
5387 dismiss();
5388 deps2.openNew();
5389 };
5390 if (typeof doc.startViewTransition === "function") {
5391 doc.startViewTransition(spawn);
5392 return;
5393 }
5394 spawn();
5395 }
5396 const VIEWPORT_MARGIN_PX = 12;
5397 const SHELL_SCHEME_VARS = [
5398 "--wp-admin-theme-color",
5399 "--desktop-mode-titlebar-bg",
5400 "--desktop-mode-titlebar-bg-focused",
5401 "--desktop-mode-titlebar-color",
5402 "--desktop-mode-titlebar-color-focused"
5403 ];
5404 function inheritShellSchemeVars(popover) {
5405 const shell = document.querySelector(".desktop-mode-shell");
5406 if (!shell) {
5407 return;
5408 }
5409 const computed = window.getComputedStyle(shell);
5410 for (const name of SHELL_SCHEME_VARS) {
5411 const value = computed.getPropertyValue(name).trim();
5412 if (value) {
5413 popover.style.setProperty(name, value);
5414 }
5415 }
5416 }
5417 function positionPopover(popover, tile2, orientation) {
5418 const rect = tile2.getBoundingClientRect();
5419 popover.dataset.orientation = orientation;
5420 if (orientation === "bottom") {
5421 popover.style.left = `${rect.left + rect.width / 2}px`;
5422 popover.style.top = `${rect.top - 12}px`;
5423 } else if (orientation === "right") {
5424 popover.style.top = `${rect.top + rect.height / 2}px`;
5425 popover.style.left = `${rect.left - 12}px`;
5426 } else {
5427 popover.style.top = `${rect.top + rect.height / 2}px`;
5428 popover.style.left = `${rect.right + 12}px`;
5429 }
5430 requestAnimationFrame(() => clampToViewport$1(popover));
5431 }
5432 function clampToViewport$1(popover, orientation) {
5433 const rect = popover.getBoundingClientRect();
5434 const vh = window.innerHeight;
5435 const vw = window.innerWidth;
5436 const min = VIEWPORT_MARGIN_PX;
5437 let dy = 0;
5438 let dx = 0;
5439 if (rect.top < min) {
5440 dy = min - rect.top;
5441 } else if (rect.bottom > vh - min) {
5442 dy = vh - min - rect.bottom;
5443 }
5444 if (rect.left < min) {
5445 dx = min - rect.left;
5446 } else if (rect.right > vw - min) {
5447 dx = vw - min - rect.right;
5448 }
5449 if (dx === 0 && dy === 0) {
5450 return;
5451 }
5452 popover.style.setProperty("--peek-clamp-x", `${dx}px`);
5453 popover.style.setProperty("--peek-clamp-y", `${dy}px`);
5454 popover.classList.add("desktop-mode-dock-peek--clamped");
5455 }
5456 function tryOpenExternalUrl(url) {
5457 try {
5458 const parsed = new URL(url, window.location.origin);
5459 if (parsed.origin === window.location.origin) {
5460 return false;
5461 }
5462 window.open(parsed.toString(), "_blank", "noopener,noreferrer");
5463 return true;
5464 } catch {
5465 return false;
5466 }
5467 }
5468 function synthDockId(desktopIconId) {
5469 return `desktop:${desktopIconId}`;
5470 }
5471 function synthIconId(dockItemId) {
5472 return `dock:${dockItemId}`;
5473 }
5474 function canonicalItemId(id) {
5475 if (id.startsWith("dock:")) {
5476 return id.slice(5);
5477 }
5478 if (id.startsWith("desktop:")) {
5479 return id.slice(8);
5480 }
5481 return id;
5482 }
5483 function resolvePlacement(id, nativeRail, visibility) {
5484 const override = visibility[id];
5485 if (override) {
5486 return override;
5487 }
5488 return nativeRail;
5489 }
5490 function shouldShowOnDock(placement) {
5491 return placement === "dock" || placement === "both";
5492 }
5493 function shouldShowOnDesktop(placement) {
5494 return placement === "desktop" || placement === "both";
5495 }
5496 function applyDockPlacement(dockItems, desktopIcons, settings, dockedNativeWindows) {
5497 const visibility = settings.itemVisibility;
5498 const order = settings.dockOrder;
5499 const kept = [];
5500 for (const item of dockItems) {
5501 const placement = resolvePlacement(item.id, "dock", visibility);
5502 if (shouldShowOnDock(placement)) {
5503 kept.push(item);
5504 }
5505 }
5506 for (const icon of desktopIcons) {
5507 const placement = resolvePlacement(icon.id, "desktop", visibility);
5508 if (!shouldShowOnDock(placement)) {
5509 continue;
5510 }
5511 if (icon.window && dockedNativeWindows && dockedNativeWindows.has(icon.window)) {
5512 continue;
5513 }
5514 kept.push({
5515 id: synthIconId(icon.id),
5516 title: icon.title,
5517 icon: icon.icon,
5518 url: icon.url || "",
5519 // Carry the native-window id forward so the dock can light
5520 // the active-dot indicator + show the hover-peek card when
5521 // the target window is open. Without this, window-target
5522 // icons (no `url`) synthesize a tile whose only id-bearing
5523 // field is an empty string — deriveWindowId('') matches
5524 // nothing the window manager has stored.
5525 windowId: icon.window || void 0,
5526 badge: 0,
5527 submenu: [],
5528 isCore: false
5529 });
5530 }
5531 return applyOrder(kept, order);
5532 }
5533 function applyDesktopPlacement(desktopIcons, dockItems, visibility) {
5534 const out = [];
5535 for (const icon of desktopIcons) {
5536 const placement = resolvePlacement(icon.id, "desktop", visibility);
5537 if (shouldShowOnDesktop(placement)) {
5538 out.push(icon);
5539 }
5540 }
5541 let synthIndex = 0;
5542 for (const item of dockItems) {
5543 const placement = resolvePlacement(item.id, "dock", visibility);
5544 if (!shouldShowOnDesktop(placement)) {
5545 continue;
5546 }
5547 out.push({
5548 id: synthDockId(item.id),
5549 title: item.title,
5550 icon: item.icon,
5551 window: "",
5552 url: item.url || "",
5553 // Place synthesized dock-promoted icons after server-registered
5554 // ones. Stable ordering by source-list index inside the bucket.
5555 position: 2e3 + synthIndex++
5556 });
5557 }
5558 return out;
5559 }
5560 function applyOrder(items, order) {
5561 if (order.length === 0 || items.length <= 1) {
5562 return items;
5563 }
5564 const byId = /* @__PURE__ */ new Map();
5565 for (const item of items) {
5566 byId.set(item.id, item);
5567 }
5568 const out = [];
5569 const placed = /* @__PURE__ */ new Set();
5570 for (const id of order) {
5571 const item = byId.get(id);
5572 if (item) {
5573 out.push(item);
5574 placed.add(id);
5575 }
5576 }
5577 for (const item of items) {
5578 if (!placed.has(item.id)) {
5579 out.push(item);
5580 }
5581 }
5582 return out;
5583 }
5584 function html(strings, ...values) {
5585 return { __wpdHtml: true, strings, values };
5586 }
5587 function isTemplateResult(v) {
5588 return !!v && v.__wpdHtml === true;
5589 }
5590 const MARKER_PREFIX = "$$wpd$$";
5591 const MARKER_RE = /\$\$wpd\$\$(\d+)\$\$/g;
5592 function joinWithMarkers(strings) {
5593 let out = strings[0];
5594 for (let i = 1; i < strings.length; i++) {
5595 out += `${MARKER_PREFIX}${i - 1}$$` + strings[i];
5596 }
5597 return out;
5598 }
5599 const compiledCache = /* @__PURE__ */ new WeakMap();
5600 function compile(strings) {
5601 const cached = compiledCache.get(strings);
5602 if (cached) {
5603 return cached;
5604 }
5605 const template = document.createElement("template");
5606 template.innerHTML = joinWithMarkers(strings);
5607 const recipes = [];
5608 const walk2 = (node, path) => {
5609 if (node.nodeType === Node.ELEMENT_NODE) {
5610 const el = node;
5611 for (const attr of Array.from(el.attributes)) {
5612 const rawName = attr.name;
5613 const rawValue = attr.value;
5614 const prefix = rawName[0];
5615 if (MARKER_RE.test(rawValue)) {
5616 MARKER_RE.lastIndex = 0;
5617 if (prefix === "@") {
5618 const match = MARKER_RE.exec(rawValue);
5619 MARKER_RE.lastIndex = 0;
5620 recipes.push({
5621 path,
5622 kind: "event",
5623 name: rawName.slice(1),
5624 valueIndex: match ? Number(match[1]) : 0
5625 });
5626 el.removeAttribute(rawName);
5627 } else if (prefix === ".") {
5628 const match = MARKER_RE.exec(rawValue);
5629 MARKER_RE.lastIndex = 0;
5630 recipes.push({
5631 path,
5632 kind: "prop",
5633 name: rawName.slice(1),
5634 valueIndex: match ? Number(match[1]) : 0
5635 });
5636 el.removeAttribute(rawName);
5637 } else if (prefix === "?") {
5638 const match = MARKER_RE.exec(rawValue);
5639 MARKER_RE.lastIndex = 0;
5640 recipes.push({
5641 path,
5642 kind: "bool",
5643 name: rawName.slice(1),
5644 valueIndex: match ? Number(match[1]) : 0
5645 });
5646 el.removeAttribute(rawName);
5647 } else {
5648 const fragments = [];
5649 const indices = [];
5650 let lastEnd = 0;
5651 let m;
5652 MARKER_RE.lastIndex = 0;
5653 while ((m = MARKER_RE.exec(rawValue)) !== null) {
5654 fragments.push(rawValue.slice(lastEnd, m.index));
5655 indices.push(Number(m[1]));
5656 lastEnd = m.index + m[0].length;
5657 }
5658 fragments.push(rawValue.slice(lastEnd));
5659 recipes.push({
5660 path,
5661 kind: "attr",
5662 name: rawName,
5663 template: fragments,
5664 valueIndices: indices
5665 });
5666 el.setAttribute(rawName, "");
5667 }
5668 }
5669 }
5670 }
5671 const children = Array.from(node.childNodes);
5672 let shift = 0;
5673 for (let i = 0; i < children.length; i++) {
5674 const child = children[i];
5675 const liveIndex = i + shift;
5676 if (child.nodeType === Node.TEXT_NODE) {
5677 const text = child.textContent || "";
5678 if (!MARKER_RE.test(text)) {
5679 MARKER_RE.lastIndex = 0;
5680 continue;
5681 }
5682 MARKER_RE.lastIndex = 0;
5683 const parent = child.parentNode;
5684 let lastEnd = 0;
5685 let m;
5686 const newNodes = [];
5687 const newRecipes = [];
5688 MARKER_RE.lastIndex = 0;
5689 while ((m = MARKER_RE.exec(text)) !== null) {
5690 if (m.index > lastEnd) {
5691 newNodes.push(document.createTextNode(text.slice(lastEnd, m.index)));
5692 }
5693 const placeholder = document.createTextNode("");
5694 newNodes.push(placeholder);
5695 newRecipes.push({
5696 path: [...path, liveIndex + newNodes.length - 1],
5697 kind: "node",
5698 valueIndex: Number(m[1])
5699 });
5700 lastEnd = m.index + m[0].length;
5701 }
5702 if (lastEnd < text.length) {
5703 newNodes.push(document.createTextNode(text.slice(lastEnd)));
5704 }
5705 for (const nn of newNodes) {
5706 parent.insertBefore(nn, child);
5707 }
5708 parent.removeChild(child);
5709 shift += newNodes.length - 1;
5710 recipes.push(...newRecipes);
5711 } else {
5712 walk2(child, [...path, liveIndex]);
5713 }
5714 }
5715 };
5716 walk2(template.content, []);
5717 const buildParts = (fragment) => {
5718 const out = [];
5719 for (const r of recipes) {
5720 let node = fragment;
5721 for (const idx of r.path) {
5722 node = node.childNodes[idx];
5723 }
5724 if (r.kind === "node") {
5725 out.push({
5726 kind: "node",
5727 valueIndex: r.valueIndex,
5728 child: {
5729 anchor: node,
5730 state: null
5731 }
5732 });
5733 } else if (r.kind === "attr") {
5734 out.push({
5735 kind: "attr",
5736 element: node,
5737 name: r.name,
5738 template: r.template,
5739 valueIndices: r.valueIndices
5740 });
5741 } else if (r.kind === "event") {
5742 out.push({
5743 kind: "event",
5744 valueIndex: r.valueIndex,
5745 element: node,
5746 name: r.name
5747 });
5748 } else if (r.kind === "prop") {
5749 out.push({
5750 kind: "prop",
5751 valueIndex: r.valueIndex,
5752 element: node,
5753 name: r.name
5754 });
5755 } else if (r.kind === "bool") {
5756 out.push({
5757 kind: "bool",
5758 valueIndex: r.valueIndex,
5759 element: node,
5760 name: r.name
5761 });
5762 }
5763 }
5764 return out;
5765 };
5766 const entry = { template, buildParts };
5767 compiledCache.set(strings, entry);
5768 return entry;
5769 }
5770 const mountState = /* @__PURE__ */ new WeakMap();
5771 function render$1(result, container) {
5772 const existing = mountState.get(container);
5773 if (existing && existing.strings === result.strings) {
5774 applyValues(existing.parts, result.values);
5775 return;
5776 }
5777 const compiled = compile(result.strings);
5778 const fragment = compiled.template.content.cloneNode(true);
5779 const parts = compiled.buildParts(fragment);
5780 while (container.firstChild) {
5781 container.removeChild(container.firstChild);
5782 }
5783 container.appendChild(fragment);
5784 applyValues(parts, result.values);
5785 mountState.set(container, { strings: result.strings, parts });
5786 }
5787 function applyValues(parts, values) {
5788 for (const part of parts) {
5789 if (part.kind === "node") {
5790 updateChildPart(part.child, values[part.valueIndex]);
5791 } else if (part.kind === "attr") {
5792 let composed = part.template[0];
5793 for (let i = 0; i < part.valueIndices.length; i++) {
5794 composed += formatText(values[part.valueIndices[i]]);
5795 composed += part.template[i + 1];
5796 }
5797 if (composed !== part.last) {
5798 part.last = composed;
5799 if (composed === "") {
5800 part.element.removeAttribute(part.name);
5801 } else {
5802 part.element.setAttribute(part.name, composed);
5803 }
5804 }
5805 } else if (part.kind === "event") {
5806 const next = values[part.valueIndex];
5807 if (next !== part.current) {
5808 if (part.current) {
5809 part.element.removeEventListener(part.name, part.current);
5810 }
5811 if (next) {
5812 part.element.addEventListener(part.name, next);
5813 }
5814 part.current = next;
5815 }
5816 } else if (part.kind === "prop") {
5817 const next = values[part.valueIndex];
5818 if (next !== part.last) {
5819 part.last = next;
5820 part.element[part.name] = next;
5821 }
5822 } else if (part.kind === "bool") {
5823 const next = !!values[part.valueIndex];
5824 if (next !== part.last) {
5825 part.last = next;
5826 if (next) {
5827 part.element.setAttribute(part.name, "");
5828 } else {
5829 part.element.removeAttribute(part.name);
5830 }
5831 }
5832 }
5833 }
5834 }
5835 function updateChildPart(child, value) {
5836 if (value === null || value === void 0 || value === false) {
5837 if (child.state) {
5838 disposeChildState(child.state);
5839 child.state = null;
5840 }
5841 return;
5842 }
5843 if (Array.isArray(value)) {
5844 updateArrayChild(child, value);
5845 return;
5846 }
5847 if (isTemplateResult(value)) {
5848 updateTemplateChild(child, value);
5849 return;
5850 }
5851 if (value instanceof Node) {
5852 updateNodeChild(child, value);
5853 return;
5854 }
5855 updateTextChild(child, formatText(value));
5856 }
5857 function updateNodeChild(child, node) {
5858 const old = child.state;
5859 if (old?.shape === "node" && old.node === node) {
5860 return;
5861 }
5862 if (old) {
5863 disposeChildState(old);
5864 }
5865 insertBeforeAnchor(child, [node]);
5866 child.state = { shape: "node", node };
5867 }
5868 function updateTextChild(child, text) {
5869 const old = child.state;
5870 if (old?.shape === "text") {
5871 if (old.text !== text) {
5872 old.node.textContent = text;
5873 old.text = text;
5874 }
5875 return;
5876 }
5877 if (old) {
5878 disposeChildState(old);
5879 }
5880 const node = document.createTextNode(text);
5881 insertBeforeAnchor(child, [node]);
5882 child.state = { shape: "text", node, text };
5883 }
5884 function updateTemplateChild(child, result) {
5885 const old = child.state;
5886 if (old?.shape === "template" && old.strings === result.strings) {
5887 applyValues(old.parts, result.values);
5888 return;
5889 }
5890 if (old) {
5891 disposeChildState(old);
5892 }
5893 const compiled = compile(result.strings);
5894 const fragment = compiled.template.content.cloneNode(true);
5895 const parts = compiled.buildParts(fragment);
5896 const topNodes = Array.from(fragment.childNodes);
5897 insertBeforeAnchor(child, [fragment]);
5898 applyValues(parts, result.values);
5899 child.state = {
5900 shape: "template",
5901 strings: result.strings,
5902 parts,
5903 nodes: topNodes
5904 };
5905 }
5906 function updateArrayChild(child, arr) {
5907 const old = child.state;
5908 if (old?.shape === "array" && old.entries.length === arr.length) {
5909 for (let i = 0; i < arr.length; i++) {
5910 updateChildPart(old.entries[i], arr[i]);
5911 }
5912 return;
5913 }
5914 if (old) {
5915 disposeChildState(old);
5916 }
5917 const entries = [];
5918 for (const v of arr) {
5919 const entryAnchor = document.createTextNode("");
5920 insertBeforeAnchor(child, [entryAnchor]);
5921 const entry = { anchor: entryAnchor, state: null };
5922 updateChildPart(entry, v);
5923 entries.push(entry);
5924 }
5925 child.state = { shape: "array", entries };
5926 }
5927 function insertBeforeAnchor(child, nodes) {
5928 const parent = child.anchor.parentNode;
5929 if (!parent) {
5930 return;
5931 }
5932 for (const node of nodes) {
5933 parent.insertBefore(node, child.anchor);
5934 }
5935 }
5936 function disposeChildState(state2) {
5937 if (state2.shape === "text") {
5938 state2.node.remove();
5939 return;
5940 }
5941 if (state2.shape === "template") {
5942 for (const node of state2.nodes) {
5943 if (node.parentNode) {
5944 node.parentNode.removeChild(node);
5945 }
5946 }
5947 return;
5948 }
5949 if (state2.shape === "node") {
5950 if (state2.node.parentNode) {
5951 state2.node.parentNode.removeChild(state2.node);
5952 }
5953 return;
5954 }
5955 for (const entry of state2.entries) {
5956 if (entry.state) {
5957 disposeChildState(entry.state);
5958 }
5959 entry.anchor.remove();
5960 }
5961 }
5962 function formatText(v) {
5963 if (v === null || v === void 0 || v === false) {
5964 return "";
5965 }
5966 return String(v);
5967 }
5968 const _Component = class _Component extends HTMLElement {
5969 constructor() {
5970 super();
5971 this._renderScheduled = false;
5972 this._propValues = {};
5973 const ctor = this.constructor;
5974 if (ctor.shadow) {
5975 this.attachShadow({ mode: "open" });
5976 this._renderRoot = this.shadowRoot;
5977 } else {
5978 this._renderRoot = this;
5979 }
5980 this._installPropAccessors();
5981 }
5982 static get observedAttributes() {
5983 return this.props.map(kebab);
5984 }
5985 connectedCallback() {
5986 this._adoptStyles();
5987 this.requestUpdate();
5988 }
5989 attributeChangedCallback(name, oldValue, newValue) {
5990 if (oldValue === newValue) {
5991 return;
5992 }
5993 const prop = camel(name);
5994 this._propValues[prop] = newValue;
5995 this.requestUpdate();
5996 }
5997 /**
5998 * Declarative class-name setter. Assign an array (or a
5999 * space-separated string) and the host's `class` attribute is
6000 * rewritten to match. Intended for programmatic styling — when
6001 * a plugin has enqueued its own stylesheet and wants to apply
6002 * one of those classes to a shell component:
6003 *
6004 * ```js
6005 * element.classNames = [ 'my-plugin-brand', 'is-active' ];
6006 * // → <wpd-select class="my-plugin-brand is-active">
6007 * ```
6008 *
6009 * The plain HTML `class="…"` attribute works just the same and
6010 * is always preferred when writing markup by hand — this setter
6011 * exists for the JS-API case where the caller has an array of
6012 * conditional classes in hand.
6013 *
6014 * Getter returns the current `classList` as a plain array for
6015 * symmetric read/write.
6016 *
6017 * @since 0.13.0
6018 */
6019 get classNames() {
6020 return Array.from(this.classList);
6021 }
6022 set classNames(next) {
6023 if (next === null || next === void 0) {
6024 this.removeAttribute("class");
6025 return;
6026 }
6027 const list2 = Array.isArray(next) ? next : String(next).split(/\s+/);
6028 const cleaned = list2.map((s) => String(s).trim()).filter((s) => s !== "");
6029 this.className = cleaned.join(" ");
6030 }
6031 /**
6032 * Request a re-render explicitly. Components rarely need this —
6033 * declare state via props + attribute observers and the render
6034 * loop picks up changes automatically.
6035 */
6036 requestUpdate() {
6037 this._scheduleRender();
6038 }
6039 /**
6040 * Dispatch a `CustomEvent` with a `detail`. Bubbles + composed
6041 * by default (matches typical WC UX — events cross shadow
6042 * boundaries, parents can listen without knowing about internal
6043 * structure).
6044 */
6045 emit(name, detail) {
6046 return this.dispatchEvent(
6047 new CustomEvent(name, {
6048 detail,
6049 bubbles: true,
6050 composed: true
6051 })
6052 );
6053 }
6054 // ------------------------------------------------------------------
6055 // Internals
6056 // ------------------------------------------------------------------
6057 /**
6058 * Wire every `static props` entry to a matched property getter +
6059 * setter on the element. Setting the property reflects into the
6060 * attribute (so downstream observers + CSS selectors see it);
6061 * reading the property falls back to the attribute.
6062 */
6063 _installPropAccessors() {
6064 const ctor = this.constructor;
6065 for (const prop of ctor.props) {
6066 if (Object.getOwnPropertyDescriptor(this, prop)) {
6067 continue;
6068 }
6069 const attr = kebab(prop);
6070 Object.defineProperty(this, prop, {
6071 get: () => {
6072 if (prop in this._propValues) {
6073 return this._propValues[prop];
6074 }
6075 return this.getAttribute(attr);
6076 },
6077 set: (value) => {
6078 let str;
6079 if (value === null || value === void 0 || value === false) {
6080 str = null;
6081 } else if (value === true) {
6082 str = "";
6083 } else {
6084 str = String(value);
6085 }
6086 this._propValues[prop] = str;
6087 if (str === null) {
6088 this.removeAttribute(attr);
6089 } else {
6090 this.setAttribute(attr, str);
6091 }
6092 this.requestUpdate();
6093 },
6094 enumerable: true,
6095 configurable: true
6096 });
6097 }
6098 }
6099 /**
6100 * Schedule a render on the next microtask. Multiple property
6101 * assignments in the same tick collapse into a single render.
6102 */
6103 _scheduleRender() {
6104 if (this._renderScheduled || !this.isConnected) {
6105 return;
6106 }
6107 this._renderScheduled = true;
6108 queueMicrotask(() => {
6109 this._renderScheduled = false;
6110 if (!this.isConnected) {
6111 return;
6112 }
6113 render$1(this.render(), this._renderRoot);
6114 });
6115 }
6116 /**
6117 * Mount adoptable stylesheets onto the shadow root (via
6118 * `adoptedStyleSheets`) or the light DOM (via one `<style>`
6119 * tag per def). No-op if `static styles` is empty.
6120 */
6121 _adoptStyles() {
6122 const ctor = this.constructor;
6123 if (ctor.styles.length === 0) {
6124 return;
6125 }
6126 if (ctor.shadow && this.shadowRoot) {
6127 const sheets = ctor.styles.map((s) => s.sheet).filter((s) => s !== null);
6128 this.shadowRoot.adoptedStyleSheets = sheets;
6129 if (sheets.length !== ctor.styles.length) {
6130 for (const s of ctor.styles) {
6131 if (!s.sheet) {
6132 const tag = document.createElement("style");
6133 tag.textContent = s.cssText;
6134 this.shadowRoot.appendChild(tag);
6135 }
6136 }
6137 }
6138 } else {
6139 this._adoptLightStyles(ctor);
6140 }
6141 }
6142 _adoptLightStyles(ctor) {
6143 if (_Component._lightStylesAdopted.has(ctor)) {
6144 return;
6145 }
6146 _Component._lightStylesAdopted.add(ctor);
6147 for (const s of ctor.styles) {
6148 const tag = document.createElement("style");
6149 tag.dataset.wpdUi = this.tagName.toLowerCase();
6150 tag.textContent = s.cssText;
6151 document.head.appendChild(tag);
6152 }
6153 }
6154 };
6155 _Component.props = [];
6156 _Component.styles = [];
6157 _Component.shadow = true;
6158 _Component._lightStylesAdopted = /* @__PURE__ */ new WeakSet();
6159 let Component = _Component;
6160 function defineComponent(tag, ctor) {
6161 if (customElements.get(tag)) {
6162 return;
6163 }
6164 customElements.define(tag, ctor);
6165 }
6166 function kebab(s) {
6167 return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
6168 }
6169 function camel(s) {
6170 return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
6171 }
6172 const SUPPORTS_CONSTRUCTABLE_SHEETS = (() => {
6173 try {
6174 const s = new CSSStyleSheet();
6175 return typeof s.replaceSync === "function";
6176 } catch {
6177 return false;
6178 }
6179 })();
6180 function css(strings, ...values) {
6181 let text = strings[0];
6182 for (let i = 1; i < strings.length; i++) {
6183 const v = values[i - 1];
6184 if (typeof v === "string" || typeof v === "number") {
6185 text += String(v);
6186 } else if (v && v.__wpdCss) {
6187 text += v.cssText;
6188 } else {
6189 throw new TypeError(
6190 "[wpd-ui] css`` interpolations must be strings, numbers, or other css`` results. Got: " + typeof v
6191 );
6192 }
6193 text += strings[i];
6194 }
6195 if (SUPPORTS_CONSTRUCTABLE_SHEETS) {
6196 const sheet = new CSSStyleSheet();
6197 sheet.replaceSync(text);
6198 return { __wpdCss: true, sheet, cssText: text };
6199 }
6200 return { __wpdCss: true, sheet: null, cssText: text };
6201 }
6202 function computeAutoId(element) {
6203 const parts = [];
6204 const tabs = [];
6205 let windowId = null;
6206 let node = element.parentElement;
6207 while (node) {
6208 if (node === document.body || node === document.documentElement) {
6209 break;
6210 }
6211 const id = node.id || "";
6212 if (id.startsWith("wp-window-")) {
6213 windowId = id.slice("wp-window-".length);
6214 break;
6215 }
6216 if (node.tagName.toLowerCase() === "wpd-tabpanel") {
6217 const forValue = node.getAttribute("for");
6218 if (forValue) {
6219 tabs.unshift(forValue);
6220 }
6221 }
6222 node = node.parentElement;
6223 }
6224 if (windowId) {
6225 parts.push(slugify(windowId));
6226 }
6227 for (const tab of tabs) {
6228 parts.push("tab-" + slugify(tab));
6229 }
6230 const label = element.getAttribute("label");
6231 if (label) {
6232 parts.push(slugify(label));
6233 }
6234 if (parts.length === 0) {
6235 return "wpd-unnamed";
6236 }
6237 return "wpd-" + parts.filter((p) => p !== "").join("-");
6238 }
6239 function slugify(s) {
6240 return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
6241 }
6242 function ensureAutoId(element) {
6243 if (element.id) {
6244 return element.id;
6245 }
6246 const id = computeAutoId(element);
6247 element.id = id;
6248 return id;
6249 }
6250 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 )}`;
6251 const _WpdConfirmDialog = class _WpdConfirmDialog extends Component {
6252 constructor() {
6253 super(...arguments);
6254 this._onKey = (e) => {
6255 if (e.key === "Escape") {
6256 e.preventDefault();
6257 this._cancel();
6258 }
6259 if (e.key === "Enter" && !e.isComposing) {
6260 e.preventDefault();
6261 this._confirm();
6262 }
6263 };
6264 this._onBackdrop = (e) => {
6265 const path = e.composedPath();
6266 const original = path.length > 0 ? path[0] : e.target;
6267 if (original === this) {
6268 this._cancel();
6269 }
6270 };
6271 this._confirm = () => {
6272 this.emit("wpd-confirm", { confirmed: true });
6273 this.removeAttribute("open");
6274 };
6275 this._cancel = () => {
6276 this.emit("wpd-cancel", { confirmed: false });
6277 this.removeAttribute("open");
6278 };
6279 }
6280 connectedCallback() {
6281 super.connectedCallback();
6282 this.setAttribute("role", "dialog");
6283 this.setAttribute("aria-modal", "true");
6284 this.addEventListener("keydown", this._onKey);
6285 this.addEventListener("click", this._onBackdrop);
6286 }
6287 disconnectedCallback() {
6288 this.removeEventListener("keydown", this._onKey);
6289 this.removeEventListener("click", this._onBackdrop);
6290 }
6291 render() {
6292 const title = this.title ?? "";
6293 const message = this.message ?? "";
6294 const confirmLabel = this["confirm-label"] || "Confirm";
6295 const cancelLabel = this["cancel-label"] || "Cancel";
6296 const isDanger = this.hasAttribute("danger");
6297 const hideCancel = this.hasAttribute("hide-cancel");
6298 const isDismissable = this.hasAttribute("dismissable");
6299 return html`
6300 <div class="dialog" tabindex="-1">
6301 ${isDismissable ? html`<button
6302 type="button"
6303 class="close"
6304 aria-label="Close"
6305 @click=${() => this._cancel()}
6306 >&times;</button>` : html``}
6307 ${title ? html`<h2 class="title">${title}</h2>` : html``}
6308 ${message ? html`<p class="message">${message}</p>` : html``}
6309 <div class="actions">
6310 ${hideCancel ? html`` : html`<button
6311 type="button"
6312 class="btn btn--secondary"
6313 @click=${() => this._cancel()}
6314 >
6315 ${cancelLabel}
6316 </button>`}
6317 <button
6318 type="button"
6319 class="btn ${isDanger ? "btn--danger" : "btn--primary"}"
6320 @click=${() => this._confirm()}
6321 >
6322 ${confirmLabel}
6323 </button>
6324 </div>
6325 </div>
6326 `;
6327 }
6328 };
6329 _WpdConfirmDialog.props = [
6330 "open",
6331 "title",
6332 "message",
6333 "confirm-label",
6334 "cancel-label",
6335 "danger",
6336 "hide-cancel",
6337 "dismissable"
6338 ];
6339 _WpdConfirmDialog.styles = [dialogStyles];
6340 _WpdConfirmDialog.help = {
6341 title: "Confirm dialog",
6342 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.",
6343 status: "experimental",
6344 since: "0.9.0",
6345 props: [
6346 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
6347 { name: "title", type: "string", description: "Heading shown at the top." },
6348 { name: "message", type: "string", description: "Body copy. Newlines preserved." },
6349 { name: "confirm-label", type: "string", default: "Confirm", description: "Confirm-button label." },
6350 { name: "cancel-label", type: "string", default: "Cancel", description: "Cancel-button label." },
6351 { name: "danger", type: "boolean attribute", description: "Renders the confirm button red." },
6352 { 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." },
6353 { name: "dismissable", type: "boolean attribute", description: "Renders an X close button in the top-right corner. Click emits `wpd-cancel`." }
6354 ],
6355 events: [
6356 {
6357 name: "wpd-confirm",
6358 description: "Fires on confirm. Detail: `{ confirmed: true }`."
6359 },
6360 {
6361 name: "wpd-cancel",
6362 description: "Fires on cancel (Cancel button, Escape, backdrop click). Detail: `{ confirmed: false }`."
6363 }
6364 ]
6365 };
6366 let WpdConfirmDialog = _WpdConfirmDialog;
6367 defineComponent("wpd-confirm-dialog", WpdConfirmDialog);
6368 function wpdConfirm$1(options) {
6369 return new Promise((resolve2) => {
6370 const dialog2 = document.createElement("wpd-confirm-dialog");
6371 dialog2.setAttribute("open", "");
6372 if (options.title) {
6373 dialog2.setAttribute("title", options.title);
6374 }
6375 dialog2.setAttribute("message", options.message);
6376 if (options.confirmLabel) {
6377 dialog2.setAttribute("confirm-label", options.confirmLabel);
6378 }
6379 if (options.cancelLabel) {
6380 dialog2.setAttribute("cancel-label", options.cancelLabel);
6381 }
6382 if (options.danger) {
6383 dialog2.setAttribute("danger", "");
6384 }
6385 if (options.hideCancel) {
6386 dialog2.setAttribute("hide-cancel", "");
6387 }
6388 if (options.dismissable) {
6389 dialog2.setAttribute("dismissable", "");
6390 }
6391 const cleanup = (ok) => {
6392 dialog2.remove();
6393 resolve2(ok);
6394 };
6395 dialog2.addEventListener("wpd-confirm", () => cleanup(true));
6396 dialog2.addEventListener("wpd-cancel", () => cleanup(false));
6397 document.body.appendChild(dialog2);
6398 const inner = dialog2.shadowRoot?.querySelector(".dialog");
6399 (inner ?? dialog2).focus?.();
6400 });
6401 }
6402 const FALLBACK_BASE = "http://localhost/";
6403 function joinRestUrl(restRoot2, path) {
6404 const base = typeof window !== "undefined" && window.location ? window.location.href : FALLBACK_BASE;
6405 const url = new URL(restRoot2, base);
6406 const trimmed = path.replace(/^\/+/, "");
6407 const queryAt = trimmed.indexOf("?");
6408 const route = queryAt === -1 ? trimmed : trimmed.slice(0, queryAt);
6409 const extraQuery = queryAt === -1 ? "" : trimmed.slice(queryAt + 1);
6410 if (url.searchParams.has("rest_route")) {
6411 const existing = url.searchParams.get("rest_route") ?? "/";
6412 const prefix = existing.endsWith("/") ? existing : existing + "/";
6413 url.searchParams.set("rest_route", prefix + route);
6414 } else {
6415 const pathname = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
6416 url.pathname = pathname + route;
6417 }
6418 if (extraQuery) {
6419 const extras = new URLSearchParams(extraQuery);
6420 extras.forEach((value, key) => {
6421 url.searchParams.append(key, value);
6422 });
6423 }
6424 return url.toString();
6425 }
6426 function getApi() {
6427 const w = window;
6428 return w.wp?.desktop ?? null;
6429 }
6430 let activeMenu$3 = null;
6431 function closeMenu$1() {
6432 if (activeMenu$3) {
6433 activeMenu$3.remove();
6434 activeMenu$3 = null;
6435 }
6436 }
6437 function writeVisibility(canonicalId, placement) {
6438 const api = getApi();
6439 if (!api?.getOsSettings || !api?.updateOsSettings) {
6440 return;
6441 }
6442 const snap = api.getOsSettings();
6443 const next = { ...snap.itemVisibility };
6444 next[canonicalId] = placement;
6445 api.updateOsSettings({ itemVisibility: next });
6446 }
6447 let openGeneration$2 = 0;
6448 function openItemVisibilityMenu(opts) {
6449 closeMenu$1();
6450 const myGen = ++openGeneration$2;
6451 openWithShellOverlays(
6452 () => myGen === openGeneration$2,
6453 () => openItemVisibilityMenuImmediate(opts)
6454 );
6455 }
6456 function openItemVisibilityMenuImmediate(opts) {
6457 closeMenu$1();
6458 const canonical = canonicalItemId(opts.id);
6459 const options = [];
6460 if (opts.surface === "dock") {
6461 options.push({
6462 id: "hide-from-dock",
6463 label: __("Hide from dock"),
6464 icon: "dashicons-hidden",
6465 onPick: () => writeVisibility(canonical, "desktop")
6466 });
6467 options.push({
6468 id: "show-on-desktop-too",
6469 label: __("Also show on desktop"),
6470 icon: "dashicons-desktop",
6471 onPick: () => writeVisibility(canonical, "both")
6472 });
6473 } else {
6474 options.push({
6475 id: "hide-from-desktop",
6476 label: __("Hide from desktop"),
6477 icon: "dashicons-hidden",
6478 onPick: () => writeVisibility(canonical, "dock")
6479 });
6480 options.push({
6481 id: "show-on-dock-too",
6482 label: __("Also show on dock"),
6483 icon: "dashicons-menu",
6484 onPick: () => writeVisibility(canonical, "both")
6485 });
6486 }
6487 options.push({
6488 id: "hide-everywhere",
6489 label: __("Hide everywhere"),
6490 icon: "dashicons-no",
6491 danger: true,
6492 onPick: () => writeVisibility(canonical, "hidden")
6493 });
6494 options.push({
6495 id: "open-settings",
6496 label: __("Apps & Icons settings…"),
6497 icon: "dashicons-admin-generic",
6498 onPick: () => {
6499 const api = getApi();
6500 api?.openOsSettings?.({ tabId: "apps-icons" });
6501 }
6502 });
6503 if (opts.pluginFile) {
6504 const pluginFile = opts.pluginFile;
6505 const pluginLabel = opts.pluginName || opts.title;
6506 options.push({ kind: "separator" });
6507 options.push({
6508 id: "deactivate-plugin",
6509 // translators: %s is the owning plugin's display name.
6510 label: sprintf(__("Deactivate %s…"), pluginLabel),
6511 icon: "dashicons-trash",
6512 danger: true,
6513 onPick: () => {
6514 void confirmAndDeactivatePlugin(pluginFile, pluginLabel);
6515 }
6516 });
6517 }
6518 const menu = document.createElement("wpd-context-menu");
6519 menu.setAttribute("open", "");
6520 menu.classList.add("desktop-mode-item-visibility-menu");
6521 menu.dataset.itemId = opts.id;
6522 menu.style.position = "fixed";
6523 menu.style.left = "-9999px";
6524 menu.style.top = "-9999px";
6525 menu.style.visibility = "hidden";
6526 menu.style.zIndex = "1000000";
6527 const byKey = /* @__PURE__ */ new Map();
6528 for (const opt of options) {
6529 if (opt.kind === "separator") {
6530 const hr = document.createElement("hr");
6531 hr.style.cssText = "border: 0; border-top: 1px solid var( --wpd-context-menu-separator-color, rgba(255,255,255,0.12) ); margin: 4px 6px;";
6532 menu.appendChild(hr);
6533 continue;
6534 }
6535 byKey.set(opt.id, opt);
6536 const node = document.createElement("wpd-context-menu-option");
6537 node.dataset.menuItemId = opt.id;
6538 node.setAttribute("value", opt.id);
6539 if (opt.icon) {
6540 node.setAttribute("icon", opt.icon);
6541 }
6542 if (opt.danger) {
6543 node.setAttribute("danger", "");
6544 }
6545 node.textContent = opt.label;
6546 menu.appendChild(node);
6547 }
6548 menu.addEventListener("wpd-context-menu-pick", (e) => {
6549 const detail = e.detail;
6550 const key = detail?.id || detail?.value || "";
6551 const opt = byKey.get(key);
6552 closeMenu$1();
6553 try {
6554 opt?.onPick();
6555 } catch {
6556 }
6557 });
6558 document.body.appendChild(menu);
6559 activeMenu$3 = menu;
6560 const positionMenu = () => {
6561 if (menu !== activeMenu$3) {
6562 return;
6563 }
6564 const rect = menu.getBoundingClientRect();
6565 const margin = 8;
6566 let left = opts.x;
6567 let top;
6568 if (opts.surface === "dock") {
6569 top = Math.max(margin, opts.y - rect.height - margin);
6570 } else {
6571 top = opts.y;
6572 if (top + rect.height + margin > window.innerHeight) {
6573 top = Math.max(margin, opts.y - rect.height);
6574 }
6575 }
6576 if (left + rect.width + margin > window.innerWidth) {
6577 left = Math.max(margin, opts.x - rect.width);
6578 }
6579 menu.style.left = `${left}px`;
6580 menu.style.top = `${top}px`;
6581 menu.style.visibility = "";
6582 };
6583 requestAnimationFrame(positionMenu);
6584 const onOutside = (ev) => {
6585 if (!activeMenu$3) {
6586 return;
6587 }
6588 if (!activeMenu$3.contains(ev.target)) {
6589 closeMenu$1();
6590 document.removeEventListener("mousedown", onOutside, true);
6591 document.removeEventListener("keydown", onKey, true);
6592 }
6593 };
6594 const onKey = (ev) => {
6595 if (ev.key === "Escape") {
6596 closeMenu$1();
6597 document.removeEventListener("mousedown", onOutside, true);
6598 document.removeEventListener("keydown", onKey, true);
6599 }
6600 };
6601 document.addEventListener("mousedown", onOutside, true);
6602 document.addEventListener("keydown", onKey, true);
6603 }
6604 async function confirmAndDeactivatePlugin(pluginFile, title) {
6605 const confirmed = await wpdConfirm$1({
6606 /* translators: %s: plugin title. */
6607 title: sprintf(__("Deactivate %s?"), title),
6608 message: __(
6609 "This plugin will stop running on the site. You can re-activate it later from the Plugins screen."
6610 ),
6611 confirmLabel: __("Deactivate"),
6612 cancelLabel: __("Cancel"),
6613 danger: true
6614 });
6615 if (!confirmed) {
6616 return;
6617 }
6618 const cfg = window.desktopModeConfig ?? {};
6619 const restRoot2 = typeof cfg.restRoot === "string" && cfg.restRoot ? cfg.restRoot : `${window.location.origin}/wp-json/`;
6620 const restNonce = typeof cfg.restNonce === "string" && cfg.restNonce ? cfg.restNonce : "";
6621 const stripped = pluginFile.endsWith(".php") ? pluginFile.slice(0, -4) : pluginFile;
6622 const encoded = stripped.split("/").map(encodeURIComponent).join("/");
6623 const url = joinRestUrl(restRoot2, `wp/v2/plugins/${encoded}`);
6624 try {
6625 const res = await trackedFetch$1(
6626 url,
6627 {
6628 method: "PUT",
6629 headers: {
6630 "Content-Type": "application/json",
6631 "X-WP-Nonce": restNonce
6632 },
6633 body: JSON.stringify({ status: "inactive" }),
6634 credentials: "same-origin"
6635 },
6636 { source: "desktop-mode/dock-deactivate-plugin" }
6637 );
6638 if (!res.ok) {
6639 throw new Error(`HTTP ${res.status}`);
6640 }
6641 } catch (err) {
6642 showToast({
6643 message: sprintf(
6644 /* translators: %s: plugin title. */
6645 __("Could not deactivate %s."),
6646 title
6647 ),
6648 duration: 4e3
6649 });
6650 console.error("[desktop-mode] deactivate plugin failed", err);
6651 return;
6652 }
6653 const closedTitles = closeWindowsForPlugin(pluginFile);
6654 const deactivatedMsg = closedTitles.length > 0 ? sprintf(
6655 /* translators: 1: plugin title. 2: number of windows that were closed. */
6656 __("%1$s deactivated. Closed %2$d window(s)."),
6657 title,
6658 closedTitles.length
6659 ) : sprintf(
6660 /* translators: %s: plugin title. */
6661 __("%s deactivated."),
6662 title
6663 );
6664 showToast({ message: deactivatedMsg, duration: 3e3 });
6665 const w = window;
6666 w.wp?.desktop?.refreshMenu?.();
6667 }
6668 function closeWindowsForPlugin(pluginFile) {
6669 const api = window.wp?.desktop;
6670 if (!api?.windowManager?.getAll) {
6671 return [];
6672 }
6673 const items = api.getMenuItems?.() ?? [];
6674 const owned = items.filter((i) => i.pluginFile === pluginFile);
6675 if (owned.length === 0) {
6676 return [];
6677 }
6678 const ownedKeys = /* @__PURE__ */ new Set();
6679 for (const item of owned) {
6680 ownedKeys.add(item.id);
6681 if (api.deriveWindowId) {
6682 ownedKeys.add(api.deriveWindowId(item.url));
6683 }
6684 }
6685 const toClose = /* @__PURE__ */ new Map();
6686 const windows = api.windowManager.getAll() ?? [];
6687 const derive = api.deriveWindowId;
6688 for (const w of windows) {
6689 if (ownedKeys.has(w.id)) {
6690 toClose.set(w.id, w);
6691 continue;
6692 }
6693 if (w.config?.baseId && ownedKeys.has(w.config.baseId)) {
6694 toClose.set(w.id, w);
6695 continue;
6696 }
6697 if (derive && w.config?.url) {
6698 const derivedFromConfig = derive(w.config.url);
6699 if (ownedKeys.has(derivedFromConfig)) {
6700 toClose.set(w.id, w);
6701 continue;
6702 }
6703 }
6704 if (derive && w.iframe) {
6705 let liveUrl = "";
6706 try {
6707 liveUrl = w.iframe.src || "";
6708 } catch {
6709 }
6710 if (liveUrl) {
6711 const derivedFromLive = derive(liveUrl);
6712 if (ownedKeys.has(derivedFromLive)) {
6713 toClose.set(w.id, w);
6714 }
6715 }
6716 }
6717 }
6718 const titles = [];
6719 for (const w of toClose.values()) {
6720 titles.push(w.config?.title ?? w.id);
6721 try {
6722 w.close();
6723 } catch {
6724 }
6725 }
6726 return titles;
6727 }
6728 const _Dock = class _Dock {
6729 constructor(container, windowManager, items, adminUrl, orientation = "left") {
6730 this.itemElements = /* @__PURE__ */ new Map();
6731 this.systemItems = [];
6732 this.systemItemElements = /* @__PURE__ */ new Map();
6733 this.systemSeparator = null;
6734 this.badgeOverrides = /* @__PURE__ */ new Map();
6735 this.attentionTimers = /* @__PURE__ */ new Map();
6736 this.peekTeardowns = /* @__PURE__ */ new Map();
6737 this.boundRefresh = () => void 0;
6738 this.container = container;
6739 this.windowManager = windowManager;
6740 this.items = items;
6741 this.adminUrl = adminUrl;
6742 this.orientation = orientation;
6743 this.rail = orientation === "bottom" ? "taskbar" : "dock";
6744 this.hooksNamespace = `desktop-mode/dock/${++_Dock.instanceCounter}`;
6745 this.container.setAttribute(
6746 "data-desktop-mode-dock-placement",
6747 orientation
6748 );
6749 const scroll = document.createElement("div");
6750 scroll.className = "desktop-mode-dock__scroll";
6751 const pinned = document.createElement("div");
6752 pinned.className = "desktop-mode-dock__pinned";
6753 container.appendChild(scroll);
6754 container.appendChild(pinned);
6755 this.itemHost = scroll;
6756 this.systemHost = pinned;
6757 this.tooltip = document.createElement("div");
6758 this.tooltip.className = "desktop-mode-dock__tooltip";
6759 this.tooltip.setAttribute("role", "tooltip");
6760 if (orientation === "bottom") {
6761 this.tooltip.classList.add("desktop-mode-dock__tooltip--above");
6762 } else if (orientation === "right") {
6763 this.tooltip.classList.add("desktop-mode-dock__tooltip--before");
6764 } else {
6765 this.tooltip.classList.add("desktop-mode-dock__tooltip--after");
6766 }
6767 document.body.appendChild(this.tooltip);
6768 this.render();
6769 this.bindWindowEvents();
6770 }
6771 /**
6772 * Build the base context object every dock decoration hook
6773 * receives. Read from `this` so a single subscriber can
6774 * disambiguate two coexisting rails by `dockId`.
6775 */
6776 buildHookContextBase() {
6777 return {
6778 rail: this.rail,
6779 orientation: this.orientation,
6780 dockId: this.container.id,
6781 container: this.container
6782 };
6783 }
6784 /**
6785 * Replace the menu-derived tile list with a fresh one, preserving
6786 * any JS-registered system tiles. Used by the live menu-refresh
6787 * path: after a plugin is activated or deactivated, the chromeless
6788 * bridge postMessages a fresh payload built from real admin
6789 * context, and the shell calls this so the dock repaints without
6790 * a tab reload.
6791 *
6792 * Old menu tiles are removed from both the DOM and the lookup
6793 * map; new tiles are inserted before the system separator (or
6794 * appended at the end if none exists yet), so the menu-items →
6795 * hairline → system-items ordering stays intact. Active-state
6796 * classes are re-computed once the new tiles are in place so
6797 * window indicators survive the swap.
6798 *
6799 * @param items New DockItem list. Pass `[]` to clear everything
6800 * menu-derived.
6801 */
6802 /**
6803 * Update the dock's orientation. Writes the new value to the
6804 * dock element's `data-desktop-mode-dock-placement` attribute (CSS
6805 * keys off it for layout) and keeps the tooltip anchor in sync.
6806 *
6807 * In practice, the layout dispatcher in `desktop.ts` rebuilds the
6808 * dock(s) from scratch on a layout change rather than re-orienting
6809 * a live instance — but this stays correct in case any caller
6810 * wants to flip orientation without the rebuild.
6811 */
6812 setOrientation(orientation) {
6813 if (this.orientation === orientation) {
6814 return;
6815 }
6816 this.orientation = orientation;
6817 this.container.setAttribute(
6818 "data-desktop-mode-dock-placement",
6819 orientation
6820 );
6821 this.tooltip.classList.remove(
6822 "desktop-mode-dock__tooltip--above",
6823 "desktop-mode-dock__tooltip--before",
6824 "desktop-mode-dock__tooltip--after"
6825 );
6826 if (orientation === "bottom") {
6827 this.tooltip.classList.add("desktop-mode-dock__tooltip--above");
6828 } else if (orientation === "right") {
6829 this.tooltip.classList.add("desktop-mode-dock__tooltip--before");
6830 } else {
6831 this.tooltip.classList.add("desktop-mode-dock__tooltip--after");
6832 }
6833 }
6834 replaceItems(items) {
6835 for (const itemId of this.itemElements.keys()) {
6836 const teardown = this.peekTeardowns.get(itemId);
6837 if (teardown) {
6838 teardown();
6839 this.peekTeardowns.delete(itemId);
6840 }
6841 }
6842 for (const el of this.itemElements.values()) {
6843 el.remove();
6844 }
6845 this.itemHost.querySelectorAll(
6846 ".desktop-mode-dock__separator--group"
6847 ).forEach((el) => el.remove());
6848 this.itemElements.clear();
6849 this.items = items;
6850 const base = this.buildHookContextBase();
6851 doAction(HOOKS.DOCK_BEFORE_RENDER, {
6852 ...base,
6853 items,
6854 tileElements: this.itemElements
6855 });
6856 let insertedGroupSeparator = false;
6857 let tilesInsertedThisPass = 0;
6858 for (const item of items) {
6859 if (!insertedGroupSeparator && item.isCore === false) {
6860 if (tilesInsertedThisPass > 0) {
6861 const sep = document.createElement("div");
6862 sep.className = "desktop-mode-dock__separator desktop-mode-dock__separator--group";
6863 sep.setAttribute("aria-hidden", "true");
6864 this.itemHost.appendChild(sep);
6865 }
6866 insertedGroupSeparator = true;
6867 }
6868 const btn = this.createItemButton(item);
6869 this.itemElements.set(item.id, btn);
6870 this.itemHost.appendChild(btn);
6871 tilesInsertedThisPass++;
6872 const override = this.badgeOverrides.get(item.id);
6873 if (override !== void 0) {
6874 const primary = btn.querySelector(
6875 ".desktop-mode-dock__item-primary"
6876 );
6877 _applyBadgeNode(primary ?? btn, override);
6878 }
6879 doAction(HOOKS.DOCK_TILE_RENDERED, {
6880 ...base,
6881 item,
6882 isSystem: false,
6883 el: btn
6884 });
6885 }
6886 this.updateActiveStates();
6887 doAction(HOOKS.DOCK_AFTER_RENDER, {
6888 ...base,
6889 items,
6890 tileElements: this.itemElements
6891 });
6892 }
6893 /**
6894 * True when the rail currently has ANY renderable tile —
6895 * either a menu-derived item or a JS-registered system item.
6896 * Lets callers (the shell's live-refresh path) decide whether
6897 * to hide the whole rail without having to peek into two
6898 * internal maps. "System tiles keep the rail alive even when
6899 * menu items are empty" is the user-visible contract we enforce.
6900 */
6901 hasItems() {
6902 return this.itemElements.size > 0 || this.systemItemElements.size > 0;
6903 }
6904 /**
6905 * Remove a previously-registered system item. Used by the
6906 * server-driven native-window sync path — when a plugin is
6907 * deactivated, its native-window entry disappears from the
6908 * server's payload and the shell calls this to pull the tile
6909 * back off the rail without a reload.
6910 *
6911 * Idempotent: an unknown id is a silent no-op. The system
6912 * separator is kept in place as long as at least one system
6913 * item remains; removing the last system item also strips the
6914 * separator so the rail doesn't dangle a divider under nothing.
6915 */
6916 removeSystemItem(id) {
6917 const tile2 = this.systemItemElements.get(id);
6918 if (!tile2) {
6919 return;
6920 }
6921 tile2.remove();
6922 this.systemItemElements.delete(id);
6923 this.systemItems = this.systemItems.filter((s) => s.id !== id);
6924 this.badgeOverrides.delete(id);
6925 if (this.systemItemElements.size === 0 && this.systemSeparator) {
6926 this.systemSeparator.remove();
6927 this.systemSeparator = null;
6928 }
6929 doAction(HOOKS.DOCK_ITEM_REMOVED, { id, placement: this.rail });
6930 }
6931 /**
6932 * Set the badge count on a tile. Live-updates without a full
6933 * dock re-render — the existing tile's badge node is mutated in
6934 * place (or created if missing). Pass `0` to remove the badge.
6935 *
6936 * Resolves the tile in id order: menu items (`data-menu-slug`)
6937 * first, then system items (`data-system-id`), so callers can
6938 * use the same id surface regardless of which rail the tile
6939 * happens to live on.
6940 *
6941 * Idempotent: applying the same count is a no-op (no DOM mutation).
6942 *
6943 * @since 0.22.0
6944 *
6945 * @param itemId Tile id (menu slug for admin pages, system id
6946 * for `appendSystemItem` / `registerSystemTile`).
6947 * @param count Non-negative integer. `>99` renders as `99+`.
6948 */
6949 setBadge(itemId, count) {
6950 const tile2 = this._resolveTileElement(itemId);
6951 if (!tile2) {
6952 return;
6953 }
6954 const safe = Math.max(0, Math.floor(Number(count) || 0));
6955 if (safe === 0) {
6956 this.badgeOverrides.delete(itemId);
6957 } else {
6958 this.badgeOverrides.set(itemId, safe);
6959 }
6960 const primary = tile2.querySelector(
6961 ".desktop-mode-dock__item-primary"
6962 );
6963 _applyBadgeNode(primary ?? tile2, safe);
6964 activity.publish("desktop-mode/badge-changed", {
6965 itemId,
6966 count: safe,
6967 rail: this.rail
6968 });
6969 }
6970 /**
6971 * Clear the badge on a tile. Equivalent to `setBadge( id, 0 )`.
6972 *
6973 * @since 0.22.0
6974 */
6975 clearBadge(itemId) {
6976 this.setBadge(itemId, 0);
6977 }
6978 /**
6979 * Apply or clear an attention animation on a tile.
6980 *
6981 * - `'pulse'` — soft halo + scale, ~1.4 s loop. Default.
6982 * - `'shake'` — short horizontal jiggle.
6983 * - `'bounce'` — vertical bob, attention-grabbing.
6984 * - `null` — clear any active attention.
6985 *
6986 * Animations are gated on `prefers-reduced-motion: no-preference`;
6987 * the reduced-motion fallback shows a static accent ring for the
6988 * same duration so the affordance still works. `durationMs` of
6989 * `0` keeps the attention until the next call clears it.
6990 *
6991 * @since 0.22.0
6992 *
6993 * @param itemId Tile id.
6994 * @param mode Animation mode or `null` to clear.
6995 * @param opts Optional duration / intensity overrides.
6996 */
6997 setAttention(itemId, mode, opts = {}) {
6998 const tile2 = this._resolveTileElement(itemId);
6999 if (!tile2) {
7000 return;
7001 }
7002 const pending2 = this.attentionTimers.get(itemId);
7003 if (pending2 !== void 0) {
7004 window.clearTimeout(pending2);
7005 this.attentionTimers.delete(itemId);
7006 }
7007 tile2.classList.remove(
7008 "desktop-mode-dock__item--attention-pulse",
7009 "desktop-mode-dock__item--attention-shake",
7010 "desktop-mode-dock__item--attention-bounce",
7011 "desktop-mode-dock__item--intensity-subtle",
7012 "desktop-mode-dock__item--intensity-normal",
7013 "desktop-mode-dock__item--intensity-strong"
7014 );
7015 if (mode === null) {
7016 return;
7017 }
7018 tile2.classList.add(`desktop-mode-dock__item--attention-${mode}`);
7019 const intensity = opts.intensity ?? "normal";
7020 tile2.classList.add(`desktop-mode-dock__item--intensity-${intensity}`);
7021 const duration = opts.durationMs ?? 4e3;
7022 if (duration > 0) {
7023 const handle = window.setTimeout(() => {
7024 this.attentionTimers.delete(itemId);
7025 this.setAttention(itemId, null);
7026 }, duration);
7027 this.attentionTimers.set(itemId, handle);
7028 }
7029 }
7030 /**
7031 * Resolve a tile element by id — checks menu items first
7032 * (`data-menu-slug`), then system items (`data-system-id`). Used
7033 * by `setBadge` / `setAttention` so callers can reach either rail
7034 * with one id surface.
7035 */
7036 _resolveTileElement(itemId) {
7037 return this.itemElements.get(itemId) ?? this.systemItemElements.get(itemId) ?? null;
7038 }
7039 /**
7040 * Append a JS-registered system item to the dock.
7041 *
7042 * System items render after the menu-derived items, separated by a
7043 * hairline divider. Use for shell affordances that don't live in
7044 * the admin menu: OS Settings today, Jorvy and desktop widgets
7045 * later. Callers supply their own `onOpen` — the dock doesn't
7046 * assume the item opens a window at all.
7047 */
7048 appendSystemItem(item) {
7049 this.systemItems.push(item);
7050 if (!this.systemSeparator) {
7051 this.systemSeparator = document.createElement("div");
7052 this.systemSeparator.className = "desktop-mode-dock__separator";
7053 this.systemSeparator.setAttribute("aria-hidden", "true");
7054 this.systemHost.appendChild(this.systemSeparator);
7055 }
7056 const tile2 = this.createSystemItemButton(item);
7057 this.systemItemElements.set(item.id, tile2);
7058 this.systemHost.appendChild(tile2);
7059 this.updateActiveStates();
7060 doAction(HOOKS.DOCK_TILE_RENDERED, {
7061 ...this.buildHookContextBase(),
7062 item,
7063 isSystem: true,
7064 el: tile2
7065 });
7066 }
7067 /**
7068 * Render the dock contents.
7069 *
7070 * Items are ordered server-side with core WordPress menus first and
7071 * plugin-contributed menus after. We insert a `--group` separator
7072 * at the first core→plugin transition so the two clusters read as
7073 * distinct groups of tiles — "default apps" and "installed apps"
7074 * in macOS-dock parlance. The separator is skipped when the menu
7075 * contains only one kind (no plugin menus, or a theme's filter
7076 * reordered everything into one class).
7077 */
7078 render() {
7079 if (_Dock.activeDragReset) {
7080 const prev = _Dock.activeDragReset;
7081 _Dock.activeDragReset = null;
7082 prev();
7083 }
7084 for (const teardown of this.peekTeardowns.values()) {
7085 teardown();
7086 }
7087 this.peekTeardowns.clear();
7088 this.itemHost.innerHTML = "";
7089 const base = this.buildHookContextBase();
7090 doAction(HOOKS.DOCK_BEFORE_RENDER, {
7091 ...base,
7092 items: this.items,
7093 tileElements: this.itemElements
7094 });
7095 let insertedGroupSeparator = false;
7096 for (const item of this.items) {
7097 if (!insertedGroupSeparator && item.isCore === false) {
7098 if (this.itemHost.childElementCount > 0) {
7099 const sep = document.createElement("div");
7100 sep.className = "desktop-mode-dock__separator desktop-mode-dock__separator--group";
7101 sep.setAttribute("aria-hidden", "true");
7102 this.itemHost.appendChild(sep);
7103 }
7104 insertedGroupSeparator = true;
7105 }
7106 const btn = this.createItemButton(item);
7107 this.itemElements.set(item.id, btn);
7108 this.itemHost.appendChild(btn);
7109 doAction(HOOKS.DOCK_TILE_RENDERED, {
7110 ...base,
7111 item,
7112 isSystem: false,
7113 el: btn
7114 });
7115 }
7116 doAction(HOOKS.DOCK_AFTER_RENDER, {
7117 ...base,
7118 items: this.items,
7119 tileElements: this.itemElements
7120 });
7121 }
7122 /**
7123 * Create a tile for a JS-registered system item. Structurally simpler
7124 * than a menu tile — no submenu, no multi-instance rail, no badge —
7125 * but uses the same base classes so the hover / focus / active
7126 * styling is shared.
7127 */
7128 createSystemItemButton(item) {
7129 const ctx = {
7130 ...this.buildHookContextBase(),
7131 item,
7132 isSystem: true
7133 };
7134 const tile2 = document.createElement("div");
7135 const baseClasses = [
7136 "desktop-mode-dock__item",
7137 "desktop-mode-dock__item--system"
7138 ];
7139 const filteredClasses = applyFilters(
7140 HOOKS.DOCK_TILE_CLASS,
7141 baseClasses,
7142 ctx
7143 );
7144 tile2.className = filteredClasses.join(" ");
7145 tile2.dataset.systemId = item.id;
7146 const primary = document.createElement("button");
7147 primary.className = "desktop-mode-dock__item-primary";
7148 primary.setAttribute("type", "button");
7149 primary.setAttribute("aria-label", item.title);
7150 primary.appendChild(this.resolveIcon(item.icon, item.title));
7151 primary.addEventListener("click", () => item.onOpen());
7152 tile2.appendChild(primary);
7153 this.bindTooltipFiltered(tile2, item.title, ctx);
7154 const teardown = attachDockPeek({
7155 tile: tile2,
7156 item: {
7157 id: item.id,
7158 title: item.title,
7159 icon: item.icon,
7160 url: ""
7161 },
7162 // System tiles target a single native-window id; that id
7163 // is also the baseId the manager stores duplicates under
7164 // when the user opens additional instances via the Ghost
7165 // Card. `getAllByBaseId` returns `[]` / `[one]` for the
7166 // singleton cases and the full set when a multi-capable
7167 // system tile (`multi: true`) has been duplicated.
7168 getInstances: () => this.windowManager.getAllByBaseId(item.id),
7169 enableGhost: !!item.multi,
7170 windowManager: this.windowManager,
7171 getOrientation: () => this.orientation,
7172 openNew: () => {
7173 const fn = item.onOpenNew ?? item.onOpen;
7174 fn();
7175 },
7176 suppressTooltip: (on) => {
7177 if (on) {
7178 this.tooltip.classList.remove(
7179 "desktop-mode-dock__tooltip--visible"
7180 );
7181 }
7182 }
7183 });
7184 this.peekTeardowns.set(`system:${item.id}`, teardown);
7185 return applyFilters(
7186 HOOKS.DOCK_TILE_ELEMENT,
7187 tile2,
7188 ctx
7189 );
7190 }
7191 /**
7192 * Create a single dock icon tile.
7193 *
7194 * A tile is a vertical stack: the primary icon button, plus — for
7195 * multi-capable pages — an instance rail rendered below it showing one
7196 * dot per open window and a trailing "+" to open another. The rail is
7197 * hydrated by {@link updateActiveStates}; here we only place the empty
7198 * container so the DOM is stable.
7199 */
7200 createItemButton(item) {
7201 const ctx = {
7202 ...this.buildHookContextBase(),
7203 item,
7204 isSystem: false
7205 };
7206 const tile2 = document.createElement("div");
7207 const baseClasses = ["desktop-mode-dock__item"];
7208 if (item.multi) {
7209 baseClasses.push("desktop-mode-dock__item--multi");
7210 }
7211 const filteredClasses = applyFilters(
7212 HOOKS.DOCK_TILE_CLASS,
7213 baseClasses,
7214 ctx
7215 );
7216 tile2.className = filteredClasses.join(" ");
7217 tile2.dataset.menuSlug = item.id;
7218 const primary = document.createElement("button");
7219 primary.className = "desktop-mode-dock__item-primary";
7220 primary.setAttribute("type", "button");
7221 primary.setAttribute("aria-label", item.title);
7222 const iconEl = this.resolveIcon(item.icon, item.title, item.url);
7223 primary.appendChild(iconEl);
7224 if (item.badge > 0) {
7225 const displayCount = item.badge > 99 ? "99+" : String(item.badge);
7226 const badge = document.createElement("span");
7227 badge.className = "desktop-mode-dock__badge";
7228 badge.textContent = displayCount;
7229 badge.setAttribute(
7230 "aria-label",
7231 sprintf(
7232 // translators: %d is the number of pending updates / items.
7233 _n("%d update", "%d updates", item.badge),
7234 item.badge
7235 )
7236 );
7237 primary.appendChild(badge);
7238 }
7239 primary.addEventListener("click", () => {
7240 this.openPage(item);
7241 });
7242 tile2.addEventListener("contextmenu", (ev) => {
7243 ev.preventDefault();
7244 openItemVisibilityMenu({
7245 x: ev.clientX,
7246 y: ev.clientY,
7247 id: item.id,
7248 title: item.title,
7249 surface: "dock",
7250 pluginFile: item.pluginFile ?? null,
7251 pluginName: item.pluginName ?? null
7252 });
7253 });
7254 tile2.appendChild(primary);
7255 this.bindTooltipFiltered(tile2, item.title, ctx);
7256 const baseId = this.resolveItemBaseId(item);
7257 const teardown = attachDockPeek({
7258 tile: tile2,
7259 item: {
7260 id: item.id,
7261 title: item.title,
7262 icon: item.icon,
7263 url: item.url
7264 },
7265 // Source instances from `getAllByBaseId` regardless of
7266 // `item.multi`. The Ghost Card spawns duplicates on every
7267 // tile (the `enableGhost: true` below), so any tile —
7268 // including ones synthesized from a desktop icon, where
7269 // `multi` is never set — can end up with >1 open instance.
7270 // A `multi`-gated singleton lookup would only return the
7271 // first window and the peek would silently underreport.
7272 // For genuine singletons that never get duplicated, the
7273 // returned array is just `[one]` (or `[]`), same shape the
7274 // old branch produced.
7275 getInstances: () => this.windowManager.getAllByBaseId(baseId),
7276 // Ghost Card on EVERY tile, regardless of `multi`. The
7277 // affordance reads consistently across the dock — every
7278 // hover-peek surfaces a "+ open another <Page>" card. For
7279 // multi-capable items, clicking it spawns a fresh
7280 // instance. For singletons it falls through to the same
7281 // open-or-focus path the tile click takes — usually a
7282 // no-op (focuses the existing window) but cheap and
7283 // visually consistent.
7284 enableGhost: true,
7285 windowManager: this.windowManager,
7286 getOrientation: () => this.orientation,
7287 openNew: () => this.openNewInstance(item),
7288 suppressTooltip: (on) => {
7289 if (on) {
7290 this.tooltip.classList.remove(
7291 "desktop-mode-dock__tooltip--visible"
7292 );
7293 }
7294 }
7295 });
7296 this.peekTeardowns.set(item.id, teardown);
7297 this.attachDragReorder(tile2, item.id);
7298 return applyFilters(
7299 HOOKS.DOCK_TILE_ELEMENT,
7300 tile2,
7301 ctx
7302 );
7303 }
7304 /**
7305 * Drag-to-reorder for menu tiles. Fixed slots — no interpolated
7306 * positioning. While dragging:
7307 *
7308 * 1. Pointer down on the primary button starts a tentative drag.
7309 * Click handling is preserved by requiring movement past a
7310 * small threshold before we claim the gesture.
7311 * 2. Once claimed, the tile gets a `--dragging` modifier so CSS
7312 * can lift it visually. Every `pointermove` checks which other
7313 * menu tile the cursor is currently over; if it's a different
7314 * tile, we splice the dragged tile in front of it (so adjacent
7315 * tiles slide into the vacated slot).
7316 * 3. On `pointerup` we read the resulting DOM order, persist the
7317 * new id list to `dockOrder` via the public settings writer,
7318 * and the layout-dispatcher subscriber re-applies. Cancellation
7319 * (Escape, pointercancel) reverts to the original order.
7320 *
7321 * @since 0.25.0
7322 */
7323 attachDragReorder(tile2, itemId) {
7324 const THRESHOLD = 5;
7325 const FLIP_MS = 200;
7326 let active2 = false;
7327 let startX = 0;
7328 let startY = 0;
7329 let originalOrder = [];
7330 let originalNext = null;
7331 let pointerId = -1;
7332 let originRect = null;
7333 let justDragged = false;
7334 const hardReset = () => {
7335 active2 = false;
7336 tile2.classList.remove("desktop-mode-dock__item--dragging");
7337 tile2.style.transform = "";
7338 tile2.style.transition = "";
7339 document.removeEventListener("pointermove", onMove);
7340 document.removeEventListener("pointerup", onUp);
7341 document.removeEventListener("pointercancel", onCancel);
7342 document.removeEventListener("keydown", onKey, true);
7343 window.removeEventListener("blur", onBlur);
7344 document.removeEventListener("visibilitychange", onVisibility);
7345 pointerId = -1;
7346 originRect = null;
7347 };
7348 const isMenuTile = (el) => {
7349 return !!el && el instanceof HTMLElement && el.classList.contains("desktop-mode-dock__item") && !el.classList.contains("desktop-mode-dock__item--system") && !!el.dataset.menuSlug;
7350 };
7351 const eachSiblingTile = (fn) => {
7352 for (const child of Array.from(this.itemHost.children)) {
7353 if (child instanceof HTMLElement && child !== tile2 && isMenuTile(child)) {
7354 fn(child);
7355 }
7356 }
7357 };
7358 const snapshotMenuOrder = () => {
7359 const ids = [];
7360 for (const child of Array.from(this.itemHost.children)) {
7361 if (isMenuTile(child)) {
7362 ids.push(child.dataset.menuSlug);
7363 }
7364 }
7365 return ids;
7366 };
7367 const flipSiblings = (prevRects) => {
7368 eachSiblingTile((sib) => {
7369 const prev = prevRects.get(sib);
7370 if (!prev) {
7371 return;
7372 }
7373 const now = sib.getBoundingClientRect();
7374 const dx = prev.left - now.left;
7375 const dy = prev.top - now.top;
7376 if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) {
7377 return;
7378 }
7379 sib.style.transition = "none";
7380 sib.style.transform = `translate(${dx}px, ${dy}px)`;
7381 void sib.offsetHeight;
7382 sib.style.transition = `transform ${FLIP_MS}ms cubic-bezier(0.2, 0.7, 0.3, 1)`;
7383 sib.style.transform = "";
7384 const onEnd = () => {
7385 sib.style.transition = "";
7386 sib.style.transform = "";
7387 sib.removeEventListener("transitionend", onEnd);
7388 };
7389 sib.addEventListener("transitionend", onEnd);
7390 });
7391 };
7392 const onMove = (ev) => {
7393 if (pointerId !== -1 && ev.pointerId !== pointerId) {
7394 return;
7395 }
7396 if (!active2) {
7397 const dx2 = ev.clientX - startX;
7398 const dy2 = ev.clientY - startY;
7399 if (dx2 * dx2 + dy2 * dy2 < THRESHOLD * THRESHOLD) {
7400 return;
7401 }
7402 active2 = true;
7403 originalOrder = snapshotMenuOrder();
7404 originalNext = tile2.nextSibling;
7405 originRect = tile2.getBoundingClientRect();
7406 tile2.classList.add("desktop-mode-dock__item--dragging");
7407 this.tooltip.classList.remove(
7408 "desktop-mode-dock__tooltip--visible"
7409 );
7410 }
7411 if (!originRect) {
7412 return;
7413 }
7414 const dx = ev.clientX - startX;
7415 const dy = ev.clientY - startY;
7416 tile2.style.transform = `translate(${dx}px, ${dy}px)`;
7417 const under = document.elementFromPoint(ev.clientX, ev.clientY);
7418 const targetTile = under?.closest(
7419 ".desktop-mode-dock__item"
7420 );
7421 if (!targetTile || targetTile === tile2) {
7422 return;
7423 }
7424 if (!isMenuTile(targetTile)) {
7425 return;
7426 }
7427 const rect = targetTile.getBoundingClientRect();
7428 let insertBefore;
7429 if (this.orientation === "bottom") {
7430 insertBefore = ev.clientX < rect.left + rect.width / 2;
7431 } else {
7432 insertBefore = ev.clientY < rect.top + rect.height / 2;
7433 }
7434 const prevRects = /* @__PURE__ */ new Map();
7435 eachSiblingTile((sib) => {
7436 prevRects.set(sib, sib.getBoundingClientRect());
7437 });
7438 let reordered = false;
7439 if (insertBefore) {
7440 if (targetTile !== tile2.nextSibling) {
7441 this.itemHost.insertBefore(tile2, targetTile);
7442 reordered = true;
7443 }
7444 } else if (targetTile.nextSibling !== tile2) {
7445 this.itemHost.insertBefore(tile2, targetTile.nextSibling);
7446 reordered = true;
7447 }
7448 if (reordered) {
7449 tile2.style.transform = "";
7450 const fresh = tile2.getBoundingClientRect();
7451 startX = fresh.left + fresh.width / 2;
7452 startY = fresh.top + fresh.height / 2;
7453 tile2.style.transform = `translate(${ev.clientX - startX}px, ${ev.clientY - startY}px)`;
7454 flipSiblings(prevRects);
7455 }
7456 };
7457 const cleanup = () => {
7458 tile2.classList.remove("desktop-mode-dock__item--dragging");
7459 tile2.style.transform = "";
7460 tile2.style.transition = "";
7461 document.removeEventListener("pointermove", onMove);
7462 document.removeEventListener("pointerup", onUp);
7463 document.removeEventListener("pointercancel", onCancel);
7464 document.removeEventListener("keydown", onKey, true);
7465 window.removeEventListener("blur", onBlur);
7466 document.removeEventListener("visibilitychange", onVisibility);
7467 pointerId = -1;
7468 originRect = null;
7469 active2 = false;
7470 if (_Dock.activeDragReset === hardReset) {
7471 _Dock.activeDragReset = null;
7472 }
7473 };
7474 const animateHome = () => {
7475 tile2.style.transition = `transform ${FLIP_MS}ms cubic-bezier(0.2, 0.7, 0.3, 1)`;
7476 tile2.style.transform = "";
7477 const onEnd = () => {
7478 tile2.style.transition = "";
7479 tile2.removeEventListener("transitionend", onEnd);
7480 };
7481 tile2.addEventListener("transitionend", onEnd);
7482 };
7483 const persistDockOrder = (finalOrder) => {
7484 const api = window.wp?.desktop;
7485 if (!api?.getOsSettings || !api?.updateOsSettings) {
7486 return;
7487 }
7488 const existing = api.getOsSettings().dockOrder;
7489 const finalSet = new Set(finalOrder);
7490 const merged = [];
7491 let injected = false;
7492 for (const id of existing) {
7493 if (finalSet.has(id)) {
7494 if (!injected) {
7495 merged.push(...finalOrder);
7496 injected = true;
7497 }
7498 continue;
7499 }
7500 merged.push(id);
7501 }
7502 if (!injected) {
7503 merged.push(...finalOrder);
7504 }
7505 api.updateOsSettings({ dockOrder: merged });
7506 };
7507 const onUp = (ev) => {
7508 if (pointerId !== -1 && ev.pointerId !== pointerId) {
7509 return;
7510 }
7511 if (!active2) {
7512 cleanup();
7513 return;
7514 }
7515 justDragged = true;
7516 const finalOrder = snapshotMenuOrder();
7517 animateHome();
7518 cleanup();
7519 const same = finalOrder.length === originalOrder.length && finalOrder.every((id, i) => id === originalOrder[i]);
7520 if (!same) {
7521 persistDockOrder(finalOrder);
7522 }
7523 setTimeout(() => {
7524 justDragged = false;
7525 }, 200);
7526 };
7527 const onCancel = (ev) => {
7528 if (ev && pointerId !== -1 && ev.pointerId !== pointerId) {
7529 return;
7530 }
7531 if (active2 && originalNext !== void 0) {
7532 const prevRects = /* @__PURE__ */ new Map();
7533 eachSiblingTile((sib) => {
7534 prevRects.set(sib, sib.getBoundingClientRect());
7535 });
7536 this.itemHost.insertBefore(tile2, originalNext);
7537 flipSiblings(prevRects);
7538 }
7539 animateHome();
7540 cleanup();
7541 };
7542 const onKey = (ev) => {
7543 if (ev.key === "Escape") {
7544 onCancel();
7545 }
7546 };
7547 const onBlur = () => onCancel();
7548 const onVisibility = () => {
7549 if (document.visibilityState !== "visible") {
7550 onCancel();
7551 }
7552 };
7553 tile2.addEventListener("pointerdown", (ev) => {
7554 if (ev.button !== 0) {
7555 return;
7556 }
7557 if (_Dock.activeDragReset) {
7558 const prev = _Dock.activeDragReset;
7559 _Dock.activeDragReset = null;
7560 prev();
7561 }
7562 if (active2 || pointerId !== -1) {
7563 hardReset();
7564 }
7565 startX = ev.clientX;
7566 startY = ev.clientY;
7567 pointerId = ev.pointerId;
7568 active2 = false;
7569 _Dock.activeDragReset = hardReset;
7570 document.addEventListener("pointermove", onMove);
7571 document.addEventListener("pointerup", onUp);
7572 document.addEventListener("pointercancel", onCancel);
7573 document.addEventListener("keydown", onKey, true);
7574 window.addEventListener("blur", onBlur);
7575 document.addEventListener("visibilitychange", onVisibility);
7576 });
7577 tile2.addEventListener(
7578 "click",
7579 (ev) => {
7580 if (justDragged) {
7581 ev.preventDefault();
7582 ev.stopImmediatePropagation();
7583 }
7584 },
7585 true
7586 );
7587 }
7588 /**
7589 * Resolve a registered icon value into a DOM element.
7590 *
7591 * Priority: dashicons class → inline SVG data URI → image URL →
7592 * letter badge derived from the item's title. The letter fallback is
7593 * important for plugin tiles: plugin authors routinely register
7594 * top-level menus with `add_menu_page()` and omit the icon argument
7595 * (defaulting to `'div'` or empty), which would otherwise render as
7596 * an indistinguishable wall of generic wrenches. A colored letter
7597 * tile gives each plugin a stable, unique-ish visual identity with
7598 * zero plugin-side effort — the hue derives deterministically from
7599 * the title so the same plugin always gets the same color.
7600 *
7601 * @param icon The icon value from the menu entry.
7602 * @param title Human-readable title, used when falling back to a
7603 * letter badge.
7604 */
7605 resolveIcon(icon, title, url) {
7606 if (icon.startsWith("dashicons-") && icon !== "dashicons-admin-generic") {
7607 const el = document.createElement("span");
7608 el.className = `dashicons ${icon}`;
7609 el.setAttribute("aria-hidden", "true");
7610 return el;
7611 }
7612 if (icon.startsWith("data:image/svg+xml;base64,")) {
7613 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
7614 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
7615 return this._makeSvgIcon(icon);
7616 }
7617 }
7618 if (icon.startsWith("url(")) {
7619 return this._makeSvgIcon(icon);
7620 }
7621 if (icon.startsWith("http://") || icon.startsWith("https://")) {
7622 const img = document.createElement("img");
7623 img.className = "desktop-mode-dock__item-img";
7624 img.src = icon;
7625 img.alt = "";
7626 img.setAttribute("aria-hidden", "true");
7627 return img;
7628 }
7629 if (url) {
7630 const native = this._extractNativeMenuIcon(url);
7631 if (native) {
7632 return native;
7633 }
7634 }
7635 if (icon === "dashicons-admin-generic") {
7636 const el = document.createElement("span");
7637 el.className = "dashicons dashicons-admin-generic";
7638 el.setAttribute("aria-hidden", "true");
7639 return el;
7640 }
7641 return this.createLetterBadge(title);
7642 }
7643 /**
7644 * Build an SVG-background icon tile. Shared between the data-URI
7645 * branch of {@link resolveIcon} and the native-menu extractor.
7646 */
7647 _makeSvgIcon(bgValue) {
7648 const el = document.createElement("span");
7649 el.className = "desktop-mode-dock__item-svg";
7650 el.style.backgroundImage = bgValue.startsWith("url(") ? bgValue : `url("${bgValue}")`;
7651 el.style.backgroundSize = "contain";
7652 el.style.backgroundRepeat = "no-repeat";
7653 el.style.backgroundPosition = "center";
7654 el.setAttribute("aria-hidden", "true");
7655 return el;
7656 }
7657 /**
7658 * Extract a plugin's icon from the hidden `#adminmenu` that still
7659 * exists in the parent shell DOM (display:none'd by desktop.css).
7660 * Handles the three shapes plugins commonly use when the menu-page
7661 * icon_url is 'none' or 'div':
7662 *
7663 * (a) `<img src="...">` nested inside `.wp-menu-image`
7664 * (b) a dashicon class on `.wp-menu-image` itself
7665 * (c) a CSS background-image on `.wp-menu-image::before` (the
7666 * `menu-icon-XYZ` pattern Yoast, WooCommerce, Jetpack, etc. use)
7667 *
7668 * Returns null when the URL doesn't match any admin-menu entry or
7669 * none of the three shapes are detectable.
7670 */
7671 _extractNativeMenuIcon(url) {
7672 const adminMenu = document.getElementById("adminmenu");
7673 if (!adminMenu) {
7674 return null;
7675 }
7676 let target2;
7677 try {
7678 const u = new URL(url, window.location.href);
7679 const filename = u.pathname.split("/").pop() || "";
7680 target2 = filename + u.search;
7681 } catch {
7682 return null;
7683 }
7684 if (!target2) {
7685 return null;
7686 }
7687 const links = adminMenu.querySelectorAll("li.menu-top > a");
7688 let matchLi = null;
7689 for (const link of Array.from(links)) {
7690 if (link.href.endsWith(target2)) {
7691 matchLi = link.closest("li.menu-top");
7692 break;
7693 }
7694 }
7695 if (!matchLi) {
7696 return null;
7697 }
7698 const imgWrap = matchLi.querySelector(".wp-menu-image");
7699 if (!imgWrap) {
7700 return null;
7701 }
7702 const img = imgWrap.querySelector("img");
7703 if (img && img.src) {
7704 const el = document.createElement("img");
7705 el.className = "desktop-mode-dock__item-img";
7706 el.src = img.src;
7707 el.alt = "";
7708 el.setAttribute("aria-hidden", "true");
7709 return el;
7710 }
7711 const dashMatch = imgWrap.className.match(/\bdashicons-[\w-]+\b/);
7712 if (dashMatch && dashMatch[0] !== "dashicons-before") {
7713 const el = document.createElement("span");
7714 el.className = `dashicons ${dashMatch[0]}`;
7715 el.setAttribute("aria-hidden", "true");
7716 return el;
7717 }
7718 const before = window.getComputedStyle(imgWrap, "::before");
7719 const bg = before.backgroundImage;
7720 if (bg && bg !== "none" && !bg.includes('url("")')) {
7721 return this._makeSvgIcon(bg);
7722 }
7723 const bgWrap = window.getComputedStyle(imgWrap).backgroundImage;
7724 if (bgWrap && bgWrap !== "none" && !bgWrap.includes('url("")')) {
7725 return this._makeSvgIcon(bgWrap);
7726 }
7727 return null;
7728 }
7729 /**
7730 * Create a letter-badge icon — a rounded square tinted with a
7731 * deterministic hue derived from the title, displaying the first
7732 * letter of the title. Mirrors the "app icon placeholder" look
7733 * macOS uses when an app ships without artwork.
7734 *
7735 * The title always drives both the letter and the hue — same plugin,
7736 * same color across reloads. An empty title falls through to a `?`
7737 * on a neutral gray tile, but the menu builder upstream guards
7738 * against empty titles, so this is a defensive branch.
7739 */
7740 createLetterBadge(title) {
7741 const el = document.createElement("span");
7742 el.className = "desktop-mode-dock__item-letter";
7743 el.setAttribute("aria-hidden", "true");
7744 const trimmed = title.trim();
7745 const firstCodePoint = trimmed ? Array.from(trimmed)[0] : "?";
7746 el.textContent = firstCodePoint.toUpperCase();
7747 const hue = hashTitleToHue(trimmed);
7748 el.style.background = `linear-gradient(135deg, hsl(${hue} 62% 55%), hsl(${(hue + 24) % 360} 58% 42%))`;
7749 return el;
7750 }
7751 /**
7752 * Bind tooltip show/hide on hover. Tooltip anchor differs per
7753 * orientation: left dock → tile's right side, right dock → tile's
7754 * left side, bottom dock → above the tile. We set the relevant
7755 * coordinate inline each enter; the CSS takes care of the rest.
7756 */
7757 /**
7758 * Resolves the tooltip text through {@link HOOKS.DOCK_TILE_TOOLTIP}
7759 * once at bind time (so the dock doesn't re-filter on every
7760 * pointerenter) and stashes the resolved text on
7761 * `tile.dataset.dockTooltip` so the multi-instance chip can
7762 * restore it on its own pointerleave without going through the
7763 * filter again.
7764 *
7765 * Returning an empty string from the filter suppresses the
7766 * tooltip — the listener short-circuits and never adds the
7767 * `--visible` class.
7768 */
7769 bindTooltipFiltered(tile2, text, ctx) {
7770 const filtered = applyFilters(
7771 HOOKS.DOCK_TILE_TOOLTIP,
7772 text,
7773 ctx
7774 );
7775 tile2.dataset.dockTooltip = filtered;
7776 if (filtered === "") {
7777 return;
7778 }
7779 tile2.addEventListener("pointerenter", () => {
7780 this.positionTooltip(tile2, filtered);
7781 this.tooltip.classList.add("desktop-mode-dock__tooltip--visible");
7782 });
7783 tile2.addEventListener("pointerleave", () => {
7784 this.tooltip.classList.remove("desktop-mode-dock__tooltip--visible");
7785 });
7786 }
7787 /**
7788 * Write the tooltip text + anchor coordinate for `el`. Split out
7789 * because the multi-instance chip's pointerenter handler also
7790 * needs to anchor to a specific element (the chip, not the tile).
7791 */
7792 positionTooltip(el, text) {
7793 const rect = el.getBoundingClientRect();
7794 this.tooltip.textContent = text;
7795 if (this.orientation === "bottom") {
7796 this.tooltip.style.left = `${rect.left + rect.width / 2}px`;
7797 this.tooltip.style.top = `${rect.top - 14}px`;
7798 } else if (this.orientation === "right") {
7799 this.tooltip.style.top = `${rect.top + rect.height / 2 - 14}px`;
7800 this.tooltip.style.left = `${rect.left}px`;
7801 } else {
7802 this.tooltip.style.top = `${rect.top + rect.height / 2 - 14}px`;
7803 this.tooltip.style.left = `${rect.right + 8}px`;
7804 }
7805 }
7806 /**
7807 * Open an admin page in a window (or focus if already open).
7808 *
7809 * Consults the native URL-remap registry first — when an opt-in
7810 * native window has registered itself as the replacement for this
7811 * admin URL (e.g. the native Posts window for `edit.php` when the
7812 * user has flipped `nativePostsEnabled`), the click is rerouted
7813 * to that window and the iframe path is skipped. The dock item
7814 * itself is untouched: same icon, same tooltip, same position —
7815 * only the destination changes.
7816 */
7817 openPage(item) {
7818 if (item.id.startsWith("dock:")) {
7819 const iconId = item.id.slice(5);
7820 const cfg = window.desktopModeConfig;
7821 const icon = cfg?.desktopIcons?.find((i) => i.id === iconId);
7822 if (icon?.window) {
7823 const wp = window.wp?.desktop;
7824 wp?.openWindow?.(icon.window);
7825 return;
7826 }
7827 if (icon?.url) {
7828 if (tryOpenExternalUrl(icon.url)) {
7829 return;
7830 }
7831 const baseId2 = this.deriveWindowId(icon.url);
7832 this.windowManager.open({
7833 id: baseId2,
7834 baseId: baseId2,
7835 url: icon.url,
7836 parentUrl: icon.url,
7837 title: icon.title,
7838 icon: icon.icon.startsWith("dashicons-") ? icon.icon : "dashicons-admin-generic",
7839 submenu: [],
7840 multi: false
7841 });
7842 return;
7843 }
7844 return;
7845 }
7846 if (tryOpenExternalUrl(item.url)) {
7847 return;
7848 }
7849 if (tryNativeUrlRemap(item.url)) {
7850 return;
7851 }
7852 const baseId = this.deriveWindowId(item.url);
7853 this.windowManager.open({
7854 id: baseId,
7855 baseId,
7856 url: item.url,
7857 parentUrl: item.url,
7858 title: item.title,
7859 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
7860 submenu: item.submenu,
7861 multi: !!item.multi
7862 });
7863 }
7864 /**
7865 * Open a brand-new instance of a page, even if one is already
7866 * open. Invoked by the "+" ghost card in the dock peek.
7867 *
7868 * The user explicitly asked for "another window of this thing,"
7869 * so we honour the request even when {@link tryNativeUrlRemap}
7870 * would otherwise route the click into a native-window
7871 * singleton. Result: clicking + while a native Posts window is
7872 * open opens a fresh iframe of `edit.php` alongside it. Two
7873 * windows of Posts is the explicit ask — that's what + is for.
7874 */
7875 openNewInstance(item) {
7876 if (tryOpenExternalUrl(item.url)) {
7877 return;
7878 }
7879 const openNewWindow = window.wp?.desktop?.openNewWindow;
7880 if (item.windowId && !item.url) {
7881 if (openNewWindow?.(item.windowId, { source: "dock-peek" })) {
7882 return;
7883 }
7884 }
7885 const remappedId = resolveNativeUrlRemap(item.url);
7886 if (remappedId) {
7887 if (openNewWindow?.(remappedId, { source: "dock-peek" })) {
7888 return;
7889 }
7890 }
7891 const baseId = this.deriveWindowId(item.url);
7892 void this.windowManager.openNew({
7893 id: baseId,
7894 baseId,
7895 url: item.url,
7896 parentUrl: item.url,
7897 title: item.title,
7898 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
7899 submenu: item.submenu,
7900 multi: true
7901 });
7902 }
7903 /**
7904 * Derive a window ID from an admin page URL.
7905 */
7906 deriveWindowId(url) {
7907 return deriveWindowId(url, this.adminUrl);
7908 }
7909 /**
7910 * Resolve the window-manager key for a dock tile, in this order:
7911 *
7912 * 1. `item.windowId` — set by `applyDockPlacement` when the tile
7913 * is synthesized from a `desktop_mode_register_icon()` entry
7914 * whose target is a native window. Native-window ids never
7915 * pass through the URL → native-window remap layer, so we
7916 * short-circuit before touching it.
7917 * 2. {@link resolveNativeUrlRemap} on `item.url` — captures the
7918 * `nativePostsEnabled` / `nativePagesEnabled` opt-ins that
7919 * repoint a URL-based tile at a native window.
7920 * 3. {@link deriveWindowId} on `item.url` — the URL-based
7921 * fallback for ordinary admin-menu tiles.
7922 *
7923 * Shared by the hover-peek card and the active/focused-dot
7924 * indicator; the two stayed in lockstep before this method existed
7925 * by hand-rolling the same chain at each call site.
7926 */
7927 resolveItemBaseId(item) {
7928 if (item.windowId) {
7929 return item.windowId;
7930 }
7931 const remapped = resolveNativeUrlRemap(item.url);
7932 return remapped ?? this.deriveWindowId(item.url);
7933 }
7934 /**
7935 * Listen to window events to update active/focused/minimized
7936 * indicators on dock items, plus the global Show Desktop body class.
7937 *
7938 * The event detail isn't used — we just need to re-query the
7939 * window manager on every change — so the handlers take no
7940 * argument and the type cast is gone with it.
7941 *
7942 * `WINDOW_MINIMIZED` / `WINDOW_RESTORED` route through the hook bus
7943 * (no DOM CustomEvent equivalent today). Without these, minimizing
7944 * a window via Show Desktop / the title-bar minimize button left
7945 * the dock's active-dot rendering stuck on "visible window" — the
7946 * user had no cue that everything had collapsed to minimized.
7947 */
7948 bindWindowEvents() {
7949 const refresh = () => this.updateActiveStates();
7950 this.boundRefresh = refresh;
7951 document.addEventListener("desktop-mode-window-opened", refresh);
7952 document.addEventListener("desktop-mode-window-closed", refresh);
7953 document.addEventListener("desktop-mode-window-focused", refresh);
7954 window.wp?.hooks?.addAction?.(
7955 "desktop-mode.desktop.switched",
7956 this.hooksNamespace,
7957 refresh
7958 );
7959 window.wp?.hooks?.addAction?.(
7960 "desktop-mode.desktop.closed",
7961 this.hooksNamespace,
7962 refresh
7963 );
7964 window.wp?.hooks?.addAction?.(
7965 HOOKS.WINDOW_MINIMIZED,
7966 this.hooksNamespace,
7967 refresh
7968 );
7969 window.wp?.hooks?.addAction?.(
7970 HOOKS.WINDOW_RESTORED,
7971 this.hooksNamespace,
7972 refresh
7973 );
7974 }
7975 /**
7976 * Tear the dock down: detach window-lifecycle listeners, clear
7977 * pending attention timers, remove the floating tooltip from
7978 * `document.body`, and empty the container's children. Used by
7979 * the layout dispatcher when the user switches `desktopLayout`
7980 * in OS Settings — old dock(s) get destroyed and a fresh set is
7981 * constructed for the new layout.
7982 *
7983 * Idempotent: calling twice is safe.
7984 */
7985 destroy() {
7986 document.removeEventListener(
7987 "desktop-mode-window-opened",
7988 this.boundRefresh
7989 );
7990 document.removeEventListener(
7991 "desktop-mode-window-closed",
7992 this.boundRefresh
7993 );
7994 document.removeEventListener(
7995 "desktop-mode-window-focused",
7996 this.boundRefresh
7997 );
7998 window.wp?.hooks?.removeAction?.(
7999 "desktop-mode.desktop.switched",
8000 this.hooksNamespace
8001 );
8002 window.wp?.hooks?.removeAction?.(
8003 "desktop-mode.desktop.closed",
8004 this.hooksNamespace
8005 );
8006 window.wp?.hooks?.removeAction?.(
8007 HOOKS.WINDOW_MINIMIZED,
8008 this.hooksNamespace
8009 );
8010 window.wp?.hooks?.removeAction?.(
8011 HOOKS.WINDOW_RESTORED,
8012 this.hooksNamespace
8013 );
8014 for (const handle of this.attentionTimers.values()) {
8015 window.clearTimeout(handle);
8016 }
8017 this.attentionTimers.clear();
8018 for (const teardown of this.peekTeardowns.values()) {
8019 teardown();
8020 }
8021 this.peekTeardowns.clear();
8022 this.tooltip.remove();
8023 while (this.container.firstChild) {
8024 this.container.removeChild(this.container.firstChild);
8025 }
8026 this.itemElements.clear();
8027 this.systemItemElements.clear();
8028 this.systemItems = [];
8029 this.systemSeparator = null;
8030 this.container.removeAttribute("data-desktop-mode-dock-placement");
8031 }
8032 /**
8033 * Update the active/focused/minimized classes on every dock item in
8034 * response to a window lifecycle event, and toggle the global Show
8035 * Desktop body class.
8036 *
8037 * For singletons the rail is absent; "active" means "the one window
8038 * is open". For multi-capable items, active means "≥1 instance is
8039 * open" and focused means "the focused window belongs to this item".
8040 *
8041 * `--all-minimized` is layered on top of `--active` and fires only
8042 * when EVERY open instance of the tile is minimized — so a partial
8043 * minimize (one of two windows hidden) keeps the solid dot. CSS
8044 * swaps the dot for a hollow ring on minimized-only tiles so the
8045 * user can tell at a glance "I have something here, it's just
8046 * hidden right now."
8047 */
8048 updateActiveStates() {
8049 const focused = this.windowManager.getFocused();
8050 const focusedBaseId = focused ? focused.config.baseId || focused.id : null;
8051 const activeDesktopId = this.windowManager.getActiveDesktopId();
8052 const onActiveDesktop = (w) => (w.config.desktopId || activeDesktopId) === activeDesktopId;
8053 const isMinimized = (w) => w.state === "minimized";
8054 for (const item of this.items) {
8055 const tile2 = this.itemElements.get(item.id);
8056 if (!tile2) {
8057 continue;
8058 }
8059 const baseId = this.resolveItemBaseId(item);
8060 const instances = this.windowManager.getAllByBaseId(baseId).filter(onActiveDesktop);
8061 const isOpen = instances.length > 0;
8062 const allMinimized = isOpen && instances.every(isMinimized);
8063 const isFocused = focusedBaseId === baseId && !!focused && onActiveDesktop(focused) && !isMinimized(focused);
8064 tile2.classList.toggle("desktop-mode-dock__item--active", isOpen);
8065 tile2.classList.toggle("desktop-mode-dock__item--focused", isFocused);
8066 tile2.classList.toggle(
8067 "desktop-mode-dock__item--all-minimized",
8068 allMinimized
8069 );
8070 }
8071 for (const sys of this.systemItems) {
8072 const tile2 = this.systemItemElements.get(sys.id);
8073 if (!tile2) {
8074 continue;
8075 }
8076 const sysWin = this.windowManager.getById(sys.id);
8077 const isOpen = sys.isOpen ? sys.isOpen() : !!sysWin;
8078 const allMinimized = !!sysWin && isMinimized(sysWin);
8079 const isFocused = !!focused && focused.id === sys.id && !isMinimized(focused);
8080 tile2.classList.toggle("desktop-mode-dock__item--active", isOpen);
8081 tile2.classList.toggle("desktop-mode-dock__item--focused", isFocused);
8082 tile2.classList.toggle(
8083 "desktop-mode-dock__item--all-minimized",
8084 allMinimized
8085 );
8086 }
8087 this.updateShowDesktopBodyClass();
8088 }
8089 /**
8090 * Toggle `body.desktop-mode-show-desktop-active` based on whether
8091 * every live window on the active desktop is minimized. Mirrors
8092 * the heuristic inside {@link WindowManager.toggleShowDesktop} so
8093 * the visual cue tracks the actual state — set by Show Desktop
8094 * gestures, restored when any window is brought back, automatically
8095 * cleared when no windows exist.
8096 *
8097 * @internal
8098 */
8099 updateShowDesktopBodyClass() {
8100 const activeDesktopId = this.windowManager.getActiveDesktopId();
8101 const live = this.windowManager.getAll().filter(
8102 (w) => (w.config.desktopId || activeDesktopId) === activeDesktopId
8103 );
8104 const showDesktop = live.length > 0 && live.every((w) => w.state === "minimized");
8105 document.body.classList.toggle(
8106 "desktop-mode-show-desktop-active",
8107 showDesktop
8108 );
8109 }
8110 };
8111 _Dock.instanceCounter = 0;
8112 _Dock.activeDragReset = null;
8113 let Dock = _Dock;
8114 function _applyBadgeNode(host, count) {
8115 const existing = host.querySelector(
8116 ":scope > .desktop-mode-dock__badge"
8117 );
8118 if (count <= 0) {
8119 existing?.remove();
8120 return;
8121 }
8122 const display = count > 99 ? "99+" : String(count);
8123 if (existing) {
8124 if (existing.textContent !== display) {
8125 existing.textContent = display;
8126 }
8127 existing.setAttribute(
8128 "aria-label",
8129 sprintf(
8130 // translators: %d is the number of pending items in a dock badge.
8131 _n("%d notification", "%d notifications", count),
8132 count
8133 )
8134 );
8135 return;
8136 }
8137 const badge = document.createElement("span");
8138 badge.className = "desktop-mode-dock__badge";
8139 badge.textContent = display;
8140 badge.setAttribute(
8141 "aria-label",
8142 sprintf(
8143 // translators: %d is the number of pending items in a dock badge.
8144 _n("%d notification", "%d notifications", count),
8145 count
8146 )
8147 );
8148 host.appendChild(badge);
8149 }
8150 const DEFAULT_RENDERER_DOCK = Symbol.for(
8151 "desktop-mode/default-dock-rail-renderer/dock"
8152 );
8153 const defaultDockRailRenderer = {
8154 id: "default",
8155 label: "Icon strip",
8156 description: "The shipped baseline — icon tiles with badges, tooltips, multi-instance chips, and attention animations.",
8157 icon: "dashicons-menu-alt",
8158 apiVersion: 1,
8159 mount(deps2) {
8160 const dock = new Dock(
8161 deps2.container,
8162 deps2.windowManager,
8163 deps2.items,
8164 deps2.adminUrl,
8165 deps2.orientation
8166 );
8167 const controller = {
8168 [DEFAULT_RENDERER_DOCK]: dock,
8169 replaceItems: (items) => dock.replaceItems(items),
8170 appendSystemItem: (item) => dock.appendSystemItem(item),
8171 removeSystemItem: (id) => dock.removeSystemItem(id),
8172 setBadge: (itemId, count) => dock.setBadge(itemId, count),
8173 setAttention: (itemId, mode, opts) => dock.setAttention(itemId, mode, opts),
8174 setOrientation: (orientation) => dock.setOrientation(orientation),
8175 destroy: () => dock.destroy()
8176 };
8177 return controller;
8178 }
8179 };
8180 function unwrapDefaultDock(controller) {
8181 if (!controller) {
8182 return null;
8183 }
8184 const probe = controller;
8185 const dock = probe[DEFAULT_RENDERER_DOCK];
8186 return dock instanceof Dock ? dock : null;
8187 }
8188 function installDefaultDockRailRenderer() {
8189 register$1(defaultDockRailRenderer);
8190 }
8191 function customGradientCss(state2) {
8192 const { from, to, angle } = state2.customGradient;
8193 return `linear-gradient(${angle}deg, ${from}, ${to})`;
8194 }
8195 function registerCustomGradient(ctx) {
8196 register$2({
8197 id: CUSTOM_GRADIENT_ID,
8198 label: __("Custom gradient"),
8199 type: "css",
8200 preview: customGradientCss(ctx.state),
8201 resolveValue: () => customGradientCss(ctx.state)
8202 });
8203 }
8204 function registerCustomImageIfPresent(state2) {
8205 if (!state2.customImage) {
8206 unregister$2(CUSTOM_IMAGE_ID);
8207 return;
8208 }
8209 const safeUrl = encodeURI(state2.customImage.url);
8210 const value = `url("${safeUrl}") center/cover no-repeat, #1d2327`;
8211 register$2({
8212 id: CUSTOM_IMAGE_ID,
8213 label: __("Custom image"),
8214 type: "css",
8215 value,
8216 preview: value
8217 });
8218 }
8219 let _panelLoadPromise = null;
8220 function loadOsSettingsPanelBundle(scriptUrl) {
8221 if (window.desktopModeRenderOsSettingsPanel) {
8222 return Promise.resolve(window.desktopModeRenderOsSettingsPanel);
8223 }
8224 if (_panelLoadPromise) {
8225 return _panelLoadPromise;
8226 }
8227 _panelLoadPromise = new Promise((resolve2, reject) => {
8228 const existing = document.querySelector(
8229 'script[data-desktop-mode-os-settings-panel="1"]'
8230 );
8231 const finish = () => {
8232 const fn = window.desktopModeRenderOsSettingsPanel;
8233 if (!fn) {
8234 reject(
8235 new Error(
8236 "[desktop-mode] os-settings-panel bundle loaded but did not register desktopModeRenderOsSettingsPanel"
8237 )
8238 );
8239 return;
8240 }
8241 resolve2(fn);
8242 };
8243 if (existing) {
8244 if (window.desktopModeRenderOsSettingsPanel) {
8245 finish();
8246 } else {
8247 existing.addEventListener("load", finish);
8248 existing.addEventListener(
8249 "error",
8250 () => reject(new Error("failed to load os-settings-panel bundle"))
8251 );
8252 }
8253 return;
8254 }
8255 const s = document.createElement("script");
8256 s.src = scriptUrl;
8257 s.async = true;
8258 s.dataset.desktopModeOsSettingsPanel = "1";
8259 s.addEventListener("load", finish);
8260 s.addEventListener(
8261 "error",
8262 () => reject(new Error("failed to load os-settings-panel bundle"))
8263 );
8264 document.head.appendChild(s);
8265 });
8266 return _panelLoadPromise;
8267 }
8268 class OsSettings {
8269 constructor(config, layer) {
8270 this.activeEditorTeardown = null;
8271 this.tabRegistryUnsubscribe = null;
8272 this.activeTabId = null;
8273 this.osSettingsListeners = /* @__PURE__ */ new Set();
8274 this._lastRenderedBody = null;
8275 this.config = config;
8276 this.layer = layer;
8277 this.state = loadState();
8278 setLastConfirmedState(this.state);
8279 document.addEventListener(
8280 "desktop-mode-os-settings-save-lifecycle",
8281 (e) => {
8282 const detail = e.detail;
8283 if (!detail || detail.phase !== "failed" || !detail.rolledBackTo) {
8284 return;
8285 }
8286 this.state = detail.rolledBackTo;
8287 this.apply();
8288 if (this._lastRenderedBody?.isConnected) {
8289 this.renderPanel(this._lastRenderedBody);
8290 }
8291 }
8292 );
8293 registerCustomGradient(this);
8294 registerCustomImageIfPresent(this.state);
8295 }
8296 /** Project the private state into the public snapshot shape. */
8297 getOsSettingsSnapshot() {
8298 return {
8299 wallpaper: this.state.wallpaper,
8300 accent: this.state.accent,
8301 dockSize: this.state.dockSize,
8302 desktopLayout: this.state.desktopLayout,
8303 dockRailRenderer: this.state.dockRailRenderer,
8304 ai: { ...this.state.ai },
8305 nativePostsEnabled: this.state.nativePostsEnabled,
8306 nativePostsHiddenColumns: this.state.nativePostsHiddenColumns.slice(),
8307 nativePagesEnabled: this.state.nativePagesEnabled,
8308 nativeUsersEnabled: this.state.nativeUsersEnabled,
8309 nativePluginsEnabled: this.state.nativePluginsEnabled,
8310 nativeCommentsEnabled: this.state.nativeCommentsEnabled,
8311 foldersSharingEnabled: this.state.foldersSharingEnabled,
8312 itemVisibility: { ...this.state.itemVisibility },
8313 dockOrder: this.state.dockOrder.slice(),
8314 dockPromotedPositions: Object.fromEntries(
8315 Object.entries(this.state.dockPromotedPositions).map(
8316 ([k, v]) => [k, { ...v }]
8317 )
8318 )
8319 };
8320 }
8321 subscribeOsSettings(cb) {
8322 this.osSettingsListeners.add(cb);
8323 return () => {
8324 this.osSettingsListeners.delete(cb);
8325 };
8326 }
8327 /**
8328 * Apply the current state: wallpaper via the layer, accent + dock
8329 * size as CSS custom properties on the shell.
8330 *
8331 * Safe to call repeatedly — calls into `layer.apply` dedupe via
8332 * generation counter; CSS property writes are idempotent.
8333 */
8334 apply() {
8335 const shell = document.getElementById("desktop-mode-shell");
8336 if (!shell) {
8337 return;
8338 }
8339 const def = get$1(this.state.wallpaper) || get$1(getDefaultWallpaperId()) || get$1(DEFAULT_WALLPAPER_ID) || all$1()[0];
8340 if (def) {
8341 this.layer.apply(def);
8342 }
8343 const accents = getAccents();
8344 const accent = accents.find((a) => a.id === this.state.accent) ?? accents[0];
8345 const dockSize = DOCK_SIZES.find((d) => d.id === this.state.dockSize) ?? DOCK_SIZES[1];
8346 const root = document.documentElement;
8347 root.style.setProperty("--wp-admin-theme-color", accent.value);
8348 root.style.setProperty("--desktop-mode-dock-width", `${dockSize.width}px`);
8349 root.style.setProperty("--desktop-mode-dock-icon-size", `${dockSize.icon}px`);
8350 shell.setAttribute(
8351 "data-desktop-mode-layout",
8352 this.state.desktopLayout
8353 );
8354 setActiveRenderer(this.state.dockRailRenderer);
8355 }
8356 save(opts = {}) {
8357 saveState(this.state, opts);
8358 if (this.osSettingsListeners.size > 0) {
8359 const snapshot = this.getOsSettingsSnapshot();
8360 const listeners2 = Array.from(this.osSettingsListeners);
8361 for (const cb of listeners2) {
8362 try {
8363 cb(snapshot);
8364 } catch (err) {
8365 if (typeof console !== "undefined") {
8366 console.error(
8367 "[desktop-mode] os-settings listener threw:",
8368 err
8369 );
8370 }
8371 }
8372 }
8373 }
8374 }
8375 /**
8376 * Render the settings panel into the given native-window body.
8377 *
8378 * Builds three sections (wallpaper, accent, dock size) and wires
8379 * each to save/apply on change. The panel is a one-shot build per
8380 * window open — closing and re-opening renders a fresh tree.
8381 */
8382 /**
8383 * Render the settings panel into the given native-window body.
8384 *
8385 * Lazy since 0.8.4 — the actual rendering logic plus every
8386 * `<wpd-*>` component the panel uses lives in
8387 * `src/settings/panel.ts`, compiled into its own Vite target
8388 * `os-settings-panel[.min].js`. The script is injected on the
8389 * first call below and the matching
8390 * `window.desktopModeRenderOsSettingsPanel( ctx, body )` global
8391 * is then invoked. Subsequent calls (registry-driven re-render,
8392 * save-failure rollback) skip the load and forward immediately.
8393 *
8394 * Why this is a `<script>`-injected sibling bundle rather than
8395 * an in-bundle dynamic import: Vite IIFE lib mode inlines
8396 * `import()` calls, so an in-bundle lazy import would give zero
8397 * byte savings. A separate Vite target is the only mechanism
8398 * that actually shrinks `desktop.min.js`. See the Stage 8
8399 * section of `BUNDLE-SIZE-REPORT.md` for the full picture.
8400 */
8401 renderPanel(body) {
8402 this._lastRenderedBody = body;
8403 const fn = window.desktopModeRenderOsSettingsPanel;
8404 if (fn) {
8405 fn(this, body);
8406 return;
8407 }
8408 void loadOsSettingsPanelBundle(
8409 this.config.osSettingsPanelBundleUrl ?? ""
8410 ).then((render2) => {
8411 if (!body.isConnected) {
8412 return;
8413 }
8414 render2(this, body);
8415 }).catch((err) => {
8416 if (typeof console !== "undefined") {
8417 console.error(
8418 "[desktop-mode] OS Settings panel failed to load:",
8419 err
8420 );
8421 }
8422 });
8423 }
8424 }
8425 const EXIT_DESKTOP_MODE_TILE_ID = "desktop-mode-exit";
8426 function getExitDesktopModeTileDef() {
8427 return {
8428 id: EXIT_DESKTOP_MODE_TILE_ID,
8429 title: __("Exit Desktop Mode"),
8430 // `dashicons-exit` (door with arrow) is the clearest "leave"
8431 // glyph in the WordPress set, distinct from `dashicons-desktop`
8432 // used by OS Settings.
8433 icon: "dashicons-exit",
8434 onOpen: () => {
8435 void exitDesktopMode();
8436 }
8437 };
8438 }
8439 async function exitDesktopMode() {
8440 const cfg = window.desktopModeAdminBar;
8441 const fallback = cfg?.classicUrl || "/wp-admin/";
8442 if (!cfg?.ajaxUrl || !cfg?.nonce) {
8443 navigateTop(fallback);
8444 return;
8445 }
8446 const body = new URLSearchParams();
8447 body.set("action", "save-desktop-mode");
8448 body.set("nonce", cfg.nonce);
8449 body.set("enabled", "");
8450 let target2 = fallback;
8451 try {
8452 const res = await fetch(cfg.ajaxUrl, {
8453 method: "POST",
8454 headers: {
8455 "Content-Type": "application/x-www-form-urlencoded"
8456 },
8457 body: body.toString(),
8458 credentials: "same-origin"
8459 });
8460 if (res.ok) {
8461 const json = await res.json();
8462 if (json?.success && json.data?.redirect) {
8463 target2 = json.data.redirect;
8464 }
8465 }
8466 } catch {
8467 }
8468 navigateTop(target2);
8469 }
8470 function navigateTop(url) {
8471 try {
8472 window.top.location.href = url;
8473 } catch {
8474 window.location.href = url;
8475 }
8476 }
8477 const _initial$1 = {
8478 userId: null,
8479 requestedAt: 0,
8480 tabRequested: false
8481 };
8482 let _store$2 = null;
8483 function getStore$1() {
8484 if (_store$2) {
8485 return _store$2;
8486 }
8487 const w = window;
8488 const factory = w.wp?.desktop?.createSharedStore;
8489 if (typeof factory !== "function") {
8490 return null;
8491 }
8492 _store$2 = factory(
8493 "desktop-mode/user-edit/target",
8494 () => ({ ..._initial$1 })
8495 );
8496 return _store$2;
8497 }
8498 function setUserEditTarget(userId) {
8499 const store2 = getStore$1();
8500 if (store2) {
8501 store2.state.userId = userId;
8502 store2.state.requestedAt = Date.now();
8503 store2.state.tabRequested = true;
8504 store2.notify();
8505 return;
8506 }
8507 const w = window;
8508 w._wpdUserEditTarget = {
8509 userId,
8510 requestedAt: Date.now(),
8511 tabRequested: true
8512 };
8513 }
8514 const pending = /* @__PURE__ */ new Map();
8515 function loadVendorScript(url, extras) {
8516 const existing = pending.get(url);
8517 if (existing) {
8518 return existing;
8519 }
8520 const promise = new Promise((resolve2, reject) => {
8521 const selector = `script[data-desktop-mode-vendor="${cssEscape(url)}"]`;
8522 const preexisting = document.querySelector(selector);
8523 if (preexisting) {
8524 if (preexisting.dataset.loaded === "1") {
8525 resolve2();
8526 return;
8527 }
8528 preexisting.addEventListener("load", () => resolve2(), { once: true });
8529 preexisting.addEventListener(
8530 "error",
8531 () => reject(new Error(`Failed to load ${url}`)),
8532 { once: true }
8533 );
8534 return;
8535 }
8536 if (extras?.translations) {
8537 injectInline(extras.translations);
8538 }
8539 for (const code of extras?.l10n ?? []) {
8540 injectInline(code);
8541 }
8542 for (const code of extras?.before ?? []) {
8543 injectInline(code);
8544 }
8545 const script = document.createElement("script");
8546 script.src = url;
8547 script.async = true;
8548 script.dataset.desktopModeVendor = url;
8549 script.addEventListener(
8550 "load",
8551 () => {
8552 script.dataset.loaded = "1";
8553 for (const code of extras?.after ?? []) {
8554 injectInline(code);
8555 }
8556 resolve2();
8557 },
8558 { once: true }
8559 );
8560 script.addEventListener(
8561 "error",
8562 () => {
8563 pending.delete(url);
8564 script.remove();
8565 reject(new Error(`Failed to load ${url}`));
8566 },
8567 { once: true }
8568 );
8569 document.head.appendChild(script);
8570 });
8571 pending.set(url, promise);
8572 return promise;
8573 }
8574 function injectInline(code) {
8575 if (!code) {
8576 return;
8577 }
8578 const tag = document.createElement("script");
8579 tag.textContent = code;
8580 tag.dataset.desktopModeVendorInline = "1";
8581 document.head.appendChild(tag);
8582 }
8583 function cssEscape(value) {
8584 if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
8585 return CSS.escape(value);
8586 }
8587 return value.replace(/["\\]/g, "\\$&");
8588 }
8589 const registry$7 = /* @__PURE__ */ new Map();
8590 function registerModule(def) {
8591 if (!def || typeof def.id !== "string" || def.id === "") {
8592 if (typeof console !== "undefined") {
8593 console.warn("[desktop-mode] Ignored invalid module registration:", def);
8594 }
8595 return;
8596 }
8597 if (typeof def.url !== "string" || def.url === "") {
8598 if (typeof console !== "undefined") {
8599 console.warn(
8600 `[desktop-mode] Module "${def.id}" has no url; ignored.`
8601 );
8602 }
8603 return;
8604 }
8605 registry$7.set(def.id, def);
8606 }
8607 function moduleIds() {
8608 return Array.from(registry$7.keys());
8609 }
8610 async function loadModules(ids) {
8611 if (!ids || ids.length === 0) {
8612 return;
8613 }
8614 const unknown = ids.filter((id) => !registry$7.has(id));
8615 if (unknown.length > 0) {
8616 throw new Error(
8617 `[desktop-mode] Unknown module(s) in needs: ${unknown.map((id) => `"${id}"`).join(", ")}. Known modules: ${moduleIds().join(", ") || "(none)"}.`
8618 );
8619 }
8620 await Promise.all(
8621 ids.map((id) => {
8622 const def = registry$7.get(id);
8623 if (!def) {
8624 return Promise.resolve();
8625 }
8626 if (def.isReady && def.isReady()) {
8627 return Promise.resolve();
8628 }
8629 return loadVendorScript(def.url);
8630 })
8631 );
8632 }
8633 function createContext(id, pluginUrl) {
8634 return {
8635 id,
8636 pluginUrl,
8637 prefersReducedMotion: prefersReducedMotion(),
8638 visible: !document.hidden
8639 };
8640 }
8641 function prefersReducedMotion() {
8642 if (typeof window.matchMedia !== "function") {
8643 return false;
8644 }
8645 return window.matchMedia("( prefers-reduced-motion: reduce )").matches;
8646 }
8647 class WallpaperLayer {
8648 constructor(element, pluginUrl) {
8649 this.generation = 0;
8650 this.active = null;
8651 this.boundVisibilityChange = () => {
8652 if (!this.active) {
8653 return;
8654 }
8655 doAction(HOOKS.WALLPAPER_VISIBILITY, {
8656 id: this.active.id,
8657 state: document.hidden ? "hidden" : "visible"
8658 });
8659 };
8660 this.element = element;
8661 this.pluginUrl = pluginUrl;
8662 document.addEventListener("visibilitychange", this.boundVisibilityChange);
8663 }
8664 /**
8665 * Apply a wallpaper definition. Safe to call from any event
8666 * handler — handles type dispatch, teardown of the prior active
8667 * canvas, and race-safe async mounts.
8668 */
8669 apply(def) {
8670 const gen = ++this.generation;
8671 this.teardownActive();
8672 if (def.type === "css") {
8673 this.applyCss(def);
8674 return;
8675 }
8676 this.applyCanvas(def, gen);
8677 }
8678 /**
8679 * Imperative teardown entry point — called from desktop.ts on
8680 * `pagehide` so a canvas wallpaper's ticker doesn't compete with
8681 * the session-beacon flush at unload.
8682 */
8683 teardownActive() {
8684 if (!this.active) {
8685 return;
8686 }
8687 const { id, teardown } = this.active;
8688 this.active = null;
8689 doAction(HOOKS.WALLPAPER_UNMOUNTING, { id });
8690 try {
8691 teardown();
8692 } catch (err) {
8693 doAction(HOOKS.SHELL_ERROR, { scope: "wallpaper-teardown", id, error: err });
8694 if (typeof console !== "undefined") {
8695 console.error(
8696 `[desktop-mode] Wallpaper "${id}" teardown threw:`,
8697 err
8698 );
8699 }
8700 }
8701 this.element.innerHTML = "";
8702 }
8703 /** Remove listeners. Not called in normal flow — reserved for tests. */
8704 dispose() {
8705 this.teardownActive();
8706 document.removeEventListener("visibilitychange", this.boundVisibilityChange);
8707 }
8708 applyCss(def) {
8709 const value = def.resolveValue ? def.resolveValue(createContext(def.id, this.pluginUrl)) : def.value;
8710 if (typeof value === "string") {
8711 this.element.style.setProperty("--desktop-mode-bg", value);
8712 const shell = document.getElementById("desktop-mode-shell");
8713 shell?.style.setProperty("--desktop-mode-bg", value);
8714 }
8715 }
8716 applyCanvas(def, gen) {
8717 const ctx = createContext(def.id, this.pluginUrl);
8718 doAction(HOOKS.WALLPAPER_MOUNTING, { id: def.id, container: this.element, ctx });
8719 const depsReady = def.needs && def.needs.length > 0 ? loadModules(def.needs) : Promise.resolve();
8720 const onResolve = (teardown) => {
8721 if (gen !== this.generation) {
8722 try {
8723 teardown();
8724 } catch {
8725 }
8726 return;
8727 }
8728 this.active = { id: def.id, teardown };
8729 doAction(HOOKS.WALLPAPER_MOUNTED, { id: def.id, container: this.element, ctx });
8730 };
8731 depsReady.then(
8732 () => {
8733 if (gen !== this.generation) {
8734 return;
8735 }
8736 let result;
8737 try {
8738 result = def.mount(this.element, ctx);
8739 } catch (err) {
8740 this.handleMountFailure(def.id, err);
8741 return;
8742 }
8743 if (isThenable$1(result)) {
8744 result.then(onResolve, (err) => {
8745 if (gen !== this.generation) {
8746 return;
8747 }
8748 this.handleMountFailure(def.id, err);
8749 });
8750 return;
8751 }
8752 onResolve(result);
8753 },
8754 (err) => {
8755 if (gen !== this.generation) {
8756 return;
8757 }
8758 this.handleMountFailure(def.id, err);
8759 }
8760 );
8761 }
8762 handleMountFailure(id, err) {
8763 this.element.innerHTML = "";
8764 doAction(HOOKS.WALLPAPER_MOUNT_FAILED, { id, error: err });
8765 doAction(HOOKS.SHELL_ERROR, { scope: "wallpaper-mount", id, error: err });
8766 if (typeof console !== "undefined") {
8767 console.error(
8768 `[desktop-mode] Wallpaper "${id}" failed to mount:`,
8769 err
8770 );
8771 }
8772 }
8773 }
8774 function isThenable$1(value) {
8775 return !!value && typeof value === "object" && typeof value.then === "function";
8776 }
8777 function createWallpaperRegistrySync(deps2) {
8778 const { osSettings } = deps2;
8779 const registered = /* @__PURE__ */ new Set();
8780 const loadedScripts = /* @__PURE__ */ new Set();
8781 const ensureScript = async (entry) => {
8782 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
8783 return;
8784 }
8785 try {
8786 await loadVendorScript(entry.scriptUrl, {
8787 translations: entry.scriptTranslations,
8788 l10n: entry.scriptL10n,
8789 before: entry.scriptBefore,
8790 after: entry.scriptAfter
8791 });
8792 } catch (err) {
8793 doAction(HOOKS.SHELL_ERROR, {
8794 scope: "wallpaper-script-load",
8795 id: entry.id,
8796 error: err
8797 });
8798 }
8799 loadedScripts.add(entry.scriptUrl);
8800 };
8801 const readDef = (id) => {
8802 const globals = window.desktopModeWallpapers || {};
8803 return globals[id] ?? null;
8804 };
8805 const defFromCssEntry = (entry) => {
8806 if (entry.type !== "css" || entry.value === "") {
8807 return null;
8808 }
8809 return {
8810 id: entry.id,
8811 label: entry.label,
8812 type: "css",
8813 value: entry.value,
8814 preview: entry.preview !== "" ? entry.preview : entry.value
8815 };
8816 };
8817 const registerEntry = async (entry) => {
8818 if (registered.has(entry.id)) {
8819 return;
8820 }
8821 const cssDef = defFromCssEntry(entry);
8822 if (cssDef) {
8823 register$2(cssDef);
8824 registered.add(entry.id);
8825 osSettings.apply();
8826 return;
8827 }
8828 await ensureScript(entry);
8829 const def = readDef(entry.id);
8830 if (!def) {
8831 doAction(HOOKS.SHELL_ERROR, {
8832 scope: "wallpaper-missing-def",
8833 id: entry.id,
8834 error: new Error(
8835 `[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.`
8836 )
8837 });
8838 return;
8839 }
8840 try {
8841 register$2(def);
8842 } catch (err) {
8843 doAction(HOOKS.SHELL_ERROR, {
8844 scope: "wallpaper-register",
8845 id: entry.id,
8846 error: err
8847 });
8848 return;
8849 }
8850 registered.add(entry.id);
8851 osSettings.apply();
8852 };
8853 const unregisterEntry = (id) => {
8854 if (!registered.has(id)) {
8855 return;
8856 }
8857 unregister$2(id);
8858 registered.delete(id);
8859 osSettings.apply();
8860 };
8861 return async (list2) => {
8862 const incoming = /* @__PURE__ */ new Set();
8863 for (const entry of list2) {
8864 incoming.add(entry.id);
8865 }
8866 for (const id of Array.from(registered)) {
8867 if (!incoming.has(id)) {
8868 unregisterEntry(id);
8869 }
8870 }
8871 for (const entry of list2) {
8872 if (!registered.has(entry.id)) {
8873 await registerEntry(entry);
8874 }
8875 }
8876 };
8877 }
8878 const COMMAND_SLUG = /^[a-z0-9_/-]+$/;
8879 const commandRegistryStore = createSharedStore(
8880 "desktop-mode/commands-registry",
8881 () => ({
8882 registry: /* @__PURE__ */ new Map(),
8883 listeners: /* @__PURE__ */ new Set()
8884 })
8885 );
8886 const registry$6 = commandRegistryStore.state.registry;
8887 const listeners$9 = commandRegistryStore.state.listeners;
8888 function registerCommand(cmd) {
8889 const errors = [];
8890 const slug = typeof cmd?.slug === "string" ? cmd.slug.trim().toLowerCase() : "";
8891 if (!cmd || typeof cmd !== "object") {
8892 errors.push("def (not an object)");
8893 } else {
8894 if (typeof cmd.slug !== "string" || cmd.slug.trim() === "") {
8895 errors.push("slug (missing)");
8896 } else if (!COMMAND_SLUG.test(slug)) {
8897 errors.push(
8898 `slug (must match ${COMMAND_SLUG} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
8899 );
8900 }
8901 if (typeof cmd.label !== "string" || cmd.label.trim() === "") {
8902 errors.push("label (missing)");
8903 }
8904 if (typeof cmd.run !== "function") {
8905 errors.push("run (must be a function)");
8906 }
8907 }
8908 throwOnRegistrationErrors("Command", errors, cmd);
8909 registry$6.set(slug, { ...cmd, slug });
8910 notify$b();
8911 }
8912 function unregisterCommand(slug) {
8913 if (registry$6.delete(slug.toLowerCase())) {
8914 notify$b();
8915 }
8916 }
8917 function unregisterByOwner(owner) {
8918 if (!owner) {
8919 return 0;
8920 }
8921 let removed = 0;
8922 for (const [slug, cmd] of Array.from(registry$6.entries())) {
8923 if (cmd.owner === owner) {
8924 registry$6.delete(slug);
8925 removed++;
8926 }
8927 }
8928 if (removed > 0) {
8929 notify$b();
8930 }
8931 return removed;
8932 }
8933 function listCommands() {
8934 return Array.from(registry$6.values());
8935 }
8936 function listAiCallableCommands() {
8937 const out = [];
8938 for (const cmd of registry$6.values()) {
8939 if (cmd.aiCallable !== true) {
8940 continue;
8941 }
8942 out.push({
8943 slug: cmd.slug,
8944 label: cmd.label,
8945 description: cmd.description ?? "",
8946 hint: cmd.hint ?? ""
8947 });
8948 }
8949 return out;
8950 }
8951 function findCommand(slug) {
8952 return registry$6.get(slug.toLowerCase()) ?? null;
8953 }
8954 function notify$b() {
8955 const snapshot = Array.from(listeners$9);
8956 for (const cb of snapshot) {
8957 try {
8958 cb();
8959 } catch (err) {
8960 if (typeof console !== "undefined") {
8961 console.error("[desktop-mode] command-registry listener threw:", err);
8962 }
8963 }
8964 }
8965 }
8966 function createCommandRegistrySync() {
8967 const loadedHandles = /* @__PURE__ */ new Set();
8968 const loadedUrls = /* @__PURE__ */ new Set();
8969 let prevSlugsByHandle = /* @__PURE__ */ new Map();
8970 const ensureScript = async (entry) => {
8971 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
8972 loadedHandles.add(entry.handle);
8973 return;
8974 }
8975 try {
8976 await loadVendorScript(entry.scriptUrl, {
8977 translations: entry.scriptTranslations,
8978 l10n: entry.scriptL10n,
8979 before: entry.scriptBefore,
8980 after: entry.scriptAfter
8981 });
8982 } catch (err) {
8983 doAction(HOOKS.SHELL_ERROR, {
8984 scope: "command-script-load",
8985 handle: entry.handle,
8986 url: entry.scriptUrl,
8987 error: err
8988 });
8989 return;
8990 }
8991 loadedUrls.add(entry.scriptUrl);
8992 loadedHandles.add(entry.handle);
8993 };
8994 const slugsByHandleFrom = (commands) => {
8995 const map = /* @__PURE__ */ new Map();
8996 if (!commands) {
8997 return map;
8998 }
8999 for (const entry of commands) {
9000 if (!entry.scriptHandle || !entry.slug) {
9001 continue;
9002 }
9003 let set = map.get(entry.scriptHandle);
9004 if (!set) {
9005 set = /* @__PURE__ */ new Set();
9006 map.set(entry.scriptHandle, set);
9007 }
9008 set.add(entry.slug);
9009 }
9010 return map;
9011 };
9012 const collectSlugsToRemove = (handle) => {
9013 const slugs = /* @__PURE__ */ new Set();
9014 for (const cmd of listCommands()) {
9015 if (cmd.owner === handle) {
9016 slugs.add(cmd.slug);
9017 }
9018 }
9019 const declared = prevSlugsByHandle.get(handle);
9020 if (declared) {
9021 for (const slug of declared) {
9022 slugs.add(slug);
9023 }
9024 }
9025 return slugs;
9026 };
9027 return async (scripts, commands) => {
9028 const incomingHandles = /* @__PURE__ */ new Set();
9029 for (const entry of scripts) {
9030 if (entry.handle) {
9031 incomingHandles.add(entry.handle);
9032 }
9033 }
9034 for (const handle of Array.from(loadedHandles)) {
9035 if (incomingHandles.has(handle)) {
9036 continue;
9037 }
9038 for (const slug of collectSlugsToRemove(handle)) {
9039 unregisterCommand(slug);
9040 }
9041 loadedHandles.delete(handle);
9042 }
9043 for (const entry of scripts) {
9044 if (!entry.handle || loadedHandles.has(entry.handle)) {
9045 continue;
9046 }
9047 await ensureScript(entry);
9048 }
9049 prevSlugsByHandle = slugsByHandleFrom(commands);
9050 };
9051 }
9052 const store$a = createSharedStore(
9053 "desktop-mode/settings-tab-registry",
9054 () => ({
9055 registry: /* @__PURE__ */ new Map(),
9056 listeners: /* @__PURE__ */ new Set()
9057 })
9058 );
9059 const registry$5 = store$a.state.registry;
9060 const listeners$8 = store$a.state.listeners;
9061 function registerSettingsTab(tab) {
9062 if (!tab || typeof tab.id !== "string" || tab.id.trim() === "") {
9063 return;
9064 }
9065 if (typeof tab.label !== "string" || tab.label.trim() === "") {
9066 return;
9067 }
9068 if (typeof tab.render !== "function") {
9069 return;
9070 }
9071 const id = tab.id.trim().toLowerCase();
9072 if (!/^[a-z0-9_\-]+$/.test(id)) {
9073 if (typeof console !== "undefined") {
9074 console.warn(
9075 "[desktop-mode] registerSettingsTab: id must be [a-z0-9_-]+, got",
9076 tab.id
9077 );
9078 }
9079 return;
9080 }
9081 registry$5.set(id, { ...tab, id });
9082 notify$a();
9083 }
9084 function unregisterSettingsTab(id) {
9085 if (registry$5.delete(id.toLowerCase())) {
9086 notify$a();
9087 }
9088 }
9089 function unregisterSettingsTabsByOwner(owner) {
9090 if (!owner) {
9091 return 0;
9092 }
9093 let removed = 0;
9094 for (const [id, tab] of Array.from(registry$5.entries())) {
9095 if (tab.owner === owner) {
9096 registry$5.delete(id);
9097 removed++;
9098 }
9099 }
9100 if (removed > 0) {
9101 notify$a();
9102 }
9103 return removed;
9104 }
9105 function listSettingsTabs() {
9106 return Array.from(registry$5.values()).sort(
9107 (a, b) => (a.order ?? 100) - (b.order ?? 100)
9108 );
9109 }
9110 function notify$a() {
9111 const snapshot = Array.from(listeners$8);
9112 for (const cb of snapshot) {
9113 try {
9114 cb();
9115 } catch (err) {
9116 if (typeof console !== "undefined") {
9117 console.error(
9118 "[desktop-mode] settings-tab-registry listener threw:",
9119 err
9120 );
9121 }
9122 }
9123 }
9124 }
9125 function createSettingsTabRegistrySync() {
9126 const loadedHandles = /* @__PURE__ */ new Set();
9127 const loadedUrls = /* @__PURE__ */ new Set();
9128 let prevIdsByHandle = /* @__PURE__ */ new Map();
9129 const ensureScript = async (entry) => {
9130 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9131 loadedHandles.add(entry.handle);
9132 return;
9133 }
9134 try {
9135 await loadVendorScript(entry.scriptUrl, {
9136 translations: entry.scriptTranslations,
9137 l10n: entry.scriptL10n,
9138 before: entry.scriptBefore,
9139 after: entry.scriptAfter
9140 });
9141 } catch (err) {
9142 doAction(HOOKS.SHELL_ERROR, {
9143 scope: "settings-tab-script-load",
9144 handle: entry.handle,
9145 url: entry.scriptUrl,
9146 error: err
9147 });
9148 return;
9149 }
9150 loadedUrls.add(entry.scriptUrl);
9151 loadedHandles.add(entry.handle);
9152 };
9153 const idsByHandleFrom = (tabs) => {
9154 const map = /* @__PURE__ */ new Map();
9155 if (!tabs) {
9156 return map;
9157 }
9158 for (const entry of tabs) {
9159 if (!entry.scriptHandle || !entry.id) {
9160 continue;
9161 }
9162 let set = map.get(entry.scriptHandle);
9163 if (!set) {
9164 set = /* @__PURE__ */ new Set();
9165 map.set(entry.scriptHandle, set);
9166 }
9167 set.add(entry.id);
9168 }
9169 return map;
9170 };
9171 const removeByHandle = (handle) => {
9172 unregisterSettingsTabsByOwner(handle);
9173 const declared = prevIdsByHandle.get(handle);
9174 if (declared) {
9175 const present = new Set(
9176 listSettingsTabs().map((t) => t.id)
9177 );
9178 for (const id of declared) {
9179 if (present.has(id)) {
9180 unregisterSettingsTab(id);
9181 }
9182 }
9183 }
9184 };
9185 return async (scripts, tabs) => {
9186 const incomingHandles = /* @__PURE__ */ new Set();
9187 for (const entry of scripts) {
9188 if (entry.handle) {
9189 incomingHandles.add(entry.handle);
9190 }
9191 }
9192 for (const handle of Array.from(loadedHandles)) {
9193 if (incomingHandles.has(handle)) {
9194 continue;
9195 }
9196 removeByHandle(handle);
9197 loadedHandles.delete(handle);
9198 }
9199 for (const entry of scripts) {
9200 if (!entry.handle || loadedHandles.has(entry.handle)) {
9201 continue;
9202 }
9203 await ensureScript(entry);
9204 }
9205 prevIdsByHandle = idsByHandleFrom(tabs);
9206 };
9207 }
9208 const store$9 = createSharedStore(
9209 "desktop-mode/title-bar-buttons-registry",
9210 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9211 );
9212 const registry$4 = store$9.state.registry;
9213 const listeners$7 = store$9.state.listeners;
9214 const TITLE_BAR_BUTTON_ID = /^[a-z0-9_/-]+$/;
9215 function registerTitleBarButton(def) {
9216 const errors = [];
9217 if (!def || typeof def !== "object") {
9218 errors.push("def (not an object)");
9219 } else {
9220 if (typeof def.id !== "string" || def.id.trim() === "") {
9221 errors.push("id (missing)");
9222 } else if (!TITLE_BAR_BUTTON_ID.test(def.id.trim().toLowerCase())) {
9223 errors.push(
9224 `id (must match ${TITLE_BAR_BUTTON_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9225 );
9226 }
9227 if (typeof def.label !== "string" || def.label.trim() === "") {
9228 errors.push("label (missing)");
9229 }
9230 if (typeof def.icon !== "string" || def.icon.trim() === "") {
9231 errors.push("icon (missing)");
9232 }
9233 if (typeof def.match !== "function") {
9234 errors.push("match (must be a function)");
9235 }
9236 if (typeof def.onClick !== "function" && typeof def.render !== "function") {
9237 errors.push("onClick|render (at least one must be a function)");
9238 }
9239 }
9240 throwOnRegistrationErrors("TitleBarButton", errors, def);
9241 const id = def.id.trim().toLowerCase();
9242 registry$4.set(id, { ...def, id });
9243 notify$9();
9244 }
9245 function unregisterTitleBarButton(id) {
9246 if (registry$4.delete(id.toLowerCase())) {
9247 notify$9();
9248 }
9249 }
9250 function unregisterTitleBarButtonsByOwner(owner) {
9251 if (!owner) {
9252 return 0;
9253 }
9254 let removed = 0;
9255 for (const [id, def] of Array.from(registry$4.entries())) {
9256 if (def.owner === owner) {
9257 registry$4.delete(id);
9258 removed++;
9259 }
9260 }
9261 if (removed > 0) {
9262 notify$9();
9263 }
9264 return removed;
9265 }
9266 function listTitleBarButtons() {
9267 return Array.from(registry$4.values()).sort(
9268 (a, b) => (a.order ?? 100) - (b.order ?? 100)
9269 );
9270 }
9271 function notify$9() {
9272 const snapshot = Array.from(listeners$7);
9273 for (const cb of snapshot) {
9274 try {
9275 cb();
9276 } catch (err) {
9277 if (typeof console !== "undefined") {
9278 console.error(
9279 "[desktop-mode] title-bar-button registry listener threw:",
9280 err
9281 );
9282 }
9283 }
9284 }
9285 }
9286 function createTitleBarButtonRegistrySync() {
9287 const loadedHandles = /* @__PURE__ */ new Set();
9288 const loadedUrls = /* @__PURE__ */ new Set();
9289 const ensureScript = async (entry) => {
9290 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9291 loadedHandles.add(entry.handle);
9292 return;
9293 }
9294 try {
9295 await loadVendorScript(entry.scriptUrl, {
9296 translations: entry.scriptTranslations,
9297 l10n: entry.scriptL10n,
9298 before: entry.scriptBefore,
9299 after: entry.scriptAfter
9300 });
9301 } catch (err) {
9302 doAction(HOOKS.SHELL_ERROR, {
9303 scope: "titlebar-button-script-load",
9304 handle: entry.handle,
9305 url: entry.scriptUrl,
9306 error: err
9307 });
9308 return;
9309 }
9310 loadedUrls.add(entry.scriptUrl);
9311 loadedHandles.add(entry.handle);
9312 };
9313 return async (scripts) => {
9314 const incomingHandles = /* @__PURE__ */ new Set();
9315 for (const entry of scripts) {
9316 if (entry.handle) {
9317 incomingHandles.add(entry.handle);
9318 }
9319 }
9320 for (const handle of Array.from(loadedHandles)) {
9321 if (incomingHandles.has(handle)) {
9322 continue;
9323 }
9324 unregisterTitleBarButtonsByOwner(handle);
9325 loadedHandles.delete(handle);
9326 }
9327 for (const entry of scripts) {
9328 if (!entry.handle || loadedHandles.has(entry.handle)) {
9329 continue;
9330 }
9331 await ensureScript(entry);
9332 }
9333 };
9334 }
9335 function createDockRailRendererSync() {
9336 const loadedHandles = /* @__PURE__ */ new Set();
9337 const loadedUrls = /* @__PURE__ */ new Set();
9338 const ensureScript = async (entry) => {
9339 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9340 loadedHandles.add(entry.handle);
9341 return;
9342 }
9343 try {
9344 await loadVendorScript(entry.scriptUrl, {
9345 translations: entry.scriptTranslations,
9346 l10n: entry.scriptL10n,
9347 before: entry.scriptBefore,
9348 after: entry.scriptAfter
9349 });
9350 } catch (err) {
9351 doAction(HOOKS.SHELL_ERROR, {
9352 scope: "dock-rail-renderer-script-load",
9353 handle: entry.handle,
9354 url: entry.scriptUrl,
9355 error: err
9356 });
9357 return;
9358 }
9359 loadedUrls.add(entry.scriptUrl);
9360 loadedHandles.add(entry.handle);
9361 };
9362 return async (scripts) => {
9363 const incomingHandles = /* @__PURE__ */ new Set();
9364 for (const entry of scripts) {
9365 if (entry.handle) {
9366 incomingHandles.add(entry.handle);
9367 }
9368 }
9369 for (const handle of Array.from(loadedHandles)) {
9370 if (incomingHandles.has(handle)) {
9371 continue;
9372 }
9373 unregisterByOwner$1(handle);
9374 loadedHandles.delete(handle);
9375 }
9376 for (const entry of scripts) {
9377 if (!entry.handle || loadedHandles.has(entry.handle)) {
9378 continue;
9379 }
9380 await ensureScript(entry);
9381 }
9382 };
9383 }
9384 const store$8 = createSharedStore(
9385 "desktop-mode/window-themes-registry",
9386 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9387 );
9388 const registry$3 = store$8.state.registry;
9389 const listeners$6 = store$8.state.listeners;
9390 const WINDOW_THEME_ID = /^[a-z0-9_/-]+$/;
9391 function registerWindowTheme(def) {
9392 const errors = [];
9393 if (!def || typeof def !== "object") {
9394 errors.push("def (not an object)");
9395 } else {
9396 if (typeof def.id !== "string" || def.id.trim() === "") {
9397 errors.push("id (missing)");
9398 } else if (!WINDOW_THEME_ID.test(def.id.trim().toLowerCase())) {
9399 errors.push(
9400 `id (must match ${WINDOW_THEME_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9401 );
9402 }
9403 if (!def.tokens || typeof def.tokens !== "object") {
9404 errors.push("tokens (must be an object of CSS custom-property → value)");
9405 } else {
9406 for (const key of Object.keys(def.tokens)) {
9407 if (!key.startsWith("--")) {
9408 errors.push(
9409 `tokens.${key} (CSS custom-property keys must start with "--")`
9410 );
9411 break;
9412 }
9413 }
9414 }
9415 if (typeof def.match !== "function") {
9416 errors.push("match (must be a function)");
9417 }
9418 }
9419 throwOnRegistrationErrors("WindowTheme", errors, def);
9420 const id = def.id.trim().toLowerCase();
9421 registry$3.set(id, { ...def, id });
9422 notify$8();
9423 }
9424 function unregisterWindowTheme(id) {
9425 if (registry$3.delete(id.toLowerCase())) {
9426 notify$8();
9427 }
9428 }
9429 function unregisterWindowThemesByOwner(owner) {
9430 if (!owner) {
9431 return 0;
9432 }
9433 let removed = 0;
9434 for (const [id, def] of Array.from(registry$3.entries())) {
9435 if (def.owner === owner) {
9436 registry$3.delete(id);
9437 removed++;
9438 }
9439 }
9440 if (removed > 0) {
9441 notify$8();
9442 }
9443 return removed;
9444 }
9445 function listWindowThemes() {
9446 return Array.from(registry$3.values()).sort(
9447 (a, b) => (a.priority ?? 100) - (b.priority ?? 100)
9448 );
9449 }
9450 function notify$8() {
9451 const snapshot = Array.from(listeners$6);
9452 for (const cb of snapshot) {
9453 try {
9454 cb();
9455 } catch (err) {
9456 if (typeof console !== "undefined") {
9457 console.error(
9458 "[desktop-mode] window-theme registry listener threw:",
9459 err
9460 );
9461 }
9462 }
9463 }
9464 }
9465 function createWindowThemeRegistrySync() {
9466 const loadedHandles = /* @__PURE__ */ new Set();
9467 const loadedUrls = /* @__PURE__ */ new Set();
9468 let prevIdsByHandle = /* @__PURE__ */ new Map();
9469 const shellRegistered = /* @__PURE__ */ new Set();
9470 const ensureScript = async (entry) => {
9471 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9472 loadedHandles.add(entry.handle);
9473 return;
9474 }
9475 try {
9476 await loadVendorScript(entry.scriptUrl, {
9477 translations: entry.scriptTranslations,
9478 l10n: entry.scriptL10n,
9479 before: entry.scriptBefore,
9480 after: entry.scriptAfter
9481 });
9482 } catch (err) {
9483 doAction(HOOKS.SHELL_ERROR, {
9484 scope: "window-theme-script-load",
9485 handle: entry.handle,
9486 url: entry.scriptUrl,
9487 error: err
9488 });
9489 return;
9490 }
9491 loadedUrls.add(entry.scriptUrl);
9492 loadedHandles.add(entry.handle);
9493 };
9494 const idsByHandleFrom = (themes) => {
9495 const map = /* @__PURE__ */ new Map();
9496 if (!themes) {
9497 return map;
9498 }
9499 for (const entry of themes) {
9500 if (!entry.scriptHandle || !entry.id) {
9501 continue;
9502 }
9503 let set = map.get(entry.scriptHandle);
9504 if (!set) {
9505 set = /* @__PURE__ */ new Set();
9506 map.set(entry.scriptHandle, set);
9507 }
9508 set.add(entry.id);
9509 }
9510 return map;
9511 };
9512 const collectIdsToRemove = (handle) => {
9513 const ids = /* @__PURE__ */ new Set();
9514 for (const def of listWindowThemes()) {
9515 if (def.owner === handle) {
9516 ids.add(def.id);
9517 }
9518 }
9519 const declared = prevIdsByHandle.get(handle);
9520 if (declared) {
9521 for (const id of declared) {
9522 ids.add(id);
9523 }
9524 }
9525 return ids;
9526 };
9527 const applyMetadata = (themes) => {
9528 if (!themes) {
9529 return;
9530 }
9531 for (const entry of themes) {
9532 if (!entry.id || !entry.tokens) {
9533 continue;
9534 }
9535 try {
9536 registerWindowTheme({
9537 id: entry.id,
9538 label: entry.label,
9539 tokens: entry.tokens,
9540 priority: entry.priority,
9541 match: () => true,
9542 owner: entry.scriptHandle || void 0
9543 });
9544 shellRegistered.add(entry.id);
9545 } catch (err) {
9546 doAction(HOOKS.SHELL_ERROR, {
9547 scope: "window-theme-shell-register",
9548 id: entry.id,
9549 error: err
9550 });
9551 }
9552 }
9553 };
9554 return async (scripts, themes) => {
9555 const incomingHandles = /* @__PURE__ */ new Set();
9556 for (const entry of scripts) {
9557 if (entry.handle) {
9558 incomingHandles.add(entry.handle);
9559 }
9560 }
9561 for (const handle of Array.from(loadedHandles)) {
9562 if (incomingHandles.has(handle)) {
9563 continue;
9564 }
9565 const ids = collectIdsToRemove(handle);
9566 for (const id of ids) {
9567 unregisterWindowTheme(id);
9568 shellRegistered.delete(id);
9569 }
9570 unregisterWindowThemesByOwner(handle);
9571 loadedHandles.delete(handle);
9572 }
9573 applyMetadata(themes);
9574 for (const entry of scripts) {
9575 if (!entry.handle || loadedHandles.has(entry.handle)) {
9576 continue;
9577 }
9578 await ensureScript(entry);
9579 }
9580 prevIdsByHandle = idsByHandleFrom(themes);
9581 };
9582 }
9583 const store$7 = createSharedStore(
9584 "desktop-mode/window-controls-registry",
9585 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9586 );
9587 const registry$2 = store$7.state.registry;
9588 const listeners$5 = store$7.state.listeners;
9589 const WINDOW_CONTROL_ID = /^[a-z0-9_/-]+$/;
9590 function registerWindowControl(def) {
9591 const errors = [];
9592 if (!def || typeof def !== "object") {
9593 errors.push("def (not an object)");
9594 } else {
9595 if (typeof def.id !== "string" || def.id.trim() === "") {
9596 errors.push("id (missing)");
9597 } else if (!WINDOW_CONTROL_ID.test(def.id.trim().toLowerCase())) {
9598 errors.push(
9599 `id (must match ${WINDOW_CONTROL_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9600 );
9601 }
9602 if (typeof def.label !== "string" || def.label.trim() === "") {
9603 errors.push("label (missing)");
9604 }
9605 if (typeof def.onClick !== "function" && typeof def.render !== "function") {
9606 errors.push("onClick|render (at least one must be a function)");
9607 }
9608 if (typeof def.render !== "function") {
9609 if (typeof def.icon !== "string" || def.icon.trim() === "") {
9610 errors.push("icon (required when render is omitted)");
9611 }
9612 }
9613 if (typeof def.match !== "function") {
9614 errors.push("match (must be a function)");
9615 }
9616 if (def.placement !== void 0 && def.placement !== "left" && def.placement !== "right" && def.placement !== "controls") {
9617 errors.push('placement (must be "left", "right", or "controls")');
9618 }
9619 }
9620 throwOnRegistrationErrors("WindowControl", errors, def);
9621 const id = def.id.trim().toLowerCase();
9622 registry$2.set(id, { ...def, id });
9623 notify$7();
9624 }
9625 function unregisterWindowControl(id) {
9626 if (registry$2.delete(id.toLowerCase())) {
9627 notify$7();
9628 }
9629 }
9630 function unregisterWindowControlsByOwner(owner) {
9631 if (!owner) {
9632 return 0;
9633 }
9634 let removed = 0;
9635 for (const [id, def] of Array.from(registry$2.entries())) {
9636 if (def.owner === owner) {
9637 registry$2.delete(id);
9638 removed++;
9639 }
9640 }
9641 if (removed > 0) {
9642 notify$7();
9643 }
9644 return removed;
9645 }
9646 function listWindowControls() {
9647 return Array.from(registry$2.values()).sort((a, b) => {
9648 const oa = a.order ?? 100;
9649 const ob = b.order ?? 100;
9650 if (oa !== ob) {
9651 return oa - ob;
9652 }
9653 return a.id.localeCompare(b.id);
9654 });
9655 }
9656 function notify$7() {
9657 const snapshot = Array.from(listeners$5);
9658 for (const cb of snapshot) {
9659 try {
9660 cb();
9661 } catch (err) {
9662 if (typeof console !== "undefined") {
9663 console.error(
9664 "[desktop-mode] window-control registry listener threw:",
9665 err
9666 );
9667 }
9668 }
9669 }
9670 }
9671 function registerBuiltInControls() {
9672 registerWindowControl({
9673 id: "core/minimize",
9674 label: __("Minimize"),
9675 icon: "minimize",
9676 placement: "controls",
9677 order: 10,
9678 core: true,
9679 match: () => true,
9680 onClick: (win) => {
9681 win.minimize();
9682 }
9683 });
9684 registerWindowControl({
9685 id: "core/maximize",
9686 label: __("Maximize"),
9687 icon: "maximize",
9688 placement: "controls",
9689 order: 20,
9690 core: true,
9691 match: () => true,
9692 onClick: (win) => {
9693 win.toggleMaximize();
9694 }
9695 });
9696 registerWindowControl({
9697 id: "core/focus-tab",
9698 label: __("Enter fullscreen"),
9699 icon: "fullscreen",
9700 placement: "controls",
9701 order: 30,
9702 core: true,
9703 match: () => true,
9704 onClick: (win) => {
9705 win.toggleFullscreen();
9706 }
9707 });
9708 registerWindowControl({
9709 id: "core/close",
9710 label: __("Close"),
9711 icon: "close",
9712 placement: "controls",
9713 order: 50,
9714 core: true,
9715 match: () => true,
9716 onClick: (win) => {
9717 win.close();
9718 }
9719 });
9720 }
9721 function createWindowControlRegistrySync() {
9722 const loadedHandles = /* @__PURE__ */ new Set();
9723 const loadedUrls = /* @__PURE__ */ new Set();
9724 let prevIdsByHandle = /* @__PURE__ */ new Map();
9725 const ensureScript = async (entry) => {
9726 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9727 loadedHandles.add(entry.handle);
9728 return;
9729 }
9730 try {
9731 await loadVendorScript(entry.scriptUrl, {
9732 translations: entry.scriptTranslations,
9733 l10n: entry.scriptL10n,
9734 before: entry.scriptBefore,
9735 after: entry.scriptAfter
9736 });
9737 } catch (err) {
9738 doAction(HOOKS.SHELL_ERROR, {
9739 scope: "window-control-script-load",
9740 handle: entry.handle,
9741 url: entry.scriptUrl,
9742 error: err
9743 });
9744 return;
9745 }
9746 loadedUrls.add(entry.scriptUrl);
9747 loadedHandles.add(entry.handle);
9748 };
9749 const idsByHandleFrom = (controls) => {
9750 const map = /* @__PURE__ */ new Map();
9751 if (!controls) {
9752 return map;
9753 }
9754 for (const entry of controls) {
9755 if (!entry.scriptHandle || !entry.id) {
9756 continue;
9757 }
9758 let set = map.get(entry.scriptHandle);
9759 if (!set) {
9760 set = /* @__PURE__ */ new Set();
9761 map.set(entry.scriptHandle, set);
9762 }
9763 set.add(entry.id);
9764 }
9765 return map;
9766 };
9767 const collectIdsToRemove = (handle) => {
9768 const ids = /* @__PURE__ */ new Set();
9769 for (const def of listWindowControls()) {
9770 if (def.owner === handle) {
9771 ids.add(def.id);
9772 }
9773 }
9774 const declared = prevIdsByHandle.get(handle);
9775 if (declared) {
9776 for (const id of declared) {
9777 ids.add(id);
9778 }
9779 }
9780 return ids;
9781 };
9782 return async (scripts, controls) => {
9783 const incomingHandles = /* @__PURE__ */ new Set();
9784 for (const entry of scripts) {
9785 if (entry.handle) {
9786 incomingHandles.add(entry.handle);
9787 }
9788 }
9789 for (const handle of Array.from(loadedHandles)) {
9790 if (incomingHandles.has(handle)) {
9791 continue;
9792 }
9793 for (const id of collectIdsToRemove(handle)) {
9794 unregisterWindowControl(id);
9795 }
9796 unregisterWindowControlsByOwner(handle);
9797 loadedHandles.delete(handle);
9798 }
9799 for (const entry of scripts) {
9800 if (!entry.handle || loadedHandles.has(entry.handle)) {
9801 continue;
9802 }
9803 await ensureScript(entry);
9804 }
9805 prevIdsByHandle = idsByHandleFrom(controls);
9806 };
9807 }
9808 const store$6 = createSharedStore(
9809 "desktop-mode/window-slots-registry",
9810 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9811 );
9812 const registry$1 = store$6.state.registry;
9813 const listeners$4 = store$6.state.listeners;
9814 const WINDOW_SLOT_ID = /^[a-z0-9_/-]+$/;
9815 const KNOWN_SLOTS = /* @__PURE__ */ new Set([
9816 "before-titlebar",
9817 "before-icon",
9818 "icon",
9819 "title",
9820 "after-title",
9821 "before-controls",
9822 "controls",
9823 "after-controls",
9824 "after-titlebar"
9825 ]);
9826 function registerWindowSlot(def) {
9827 const errors = [];
9828 if (!def || typeof def !== "object") {
9829 errors.push("def (not an object)");
9830 } else {
9831 if (typeof def.id !== "string" || def.id.trim() === "") {
9832 errors.push("id (missing)");
9833 } else if (!WINDOW_SLOT_ID.test(def.id.trim().toLowerCase())) {
9834 errors.push(
9835 `id (must match ${WINDOW_SLOT_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9836 );
9837 }
9838 if (typeof def.slot !== "string" || def.slot.trim() === "") {
9839 errors.push("slot (missing)");
9840 } else if (!KNOWN_SLOTS.has(def.slot)) {
9841 errors.push(
9842 `slot (must be one of ${Array.from(KNOWN_SLOTS).join(", ")})`
9843 );
9844 }
9845 if (typeof def.match !== "function") {
9846 errors.push("match (must be a function)");
9847 }
9848 if (typeof def.render !== "function") {
9849 errors.push("render (must be a function)");
9850 }
9851 }
9852 throwOnRegistrationErrors("WindowSlot", errors, def);
9853 const id = def.id.trim().toLowerCase();
9854 registry$1.set(id, { ...def, id });
9855 notify$6();
9856 }
9857 function unregisterWindowSlot(id) {
9858 if (registry$1.delete(id.toLowerCase())) {
9859 notify$6();
9860 }
9861 }
9862 function unregisterWindowSlotsByOwner(owner) {
9863 if (!owner) {
9864 return 0;
9865 }
9866 let removed = 0;
9867 for (const [id, def] of Array.from(registry$1.entries())) {
9868 if (def.owner === owner) {
9869 registry$1.delete(id);
9870 removed++;
9871 }
9872 }
9873 if (removed > 0) {
9874 notify$6();
9875 }
9876 return removed;
9877 }
9878 function listWindowSlots() {
9879 return Array.from(registry$1.values()).sort((a, b) => {
9880 const oa = a.order ?? 100;
9881 const ob = b.order ?? 100;
9882 if (oa !== ob) {
9883 return oa - ob;
9884 }
9885 return a.id.localeCompare(b.id);
9886 });
9887 }
9888 function notify$6() {
9889 const snapshot = Array.from(listeners$4);
9890 for (const cb of snapshot) {
9891 try {
9892 cb();
9893 } catch (err) {
9894 if (typeof console !== "undefined") {
9895 console.error(
9896 "[desktop-mode] window-slot registry listener threw:",
9897 err
9898 );
9899 }
9900 }
9901 }
9902 }
9903 function createWindowSlotRegistrySync() {
9904 const loadedHandles = /* @__PURE__ */ new Set();
9905 const loadedUrls = /* @__PURE__ */ new Set();
9906 let prevIdsByHandle = /* @__PURE__ */ new Map();
9907 const ensureScript = async (entry) => {
9908 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9909 loadedHandles.add(entry.handle);
9910 return;
9911 }
9912 try {
9913 await loadVendorScript(entry.scriptUrl, {
9914 translations: entry.scriptTranslations,
9915 l10n: entry.scriptL10n,
9916 before: entry.scriptBefore,
9917 after: entry.scriptAfter
9918 });
9919 } catch (err) {
9920 doAction(HOOKS.SHELL_ERROR, {
9921 scope: "window-slot-script-load",
9922 handle: entry.handle,
9923 url: entry.scriptUrl,
9924 error: err
9925 });
9926 return;
9927 }
9928 loadedUrls.add(entry.scriptUrl);
9929 loadedHandles.add(entry.handle);
9930 };
9931 const idsByHandleFrom = (slots) => {
9932 const map = /* @__PURE__ */ new Map();
9933 if (!slots) {
9934 return map;
9935 }
9936 for (const entry of slots) {
9937 if (!entry.scriptHandle || !entry.id) {
9938 continue;
9939 }
9940 let set = map.get(entry.scriptHandle);
9941 if (!set) {
9942 set = /* @__PURE__ */ new Set();
9943 map.set(entry.scriptHandle, set);
9944 }
9945 set.add(entry.id);
9946 }
9947 return map;
9948 };
9949 const collectIdsToRemove = (handle) => {
9950 const ids = /* @__PURE__ */ new Set();
9951 for (const def of listWindowSlots()) {
9952 if (def.owner === handle) {
9953 ids.add(def.id);
9954 }
9955 }
9956 const declared = prevIdsByHandle.get(handle);
9957 if (declared) {
9958 for (const id of declared) {
9959 ids.add(id);
9960 }
9961 }
9962 return ids;
9963 };
9964 return async (scripts, slots) => {
9965 const incomingHandles = /* @__PURE__ */ new Set();
9966 for (const entry of scripts) {
9967 if (entry.handle) {
9968 incomingHandles.add(entry.handle);
9969 }
9970 }
9971 for (const handle of Array.from(loadedHandles)) {
9972 if (incomingHandles.has(handle)) {
9973 continue;
9974 }
9975 for (const id of collectIdsToRemove(handle)) {
9976 unregisterWindowSlot(id);
9977 }
9978 unregisterWindowSlotsByOwner(handle);
9979 loadedHandles.delete(handle);
9980 }
9981 for (const entry of scripts) {
9982 if (!entry.handle || loadedHandles.has(entry.handle)) {
9983 continue;
9984 }
9985 await ensureScript(entry);
9986 }
9987 prevIdsByHandle = idsByHandleFrom(slots);
9988 };
9989 }
9990 const KEY_PREFIX = "desktop-mode-notice-dismissed";
9991 function currentUserSuffix() {
9992 const w = window.wp;
9993 const uid = w?.desktop?.config?.currentUserId;
9994 if (typeof uid === "number" && uid > 0) {
9995 return String(uid);
9996 }
9997 return "anon";
9998 }
9999 function storageKey() {
10000 return `${KEY_PREFIX}:${currentUserSuffix()}`;
10001 }
10002 function readMap() {
10003 try {
10004 const raw = window.localStorage.getItem(storageKey());
10005 if (!raw) {
10006 return {};
10007 }
10008 const parsed = JSON.parse(raw);
10009 if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
10010 return parsed;
10011 }
10012 } catch {
10013 }
10014 return {};
10015 }
10016 function writeMap(map) {
10017 try {
10018 window.localStorage.setItem(storageKey(), JSON.stringify(map));
10019 } catch {
10020 }
10021 }
10022 function isNoticeDismissed(id) {
10023 if (!id) {
10024 return false;
10025 }
10026 return readMap()[id] === true;
10027 }
10028 function markNoticeDismissed(id) {
10029 if (!id) {
10030 return;
10031 }
10032 const map = readMap();
10033 map[id] = true;
10034 writeMap(map);
10035 }
10036 function clearNoticeDismissed(id) {
10037 if (!id) {
10038 return;
10039 }
10040 const map = readMap();
10041 if (map[id]) {
10042 delete map[id];
10043 writeMap(map);
10044 }
10045 }
10046 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 ) )}`;
10047 const _WpdNotice = class _WpdNotice extends Component {
10048 connectedCallback() {
10049 super.connectedCallback();
10050 if (!this.hasAttribute("role")) {
10051 this.setAttribute("role", "status");
10052 }
10053 if (!this.hasAttribute("tone")) {
10054 this.setAttribute("tone", "info");
10055 }
10056 const id = this.getAttribute("notice-id");
10057 if (id && isNoticeDismissed(id)) {
10058 this.hidden = true;
10059 }
10060 }
10061 /**
10062 * Imperatively dismiss the notice — hides the host and records
10063 * the dismissal in localStorage when `notice-id` is set.
10064 */
10065 dismiss() {
10066 this.hidden = true;
10067 const id = this.getAttribute("notice-id");
10068 if (id) {
10069 markNoticeDismissed(id);
10070 }
10071 this.emit("wpd-notice-dismiss", { noticeId: id ?? void 0 });
10072 }
10073 /**
10074 * Clear a previously recorded dismissal and re-show the notice.
10075 * Useful in tests and for "Show again" affordances.
10076 */
10077 undismiss() {
10078 const id = this.getAttribute("notice-id");
10079 if (id) {
10080 clearNoticeDismissed(id);
10081 }
10082 this.hidden = false;
10083 }
10084 render() {
10085 const icon = this.getAttribute("icon");
10086 const dismissible = !this.hasAttribute("not-dismissible");
10087 return html`
10088 <span
10089 class="wpd-notice__icon dashicons ${icon ?? ""}"
10090 ?hidden=${!icon}
10091 aria-hidden="true"
10092 ></span>
10093 <span class="wpd-notice__label"><slot></slot></span>
10094 <button
10095 type="button"
10096 class="wpd-notice__close"
10097 ?hidden=${!dismissible}
10098 aria-label=${__("Dismiss notice")}
10099 @click=${(e) => this._onDismiss(e)}
10100 >
10101 <svg viewBox="0 0 14 14" aria-hidden="true">
10102 <path
10103 d="M3 3 L11 11 M11 3 L3 11"
10104 stroke="currentColor"
10105 stroke-width="1.6"
10106 stroke-linecap="round"
10107 fill="none"
10108 ></path>
10109 </svg>
10110 </button>
10111 `;
10112 }
10113 _onDismiss(e) {
10114 e.preventDefault();
10115 e.stopPropagation();
10116 this.dismiss();
10117 }
10118 };
10119 _WpdNotice.props = ["tone", "notDismissible", "icon", "noticeId"];
10120 _WpdNotice.styles = [styles$6];
10121 _WpdNotice.help = {
10122 title: "Notice",
10123 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.",
10124 status: "experimental",
10125 since: "0.22.0",
10126 props: [
10127 {
10128 name: "tone",
10129 type: '"info" | "success" | "warning" | "error" | "danger" | "neutral"',
10130 description: "Color palette. Defaults to `info`. `error` and `danger` are aliases."
10131 },
10132 {
10133 name: "not-dismissible",
10134 type: "boolean",
10135 description: "Suppress the trailing close button. Defaults to dismissible."
10136 },
10137 {
10138 name: "icon",
10139 type: "string",
10140 description: "Optional Dashicons class for a leading glyph (e.g. `dashicons-info`)."
10141 },
10142 {
10143 name: "notice-id",
10144 type: "string",
10145 description: "Persistence key. When set, the notice records its dismissed state in localStorage so it stays closed across reloads for the same user."
10146 }
10147 ],
10148 slots: [
10149 {
10150 name: "(default)",
10151 description: "Message HTML. Links, `<strong>`, `<em>`, and other inline formatting are allowed."
10152 }
10153 ],
10154 events: [
10155 {
10156 name: "wpd-notice-dismiss",
10157 description: "Fires after the user clicks the close button.",
10158 detail: "{ noticeId?: string }"
10159 }
10160 ],
10161 cssProps: [
10162 { name: "--wpd-notice-bg", description: "Background color." },
10163 { name: "--wpd-notice-accent", description: "Left-edge stripe + icon color." },
10164 { name: "--wpd-notice-color", description: "Text color." },
10165 { name: "--wpd-notice-border", description: "Bottom border color." },
10166 { name: "--wpd-notice-link", description: "Color for slotted <a> elements." }
10167 ],
10168 example: html`
10169 <wpd-notice tone="warning" notice-id="docs/example">
10170 Heads up — this is a demo notice.
10171 <a href="#">Learn more</a>.
10172 </wpd-notice>
10173 `
10174 };
10175 let WpdNotice = _WpdNotice;
10176 defineComponent("wpd-notice", WpdNotice);
10177 const store$5 = createSharedStore(
10178 "desktop-mode/window-notices",
10179 () => ({ entries: /* @__PURE__ */ new Map() })
10180 );
10181 const ID_PATTERN = /^[a-z0-9_/-]+$/;
10182 function slotIdFor(id) {
10183 return `desktop-mode-notice/${id.toLowerCase()}`;
10184 }
10185 function buildNoticeElement(entry) {
10186 const el = document.createElement("wpd-notice");
10187 el.setAttribute("tone", entry.tone ?? "info");
10188 el.setAttribute("notice-id", entry.id);
10189 if (entry.dismissible === false) {
10190 el.setAttribute("not-dismissible", "");
10191 }
10192 if (entry.icon) {
10193 el.setAttribute("icon", entry.icon);
10194 }
10195 el.innerHTML = entry.message;
10196 return el;
10197 }
10198 function registerWindowNotice(entry) {
10199 if (!entry || typeof entry !== "object") {
10200 return () => {
10201 };
10202 }
10203 const id = String(entry.id ?? "").trim().toLowerCase();
10204 if (!id || !ID_PATTERN.test(id)) {
10205 return () => {
10206 };
10207 }
10208 if (typeof entry.message !== "string" || entry.message === "") {
10209 return () => {
10210 };
10211 }
10212 const normalised = { ...entry, id };
10213 store$5.state.entries.set(id, normalised);
10214 const slotId = slotIdFor(id);
10215 registerWindowSlot({
10216 id: slotId,
10217 slot: "after-titlebar",
10218 order: normalised.order ?? 100,
10219 // Append rather than clear — every notice slot entry appends
10220 // its own `<wpd-notice>` so multiple notices stack.
10221 replace: false,
10222 owner: normalised.owner,
10223 match: (win) => {
10224 const def = store$5.state.entries.get(id);
10225 if (!def) {
10226 return false;
10227 }
10228 if (typeof def.match !== "function") {
10229 return true;
10230 }
10231 try {
10232 return def.match(win) === true;
10233 } catch {
10234 return false;
10235 }
10236 },
10237 render: (host) => {
10238 const def = store$5.state.entries.get(id);
10239 if (!def) {
10240 return;
10241 }
10242 host.appendChild(buildNoticeElement(def));
10243 }
10244 });
10245 return () => unregisterWindowNotice(id);
10246 }
10247 function unregisterWindowNotice(id) {
10248 const key = String(id ?? "").trim().toLowerCase();
10249 if (!key) {
10250 return;
10251 }
10252 if (store$5.state.entries.delete(key)) {
10253 unregisterWindowSlot(slotIdFor(key));
10254 }
10255 }
10256 function listWindowNotices() {
10257 return Array.from(store$5.state.entries.values()).sort((a, b) => {
10258 const oa = a.order ?? 100;
10259 const ob = b.order ?? 100;
10260 if (oa !== ob) {
10261 return oa - ob;
10262 }
10263 return a.id.localeCompare(b.id);
10264 });
10265 }
10266 function dismissWindowNotice(id) {
10267 const key = String(id ?? "").trim().toLowerCase();
10268 if (!key) {
10269 return;
10270 }
10271 markNoticeDismissed(key);
10272 }
10273 function undismissWindowNotice(id) {
10274 const key = String(id ?? "").trim().toLowerCase();
10275 if (!key) {
10276 return;
10277 }
10278 clearNoticeDismissed(key);
10279 }
10280 function buildMatcher(match) {
10281 if (!match) {
10282 return void 0;
10283 }
10284 const ids = /* @__PURE__ */ new Set();
10285 if (typeof match.window === "string" && match.window !== "") {
10286 ids.add(match.window);
10287 }
10288 if (Array.isArray(match.windows)) {
10289 for (const id of match.windows) {
10290 if (typeof id === "string" && id !== "") {
10291 ids.add(id);
10292 }
10293 }
10294 }
10295 const needle = typeof match.urlContains === "string" && match.urlContains !== "" ? match.urlContains.toLowerCase() : null;
10296 if (ids.size === 0 && needle === null) {
10297 return void 0;
10298 }
10299 return (w) => {
10300 if (ids.size > 0 && !ids.has(w.id)) {
10301 return false;
10302 }
10303 if (needle !== null) {
10304 const url = typeof w.config.url === "string" ? w.config.url.toLowerCase() : "";
10305 if (!url.includes(needle)) {
10306 return false;
10307 }
10308 }
10309 return true;
10310 };
10311 }
10312 function applyServerWindowNotices(entries) {
10313 const wanted = /* @__PURE__ */ new Set();
10314 for (const entry of entries) {
10315 if (!entry || typeof entry.id !== "string" || !entry.id) {
10316 continue;
10317 }
10318 wanted.add(entry.id.toLowerCase());
10319 registerWindowNotice({
10320 id: entry.id,
10321 message: entry.message,
10322 tone: entry.tone,
10323 dismissible: entry.dismissible !== false,
10324 icon: entry.icon,
10325 match: buildMatcher(entry.match),
10326 order: typeof entry.order === "number" ? entry.order : void 0,
10327 // `owner` tag marks every server-shipped notice so a
10328 // targeted cleanup is trivial if/when we surface a sweep
10329 // helper later. Matches the convention used by the
10330 // command / settings-tab sync modules.
10331 owner: "__server__"
10332 });
10333 }
10334 for (const existing of listWindowNotices()) {
10335 if (existing.owner !== "__server__") {
10336 continue;
10337 }
10338 if (!wanted.has(existing.id)) {
10339 unregisterWindowNotice(existing.id);
10340 }
10341 }
10342 }
10343 const store$4 = createSharedStore(
10344 "desktop-mode/window-chrome-registry",
10345 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
10346 );
10347 const registry = store$4.state.registry;
10348 const listeners$3 = store$4.state.listeners;
10349 const WINDOW_CHROME_ID = /^[a-z0-9_/-]+$/;
10350 function registerWindowChrome(def) {
10351 const errors = [];
10352 if (!def || typeof def !== "object") {
10353 errors.push("def (not an object)");
10354 } else {
10355 if (typeof def.id !== "string" || def.id.trim() === "") {
10356 errors.push("id (missing)");
10357 } else if (!WINDOW_CHROME_ID.test(def.id.trim().toLowerCase())) {
10358 errors.push(
10359 `id (must match ${WINDOW_CHROME_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
10360 );
10361 }
10362 if (typeof def.match !== "function") {
10363 errors.push("match (must be a function)");
10364 }
10365 if (typeof def.render !== "function") {
10366 errors.push("render (must be a function)");
10367 }
10368 }
10369 throwOnRegistrationErrors("WindowChrome", errors, def);
10370 const id = def.id.trim().toLowerCase();
10371 registry.set(id, { ...def, id });
10372 notify$5();
10373 }
10374 function unregisterWindowChrome(id) {
10375 if (registry.delete(id.toLowerCase())) {
10376 notify$5();
10377 }
10378 }
10379 function unregisterWindowChromesByOwner(owner) {
10380 if (!owner) {
10381 return 0;
10382 }
10383 let removed = 0;
10384 for (const [id, def] of Array.from(registry.entries())) {
10385 if (def.owner === owner) {
10386 registry.delete(id);
10387 removed++;
10388 }
10389 }
10390 if (removed > 0) {
10391 notify$5();
10392 }
10393 return removed;
10394 }
10395 function listWindowChromes() {
10396 return Array.from(registry.values()).sort(
10397 (a, b) => a.id.localeCompare(b.id)
10398 );
10399 }
10400 function notify$5() {
10401 const snapshot = Array.from(listeners$3);
10402 for (const cb of snapshot) {
10403 try {
10404 cb();
10405 } catch (err) {
10406 if (typeof console !== "undefined") {
10407 console.error(
10408 "[desktop-mode] window-chrome registry listener threw:",
10409 err
10410 );
10411 }
10412 }
10413 }
10414 }
10415 function createWindowChromeRegistrySync() {
10416 const loadedHandles = /* @__PURE__ */ new Set();
10417 const loadedUrls = /* @__PURE__ */ new Set();
10418 let prevIdsByHandle = /* @__PURE__ */ new Map();
10419 const ensureScript = async (entry) => {
10420 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
10421 loadedHandles.add(entry.handle);
10422 return;
10423 }
10424 try {
10425 await loadVendorScript(entry.scriptUrl, {
10426 translations: entry.scriptTranslations,
10427 l10n: entry.scriptL10n,
10428 before: entry.scriptBefore,
10429 after: entry.scriptAfter
10430 });
10431 } catch (err) {
10432 doAction(HOOKS.SHELL_ERROR, {
10433 scope: "window-chrome-script-load",
10434 handle: entry.handle,
10435 url: entry.scriptUrl,
10436 error: err
10437 });
10438 return;
10439 }
10440 loadedUrls.add(entry.scriptUrl);
10441 loadedHandles.add(entry.handle);
10442 };
10443 const idsByHandleFrom = (chromes) => {
10444 const map = /* @__PURE__ */ new Map();
10445 if (!chromes) {
10446 return map;
10447 }
10448 for (const entry of chromes) {
10449 if (!entry.scriptHandle || !entry.id) {
10450 continue;
10451 }
10452 let set = map.get(entry.scriptHandle);
10453 if (!set) {
10454 set = /* @__PURE__ */ new Set();
10455 map.set(entry.scriptHandle, set);
10456 }
10457 set.add(entry.id);
10458 }
10459 return map;
10460 };
10461 const collectIdsToRemove = (handle) => {
10462 const ids = /* @__PURE__ */ new Set();
10463 for (const def of listWindowChromes()) {
10464 if (def.owner === handle) {
10465 ids.add(def.id);
10466 }
10467 }
10468 const declared = prevIdsByHandle.get(handle);
10469 if (declared) {
10470 for (const id of declared) {
10471 ids.add(id);
10472 }
10473 }
10474 return ids;
10475 };
10476 return async (scripts, chromes) => {
10477 const incomingHandles = /* @__PURE__ */ new Set();
10478 for (const entry of scripts) {
10479 if (entry.handle) {
10480 incomingHandles.add(entry.handle);
10481 }
10482 }
10483 for (const handle of Array.from(loadedHandles)) {
10484 if (incomingHandles.has(handle)) {
10485 continue;
10486 }
10487 for (const id of collectIdsToRemove(handle)) {
10488 unregisterWindowChrome(id);
10489 }
10490 unregisterWindowChromesByOwner(handle);
10491 loadedHandles.delete(handle);
10492 }
10493 for (const entry of scripts) {
10494 if (!entry.handle || loadedHandles.has(entry.handle)) {
10495 continue;
10496 }
10497 await ensureScript(entry);
10498 }
10499 prevIdsByHandle = idsByHandleFrom(chromes);
10500 };
10501 }
10502 const INITIAL_ORIGIN$2 = window.location.origin;
10503 let _connSeq = 0;
10504 const _connections = /* @__PURE__ */ new Map();
10505 const _connectionsByTarget = /* @__PURE__ */ new Map();
10506 const _syntheticIframes = /* @__PURE__ */ new Map();
10507 function registerSyntheticIframe(windowId, iframe) {
10508 _syntheticIframes.set(windowId, iframe);
10509 return () => {
10510 if (_syntheticIframes.get(windowId) === iframe) {
10511 _syntheticIframes.delete(windowId);
10512 }
10513 };
10514 }
10515 function nextId() {
10516 return `desktop-mode-conn-${++_connSeq}`;
10517 }
10518 function createConnectionBridge(manager) {
10519 const sendToIframe = (win, message) => {
10520 try {
10521 win.contentWindow?.postMessage(message, INITIAL_ORIGIN$2);
10522 } catch (err) {
10523 if (typeof console !== "undefined") {
10524 console.error(
10525 "[desktop-mode] connection: postMessage failed",
10526 err
10527 );
10528 }
10529 }
10530 };
10531 const connect = (targetWindowId, opts = {}) => {
10532 const id = nextId();
10533 const topics = Array.isArray(opts.topics) ? [...opts.topics] : [];
10534 const subs = /* @__PURE__ */ new Map();
10535 const queue = [];
10536 let isOpen = false;
10537 let destroyed = false;
10538 const targetIframe = () => {
10539 const synth = _syntheticIframes.get(targetWindowId);
10540 if (synth) {
10541 return synth;
10542 }
10543 const w = manager.getById(targetWindowId);
10544 return w?.iframe ?? null;
10545 };
10546 const isNativeTarget = () => {
10547 if (targetIframe()) {
10548 return false;
10549 }
10550 const w = manager.getById(targetWindowId);
10551 return !!w && w.config?.native === true;
10552 };
10553 const nativeSubUnsubs = [];
10554 const flushQueue = () => {
10555 const iframe2 = targetIframe();
10556 if (!iframe2) {
10557 return;
10558 }
10559 while (queue.length) {
10560 const msg = queue.shift();
10561 sendToIframe(iframe2, {
10562 type: "desktop-mode-bridge-publish",
10563 connectionId: id,
10564 topic: msg.topic,
10565 payload: msg.payload
10566 });
10567 }
10568 };
10569 const conn = {
10570 id,
10571 target: targetWindowId,
10572 isOpen: () => isOpen,
10573 subscribe(topic, cb) {
10574 const wrapped = cb;
10575 if (isNativeTarget()) {
10576 const off = addParentSubscriber(
10577 targetWindowId,
10578 topic,
10579 (payload, meta) => {
10580 doAction(HOOKS.CONNECTION_MESSAGE, {
10581 connectionId: id,
10582 topic: meta.channel,
10583 direction: "in"
10584 });
10585 try {
10586 wrapped(payload, { topic: meta.channel });
10587 } catch (err) {
10588 if (typeof console !== "undefined") {
10589 console.error(
10590 "[desktop-mode] connection subscriber threw:",
10591 err
10592 );
10593 }
10594 }
10595 }
10596 );
10597 nativeSubUnsubs.push(off);
10598 return off;
10599 }
10600 let bucket22 = subs.get(topic);
10601 if (!bucket22) {
10602 bucket22 = /* @__PURE__ */ new Set();
10603 subs.set(topic, bucket22);
10604 }
10605 bucket22.add(wrapped);
10606 return () => {
10607 bucket22?.delete(wrapped);
10608 };
10609 },
10610 send(topic, payload) {
10611 if (destroyed) {
10612 return;
10613 }
10614 doAction(HOOKS.CONNECTION_MESSAGE, {
10615 connectionId: id,
10616 topic,
10617 direction: "out"
10618 });
10619 if (isNativeTarget()) {
10620 dispatchToNative(targetWindowId, topic, payload);
10621 return;
10622 }
10623 if (!isOpen) {
10624 queue.push({ topic, payload });
10625 return;
10626 }
10627 const iframe2 = targetIframe();
10628 if (!iframe2) {
10629 return;
10630 }
10631 sendToIframe(iframe2, {
10632 type: "desktop-mode-bridge-publish",
10633 connectionId: id,
10634 topic,
10635 payload
10636 });
10637 },
10638 disconnect() {
10639 conn._destroy("disconnect");
10640 },
10641 _targetWindow: targetIframe,
10642 _handleIframeMessage(data) {
10643 if (!data || typeof data !== "object") {
10644 return;
10645 }
10646 const msg = data;
10647 if (msg.type === "desktop-mode-bridge-handshake-ack") {
10648 if (isOpen) {
10649 return;
10650 }
10651 isOpen = true;
10652 doAction(HOOKS.CONNECTION_OPENED, {
10653 connectionId: id,
10654 targetWindowId,
10655 topics,
10656 // Ship the live Connection alongside the id so
10657 // iframe-initiated connections can be subscribed
10658 // to directly from the hook handler — without
10659 // `wp.desktop.getConnection(id)` plumbing the
10660 // payload would carry the id but no way to call
10661 // `.subscribe()` against it.
10662 connection: conn
10663 });
10664 try {
10665 opts.onOpen?.();
10666 } catch (err) {
10667 if (typeof console !== "undefined") {
10668 console.error(
10669 "[desktop-mode] connection.onOpen threw:",
10670 err
10671 );
10672 }
10673 }
10674 flushQueue();
10675 return;
10676 }
10677 if (msg.type === "desktop-mode-bridge-publish") {
10678 const m = data;
10679 const topic = typeof m.topic === "string" ? m.topic : "";
10680 if (!topic) {
10681 return;
10682 }
10683 doAction(HOOKS.CONNECTION_MESSAGE, {
10684 connectionId: id,
10685 topic,
10686 direction: "in"
10687 });
10688 const exact = subs.get(topic);
10689 if (exact) {
10690 for (const cb of Array.from(exact)) {
10691 try {
10692 cb(m.payload, { topic });
10693 } catch (err) {
10694 if (typeof console !== "undefined") {
10695 console.error(
10696 "[desktop-mode] connection subscriber threw:",
10697 err
10698 );
10699 }
10700 }
10701 }
10702 }
10703 const wildcard = subs.get("*");
10704 if (wildcard) {
10705 for (const cb of Array.from(wildcard)) {
10706 try {
10707 cb(m.payload, { topic });
10708 } catch (err) {
10709 if (typeof console !== "undefined") {
10710 console.error(
10711 "[desktop-mode] connection wildcard subscriber threw:",
10712 err
10713 );
10714 }
10715 }
10716 }
10717 }
10718 return;
10719 }
10720 if (msg.type === "desktop-mode-bridge-disconnect") {
10721 conn._destroy("disconnect");
10722 }
10723 },
10724 _destroy(reason) {
10725 if (destroyed) {
10726 return;
10727 }
10728 destroyed = true;
10729 const wasOpen = isOpen;
10730 isOpen = false;
10731 _connections.delete(id);
10732 const targetSet = _connectionsByTarget.get(targetWindowId);
10733 if (targetSet) {
10734 targetSet.delete(id);
10735 if (targetSet.size === 0) {
10736 _connectionsByTarget.delete(targetWindowId);
10737 }
10738 }
10739 for (const off of nativeSubUnsubs.splice(0)) {
10740 try {
10741 off();
10742 } catch {
10743 }
10744 }
10745 if (wasOpen) {
10746 const iframe2 = targetIframe();
10747 if (iframe2) {
10748 sendToIframe(iframe2, {
10749 type: "desktop-mode-bridge-disconnect",
10750 connectionId: id
10751 });
10752 }
10753 }
10754 doAction(HOOKS.CONNECTION_CLOSED, {
10755 connectionId: id,
10756 reason
10757 });
10758 try {
10759 opts.onClose?.(reason);
10760 } catch (err) {
10761 if (typeof console !== "undefined") {
10762 console.error(
10763 "[desktop-mode] connection.onClose threw:",
10764 err
10765 );
10766 }
10767 }
10768 }
10769 };
10770 _connections.set(id, conn);
10771 let bucket2 = _connectionsByTarget.get(targetWindowId);
10772 if (!bucket2) {
10773 bucket2 = /* @__PURE__ */ new Set();
10774 _connectionsByTarget.set(targetWindowId, bucket2);
10775 }
10776 bucket2.add(id);
10777 if (isNativeTarget()) {
10778 Promise.resolve().then(() => {
10779 if (destroyed || isOpen) {
10780 return;
10781 }
10782 isOpen = true;
10783 doAction(HOOKS.CONNECTION_OPENED, {
10784 connectionId: id,
10785 targetWindowId,
10786 topics
10787 });
10788 try {
10789 opts.onOpen?.();
10790 } catch (err) {
10791 if (typeof console !== "undefined") {
10792 console.error(
10793 "[desktop-mode] connection.onOpen threw:",
10794 err
10795 );
10796 }
10797 }
10798 });
10799 return conn;
10800 }
10801 const iframe = targetIframe();
10802 if (iframe) {
10803 sendToIframe(iframe, {
10804 type: "desktop-mode-bridge-handshake",
10805 connectionId: id,
10806 targetWindowId,
10807 topics
10808 });
10809 }
10810 return conn;
10811 };
10812 const routeIncomingFromIframe = (data, windowId) => {
10813 if (!data || typeof data !== "object") {
10814 return;
10815 }
10816 const msg = data;
10817 if (typeof msg.type !== "string" || !msg.type.startsWith("desktop-mode-bridge-")) {
10818 return;
10819 }
10820 if (msg.type === "desktop-mode-bridge-connection-request" && typeof msg.requestId === "string" && typeof windowId === "string" && windowId !== "") {
10821 handleConnectionRequest(windowId, msg.requestId, Array.isArray(msg.topics) ? msg.topics : []);
10822 return;
10823 }
10824 if (typeof msg.connectionId !== "string") {
10825 return;
10826 }
10827 const conn = _connections.get(msg.connectionId);
10828 conn?._handleIframeMessage(data);
10829 };
10830 const handleConnectionRequest = (windowId, requestId, topics) => {
10831 const synth = _syntheticIframes.get(windowId);
10832 const iframe = synth ?? manager.getById(windowId)?.iframe ?? null;
10833 if (!iframe) {
10834 return;
10835 }
10836 const decision = applyFilters(
10837 HOOKS.IFRAME_CONNECTION_REQUEST,
10838 true,
10839 { windowId, requestId, topics: topics.slice() }
10840 );
10841 if (decision === false) {
10842 try {
10843 iframe.contentWindow?.postMessage({
10844 type: "desktop-mode-bridge-connection-ack",
10845 requestId,
10846 accepted: false,
10847 reason: "rejected"
10848 }, INITIAL_ORIGIN$2);
10849 } catch {
10850 }
10851 return;
10852 }
10853 const finalTopics = decision && typeof decision === "object" && Array.isArray(decision.topics) ? decision.topics : topics;
10854 const conn = connect(windowId, { topics: finalTopics });
10855 try {
10856 iframe.contentWindow?.postMessage({
10857 type: "desktop-mode-bridge-connection-ack",
10858 requestId,
10859 accepted: true,
10860 connectionId: conn.id
10861 }, INITIAL_ORIGIN$2);
10862 } catch {
10863 }
10864 };
10865 const onIframeReady = (windowId) => {
10866 const bucket2 = _connectionsByTarget.get(windowId);
10867 if (!bucket2) {
10868 return;
10869 }
10870 for (const connId of Array.from(bucket2)) {
10871 const conn = _connections.get(connId);
10872 if (!conn || conn.isOpen()) {
10873 continue;
10874 }
10875 const iframe = conn._targetWindow();
10876 if (!iframe) {
10877 continue;
10878 }
10879 sendToIframe(iframe, {
10880 type: "desktop-mode-bridge-handshake",
10881 connectionId: conn.id,
10882 targetWindowId: conn.target,
10883 topics: []
10884 // already negotiated client-side; iframe re-uses
10885 });
10886 }
10887 };
10888 const onWindowClosed = (windowId) => {
10889 const bucket2 = _connectionsByTarget.get(windowId);
10890 if (!bucket2) {
10891 return;
10892 }
10893 for (const connId of Array.from(bucket2)) {
10894 const conn = _connections.get(connId);
10895 conn?._destroy("window-closed");
10896 }
10897 };
10898 const getConnection = (connectionId) => {
10899 const conn = _connections.get(connectionId);
10900 return conn ?? null;
10901 };
10902 return {
10903 connect,
10904 getConnection,
10905 routeIncomingFromIframe,
10906 onIframeReady,
10907 onWindowClosed
10908 };
10909 }
10910 const __vite_import_meta_env__ = {};
10911 function devLog(...args) {
10912 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;
10913 if (mode !== "production") {
10914 console.log(...args);
10915 }
10916 }
10917 const OWNER_PREFIX = "iframe:";
10918 function ownerFor(windowId) {
10919 return OWNER_PREFIX + windowId;
10920 }
10921 function iconFor(harvested) {
10922 if (harvested.icon && typeof harvested.icon === "string" && harvested.icon.startsWith("dashicons-")) {
10923 return harvested.icon;
10924 }
10925 return harvested.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
10926 }
10927 function slugFor(windowId, name) {
10928 const safeName = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
10929 const safeWin = windowId.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
10930 return `win-${safeWin}-${safeName}`;
10931 }
10932 class IframeCommandBridge {
10933 constructor(opts) {
10934 this.subscribedWindowId = null;
10935 this.manager = opts.manager;
10936 this.adminUrl = opts.adminUrl;
10937 }
10938 /** Wire up the focus / close / message listeners. Idempotent. */
10939 install() {
10940 document.addEventListener("desktop-mode-window-focused", (e) => {
10941 const detail = e.detail;
10942 if (detail && typeof detail.windowId === "string") {
10943 this.onFocused(detail.windowId);
10944 }
10945 });
10946 document.addEventListener("desktop-mode-window-closed", (e) => {
10947 const detail = e.detail;
10948 if (detail && typeof detail.windowId === "string") {
10949 unregisterByOwner(ownerFor(detail.windowId));
10950 if (this.subscribedWindowId === detail.windowId) {
10951 this.subscribedWindowId = null;
10952 }
10953 }
10954 });
10955 document.addEventListener("desktop-mode-window-changed", (e) => {
10956 const detail = e.detail;
10957 if (!detail || typeof detail.windowId !== "string") {
10958 return;
10959 }
10960 if (detail.reason !== "state") {
10961 return;
10962 }
10963 if (detail.state !== "minimized") {
10964 return;
10965 }
10966 if (this.subscribedWindowId === detail.windowId) {
10967 this.subscribedWindowId = null;
10968 }
10969 });
10970 window.addEventListener("message", (e) => {
10971 if (e.origin !== window.location.origin) {
10972 return;
10973 }
10974 const data = e.data;
10975 if (!data || typeof data.type !== "string") {
10976 return;
10977 }
10978 if (data.type === "desktop-mode-bridge-ready") {
10979 const win2 = this.manager.findByIframeSource(e.source);
10980 if (win2 && win2.id === this.subscribedWindowId) {
10981 this.sendSubscribe(win2.id);
10982 }
10983 return;
10984 }
10985 if (data.type !== "desktop-mode-commands-list") {
10986 return;
10987 }
10988 if (!Array.isArray(data.commands)) {
10989 return;
10990 }
10991 const win = this.manager.findByIframeSource(e.source);
10992 if (!win) {
10993 return;
10994 }
10995 if (win.id !== this.subscribedWindowId) {
10996 return;
10997 }
10998 this.applyList(win.id, data.commands);
10999 });
11000 const focused = this.manager.getFocused();
11001 if (focused) {
11002 this.onFocused(focused.id);
11003 }
11004 }
11005 onFocused(windowId) {
11006 if (this.subscribedWindowId === windowId) {
11007 return;
11008 }
11009 if (this.subscribedWindowId) {
11010 const prev = this.manager.getById(this.subscribedWindowId);
11011 if (prev && prev.iframe && prev.iframe.contentWindow) {
11012 try {
11013 prev.iframe.contentWindow.postMessage(
11014 { type: "desktop-mode-commands-unsubscribe" },
11015 window.location.origin
11016 );
11017 } catch {
11018 }
11019 }
11020 unregisterByOwner(ownerFor(this.subscribedWindowId));
11021 }
11022 this.subscribedWindowId = windowId;
11023 this.sendSubscribe(windowId);
11024 }
11025 sendSubscribe(windowId) {
11026 const win = this.manager.getById(windowId);
11027 if (!win) {
11028 return;
11029 }
11030 if (!win.iframe) {
11031 return;
11032 }
11033 if (!win.iframe.contentWindow) {
11034 return;
11035 }
11036 try {
11037 win.iframe.contentWindow.postMessage(
11038 { type: "desktop-mode-commands-subscribe" },
11039 window.location.origin
11040 );
11041 } catch (err) {
11042 devLog("[wpd-cmd:parent] sendSubscribe: postMessage threw", err);
11043 }
11044 }
11045 applyList(windowId, commands) {
11046 const owner = ownerFor(windowId);
11047 unregisterByOwner(owner);
11048 for (const cmd of commands) {
11049 if (!cmd || !cmd.name || !cmd.label) {
11050 continue;
11051 }
11052 const slug = slugFor(windowId, cmd.name);
11053 const safeSvg = typeof cmd.iconSvg === "string" && cmd.iconSvg !== "" ? sanitizeIconSvg(cmd.iconSvg) : "";
11054 const def = {
11055 slug,
11056 label: cmd.label,
11057 icon: iconFor(cmd),
11058 iconSvg: safeSvg !== "" ? safeSvg : void 0,
11059 owner,
11060 // Harvested commands are contextual by construction —
11061 // they come from whichever window has focus. Surface
11062 // them eagerly so the user sees "Duplicate block" /
11063 // "Toggle distraction free" without having to type `/`
11064 // first.
11065 eager: true,
11066 run: cmd.kind === "navigate" && cmd.url ? this.runNavigate(cmd.url, cmd.label, iconFor(cmd)) : this.runProxy(windowId, cmd.name)
11067 };
11068 try {
11069 registerCommand(def);
11070 } catch (err) {
11071 console.error(
11072 "[desktop-mode] iframe-bridge: dropping bad command",
11073 def,
11074 err
11075 );
11076 }
11077 }
11078 }
11079 runNavigate(url, title, icon) {
11080 return (_args, ctx) => {
11081 ctx.close();
11082 if (tryNativeUrlRemap(url)) {
11083 return;
11084 }
11085 const id = deriveWindowId(url, this.adminUrl);
11086 this.manager.open({ id, baseId: id, url, title, icon });
11087 };
11088 }
11089 runProxy(windowId, name) {
11090 return (_args, ctx) => {
11091 ctx.close();
11092 const win = this.manager.getById(windowId);
11093 if (!win || !win.iframe || !win.iframe.contentWindow) {
11094 return;
11095 }
11096 try {
11097 win.iframe.contentWindow.postMessage(
11098 { type: "desktop-mode-commands-invoke", name },
11099 window.location.origin
11100 );
11101 } catch {
11102 }
11103 this.manager.focus(win);
11104 };
11105 }
11106 }
11107 const OWNER = "global";
11108 const NAV_HREF_LITERAL_RE = /(?:document\.location\.href|window\.location\.href|location\.href)\s*=\s*['"]([^'"$]+?)['"]/;
11109 const NAV_ASSIGN_LITERAL_RE = /(?:document\.location|window\.location|location)\s*=\s*['"]([^'"$]+?)['"]/;
11110 const NAV_CALL_LITERAL_RE = /location\.(?:assign|replace)\s*\(\s*['"]([^'"$]+?)['"]\s*\)/;
11111 const NAV_INTENT_RE = /(?:document\.location|window\.location|location)\s*(?:\.href\s*)?=|location\.(?:assign|replace)\s*\(/;
11112 const SITE_EDITOR_INTENT_RE = /getSiteEditorPage\s*\(|site-editor\.php/;
11113 const SITE_EDITOR_NAME_RE = /^(wp_template_part|wp_template|wp_navigation|wp_block)-(.+)$/;
11114 function lookupMenuCommand(name) {
11115 const list2 = window.__desktopModeMenuCommands;
11116 if (!Array.isArray(list2)) {
11117 return null;
11118 }
11119 for (const entry of list2) {
11120 if (entry && typeof entry === "object" && entry.name === name && typeof entry.url === "string" && entry.url !== "") {
11121 return {
11122 label: typeof entry.label === "string" ? entry.label : "",
11123 url: entry.url
11124 };
11125 }
11126 }
11127 return null;
11128 }
11129 class ShellCommandHarvester {
11130 constructor(opts) {
11131 this.mounted = false;
11132 this.host = null;
11133 this.root = null;
11134 this.kindCache = /* @__PURE__ */ Object.create(null);
11135 this.callbackCache = /* @__PURE__ */ Object.create(null);
11136 this.lastFingerprint = "";
11137 this.manager = opts.manager;
11138 this.adminUrl = opts.adminUrl;
11139 }
11140 /** Mount the harvester. Idempotent. Safe to call before `wp.data` loads. */
11141 install() {
11142 this.tryMount(0);
11143 }
11144 tryMount(attempt) {
11145 if (this.mounted) {
11146 return;
11147 }
11148 const wp = window.wp;
11149 if (!wp || !wp.data || !wp.element || typeof wp.data.subscribe !== "function") {
11150 if (attempt < 40) {
11151 window.setTimeout(() => this.tryMount(attempt + 1), 150);
11152 }
11153 return;
11154 }
11155 this.mount();
11156 }
11157 mount() {
11158 const wp = window.wp;
11159 const el = wp.element;
11160 const data = wp.data;
11161 const createEl = el.createElement;
11162 const useEffect = el.useEffect;
11163 const useRef = el.useRef;
11164 const useMemo = el.useMemo;
11165 const useSelect = data.useSelect;
11166 if (typeof createEl !== "function" || typeof useEffect !== "function" || typeof useRef !== "function" || typeof useMemo !== "function" || typeof useSelect !== "function" || typeof el.createRoot !== "function") {
11167 return;
11168 }
11169 this.mounted = true;
11170 const host = document.createElement("div");
11171 host.setAttribute("aria-hidden", "true");
11172 host.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;left:-9999px;top:-9999px;";
11173 (document.body || document.documentElement).appendChild(host);
11174 this.host = host;
11175 const bucket2 = {
11176 perLoader: {},
11177 statics: [],
11178 loadersList: []
11179 };
11180 const fingerprint2 = (cmds) => {
11181 if (!Array.isArray(cmds) || cmds.length === 0) {
11182 return "";
11183 }
11184 const keys = new Array(cmds.length);
11185 for (let i = 0; i < cmds.length; i++) {
11186 const c = cmds[i];
11187 keys[i] = c && c.name ? c.name : "";
11188 }
11189 return keys.join("|");
11190 };
11191 const mergeAndPublish = () => {
11192 let merged = [];
11193 for (const name of bucket2.loadersList) {
11194 const slice = bucket2.perLoader[name];
11195 if (Array.isArray(slice)) {
11196 merged = merged.concat(slice);
11197 }
11198 }
11199 if (Array.isArray(bucket2.statics)) {
11200 merged = merged.concat(bucket2.statics);
11201 }
11202 this.callbackCache = /* @__PURE__ */ Object.create(null);
11203 for (const cc of merged) {
11204 if (cc && cc.name && typeof cc.callback === "function") {
11205 this.callbackCache[cc.name] = cc.callback;
11206 }
11207 }
11208 this.publish(merged);
11209 };
11210 const LoaderSlot = (props) => {
11211 const loader = props.loader;
11212 let result = null;
11213 try {
11214 result = loader.hook({ search: "" });
11215 } catch {
11216 }
11217 const cmds = result && Array.isArray(result.commands) ? result.commands : [];
11218 const key = useMemo(() => fingerprint2(cmds), [cmds]);
11219 useEffect(() => {
11220 bucket2.perLoader[loader.name] = cmds;
11221 mergeAndPublish();
11222 }, [key]);
11223 useEffect(() => {
11224 return () => {
11225 delete bucket2.perLoader[loader.name];
11226 mergeAndPublish();
11227 };
11228 }, []);
11229 return null;
11230 };
11231 const Harvester = () => {
11232 const loaders = useSelect((s) => {
11233 const ss = s("core/commands");
11234 if (!ss || typeof ss.getCommandLoaders !== "function") {
11235 return [];
11236 }
11237 return [
11238 ...ss.getCommandLoaders(false) || [],
11239 ...ss.getCommandLoaders(true) || []
11240 ];
11241 }, []);
11242 const staticCmds = useSelect((s) => {
11243 const ss = s("core/commands");
11244 if (!ss || typeof ss.getCommands !== "function") {
11245 return [];
11246 }
11247 return [
11248 ...ss.getCommands(false) || [],
11249 ...ss.getCommands(true) || []
11250 ];
11251 }, []);
11252 const loadersNames = useMemo(() => {
11253 return Array.isArray(loaders) ? loaders.map((l) => l ? l.name || "" : "") : [];
11254 }, [loaders]);
11255 const loadersKey = loadersNames.join("|");
11256 useEffect(() => {
11257 bucket2.loadersList = loadersNames;
11258 mergeAndPublish();
11259 }, [loadersKey]);
11260 const staticKey = useMemo(
11261 () => fingerprint2(Array.isArray(staticCmds) ? staticCmds : []),
11262 [staticCmds]
11263 );
11264 useEffect(() => {
11265 bucket2.statics = Array.isArray(staticCmds) ? staticCmds : [];
11266 mergeAndPublish();
11267 }, [staticKey]);
11268 if (!Array.isArray(loaders) || loaders.length === 0) {
11269 return null;
11270 }
11271 const children = [];
11272 for (const loader of loaders) {
11273 if (!loader || typeof loader.hook !== "function") {
11274 continue;
11275 }
11276 children.push(
11277 createEl(LoaderSlot, { key: loader.name, loader })
11278 );
11279 }
11280 return createEl(el.Fragment || "div", null, children);
11281 };
11282 try {
11283 this.root = el.createRoot(host);
11284 this.root.render(createEl(Harvester));
11285 } catch {
11286 this.mounted = false;
11287 this.root = null;
11288 if (this.host && this.host.parentNode) {
11289 this.host.parentNode.removeChild(this.host);
11290 }
11291 this.host = null;
11292 }
11293 }
11294 publish(raw) {
11295 const seen = /* @__PURE__ */ Object.create(null);
11296 const classified = [];
11297 for (const cmd of raw) {
11298 if (!cmd || !cmd.name || !cmd.label) {
11299 continue;
11300 }
11301 if (cmd.disabled) {
11302 continue;
11303 }
11304 if (seen[cmd.name]) {
11305 continue;
11306 }
11307 seen[cmd.name] = true;
11308 classified.push(this.classify(cmd));
11309 }
11310 let key = "";
11311 for (const c of classified) {
11312 key += `${c.name}|${c.kind}|${c.url || ""}
11313 `;
11314 }
11315 if (key === this.lastFingerprint) {
11316 return;
11317 }
11318 this.lastFingerprint = key;
11319 unregisterByOwner(OWNER);
11320 for (const c of classified) {
11321 if (c.kind === "skip") {
11322 continue;
11323 }
11324 const slug = `global-${c.name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-")}`;
11325 const icon = this.iconFor(c);
11326 const def = {
11327 slug,
11328 label: c.label,
11329 icon,
11330 iconSvg: c.iconSvg && c.iconSvg !== "" ? sanitizeIconSvg(c.iconSvg) : void 0,
11331 owner: OWNER,
11332 // NOT eager. The palette splits the registry into two
11333 // disjoint surfaces: `eager` commands show on empty
11334 // input (and are excluded from slash search at
11335 // `src/ai-assistant/impl.ts:494`); non-eager commands
11336 // show when the user types `/<query>`. The WP baseline
11337 // is large (~150 entries) and meant to be searched —
11338 // surfacing it eagerly would drown the iframe-harvested
11339 // contextual shortcuts on every open. Slash-search is
11340 // the right surface for it, matching the native WP
11341 // palette UX (open, type, find).
11342 run: c.kind === "navigate" && c.url ? this.runNavigate(c.url, c.windowTitle || c.label, icon) : this.runInvoke(c.name, c.label, icon)
11343 };
11344 try {
11345 registerCommand(def);
11346 } catch (err) {
11347 console.error(
11348 "[desktop-mode] shell-harvester: dropping bad command",
11349 def,
11350 err
11351 );
11352 }
11353 }
11354 }
11355 classify(cmd) {
11356 const out = {
11357 name: String(cmd.name),
11358 label: String(cmd.label),
11359 icon: typeof cmd.icon === "string" ? cmd.icon : void 0,
11360 iconSvg: void 0,
11361 kind: "action",
11362 url: void 0,
11363 callback: typeof cmd.callback === "function" ? cmd.callback : void 0
11364 };
11365 const cached = this.kindCache[out.name];
11366 if (cached) {
11367 out.kind = cached.kind;
11368 out.url = cached.url;
11369 out.iconSvg = cached.iconSvg;
11370 return out;
11371 }
11372 if (cmd.icon && typeof cmd.icon !== "string") {
11373 out.iconSvg = this.renderIcon(cmd.icon);
11374 }
11375 const menuEntry = lookupMenuCommand(out.name);
11376 if (menuEntry) {
11377 try {
11378 out.url = new URL(menuEntry.url, this.adminUrl).toString();
11379 out.kind = "navigate";
11380 if (menuEntry.label !== "") {
11381 out.windowTitle = menuEntry.label;
11382 }
11383 } catch {
11384 out.kind = "skip";
11385 }
11386 this.kindCache[out.name] = {
11387 kind: out.kind,
11388 url: out.url,
11389 iconSvg: out.iconSvg
11390 };
11391 return out;
11392 }
11393 if (typeof cmd.callback === "function") {
11394 let src = "";
11395 try {
11396 src = Function.prototype.toString.call(cmd.callback);
11397 } catch {
11398 src = "";
11399 }
11400 const literal = src.match(NAV_HREF_LITERAL_RE) || src.match(NAV_ASSIGN_LITERAL_RE) || src.match(NAV_CALL_LITERAL_RE);
11401 if (literal && literal[1]) {
11402 try {
11403 out.url = new URL(literal[1], window.location.href).toString();
11404 out.kind = "navigate";
11405 } catch {
11406 out.kind = "action";
11407 }
11408 } else if (NAV_INTENT_RE.test(src)) {
11409 const isSiteEditorIntent = SITE_EDITOR_INTENT_RE.test(src);
11410 const nameMatch = isSiteEditorIntent ? out.name.match(SITE_EDITOR_NAME_RE) : null;
11411 if (nameMatch) {
11412 const entityType = nameMatch[1];
11413 const entityId = nameMatch[2];
11414 const p = `/${entityType}/${entityId}`;
11415 try {
11416 const siteEditor = new URL("site-editor.php", this.adminUrl);
11417 siteEditor.searchParams.set("p", p);
11418 siteEditor.searchParams.set("canvas", "edit");
11419 out.url = siteEditor.toString();
11420 out.kind = "navigate";
11421 } catch {
11422 out.kind = "skip";
11423 }
11424 } else {
11425 out.kind = "skip";
11426 }
11427 }
11428 }
11429 this.kindCache[out.name] = {
11430 kind: out.kind,
11431 url: out.url,
11432 iconSvg: out.iconSvg
11433 };
11434 return out;
11435 }
11436 renderIcon(icon) {
11437 const wp = window.wp;
11438 if (!wp || !wp.element || typeof wp.element.renderToString !== "function") {
11439 return "";
11440 }
11441 try {
11442 const rendered = wp.element.renderToString(icon);
11443 if (typeof rendered === "string" && rendered.toLowerCase().startsWith("<svg")) {
11444 return rendered;
11445 }
11446 } catch {
11447 }
11448 return "";
11449 }
11450 iconFor(c) {
11451 if (c.icon && c.icon.startsWith("dashicons-")) {
11452 return c.icon;
11453 }
11454 return c.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
11455 }
11456 runNavigate(url, title, icon) {
11457 return (_args, ctx) => {
11458 ctx.close();
11459 if (tryNativeUrlRemap(url)) {
11460 return;
11461 }
11462 const id = deriveWindowId(url, this.adminUrl);
11463 this.manager.open({ id, baseId: id, url, title, icon });
11464 };
11465 }
11466 runInvoke(name, title, icon) {
11467 return (_args, ctx) => {
11468 ctx.close();
11469 const cb = this.callbackCache[name];
11470 if (typeof cb !== "function") {
11471 return;
11472 }
11473 const captured = this.runWithNavCapture(cb);
11474 if (captured) {
11475 const id = deriveWindowId(captured, this.adminUrl);
11476 this.manager.open({ id, baseId: id, url: captured, title, icon });
11477 }
11478 };
11479 }
11480 /**
11481 * Invoke `cb` with navigation sinks (`document.location`,
11482 * `window.location`, `location.assign`, `location.replace`)
11483 * shadowed so any assignment is captured instead of navigating
11484 * the shell. Returns the captured URL or `null` if the callback
11485 * was a pure JS action.
11486 *
11487 * The shadow uses `Object.defineProperty` on the document /
11488 * window instance to override the prototype's accessor for the
11489 * duration of the call. `delete` afterwards unshadows so the
11490 * native setter is restored.
11491 */
11492 runWithNavCapture(cb) {
11493 let captured = null;
11494 const setCaptured = (v) => {
11495 if (captured === null && typeof v === "string" && v !== "") {
11496 captured = v;
11497 }
11498 };
11499 const realLocation = window.location;
11500 const locationProxy = new Proxy(realLocation, {
11501 get(target2, prop) {
11502 const value = target2[prop];
11503 if (prop === "assign" || prop === "replace") {
11504 return (url) => setCaptured(url);
11505 }
11506 if (typeof value === "function") {
11507 return value.bind(target2);
11508 }
11509 return value;
11510 },
11511 set(_target, prop, value) {
11512 if (prop === "href") {
11513 setCaptured(value);
11514 return true;
11515 }
11516 return true;
11517 }
11518 });
11519 const shadowed = [];
11520 const installShadow = (obj) => {
11521 try {
11522 Object.defineProperty(obj, "location", {
11523 configurable: true,
11524 get: () => locationProxy,
11525 set: (v) => setCaptured(v)
11526 });
11527 shadowed.push({ obj, key: "location" });
11528 } catch {
11529 }
11530 };
11531 installShadow(document);
11532 installShadow(window);
11533 try {
11534 cb({ close: () => {
11535 } });
11536 } catch {
11537 } finally {
11538 for (const s of shadowed) {
11539 try {
11540 delete s.obj[s.key];
11541 } catch {
11542 }
11543 }
11544 }
11545 return captured;
11546 }
11547 }
11548 const seed$2 = [];
11549 function register(def) {
11550 throwOnRegistrationErrors(
11551 "Widget",
11552 collectRegistrationErrors(def, WIDGET_CHECKS),
11553 def
11554 );
11555 const idx = seed$2.findIndex((w) => w.id === def.id);
11556 if (idx >= 0) {
11557 seed$2[idx] = def;
11558 } else {
11559 seed$2.push(def);
11560 }
11561 }
11562 function unregister(id) {
11563 const idx = seed$2.findIndex((w) => w.id === id);
11564 if (idx >= 0) {
11565 seed$2.splice(idx, 1);
11566 }
11567 }
11568 function all() {
11569 const copy = seed$2.slice();
11570 const filtered = applyFilters(HOOKS.WIDGETS, copy);
11571 if (!Array.isArray(filtered)) {
11572 if (typeof console !== "undefined") {
11573 console.warn(
11574 "[desktop-mode] `desktop-mode.widgets` filter returned a non-array; falling back to seed list."
11575 );
11576 }
11577 return copy;
11578 }
11579 return filtered.filter(isValidDef);
11580 }
11581 function get(id) {
11582 return all().find((w) => w.id === id);
11583 }
11584 const WIDGET_CHECKS = [
11585 {
11586 field: "id",
11587 message: "missing or not a non-empty string",
11588 valid: (d) => typeof d.id === "string" && d.id !== ""
11589 },
11590 {
11591 field: "label",
11592 message: "missing or not a non-empty string",
11593 valid: (d) => typeof d.label === "string" && d.label !== ""
11594 },
11595 {
11596 field: "description",
11597 message: "not a string",
11598 valid: (d) => typeof d.description === "string"
11599 },
11600 {
11601 field: "icon",
11602 message: "missing or not a non-empty string",
11603 valid: (d) => typeof d.icon === "string" && d.icon !== ""
11604 },
11605 {
11606 field: "mount",
11607 message: "not a function",
11608 valid: (d) => typeof d.mount === "function"
11609 }
11610 ];
11611 function isValidDef(def) {
11612 return collectRegistrationErrors(def, WIDGET_CHECKS).length === 0;
11613 }
11614 let active$2 = null;
11615 function openWidgetPicker(options) {
11616 if (active$2) {
11617 return;
11618 }
11619 const panel2 = document.createElement("div");
11620 panel2.className = "desktop-mode-widget-picker";
11621 panel2.setAttribute("role", "menu");
11622 panel2.setAttribute("aria-label", __("Add widget"));
11623 const title = document.createElement("div");
11624 title.className = "desktop-mode-widget-picker__title";
11625 title.textContent = __("Add widget");
11626 panel2.appendChild(title);
11627 const list2 = document.createElement("div");
11628 list2.className = "desktop-mode-widget-picker__list";
11629 panel2.appendChild(list2);
11630 paintList(list2, options);
11631 document.body.appendChild(panel2);
11632 positionPanel(panel2, options.anchor);
11633 const onOutsidePointerDown = (e) => {
11634 const target2 = e.target;
11635 if (!target2) {
11636 return;
11637 }
11638 if (panel2.contains(target2) || options.anchor.contains(target2)) {
11639 return;
11640 }
11641 closeWidgetPicker();
11642 };
11643 window.setTimeout(() => {
11644 document.addEventListener("pointerdown", onOutsidePointerDown, true);
11645 }, 0);
11646 const onKeyDown = (e) => {
11647 if (e.key === "Escape") {
11648 closeWidgetPicker();
11649 }
11650 };
11651 document.addEventListener("keydown", onKeyDown);
11652 active$2 = { panel: panel2, options, onOutsidePointerDown, onKeyDown };
11653 const first = list2.querySelector(
11654 "button:not([disabled])"
11655 );
11656 first?.focus();
11657 }
11658 function refreshWidgetPicker() {
11659 if (!active$2) {
11660 return;
11661 }
11662 const list2 = active$2.panel.querySelector(
11663 ".desktop-mode-widget-picker__list"
11664 );
11665 if (list2) {
11666 paintList(list2, active$2.options);
11667 }
11668 }
11669 function closeWidgetPicker() {
11670 if (!active$2) {
11671 return;
11672 }
11673 document.removeEventListener(
11674 "pointerdown",
11675 active$2.onOutsidePointerDown,
11676 true
11677 );
11678 document.removeEventListener("keydown", active$2.onKeyDown);
11679 active$2.panel.remove();
11680 active$2 = null;
11681 }
11682 function paintList(list2, options) {
11683 list2.innerHTML = "";
11684 const enabled = new Set(options.enabledIds());
11685 const defs = options.registry();
11686 if (defs.length === 0) {
11687 const empty = document.createElement("div");
11688 empty.className = "desktop-mode-widget-picker__empty";
11689 empty.textContent = __(
11690 "No widgets available. Activate a plugin that registers one, or see the docs for the registerWidget API."
11691 );
11692 list2.appendChild(empty);
11693 return;
11694 }
11695 for (const def of defs) {
11696 const entry = document.createElement("button");
11697 entry.type = "button";
11698 entry.className = "desktop-mode-widget-picker__entry";
11699 const isAdded = enabled.has(def.id);
11700 if (isAdded) {
11701 entry.classList.add(
11702 "desktop-mode-widget-picker__entry--added"
11703 );
11704 entry.disabled = true;
11705 entry.setAttribute("aria-disabled", "true");
11706 }
11707 entry.setAttribute("role", "menuitem");
11708 let ariaLabel;
11709 if (isAdded) {
11710 ariaLabel = sprintf(__("%s (already added)"), def.label);
11711 } else {
11712 ariaLabel = sprintf(__("Add %s"), def.label);
11713 }
11714 entry.setAttribute("aria-label", ariaLabel);
11715 const icon = document.createElement("span");
11716 icon.className = `desktop-mode-widget-picker__entry-icon dashicons ${def.icon}`;
11717 icon.setAttribute("aria-hidden", "true");
11718 entry.appendChild(icon);
11719 const textWrap = document.createElement("span");
11720 textWrap.className = "desktop-mode-widget-picker__entry-text";
11721 const label = document.createElement("span");
11722 label.className = "desktop-mode-widget-picker__entry-label";
11723 label.textContent = def.label;
11724 textWrap.appendChild(label);
11725 if (def.description) {
11726 const desc = document.createElement("span");
11727 desc.className = "desktop-mode-widget-picker__entry-description";
11728 desc.textContent = def.description;
11729 textWrap.appendChild(desc);
11730 }
11731 entry.appendChild(textWrap);
11732 if (isAdded) {
11733 const status = document.createElement("span");
11734 status.className = "desktop-mode-widget-picker__entry-status";
11735 status.textContent = __("Added");
11736 entry.appendChild(status);
11737 }
11738 if (!isAdded) {
11739 entry.addEventListener("click", (e) => {
11740 e.preventDefault();
11741 e.stopPropagation();
11742 options.onAdd(def.id);
11743 });
11744 }
11745 list2.appendChild(entry);
11746 }
11747 }
11748 function positionPanel(panel2, anchor) {
11749 const rect = anchor.getBoundingClientRect();
11750 panel2.style.position = "fixed";
11751 panel2.style.left = "0px";
11752 panel2.style.top = "0px";
11753 panel2.style.visibility = "hidden";
11754 const panelRect = panel2.getBoundingClientRect();
11755 const width = panelRect.width || 320;
11756 const height = panelRect.height || 200;
11757 const gap = 6;
11758 let left = rect.right - width;
11759 let top = rect.top - height - gap;
11760 if (left < 8) {
11761 left = 8;
11762 }
11763 if (top < 8) {
11764 top = rect.bottom + gap;
11765 }
11766 panel2.style.left = `${Math.round(left)}px`;
11767 panel2.style.top = `${Math.round(top)}px`;
11768 panel2.style.visibility = "";
11769 }
11770 const FLOATING_CLASS = "desktop-mode-widgets__card--floating";
11771 const MOVABLE_CLASS = "desktop-mode-widgets__card--movable";
11772 const RESIZABLE_CLASS = "desktop-mode-widgets__card--resizable";
11773 const DRAGGING_CLASS = "desktop-mode-widgets__card--dragging";
11774 const RESIZING_CLASS = "desktop-mode-widgets__card--resizing";
11775 const DEFAULT_MIN_WIDTH = 160;
11776 const DEFAULT_MIN_HEIGHT = 80;
11777 const DEFAULT_WIDTH$1 = 280;
11778 const DEFAULT_HEIGHT$1 = 180;
11779 const VIEWPORT_MARGIN = 20;
11780 const DRAG_THRESHOLD_PX$1 = 5;
11781 const DRAG_THRESHOLD_SQUARED = DRAG_THRESHOLD_PX$1 * DRAG_THRESHOLD_PX$1;
11782 const DRAG_EXCLUDED_SELECTORS = 'input, textarea, select, button, a, [contenteditable="true"]';
11783 function buildFrame(def, ctx, handlers) {
11784 const card = document.createElement("div");
11785 card.className = "desktop-mode-widgets__card";
11786 card.dataset.widgetId = def.id;
11787 const movable = def.movable === true;
11788 const resizable = def.resizable === true;
11789 if (movable) {
11790 card.classList.add(MOVABLE_CLASS);
11791 }
11792 if (resizable) {
11793 card.classList.add(RESIZABLE_CLASS);
11794 }
11795 if (movable) {
11796 card.appendChild(buildChrome(def, handlers.onRemove, handlers.onRedock));
11797 } else {
11798 card.appendChild(buildCornerClose(def, handlers.onRemove));
11799 }
11800 const body = document.createElement("div");
11801 body.className = "desktop-mode-widgets__card-body";
11802 card.appendChild(body);
11803 let isFloating = false;
11804 if (ctx.geometry) {
11805 applyGeometry(card, ctx.geometry);
11806 card.classList.add(FLOATING_CLASS);
11807 isFloating = true;
11808 }
11809 const resizeCleanups = [];
11810 if (resizable) {
11811 for (const dir of allHandleDirs()) {
11812 const handle = document.createElement("div");
11813 handle.className = `desktop-mode-widgets__resize desktop-mode-widgets__resize--${dir}`;
11814 handle.setAttribute("aria-hidden", "true");
11815 handle.dataset.dir = dir;
11816 card.appendChild(handle);
11817 resizeCleanups.push(
11818 attachResize(card, handle, dir, def, ctx, handlers, () => isFloating)
11819 );
11820 }
11821 }
11822 let dragCleanup = null;
11823 if (movable) {
11824 const chrome = card.querySelector(
11825 ".desktop-mode-widgets__chrome"
11826 );
11827 if (chrome) {
11828 dragCleanup = attachDrag(card, chrome, def, ctx, handlers, (next) => {
11829 isFloating = next;
11830 });
11831 }
11832 }
11833 return {
11834 card,
11835 body,
11836 dispose: () => {
11837 for (const fn of resizeCleanups) {
11838 try {
11839 fn();
11840 } catch {
11841 }
11842 }
11843 if (dragCleanup) {
11844 try {
11845 dragCleanup();
11846 } catch {
11847 }
11848 }
11849 card.remove();
11850 }
11851 };
11852 }
11853 function buildChrome(def, onRemove, onRedock) {
11854 const chrome = document.createElement("header");
11855 chrome.className = "desktop-mode-widgets__chrome";
11856 const grip = document.createElement("span");
11857 grip.className = "desktop-mode-widgets__grip";
11858 grip.setAttribute("aria-hidden", "true");
11859 chrome.appendChild(grip);
11860 const title = document.createElement("span");
11861 title.className = "desktop-mode-widgets__title";
11862 title.textContent = def.label;
11863 chrome.appendChild(title);
11864 chrome.appendChild(buildRedockButton(def, onRedock));
11865 const close = buildCloseButton(def, onRemove);
11866 chrome.appendChild(close);
11867 return chrome;
11868 }
11869 function buildRedockButton(def, onRedock) {
11870 const btn = document.createElement("button");
11871 btn.type = "button";
11872 btn.className = "desktop-mode-widgets__card-redock";
11873 btn.setAttribute(
11874 "aria-label",
11875 // translators: %s is the widget label (e.g., "Clock")
11876 sprintf(__("Dock %s back to widget column"), def.label)
11877 );
11878 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>';
11879 btn.addEventListener("click", (e) => {
11880 e.preventDefault();
11881 e.stopPropagation();
11882 onRedock();
11883 });
11884 btn.dataset.noDrag = "true";
11885 return btn;
11886 }
11887 function buildCornerClose(def, onRemove) {
11888 const close = buildCloseButton(def, onRemove);
11889 close.classList.add("desktop-mode-widgets__card-close--corner");
11890 return close;
11891 }
11892 function buildCloseButton(def, onRemove) {
11893 const close = document.createElement("button");
11894 close.type = "button";
11895 close.className = "desktop-mode-widgets__card-close";
11896 close.setAttribute("aria-label", sprintf(__("Remove %s"), def.label));
11897 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>';
11898 close.addEventListener("click", (e) => {
11899 e.preventDefault();
11900 e.stopPropagation();
11901 onRemove();
11902 });
11903 return close;
11904 }
11905 function attachDrag(card, chrome, def, ctx, handlers, setFloating) {
11906 let pointerId = null;
11907 let startX = 0;
11908 let startY = 0;
11909 let initialLeft = 0;
11910 let initialTop = 0;
11911 let committed = false;
11912 const onDown = (e) => {
11913 if (e.button !== 0) {
11914 return;
11915 }
11916 const target2 = e.target;
11917 if (target2 && target2.closest(DRAG_EXCLUDED_SELECTORS)) {
11918 return;
11919 }
11920 e.preventDefault();
11921 pointerId = e.pointerId;
11922 startX = e.clientX;
11923 startY = e.clientY;
11924 committed = false;
11925 initialLeft = parseFloat(card.style.left) || 0;
11926 initialTop = parseFloat(card.style.top) || 0;
11927 chrome.setPointerCapture(pointerId);
11928 };
11929 const commitDrag = () => {
11930 if (!card.classList.contains(FLOATING_CLASS)) {
11931 const parentRect = ctx.floatingParent.getBoundingClientRect();
11932 const rect = card.getBoundingClientRect();
11933 const initial = {
11934 x: rect.left - parentRect.left,
11935 y: rect.top - parentRect.top,
11936 width: rect.width || def.defaultWidth || DEFAULT_WIDTH$1,
11937 height: rect.height || def.defaultHeight || DEFAULT_HEIGHT$1
11938 };
11939 applyGeometry(card, initial);
11940 card.classList.add(FLOATING_CLASS);
11941 setFloating(true);
11942 handlers.onLiberate(initial);
11943 initialLeft = parseFloat(card.style.left) || 0;
11944 initialTop = parseFloat(card.style.top) || 0;
11945 }
11946 card.classList.add(DRAGGING_CLASS);
11947 };
11948 const onMove = (e) => {
11949 if (pointerId === null || e.pointerId !== pointerId) {
11950 return;
11951 }
11952 const dx = e.clientX - startX;
11953 const dy = e.clientY - startY;
11954 if (!committed) {
11955 if (dx * dx + dy * dy < DRAG_THRESHOLD_SQUARED) {
11956 return;
11957 }
11958 committed = true;
11959 commitDrag();
11960 }
11961 const clamped = clampToParent(
11962 initialLeft + dx,
11963 initialTop + dy,
11964 card.offsetWidth,
11965 card.offsetHeight,
11966 ctx.floatingParent
11967 );
11968 card.style.left = `${clamped.x}px`;
11969 card.style.top = `${clamped.y}px`;
11970 };
11971 const onUp = (e) => {
11972 if (pointerId === null || e.pointerId !== pointerId) {
11973 return;
11974 }
11975 try {
11976 chrome.releasePointerCapture(pointerId);
11977 } catch {
11978 }
11979 pointerId = null;
11980 if (!committed) {
11981 return;
11982 }
11983 committed = false;
11984 card.classList.remove(DRAGGING_CLASS);
11985 handlers.onGeometryChanged(currentGeometry(card));
11986 };
11987 chrome.addEventListener("pointerdown", onDown);
11988 chrome.addEventListener("pointermove", onMove);
11989 chrome.addEventListener("pointerup", onUp);
11990 chrome.addEventListener("pointercancel", onUp);
11991 return () => {
11992 chrome.removeEventListener("pointerdown", onDown);
11993 chrome.removeEventListener("pointermove", onMove);
11994 chrome.removeEventListener("pointerup", onUp);
11995 chrome.removeEventListener("pointercancel", onUp);
11996 };
11997 }
11998 function attachResize(card, handle, dir, def, ctx, handlers, isFloating) {
11999 let pointerId = null;
12000 let startX = 0;
12001 let startY = 0;
12002 let startLeft = 0;
12003 let startTop = 0;
12004 let startW = 0;
12005 let startH = 0;
12006 const onDown = (e) => {
12007 if (e.button !== 0) {
12008 return;
12009 }
12010 if (!isFloating() && !isHeightOnlyDir(dir)) {
12011 return;
12012 }
12013 e.preventDefault();
12014 e.stopPropagation();
12015 pointerId = e.pointerId;
12016 startX = e.clientX;
12017 startY = e.clientY;
12018 const rect = card.getBoundingClientRect();
12019 const parentRect = ctx.floatingParent.getBoundingClientRect();
12020 startLeft = rect.left - parentRect.left;
12021 startTop = rect.top - parentRect.top;
12022 startW = rect.width;
12023 startH = rect.height;
12024 handle.setPointerCapture(pointerId);
12025 card.classList.add(RESIZING_CLASS);
12026 };
12027 const onMove = (e) => {
12028 if (pointerId === null || e.pointerId !== pointerId) {
12029 return;
12030 }
12031 const dx = e.clientX - startX;
12032 const dy = e.clientY - startY;
12033 const next = computeResize(
12034 dir,
12035 dx,
12036 dy,
12037 startLeft,
12038 startTop,
12039 startW,
12040 startH,
12041 def,
12042 ctx.floatingParent,
12043 isFloating()
12044 );
12045 if (isFloating()) {
12046 card.style.left = `${next.x}px`;
12047 card.style.top = `${next.y}px`;
12048 card.style.width = `${next.width}px`;
12049 }
12050 card.style.height = `${next.height}px`;
12051 };
12052 const onUp = (e) => {
12053 if (pointerId === null || e.pointerId !== pointerId) {
12054 return;
12055 }
12056 try {
12057 handle.releasePointerCapture(pointerId);
12058 } catch {
12059 }
12060 pointerId = null;
12061 card.classList.remove(RESIZING_CLASS);
12062 handlers.onGeometryChanged(currentGeometry(card));
12063 };
12064 handle.addEventListener("pointerdown", onDown);
12065 handle.addEventListener("pointermove", onMove);
12066 handle.addEventListener("pointerup", onUp);
12067 handle.addEventListener("pointercancel", onUp);
12068 return () => {
12069 handle.removeEventListener("pointerdown", onDown);
12070 handle.removeEventListener("pointermove", onMove);
12071 handle.removeEventListener("pointerup", onUp);
12072 handle.removeEventListener("pointercancel", onUp);
12073 };
12074 }
12075 function allHandleDirs() {
12076 return ["n", "e", "s", "w", "ne", "nw", "se", "sw"];
12077 }
12078 function isHeightOnlyDir(dir) {
12079 return dir === "s";
12080 }
12081 function applyGeometry(card, geometry) {
12082 card.style.left = `${geometry.x}px`;
12083 card.style.top = `${geometry.y}px`;
12084 card.style.width = `${geometry.width}px`;
12085 card.style.height = `${geometry.height}px`;
12086 }
12087 function currentGeometry(card) {
12088 return {
12089 x: parseFloat(card.style.left) || 0,
12090 y: parseFloat(card.style.top) || 0,
12091 width: card.offsetWidth,
12092 height: card.offsetHeight
12093 };
12094 }
12095 function clampToParent(x, y, width, height, parent) {
12096 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
12097 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
12098 const maxX = Math.max(0, parentWidth - width - VIEWPORT_MARGIN);
12099 const maxY = Math.max(0, parentHeight - height - VIEWPORT_MARGIN);
12100 return {
12101 x: Math.min(Math.max(VIEWPORT_MARGIN, x), maxX),
12102 y: Math.min(Math.max(VIEWPORT_MARGIN, y), maxY)
12103 };
12104 }
12105 function computeResize(dir, dx, dy, startLeft, startTop, startW, startH, def, parent, floating) {
12106 const minW = def.minWidth ?? DEFAULT_MIN_WIDTH;
12107 const minH = def.minHeight ?? DEFAULT_MIN_HEIGHT;
12108 const maxW = def.maxWidth ?? Infinity;
12109 const maxH = def.maxHeight ?? Infinity;
12110 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
12111 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
12112 let x = startLeft;
12113 let y = startTop;
12114 let width = startW;
12115 let height = startH;
12116 if (dir === "e" || dir === "ne" || dir === "se") {
12117 width = clamp$1(startW + dx, minW, Math.min(maxW, parentWidth - startLeft));
12118 }
12119 if (dir === "w" || dir === "nw" || dir === "sw") {
12120 const nextWidth = clamp$1(startW - dx, minW, Math.min(maxW, startLeft + startW));
12121 x = startLeft + (startW - nextWidth);
12122 width = nextWidth;
12123 }
12124 if (dir === "s" || dir === "se" || dir === "sw") {
12125 height = clamp$1(
12126 startH + dy,
12127 minH,
12128 Math.min(maxH, parentHeight - startTop)
12129 );
12130 }
12131 if (dir === "n" || dir === "ne" || dir === "nw") {
12132 const nextHeight = clamp$1(startH - dy, minH, Math.min(maxH, startTop + startH));
12133 y = startTop + (startH - nextHeight);
12134 height = nextHeight;
12135 }
12136 if (!floating) {
12137 width = startW;
12138 x = startLeft;
12139 }
12140 return { x, y, width, height };
12141 }
12142 function clamp$1(value, min, max) {
12143 if (max < min) {
12144 return min;
12145 }
12146 return Math.min(Math.max(value, min), max);
12147 }
12148 const IDS_KEY = "desktop-mode-widgets";
12149 const GEOMETRY_KEY$1 = "desktop-mode-widgets-geometry";
12150 function readRawEnabled() {
12151 try {
12152 return window.localStorage.getItem(IDS_KEY);
12153 } catch {
12154 return null;
12155 }
12156 }
12157 function loadEnabledIds() {
12158 const raw = readRawEnabled();
12159 if (raw === null) {
12160 return [];
12161 }
12162 try {
12163 const parsed = JSON.parse(raw);
12164 if (!Array.isArray(parsed)) {
12165 return [];
12166 }
12167 return parsed.filter((x) => typeof x === "string");
12168 } catch {
12169 return [];
12170 }
12171 }
12172 function saveEnabledIds(ids) {
12173 try {
12174 window.localStorage.setItem(IDS_KEY, JSON.stringify(ids));
12175 } catch {
12176 }
12177 }
12178 function loadGeometry$1() {
12179 try {
12180 const raw = window.localStorage.getItem(GEOMETRY_KEY$1);
12181 if (!raw) {
12182 return {};
12183 }
12184 const parsed = JSON.parse(raw);
12185 if (!parsed || typeof parsed !== "object") {
12186 return {};
12187 }
12188 const out = {};
12189 for (const [id, rawEntry] of Object.entries(parsed)) {
12190 const entry = sanitizeGeometry(rawEntry);
12191 if (entry) {
12192 out[id] = entry;
12193 }
12194 }
12195 return out;
12196 } catch {
12197 return {};
12198 }
12199 }
12200 function saveGeometry$1(geometry) {
12201 try {
12202 window.localStorage.setItem(GEOMETRY_KEY$1, JSON.stringify(geometry));
12203 } catch {
12204 }
12205 }
12206 function sanitizeGeometry(raw) {
12207 if (!raw || typeof raw !== "object") {
12208 return null;
12209 }
12210 const { x, y, width, height } = raw;
12211 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) {
12212 return null;
12213 }
12214 return { x, y, width, height };
12215 }
12216 function createWidgetStorage(widgetId) {
12217 const prefix = `desktop-mode.widget.${widgetId}.`;
12218 const safeGet = (key) => {
12219 try {
12220 return localStorage.getItem(prefix + key);
12221 } catch {
12222 return null;
12223 }
12224 };
12225 return {
12226 get(key) {
12227 const raw = safeGet(key);
12228 if (raw === null) {
12229 return null;
12230 }
12231 try {
12232 return JSON.parse(raw);
12233 } catch {
12234 return null;
12235 }
12236 },
12237 set(key, value) {
12238 try {
12239 localStorage.setItem(prefix + key, JSON.stringify(value));
12240 } catch {
12241 }
12242 },
12243 remove(key) {
12244 try {
12245 localStorage.removeItem(prefix + key);
12246 } catch {
12247 }
12248 },
12249 clear() {
12250 try {
12251 for (let i = localStorage.length - 1; i >= 0; i--) {
12252 const key = localStorage.key(i);
12253 if (key && key.startsWith(prefix)) {
12254 localStorage.removeItem(key);
12255 }
12256 }
12257 } catch {
12258 }
12259 }
12260 };
12261 }
12262 const DEFAULT_ENABLED_IDS = ["clock"];
12263 class WidgetLayer {
12264 /**
12265 * @param root The column element (`#desktop-mode-widgets`).
12266 * @param pluginUrl Absolute plugin URL — passed to widget ctx.
12267 * @param floatingHost Parent for liberated (floating) widgets.
12268 * Defaults to the column's parent (the desktop
12269 * area) so floats are bounded by the visible
12270 * desktop, not the 320 px-wide column.
12271 */
12272 constructor(root, pluginUrl, floatingHost) {
12273 this.mounted = /* @__PURE__ */ new Map();
12274 this.generation = 0;
12275 this.root = root;
12276 this.pluginUrl = pluginUrl;
12277 this.enabledIds = loadEnabledIds();
12278 this.geometry = loadGeometry$1();
12279 this.floatingHost = floatingHost ?? root.parentElement ?? root;
12280 this.listEl = document.createElement("div");
12281 this.listEl.className = "desktop-mode-widgets__list";
12282 this.root.appendChild(this.listEl);
12283 this.addTile = this.buildAddTile();
12284 this.root.appendChild(this.addTile);
12285 this.paintEmptyState();
12286 }
12287 /**
12288 * Mount every widget the user has enabled (per localStorage).
12289 * Called once during shell boot, AFTER the registry seed has run
12290 * so built-ins are available. Safe to call multiple times — the
12291 * `mounted` map dedupes.
12292 */
12293 hydrate() {
12294 if (readRawEnabled() === null) {
12295 this.enabledIds = DEFAULT_ENABLED_IDS.filter(
12296 (id) => !!get(id)
12297 );
12298 saveEnabledIds(this.enabledIds);
12299 }
12300 for (const id of this.enabledIds) {
12301 if (this.mounted.has(id)) {
12302 continue;
12303 }
12304 this.mountById(id);
12305 }
12306 this.paintEmptyState();
12307 }
12308 /**
12309 * Add a widget by id — called by the picker after the user
12310 * selects an available entry. Idempotent.
12311 */
12312 add(id) {
12313 if (this.enabledIds.includes(id)) {
12314 return;
12315 }
12316 if (!get(id)) {
12317 return;
12318 }
12319 this.enabledIds.push(id);
12320 saveEnabledIds(this.enabledIds);
12321 this.mountById(id);
12322 this.paintEmptyState();
12323 doAction(HOOKS.WIDGET_ADDED, { id });
12324 refreshWidgetPicker();
12325 }
12326 /**
12327 * Remove a widget by id — called from the card's × button and
12328 * from the picker. Idempotent.
12329 */
12330 remove(id) {
12331 const before = this.enabledIds.length;
12332 this.enabledIds = this.enabledIds.filter((e) => e !== id);
12333 if (this.enabledIds.length === before) {
12334 return;
12335 }
12336 saveEnabledIds(this.enabledIds);
12337 if (this.geometry[id]) {
12338 delete this.geometry[id];
12339 saveGeometry$1(this.geometry);
12340 }
12341 this.unmountById(id);
12342 this.paintEmptyState();
12343 doAction(HOOKS.WIDGET_REMOVED, { id });
12344 refreshWidgetPicker();
12345 }
12346 /** Public read for the picker / external callers. */
12347 getEnabledIds() {
12348 return [...this.enabledIds];
12349 }
12350 /**
12351 * Mount a widget ONLY if it's already in the user's enabled
12352 * list AND not currently mounted. No-op when the widget isn't
12353 * enabled (user never opted in) and no-op when it's already on
12354 * screen. Used by the server-driven sync: when a plugin
12355 * activates mid-session, its widget def registers via the
12356 * sync's path; if the user had previously enabled that widget
12357 * (in a prior session or before the plugin was deactivated),
12358 * we want to bring it back on screen without toggling the
12359 * "enabled" state or firing a `WIDGET_ADDED` action.
12360 *
12361 * The net behaviour is "rehydrate this one widget now that
12362 * its def is finally registered," which is subtly different
12363 * from `ensureMounted` (which OPT-INs the user into enabling
12364 * the widget for the first time).
12365 */
12366 mountIfEnabled(id) {
12367 if (!get(id)) {
12368 return;
12369 }
12370 if (!this.enabledIds.includes(id)) {
12371 return;
12372 }
12373 if (this.mounted.has(id)) {
12374 return;
12375 }
12376 this.mountById(id);
12377 this.paintEmptyState();
12378 }
12379 /**
12380 * Unmount a widget without touching the persisted enablement.
12381 * Used by the server-driven widget-registry sync: when a plugin
12382 * deactivates mid-session, its widget defs disappear from the
12383 * registry and we need to pull any mounted instance off the
12384 * screen — but we deliberately KEEP the id in the user's
12385 * enabled list so re-activating the plugin re-mounts it
12386 * automatically through `hydrate()`.
12387 *
12388 * Idempotent; a no-op when the widget isn't currently mounted.
12389 */
12390 unmount(id) {
12391 if (!this.mounted.has(id)) {
12392 return;
12393 }
12394 this.unmountById(id);
12395 this.paintEmptyState();
12396 }
12397 /**
12398 * Guarantee the widget identified by `id` is currently mounted,
12399 * adding it to the enabled list if it isn't. No-op when the
12400 * widget is already on screen. Intended for companion plugins
12401 * that want to pin their widget programmatically — a monitor
12402 * plugin that auto-pins itself on the first error burst, a
12403 * first-run onboarding flow that ensures the quick-start widget
12404 * is present, etc.
12405 *
12406 * Returns `true` when the widget is mounted (either newly added
12407 * or already present), `false` when the id isn't registered —
12408 * callers can branch on the failure without having to maintain
12409 * their own registry snapshot.
12410 */
12411 ensureMounted(id) {
12412 if (!get(id)) {
12413 return false;
12414 }
12415 if (this.enabledIds.includes(id)) {
12416 return true;
12417 }
12418 this.add(id);
12419 return true;
12420 }
12421 /**
12422 * Tear down every widget. Called on shell unload via `pagehide`
12423 * so intervals / RAF loops stop before the beacon flush.
12424 */
12425 disposeAll() {
12426 for (const id of Array.from(this.mounted.keys())) {
12427 this.unmountById(id);
12428 }
12429 }
12430 // --- Internal ---------------------------------------------------
12431 mountById(id) {
12432 const def = get(id);
12433 if (!def) {
12434 return;
12435 }
12436 const gen = ++this.generation;
12437 const initialGeometry = def.movable === true ? this.geometry[id] : void 0;
12438 const frame = buildFrame(
12439 def,
12440 { floatingParent: this.floatingHost, geometry: initialGeometry },
12441 {
12442 onRemove: () => this.remove(id),
12443 onGeometryChanged: (geom) => this.persistGeometry(id, geom),
12444 onLiberate: (geom) => this.liberate(id, geom),
12445 onRedock: () => this.redock(id)
12446 }
12447 );
12448 const floating = !!initialGeometry;
12449 const record = {
12450 id,
12451 frame,
12452 generation: gen,
12453 teardown: null,
12454 floating
12455 };
12456 this.mounted.set(id, record);
12457 this.placeCard(frame.card, floating);
12458 const ctx = {
12459 id,
12460 pluginUrl: this.pluginUrl,
12461 storage: createWidgetStorage(id)
12462 };
12463 doAction(HOOKS.WIDGET_MOUNTING, { id, container: frame.body, ctx });
12464 const onResolve = (teardown) => {
12465 const current = this.mounted.get(id);
12466 if (!current || current.generation !== gen) {
12467 try {
12468 teardown();
12469 } catch {
12470 }
12471 return;
12472 }
12473 current.teardown = teardown;
12474 doAction(HOOKS.WIDGET_MOUNTED, { id, container: frame.body, ctx });
12475 };
12476 let result;
12477 try {
12478 result = def.mount(frame.body, ctx);
12479 } catch (err) {
12480 this.handleMountFailure(id, err);
12481 return;
12482 }
12483 if (isThenable(result)) {
12484 result.then(onResolve, (err) => {
12485 if (this.mounted.get(id)?.generation === gen) {
12486 this.handleMountFailure(id, err);
12487 }
12488 });
12489 return;
12490 }
12491 onResolve(result);
12492 }
12493 unmountById(id) {
12494 const record = this.mounted.get(id);
12495 if (!record) {
12496 return;
12497 }
12498 doAction(HOOKS.WIDGET_UNMOUNTING, { id });
12499 try {
12500 record.teardown?.();
12501 } catch (err) {
12502 doAction(HOOKS.SHELL_ERROR, { scope: "widget-teardown", id, error: err });
12503 if (typeof console !== "undefined") {
12504 console.error(
12505 `[desktop-mode] Widget "${id}" teardown threw:`,
12506 err
12507 );
12508 }
12509 }
12510 this.generation++;
12511 record.frame.dispose();
12512 this.mounted.delete(id);
12513 }
12514 handleMountFailure(id, err) {
12515 const record = this.mounted.get(id);
12516 if (record) {
12517 record.frame.dispose();
12518 this.mounted.delete(id);
12519 }
12520 doAction(HOOKS.WIDGET_MOUNT_FAILED, { id, error: err });
12521 doAction(HOOKS.SHELL_ERROR, { scope: "widget-mount", id, error: err });
12522 if (typeof console !== "undefined") {
12523 console.error(
12524 `[desktop-mode] Widget "${id}" failed to mount:`,
12525 err
12526 );
12527 }
12528 }
12529 buildAddTile() {
12530 const tile2 = document.createElement("button");
12531 tile2.type = "button";
12532 tile2.className = "desktop-mode-widgets__add";
12533 tile2.setAttribute("aria-label", __("Add widget"));
12534 const plus = document.createElement("span");
12535 plus.className = "desktop-mode-widgets__add-plus";
12536 plus.setAttribute("aria-hidden", "true");
12537 plus.textContent = "+";
12538 const label = document.createElement("span");
12539 label.className = "desktop-mode-widgets__add-label";
12540 label.textContent = __("Add widget");
12541 tile2.appendChild(plus);
12542 tile2.appendChild(label);
12543 tile2.addEventListener("click", (e) => {
12544 e.preventDefault();
12545 e.stopPropagation();
12546 openWidgetPicker({
12547 anchor: tile2,
12548 registry: () => all(),
12549 enabledIds: () => [...this.enabledIds],
12550 onAdd: (id) => this.add(id)
12551 });
12552 });
12553 return tile2;
12554 }
12555 /**
12556 * Drop a card into the right parent based on its floating state.
12557 * Docked cards append to the column list above the `+` tile;
12558 * floating cards append to the desktop-area-level host so they
12559 * sit above the wallpaper and can range across the viewport.
12560 */
12561 placeCard(card, floating) {
12562 if (floating) {
12563 this.floatingHost.appendChild(card);
12564 } else {
12565 this.listEl.appendChild(card);
12566 }
12567 }
12568 /**
12569 * Move a widget from the column into the floating host. Called by
12570 * the frame on the user's first drag of a movable widget.
12571 */
12572 liberate(id, geometry) {
12573 const record = this.mounted.get(id);
12574 if (!record || record.floating) {
12575 return;
12576 }
12577 record.floating = true;
12578 this.floatingHost.appendChild(record.frame.card);
12579 applyGeometry(record.frame.card, geometry);
12580 this.persistGeometry(id, geometry);
12581 this.paintEmptyState();
12582 }
12583 /**
12584 * Inverse of {@link liberate}: move a floating card back into
12585 * the column and drop its persisted geometry so a subsequent
12586 * shell boot brings it up docked. Called when the user clicks
12587 * the re-dock button in the card's chrome header, or
12588 * programmatically by companion plugins via
12589 * `wp.desktop.widgets.redock( id )` /
12590 * `wp.desktop.widgetLayer.redock( id )`.
12591 *
12592 * Idempotent — a docked widget silently no-ops, an unknown id
12593 * silently no-ops. The `--floating` class on the card is
12594 * removed as part of the same write so CSS rules that depend
12595 * on it (re-dock button visibility, absolute positioning) flip
12596 * back in one paint.
12597 *
12598 * @since 0.7.0 (private)
12599 * @since 0.25.0 (public)
12600 */
12601 redock(id) {
12602 const record = this.mounted.get(id);
12603 if (!record || !record.floating) {
12604 return;
12605 }
12606 record.floating = false;
12607 if (this.geometry[id]) {
12608 delete this.geometry[id];
12609 saveGeometry$1(this.geometry);
12610 }
12611 const card = record.frame.card;
12612 card.classList.remove("desktop-mode-widgets__card--floating");
12613 card.style.left = "";
12614 card.style.top = "";
12615 card.style.width = "";
12616 card.style.height = "";
12617 this.listEl.appendChild(card);
12618 this.paintEmptyState();
12619 }
12620 persistGeometry(id, geometry) {
12621 this.geometry[id] = geometry;
12622 saveGeometry$1(this.geometry);
12623 }
12624 /**
12625 * Toggle a `--has-widgets` modifier so CSS can hide the column's
12626 * decorative backdrop when nothing's mounted (keeps the empty
12627 * state clean — just the `+` tile floating in the corner).
12628 *
12629 * Floating widgets don't count toward "has widgets" in the column
12630 * sense — if every enabled widget is floating, the column itself
12631 * shows only the empty state + add tile.
12632 */
12633 paintEmptyState() {
12634 let docked = 0;
12635 for (const record of this.mounted.values()) {
12636 if (!record.floating) {
12637 docked++;
12638 }
12639 }
12640 this.root.classList.toggle(
12641 "desktop-mode-widgets--has-widgets",
12642 docked > 0
12643 );
12644 }
12645 }
12646 function isThenable(x) {
12647 return !!x && (typeof x === "object" || typeof x === "function") && typeof x.then === "function";
12648 }
12649 const DEFAULT_NATIVE_MIN_WIDTH = 280;
12650 const DEFAULT_NATIVE_MIN_HEIGHT = 220;
12651 const DEFAULT_NATIVE_WIDTH = 520;
12652 const DEFAULT_NATIVE_HEIGHT = 400;
12653 function buildIframeContentRender(cfg, cleanups, windowId) {
12654 return (body) => {
12655 const iframe = document.createElement("iframe");
12656 iframe.style.width = "100%";
12657 iframe.style.height = "100%";
12658 iframe.style.border = "0";
12659 iframe.setAttribute("src", cfg.url);
12660 if (typeof cfg.sandbox === "string" && cfg.sandbox !== "") {
12661 iframe.setAttribute("sandbox", cfg.sandbox);
12662 }
12663 body.style.padding = "0";
12664 body.appendChild(iframe);
12665 const unregisterSynth = registerSyntheticIframe(windowId, iframe);
12666 cleanups.push(unregisterSynth);
12667 let targetOrigin;
12668 try {
12669 targetOrigin = new URL(cfg.url, window.location.origin).origin;
12670 } catch {
12671 targetOrigin = window.location.origin;
12672 }
12673 let resolveReady = null;
12674 const readyPromise = new Promise((resolve2) => {
12675 resolveReady = resolve2;
12676 });
12677 const onLoad = () => {
12678 if (cfg.bridge) {
12679 try {
12680 const doc = iframe.contentDocument;
12681 if (doc && !doc.querySelector("script[data-desktop-mode-iframe-bridge]")) {
12682 const bridgeUrl = window.desktopModeConfig?.iframeBridgeUrl;
12683 if (bridgeUrl) {
12684 const s = doc.createElement("script");
12685 s.src = bridgeUrl;
12686 s.setAttribute("data-desktop-mode-iframe-bridge", "1");
12687 doc.head?.appendChild(s);
12688 }
12689 }
12690 } catch {
12691 }
12692 }
12693 markWindowContentReady(windowId);
12694 resolveReady?.();
12695 };
12696 iframe.addEventListener("load", onLoad);
12697 const onMessage = (e) => {
12698 if (!iframe.contentWindow || e.source !== iframe.contentWindow) {
12699 return;
12700 }
12701 if (e.origin !== targetOrigin && e.origin !== window.location.origin) {
12702 return;
12703 }
12704 const data = e.data;
12705 if (data && typeof data === "object" && typeof data.type === "string" && data.type.startsWith("desktop-mode-bridge-")) {
12706 const bridgeRouter = window.__desktopModeConnectionBridge;
12707 bridgeRouter?.routeIncomingFromIframe(data, windowId);
12708 }
12709 if (data && typeof data === "object" && data.type === "desktop-mode-window-publish" && typeof data.channel === "string" && data.channel !== "") {
12710 dispatchFromWindow(
12711 windowId,
12712 data.channel,
12713 data.payload
12714 );
12715 }
12716 try {
12717 cfg.onMessage?.(e.data);
12718 } catch (err) {
12719 if (typeof console !== "undefined") {
12720 console.error(
12721 "[desktop-mode] iframeContent.onMessage threw:",
12722 err
12723 );
12724 }
12725 }
12726 };
12727 window.addEventListener("message", onMessage);
12728 cleanups.push(() => {
12729 window.removeEventListener("message", onMessage);
12730 iframe.removeEventListener("load", onLoad);
12731 });
12732 return readyPromise;
12733 };
12734 }
12735 function createRegisterWindow(manager) {
12736 return async (def) => {
12737 const userRender = def.render;
12738 let render2 = userRender;
12739 const cleanups = [];
12740 if (def.iframeContent) {
12741 if (userRender && typeof console !== "undefined") {
12742 console.warn(
12743 "[desktop-mode] registerWindow: both `render` and `iframeContent` provided — ignoring `render` and using the iframe shorthand. Drop one."
12744 );
12745 }
12746 render2 = buildIframeContentRender(
12747 def.iframeContent,
12748 cleanups,
12749 def.id
12750 );
12751 }
12752 const userOnClose = def.onClose;
12753 const onClose = cleanups.length ? () => {
12754 for (const fn of cleanups) {
12755 try {
12756 fn();
12757 } catch {
12758 }
12759 }
12760 userOnClose?.();
12761 } : userOnClose;
12762 const win = await manager.open({
12763 id: def.id,
12764 baseId: def.baseId || def.id,
12765 native: true,
12766 url: def.url || `#${def.id}`,
12767 title: def.title,
12768 icon: def.icon,
12769 x: def.x ?? 0,
12770 y: def.y ?? 0,
12771 width: def.width ?? DEFAULT_NATIVE_WIDTH,
12772 height: def.height ?? DEFAULT_NATIVE_HEIGHT,
12773 minWidth: def.minWidth ?? DEFAULT_NATIVE_MIN_WIDTH,
12774 minHeight: def.minHeight ?? DEFAULT_NATIVE_MIN_HEIGHT,
12775 render: render2,
12776 onClose,
12777 onResize: def.onResize,
12778 autofocus: def.autofocus,
12779 initialState: def.initialState,
12780 ownerHandle: def.ownerHandle,
12781 multi: def.multi,
12782 desktopId: def.desktopId
12783 });
12784 return win;
12785 };
12786 }
12787 let onWindowInstanceCounter = 0;
12788 function onWindow(id, handlers, options = {}) {
12789 const namespace = `desktop-mode/on-window/${id}/${++onWindowInstanceCounter}`;
12790 const persistent = options.persistent === true;
12791 const bindings = [
12792 ["opened", HOOKS.WINDOW_OPENED],
12793 ["reopened", HOOKS.WINDOW_REOPENED],
12794 ["focused", HOOKS.WINDOW_FOCUSED],
12795 ["blurred", HOOKS.WINDOW_BLURRED],
12796 ["closing", HOOKS.WINDOW_CLOSING],
12797 ["closed", HOOKS.WINDOW_CLOSED],
12798 ["minimized", HOOKS.WINDOW_MINIMIZED],
12799 ["restored", HOOKS.WINDOW_RESTORED],
12800 ["maximized", HOOKS.WINDOW_MAXIMIZED],
12801 ["unmaximized", HOOKS.WINDOW_UNMAXIMIZED],
12802 ["fullscreenEntered", HOOKS.WINDOW_FULLSCREEN_ENTERED],
12803 ["fullscreenExited", HOOKS.WINDOW_FULLSCREEN_EXITED],
12804 ["resized", HOOKS.WINDOW_RESIZED],
12805 ["bodyResized", HOOKS.WINDOW_BODY_RESIZED],
12806 ["boundsChanged", HOOKS.WINDOW_BOUNDS_CHANGED]
12807 ];
12808 const registered = [];
12809 let disposed = false;
12810 const unsubscribe = () => {
12811 if (disposed) {
12812 return;
12813 }
12814 disposed = true;
12815 for (const hookName2 of registered) {
12816 removeAction(hookName2, namespace);
12817 }
12818 };
12819 for (const [key, hookName2] of bindings) {
12820 const handler = handlers[key];
12821 if (!handler) {
12822 continue;
12823 }
12824 registered.push(hookName2);
12825 addAction(hookName2, namespace, (payload) => {
12826 const p = payload;
12827 if (p.windowId !== id) {
12828 return;
12829 }
12830 const { windowId: _w, ...rest } = p;
12831 handler(rest);
12832 if (key === "closed" && !persistent) {
12833 unsubscribe();
12834 }
12835 });
12836 }
12837 return unsubscribe;
12838 }
12839 function createNativeWindowSync(deps2) {
12840 const { manager, appendSystemTile, removeSystemTile } = deps2;
12841 const registered = /* @__PURE__ */ new Set();
12842 const injectedTemplates = /* @__PURE__ */ new Set();
12843 const loadedScripts = /* @__PURE__ */ new Set();
12844 const loadedStyles = /* @__PURE__ */ new Set();
12845 const entriesById = /* @__PURE__ */ new Map();
12846 const resolveSizeForEntry = (entry) => {
12847 const saved = loadNativeWindowGeometry(entry.id);
12848 if (!saved) {
12849 return { width: entry.width, height: entry.height };
12850 }
12851 return {
12852 width: Math.max(saved.width, entry.minWidth),
12853 height: Math.max(saved.height, entry.minHeight)
12854 };
12855 };
12856 const ensureTemplate = (entry) => {
12857 if (injectedTemplates.has(entry.templateId)) {
12858 return;
12859 }
12860 if (document.getElementById(entry.templateId)) {
12861 injectedTemplates.add(entry.templateId);
12862 return;
12863 }
12864 if (!entry.templateHtml) {
12865 return;
12866 }
12867 const tpl = document.createElement("template");
12868 tpl.id = entry.templateId;
12869 tpl.innerHTML = entry.templateHtml;
12870 document.body.appendChild(tpl);
12871 injectedTemplates.add(entry.templateId);
12872 };
12873 const ensureStyle = (entry) => {
12874 const url = entry.styleUrl;
12875 if (!url || loadedStyles.has(url)) {
12876 return;
12877 }
12878 const safeUrl = url.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
12879 const existing = document.head.querySelector(
12880 `link[rel="stylesheet"][href="${safeUrl}"]`
12881 );
12882 if (!existing) {
12883 const link = document.createElement("link");
12884 link.rel = "stylesheet";
12885 link.href = url;
12886 if (entry.styleHandle) {
12887 link.dataset.desktopModeStyleHandle = entry.styleHandle;
12888 }
12889 document.head.appendChild(link);
12890 }
12891 if (Array.isArray(entry.styleInline)) {
12892 for (const css2 of entry.styleInline) {
12893 if (typeof css2 !== "string" || css2 === "") {
12894 continue;
12895 }
12896 const style = document.createElement("style");
12897 if (entry.styleHandle) {
12898 style.dataset.desktopModeStyleHandle = entry.styleHandle;
12899 }
12900 style.textContent = css2;
12901 document.head.appendChild(style);
12902 }
12903 }
12904 loadedStyles.add(url);
12905 };
12906 const ensureScript = async (entry) => {
12907 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
12908 return;
12909 }
12910 try {
12911 await loadVendorScript(entry.scriptUrl, {
12912 translations: entry.scriptTranslations,
12913 l10n: entry.scriptL10n,
12914 before: entry.scriptBefore,
12915 after: entry.scriptAfter
12916 });
12917 } catch (err) {
12918 doAction(HOOKS.SHELL_ERROR, {
12919 scope: "native-window-script-load",
12920 id: entry.id,
12921 error: err
12922 });
12923 }
12924 loadedScripts.add(entry.scriptUrl);
12925 };
12926 const openFromEntry = (entry) => {
12927 const globalRegistry = window.desktopModeNativeWindows || {};
12928 const render2 = globalRegistry[entry.id];
12929 const finalRender = (body, ctx) => {
12930 body.appendChild(cloneTemplate(entry.templateId));
12931 return render2?.(body, ctx);
12932 };
12933 const size = resolveSizeForEntry(entry);
12934 void manager.open({
12935 id: entry.id,
12936 baseId: entry.id,
12937 native: true,
12938 url: `#${entry.id}`,
12939 title: entry.title,
12940 icon: entry.icon,
12941 width: size.width,
12942 height: size.height,
12943 minWidth: entry.minWidth,
12944 minHeight: entry.minHeight,
12945 render: finalRender,
12946 autofocus: entry.autofocus,
12947 ownerHandle: entry.ownerHandle || entry.scriptHandle
12948 });
12949 };
12950 const openNewFromEntry = (entry) => {
12951 const globalRegistry = window.desktopModeNativeWindows || {};
12952 const render2 = globalRegistry[entry.id];
12953 const finalRender = (body, ctx) => {
12954 body.appendChild(cloneTemplate(entry.templateId));
12955 return render2?.(body, ctx);
12956 };
12957 const size = resolveSizeForEntry(entry);
12958 void manager.openNew({
12959 id: entry.id,
12960 baseId: entry.id,
12961 native: true,
12962 url: `#${entry.id}`,
12963 title: entry.title,
12964 icon: entry.icon,
12965 width: size.width,
12966 height: size.height,
12967 minWidth: entry.minWidth,
12968 minHeight: entry.minHeight,
12969 initialState: "normal",
12970 render: finalRender,
12971 autofocus: entry.autofocus,
12972 ownerHandle: entry.ownerHandle || entry.scriptHandle
12973 });
12974 };
12975 const registerTile = async (entry) => {
12976 if (registered.has(entry.id)) {
12977 return;
12978 }
12979 if ("none" === entry.placement) {
12980 ensureTemplate(entry);
12981 ensureStyle(entry);
12982 await ensureScript(entry);
12983 registered.add(entry.id);
12984 return;
12985 }
12986 ensureTemplate(entry);
12987 ensureStyle(entry);
12988 await ensureScript(entry);
12989 appendSystemTile({
12990 id: entry.id,
12991 title: entry.title,
12992 icon: entry.icon,
12993 isOpen: () => !!manager.getById(entry.id),
12994 onOpen: () => openFromEntry(entry)
12995 });
12996 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: entry.id });
12997 registered.add(entry.id);
12998 };
12999 const unregisterTile = (id) => {
13000 if (!registered.has(id)) {
13001 return;
13002 }
13003 removeSystemTile(id);
13004 registered.delete(id);
13005 entriesById.delete(id);
13006 };
13007 const sync = async (list2) => {
13008 const incoming = /* @__PURE__ */ new Set();
13009 for (const entry of list2) {
13010 incoming.add(entry.id);
13011 entriesById.set(entry.id, entry);
13012 }
13013 for (const id of Array.from(registered)) {
13014 if (!incoming.has(id)) {
13015 unregisterTile(id);
13016 }
13017 }
13018 for (const entry of list2) {
13019 if (!registered.has(entry.id)) {
13020 await registerTile(entry);
13021 }
13022 }
13023 };
13024 const openById = (id, opts = {}) => {
13025 const entry = entriesById.get(id);
13026 if (!entry) {
13027 return false;
13028 }
13029 activity.publish("desktop-mode/open-requested", {
13030 windowId: id,
13031 source: opts.source ?? "api"
13032 });
13033 openFromEntry(entry);
13034 return true;
13035 };
13036 const openNewById = (id, opts = {}) => {
13037 const entry = entriesById.get(id);
13038 if (!entry) {
13039 return false;
13040 }
13041 activity.publish("desktop-mode/open-requested", {
13042 windowId: id,
13043 source: opts.source ?? "api"
13044 });
13045 openNewFromEntry(entry);
13046 return true;
13047 };
13048 addAction(
13049 HOOKS.WINDOW_RESIZE_END,
13050 "desktop-mode-native-window-geometry",
13051 (payload) => {
13052 const p = payload;
13053 const windowId = p?.windowId;
13054 const width = p?.width;
13055 const height = p?.height;
13056 if (!windowId || typeof width !== "number" || typeof height !== "number") {
13057 return;
13058 }
13059 const win = manager.getById(windowId);
13060 if (!win) {
13061 return;
13062 }
13063 if (win.state !== "normal") {
13064 return;
13065 }
13066 const baseId = win.config.baseId || win.id;
13067 saveNativeWindowGeometry(baseId, { width, height });
13068 if (win.element) {
13069 saveNativeWindowPosition(baseId, {
13070 x: win.element.offsetLeft,
13071 y: win.element.offsetTop
13072 });
13073 }
13074 }
13075 );
13076 addAction(
13077 HOOKS.WINDOW_DRAG_END,
13078 "desktop-mode-native-window-geometry",
13079 (payload) => {
13080 const windowId = payload?.windowId;
13081 if (!windowId) {
13082 return;
13083 }
13084 const win = manager.getById(windowId);
13085 if (!win) {
13086 return;
13087 }
13088 if (win.state !== "normal") {
13089 return;
13090 }
13091 if (!win.element) {
13092 return;
13093 }
13094 const baseId = win.config.baseId || win.id;
13095 saveNativeWindowGeometry(baseId, {
13096 width: win.element.offsetWidth,
13097 height: win.element.offsetHeight
13098 });
13099 saveNativeWindowPosition(baseId, {
13100 x: win.element.offsetLeft,
13101 y: win.element.offsetTop
13102 });
13103 }
13104 );
13105 addAction(
13106 HOOKS.WINDOW_MAXIMIZED,
13107 "desktop-mode-native-window-geometry",
13108 (payload) => {
13109 const windowId = payload?.windowId;
13110 if (!windowId) {
13111 return;
13112 }
13113 const win = manager.getById(windowId);
13114 if (!win) {
13115 return;
13116 }
13117 const baseId = win.config.baseId || win.id;
13118 const entry = entriesById.get(baseId);
13119 const defaults = entry ? { width: entry.width, height: entry.height } : { width: win.config.width, height: win.config.height };
13120 setNativeWindowSavedState(baseId, "maximized", defaults);
13121 }
13122 );
13123 addAction(
13124 HOOKS.WINDOW_UNMAXIMIZED,
13125 "desktop-mode-native-window-geometry",
13126 (payload) => {
13127 const windowId = payload?.windowId;
13128 if (!windowId) {
13129 return;
13130 }
13131 const win = manager.getById(windowId);
13132 if (!win) {
13133 return;
13134 }
13135 const baseId = win.config.baseId || win.id;
13136 setNativeWindowSavedState(baseId, null);
13137 }
13138 );
13139 return { sync, openById, openNewById };
13140 }
13141 function cloneTemplate(template) {
13142 let tpl = null;
13143 if (typeof template === "string") {
13144 const found = document.getElementById(template);
13145 if (found instanceof HTMLTemplateElement) {
13146 tpl = found;
13147 }
13148 } else {
13149 tpl = template;
13150 }
13151 if (!tpl) {
13152 throw new Error(
13153 `[desktop-mode] cloneTemplate: no <template> found for ${typeof template === "string" ? `#${template}` : "<reference>"}`
13154 );
13155 }
13156 return tpl.content.cloneNode(true);
13157 }
13158 function renderIcon(icon, opts) {
13159 const className = opts.className ?? "";
13160 const title = opts.title ?? "";
13161 if (typeof icon === "string" && icon.startsWith("dashicons-")) {
13162 const el = document.createElement("span");
13163 el.className = `dashicons ${icon} ${className}`.trim();
13164 el.setAttribute("aria-hidden", "true");
13165 return el;
13166 }
13167 if (typeof icon === "string" && icon.startsWith("data:image/svg+xml;base64,")) {
13168 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
13169 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
13170 const el = document.createElement("span");
13171 el.className = className;
13172 el.setAttribute("aria-hidden", "true");
13173 el.style.backgroundImage = `url("${icon}")`;
13174 el.style.backgroundRepeat = "no-repeat";
13175 el.style.backgroundPosition = "center";
13176 el.style.backgroundSize = "contain";
13177 el.style.display = "inline-block";
13178 return el;
13179 }
13180 }
13181 if (typeof icon === "string" && /^data:image\/(png|jpeg|jpg|gif|webp|x-icon|vnd\.microsoft\.icon);base64,/i.test(icon)) {
13182 const commaIdx = icon.indexOf(",");
13183 const payload = commaIdx >= 0 ? icon.slice(commaIdx + 1) : "";
13184 if (/^[A-Za-z0-9+/=]+$/.test(payload)) {
13185 return makeImgIcon(icon, className);
13186 }
13187 }
13188 if (typeof icon === "string" && (icon.startsWith("http://") || icon.startsWith("https://"))) {
13189 return makeImgIcon(icon, className);
13190 }
13191 const span = document.createElement("span");
13192 span.className = `${className} desktop-mode-icon-letter`.trim();
13193 span.setAttribute("aria-hidden", "true");
13194 const letters = letterFromTitle(title);
13195 span.textContent = letters;
13196 const hue = hashTitleToHue(title);
13197 span.style.backgroundColor = `hsl( ${hue}, 60%, 45% )`;
13198 span.style.color = "#fff";
13199 span.style.display = "inline-flex";
13200 span.style.alignItems = "center";
13201 span.style.justifyContent = "center";
13202 span.style.fontWeight = "600";
13203 span.style.borderRadius = "4px";
13204 return span;
13205 }
13206 function makeImgIcon(src, className) {
13207 const img = document.createElement("img");
13208 img.className = className;
13209 img.src = src;
13210 img.alt = "";
13211 img.setAttribute("aria-hidden", "true");
13212 img.draggable = false;
13213 return img;
13214 }
13215 function letterFromTitle(title) {
13216 const trimmed = (title ?? "").trim();
13217 if (trimmed === "") {
13218 return "?";
13219 }
13220 const words = trimmed.split(/\s+/);
13221 if (words.length >= 2) {
13222 return (words[0][0] + words[1][0]).toUpperCase();
13223 }
13224 const first = words[0];
13225 if (first.length >= 2) {
13226 return first.slice(0, 2).toUpperCase();
13227 }
13228 return first.toUpperCase();
13229 }
13230 const BADGE_CLASS = "desktop-mode-icon__badge";
13231 const _badges = /* @__PURE__ */ new Map();
13232 function _safeBadge(count) {
13233 return Math.max(0, Math.floor(Number(count) || 0));
13234 }
13235 function setIconBadge(iconId, count) {
13236 if (!iconId) {
13237 return;
13238 }
13239 const tile2 = _findIconTile(iconId);
13240 if (!tile2) {
13241 return;
13242 }
13243 const safe = _safeBadge(count);
13244 const previous = _badges.get(iconId) ?? 0;
13245 if (safe === previous) {
13246 return;
13247 }
13248 if (safe === 0) {
13249 _badges.delete(iconId);
13250 } else {
13251 _badges.set(iconId, safe);
13252 }
13253 _paintBadgeNode(tile2, safe);
13254 activity.publish("desktop-mode/badge-changed", {
13255 itemId: iconId,
13256 count: safe,
13257 rail: "icon"
13258 });
13259 doAction(HOOKS.ICON_BADGE_CHANGED, {
13260 iconId,
13261 count: safe,
13262 previousCount: previous
13263 });
13264 }
13265 function clearIconBadge(iconId) {
13266 setIconBadge(iconId, 0);
13267 }
13268 function getIconBadge(iconId) {
13269 return _badges.get(iconId) ?? 0;
13270 }
13271 const iconsApi = {
13272 setBadge: setIconBadge,
13273 clearBadge: clearIconBadge,
13274 getBadge: getIconBadge
13275 };
13276 function fingerprintIcons(icons) {
13277 if (!icons || icons.length === 0) {
13278 return "";
13279 }
13280 return icons.map(
13281 (i) => `${i.id}|${i.title}|${i.icon}|${i.window ?? ""}|${i.url ?? ""}|${i.position ?? 0}|${i.pinned ? 1 : 0}`
13282 ).join(";");
13283 }
13284 let _lastFingerprint = "";
13285 function renderDesktopIcons(host, icons, deps2) {
13286 const fp = fingerprintIcons(icons);
13287 if (fp === _lastFingerprint && host.querySelector(":scope > .desktop-mode-icons")) {
13288 return;
13289 }
13290 _lastFingerprint = fp;
13291 const existing = host.querySelector(":scope > .desktop-mode-icons");
13292 if (existing) {
13293 existing.remove();
13294 }
13295 if (!icons || icons.length === 0) {
13296 return;
13297 }
13298 const container = document.createElement("div");
13299 container.className = "desktop-mode-icons";
13300 container.setAttribute("role", "list");
13301 container.setAttribute("aria-label", __("Desktop icons"));
13302 const ordered = [...icons].sort((a, b) => {
13303 const ap = a.pinned ? 0 : 1;
13304 const bp = b.pinned ? 0 : 1;
13305 return ap - bp;
13306 });
13307 const tiles = /* @__PURE__ */ new Map();
13308 for (const entry of ordered) {
13309 const tile2 = buildIcon(entry, deps2);
13310 const stored = _badges.get(entry.id) ?? 0;
13311 if (stored > 0) {
13312 _paintBadgeNode(tile2, stored);
13313 }
13314 container.appendChild(tile2);
13315 tiles.set(entry.id, tile2);
13316 }
13317 host.appendChild(container);
13318 doAction(HOOKS.DESKTOP_ICONS_RENDERED, {
13319 ids: (icons ?? []).map((i) => i.id),
13320 container,
13321 tiles
13322 });
13323 }
13324 function _findIconTile(iconId) {
13325 if (!iconId) {
13326 return null;
13327 }
13328 const container = document.querySelector(
13329 ".desktop-mode-icons"
13330 );
13331 if (!container) {
13332 return null;
13333 }
13334 return container.querySelector(
13335 `[data-icon-id="${_cssEscape(iconId)}"]`
13336 );
13337 }
13338 function _paintBadgeNode(host, count) {
13339 const existing = host.querySelector(
13340 `:scope > .${BADGE_CLASS}`
13341 );
13342 if (count <= 0) {
13343 existing?.remove();
13344 return;
13345 }
13346 const display = count > 99 ? "99+" : String(count);
13347 const ariaLabel = sprintf(
13348 // translators: %d is the number of pending items in a desktop-icon badge.
13349 _n("%d notification", "%d notifications", count),
13350 count
13351 );
13352 if (existing) {
13353 if (existing.textContent !== display) {
13354 existing.textContent = display;
13355 }
13356 existing.setAttribute("aria-label", ariaLabel);
13357 return;
13358 }
13359 const badge = document.createElement("span");
13360 badge.className = BADGE_CLASS;
13361 badge.textContent = display;
13362 badge.setAttribute("aria-label", ariaLabel);
13363 host.appendChild(badge);
13364 }
13365 function _cssEscape(value) {
13366 const c = window.CSS;
13367 return c?.escape ? c.escape(value) : value;
13368 }
13369 function buildIcon(entry, deps2) {
13370 const tile2 = document.createElement("button");
13371 tile2.type = "button";
13372 tile2.className = entry.pinned ? "desktop-mode-icon desktop-mode-icon--pinned" : "desktop-mode-icon";
13373 tile2.dataset.iconId = entry.id;
13374 if (entry.pinned) {
13375 tile2.dataset.pinned = "1";
13376 }
13377 tile2.setAttribute("role", "listitem");
13378 tile2.setAttribute("aria-label", entry.title);
13379 const icon = renderIcon(entry.icon, {
13380 title: entry.title,
13381 className: "desktop-mode-icon__image"
13382 });
13383 tile2.appendChild(icon);
13384 const label = document.createElement("span");
13385 label.className = "desktop-mode-icon__label";
13386 label.textContent = entry.title;
13387 tile2.appendChild(label);
13388 tile2.addEventListener("click", (e) => {
13389 e.stopPropagation();
13390 doAction(HOOKS.DESKTOP_ICON_CLICKED, {
13391 id: entry.id,
13392 target: entry.window ? "window" : "url"
13393 });
13394 openTarget(entry, deps2);
13395 });
13396 tile2.addEventListener("contextmenu", (e) => {
13397 if (entry.pinned) {
13398 return;
13399 }
13400 e.preventDefault();
13401 e.stopPropagation();
13402 openItemVisibilityMenu({
13403 x: e.clientX,
13404 y: e.clientY,
13405 id: entry.id,
13406 title: entry.title,
13407 surface: "desktop"
13408 });
13409 });
13410 return tile2;
13411 }
13412 function openTarget(entry, deps2) {
13413 if (entry.window) {
13414 const opened = deps2.openWindow(entry.window);
13415 if (!opened) {
13416 return;
13417 }
13418 return;
13419 }
13420 if (entry.url) {
13421 if (tryOpenExternalUrl(entry.url)) {
13422 return;
13423 }
13424 try {
13425 const parsed = new URL(entry.url, window.location.origin);
13426 const windowId = deps2.deriveWindowId(parsed.toString());
13427 void deps2.manager.open({
13428 id: windowId,
13429 baseId: windowId,
13430 url: parsed.toString(),
13431 title: entry.title,
13432 icon: entry.icon
13433 });
13434 } catch {
13435 }
13436 }
13437 }
13438 const SIDE_DOCK_ID = "desktop-mode-side-dock";
13439 function coreItemToIconEntry(item, index2) {
13440 return {
13441 id: `dock-core:${item.id}`,
13442 title: item.title,
13443 icon: item.icon,
13444 window: "",
13445 url: item.url,
13446 // Synthesized icons render after server-registered ones; the
13447 // large offset leaves headroom for plugin authors who set
13448 // explicit `position` values.
13449 position: 1e3 + index2
13450 };
13451 }
13452 function createLayoutDispatcher(deps2, initialLayout, initialDockItems, initialServerIcons) {
13453 let layout = initialLayout;
13454 let items = initialDockItems;
13455 let serverIcons = initialServerIcons ?? [];
13456 let primary = null;
13457 let side = null;
13458 let primaryDock = null;
13459 let sideDock = null;
13460 let sideDockEl = null;
13461 const systemTiles = /* @__PURE__ */ new Map();
13462 const railFor = (affinity) => {
13463 if (affinity === "core" && side) {
13464 return side;
13465 }
13466 return primary;
13467 };
13468 const ensureSideDockEl = () => {
13469 const existing = document.getElementById(
13470 SIDE_DOCK_ID
13471 );
13472 if (existing) {
13473 return existing;
13474 }
13475 const el = document.createElement("nav");
13476 el.id = SIDE_DOCK_ID;
13477 el.className = "desktop-mode-dock";
13478 el.setAttribute("role", "toolbar");
13479 el.setAttribute("aria-label", "Core admin navigation");
13480 deps2.shellBody.insertBefore(el, deps2.shellBody.firstChild);
13481 return el;
13482 };
13483 const removeSideDockEl = () => {
13484 if (sideDockEl && sideDockEl.parentNode) {
13485 sideDockEl.parentNode.removeChild(sideDockEl);
13486 }
13487 sideDockEl = null;
13488 };
13489 const readSettings = () => deps2.getSettings?.() ?? { itemVisibility: {}, dockOrder: [] };
13490 const effectiveDockItems = () => {
13491 const dockedNativeWindows = /* @__PURE__ */ new Set();
13492 for (const entry of systemTiles.values()) {
13493 dockedNativeWindows.add(entry.item.id);
13494 }
13495 return applyDockPlacement(
13496 items,
13497 serverIcons,
13498 readSettings(),
13499 dockedNativeWindows
13500 );
13501 };
13502 const partition = () => {
13503 const effective = effectiveDockItems();
13504 const core = [];
13505 const plugin = [];
13506 for (const item of effective) {
13507 if (item.isCore) {
13508 core.push(item);
13509 } else {
13510 plugin.push(item);
13511 }
13512 }
13513 return { core, plugin };
13514 };
13515 const repaintIcons = () => {
13516 const settings = readSettings();
13517 if (layout !== "spatial") {
13518 deps2.renderIcons(
13519 applyDesktopPlacement(serverIcons, items, settings.itemVisibility)
13520 );
13521 return;
13522 }
13523 const { core } = partition();
13524 const synthesized = core.map(coreItemToIconEntry);
13525 const explicitlyPromoted = [];
13526 let synthIndex = 0;
13527 for (const item of items) {
13528 const placement = settings.itemVisibility[item.id];
13529 if (placement === "desktop" || placement === "both") {
13530 explicitlyPromoted.push({
13531 id: `dock:${item.id}`,
13532 title: item.title,
13533 icon: item.icon,
13534 window: "",
13535 url: item.url || "",
13536 position: 2e3 + synthIndex++
13537 });
13538 }
13539 }
13540 deps2.renderIcons([...synthesized, ...explicitlyPromoted]);
13541 };
13542 const tearDownDocks = () => {
13543 if (primary) {
13544 try {
13545 primary.destroy();
13546 } catch (err) {
13547 doAction(HOOKS.SHELL_ERROR, {
13548 scope: "dock-rail-renderer/destroy",
13549 error: err
13550 });
13551 }
13552 primary = null;
13553 primaryDock = null;
13554 }
13555 if (side) {
13556 try {
13557 side.destroy();
13558 } catch (err) {
13559 doAction(HOOKS.SHELL_ERROR, {
13560 scope: "dock-rail-renderer/destroy",
13561 error: err
13562 });
13563 }
13564 side = null;
13565 sideDock = null;
13566 }
13567 };
13568 const mountRail = (mountDeps) => {
13569 const renderer = resolveActive();
13570 if (!renderer) {
13571 doAction(HOOKS.SHELL_ERROR, {
13572 scope: "dock-rail-renderer",
13573 message: "No dock rail renderer is registered."
13574 });
13575 return null;
13576 }
13577 try {
13578 return renderer.mount(mountDeps);
13579 } catch (err) {
13580 doAction(HOOKS.SHELL_ERROR, {
13581 scope: "dock-rail-renderer/mount",
13582 rendererId: renderer.id,
13583 error: err
13584 });
13585 if (renderer === defaultDockRailRenderer) {
13586 return null;
13587 }
13588 try {
13589 return defaultDockRailRenderer.mount(mountDeps);
13590 } catch {
13591 return null;
13592 }
13593 }
13594 };
13595 const buildMountDeps = (container, railItems, orientation) => ({
13596 container,
13597 items: railItems,
13598 // `fullMenu` is the complete admin-menu list. Renderers that
13599 // want to ignore the layout's partitioning (e.g., paint
13600 // every menu item in one ring regardless of `isCore`) read
13601 // this instead of `items`. Snapshot per-mount so a renderer
13602 // holding the array sees a stable list; live updates flow
13603 // through `replaceItems`.
13604 fullMenu: items.slice(),
13605 // Same idea for system tiles — OS Settings, plugin-owned
13606 // native-window launchers, etc. Lets a renderer apply
13607 // uniform treatment across menu + system cohorts in one
13608 // pass. Live updates flow through `appendSystemItem` /
13609 // `removeSystemItem`.
13610 fullSystemTiles: Array.from(systemTiles.values()).map(
13611 (entry) => entry.item
13612 ),
13613 orientation,
13614 windowManager: deps2.windowManager,
13615 adminUrl: deps2.adminUrl,
13616 // `openItem` / `openSubmenuPick` / `openSystemItem` /
13617 // `requestSubmenu` are routing callbacks for custom
13618 // renderers. They mirror exactly what the default renderer
13619 // (`Dock.openPage` / `Dock.openSubmenuPick`) does internally
13620 // — same `deriveWindowId(url, adminUrl)` call, same window-
13621 // config shape — so a custom renderer addresses the same
13622 // window with the same id at runtime. Switching renderer
13623 // mid-session doesn't lose the user's open windows.
13624 openItem: (item) => {
13625 const baseId = deriveWindowId(item.url, deps2.adminUrl);
13626 deps2.windowManager.open({
13627 id: baseId,
13628 baseId,
13629 url: item.url,
13630 parentUrl: item.url,
13631 title: item.title,
13632 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
13633 submenu: item.submenu,
13634 multi: !!item.multi
13635 });
13636 },
13637 openSubmenuPick: (item, sub) => {
13638 deps2.windowManager.open({
13639 id: deriveWindowId(sub.url, deps2.adminUrl),
13640 baseId: deriveWindowId(item.url, deps2.adminUrl),
13641 url: sub.url,
13642 // Pin the synthetic parent tab to the dock landing
13643 // page, not to the sub-page the user picked. Without
13644 // this, a submenu-pick (e.g. clicking "Editor" inside
13645 // Appearance's submenu popover) would open at
13646 // site-editor.php with no way back to themes.php.
13647 parentUrl: item.url,
13648 title: item.title,
13649 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
13650 submenu: item.submenu,
13651 multi: !!item.multi
13652 });
13653 },
13654 openSystemItem: (item) => item.onOpen()
13655 });
13656 const buildDocksForCurrentLayout = () => {
13657 tearDownDocks();
13658 const { core, plugin } = partition();
13659 if (layout === "classic") {
13660 sideDockEl = ensureSideDockEl();
13661 side = mountRail(
13662 buildMountDeps(sideDockEl, core, "left")
13663 );
13664 sideDock = unwrapDefaultDock(side);
13665 primary = mountRail(
13666 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
13667 );
13668 primaryDock = unwrapDefaultDock(primary);
13669 } else if (layout === "unified") {
13670 removeSideDockEl();
13671 primary = mountRail(
13672 buildMountDeps(deps2.bottomDockEl, effectiveDockItems(), "bottom")
13673 );
13674 primaryDock = unwrapDefaultDock(primary);
13675 } else {
13676 removeSideDockEl();
13677 primary = mountRail(
13678 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
13679 );
13680 primaryDock = unwrapDefaultDock(primary);
13681 }
13682 for (const entry of systemTiles.values()) {
13683 railFor(entry.affinity)?.appendSystemItem(entry.item);
13684 }
13685 };
13686 const dispatcher = {
13687 getLayout: () => layout,
13688 getPrimary: () => primaryDock,
13689 getSide: () => sideDock,
13690 setLayout: (next) => {
13691 if (next === layout) {
13692 return;
13693 }
13694 layout = next;
13695 deps2.shellRoot.setAttribute("data-desktop-mode-layout", next);
13696 buildDocksForCurrentLayout();
13697 repaintIcons();
13698 document.dispatchEvent(
13699 new CustomEvent("desktop-mode-layout-changed", {
13700 detail: {
13701 layout: next,
13702 primary: primaryDock,
13703 side: sideDock
13704 }
13705 })
13706 );
13707 },
13708 applyDockItems: (nextItems) => {
13709 items = nextItems;
13710 const { core, plugin } = partition();
13711 if (layout === "classic") {
13712 side?.replaceItems(core);
13713 primary?.replaceItems(plugin);
13714 } else if (layout === "unified") {
13715 primary?.replaceItems(effectiveDockItems());
13716 } else {
13717 primary?.replaceItems(plugin);
13718 }
13719 repaintIcons();
13720 },
13721 applyDesktopIcons: (next) => {
13722 serverIcons = next ?? [];
13723 repaintIcons();
13724 },
13725 appendSystemTile: (item, affinity = "plugin") => {
13726 systemTiles.set(item.id, { item, affinity });
13727 railFor(affinity)?.appendSystemItem(item);
13728 },
13729 removeSystemTile: (id) => {
13730 const entry = systemTiles.get(id);
13731 if (!entry) {
13732 return;
13733 }
13734 systemTiles.delete(id);
13735 railFor(entry.affinity)?.removeSystemItem(id);
13736 },
13737 listSystemTiles: () => Array.from(systemTiles.values()).map((entry) => ({
13738 id: entry.item.id,
13739 title: entry.item.title,
13740 icon: entry.item.icon,
13741 affinity: entry.affinity
13742 })),
13743 getSystemTile: (id) => systemTiles.get(id)?.item ?? null,
13744 getMenuItems: () => items.slice(),
13745 refresh: () => {
13746 const { core, plugin } = partition();
13747 if (layout === "classic") {
13748 side?.replaceItems(core);
13749 primary?.replaceItems(plugin);
13750 } else if (layout === "unified") {
13751 primary?.replaceItems(effectiveDockItems());
13752 } else {
13753 primary?.replaceItems(plugin);
13754 }
13755 repaintIcons();
13756 },
13757 destroy: () => {
13758 tearDownDocks();
13759 removeSideDockEl();
13760 }
13761 };
13762 deps2.shellRoot.setAttribute("data-desktop-mode-layout", layout);
13763 buildDocksForCurrentLayout();
13764 repaintIcons();
13765 let lastResolvedId = resolveActive()?.id ?? null;
13766 subscribe$3(() => {
13767 const nextId2 = resolveActive()?.id ?? null;
13768 if (nextId2 === lastResolvedId) {
13769 return;
13770 }
13771 lastResolvedId = nextId2;
13772 buildDocksForCurrentLayout();
13773 repaintIcons();
13774 document.dispatchEvent(
13775 new CustomEvent("desktop-mode-layout-changed", {
13776 detail: {
13777 layout,
13778 primary: primaryDock,
13779 side: sideDock
13780 }
13781 })
13782 );
13783 });
13784 return dispatcher;
13785 }
13786 function loadImpl(scriptUrl) {
13787 if (window.desktopModeCreateAiAssistant) {
13788 return Promise.resolve(window.desktopModeCreateAiAssistant);
13789 }
13790 return new Promise((resolve2, reject) => {
13791 const existing = document.querySelector(
13792 `script[data-desktop-mode-ai="1"]`
13793 );
13794 const finish = () => {
13795 const factory = window.desktopModeCreateAiAssistant;
13796 if (!factory) {
13797 reject(
13798 new Error(
13799 "[desktop-mode] ai-assistant bundle loaded but did not register desktopModeCreateAiAssistant"
13800 )
13801 );
13802 return;
13803 }
13804 resolve2(factory);
13805 };
13806 if (existing) {
13807 if (window.desktopModeCreateAiAssistant) {
13808 finish();
13809 } else {
13810 existing.addEventListener("load", finish);
13811 existing.addEventListener(
13812 "error",
13813 () => reject(new Error("failed to load ai-assistant bundle"))
13814 );
13815 }
13816 return;
13817 }
13818 const s = document.createElement("script");
13819 s.src = scriptUrl;
13820 s.async = true;
13821 s.dataset.desktopModeAi = "1";
13822 s.addEventListener("load", finish);
13823 s.addEventListener(
13824 "error",
13825 () => reject(new Error("failed to load ai-assistant bundle"))
13826 );
13827 document.head.appendChild(s);
13828 });
13829 }
13830 class AiAssistantStub {
13831 constructor(config, scriptUrl) {
13832 this._real = null;
13833 this._loadPromise = null;
13834 this._pendingAsk = null;
13835 this._intendOpen = false;
13836 this.ask = (...args) => {
13837 return this._ensure().then((r) => r.ask(...args));
13838 };
13839 this._config = config;
13840 this._scriptUrl = scriptUrl;
13841 }
13842 _ensure() {
13843 if (this._loadPromise) {
13844 return this._loadPromise;
13845 }
13846 this._loadPromise = loadImpl(this._scriptUrl).then((factory) => {
13847 const real = factory(this._config);
13848 if (this._pendingAsk) {
13849 real.attachAsk(this._pendingAsk);
13850 }
13851 this._real = real;
13852 return real;
13853 });
13854 return this._loadPromise;
13855 }
13856 open() {
13857 this._intendOpen = true;
13858 void this._ensure().then((r) => r.open());
13859 }
13860 close() {
13861 this._intendOpen = false;
13862 if (this._real) {
13863 this._real.close();
13864 }
13865 }
13866 toggle() {
13867 if (this.isOpen) {
13868 this.close();
13869 } else {
13870 this.open();
13871 }
13872 }
13873 get isOpen() {
13874 return this._real ? this._real.isOpen : this._intendOpen;
13875 }
13876 /**
13877 * Late-bind the programmatic `ask` callback. Mirrors the real
13878 * class's `attachAsk` signature so `desktop.ts`'s call site is
13879 * identical whether it's wiring the stub or the impl.
13880 */
13881 attachAsk(fn) {
13882 this._pendingAsk = fn;
13883 if (this._real) {
13884 this._real.attachAsk(fn);
13885 }
13886 }
13887 }
13888 const isAbortError = (err) => {
13889 if (!err || typeof err !== "object") {
13890 return false;
13891 }
13892 return err.name === "AbortError";
13893 };
13894 const normaliseToolsOpt = (tools) => {
13895 if (!tools) {
13896 return [];
13897 }
13898 const all2 = listAiCallableCommands();
13899 if (tools === true || tools === "aiCallable") {
13900 return all2;
13901 }
13902 if (Array.isArray(tools)) {
13903 const allowed = new Set(tools.map((s) => s.toLowerCase()));
13904 return all2.filter((c) => allowed.has(c.slug));
13905 }
13906 if (typeof tools === "function") {
13907 return all2.filter((c) => {
13908 try {
13909 return tools(c.slug) === true;
13910 } catch {
13911 return false;
13912 }
13913 });
13914 }
13915 return [];
13916 };
13917 const normaliseSystemPrompt = (sp) => {
13918 if (!sp) {
13919 return null;
13920 }
13921 if (typeof sp === "string") {
13922 return { text: sp, mode: "append" };
13923 }
13924 if (typeof sp === "object" && typeof sp.text === "string" && sp.text !== "") {
13925 return {
13926 text: sp.text,
13927 mode: sp.mode === "replace" ? "replace" : "append"
13928 };
13929 }
13930 return null;
13931 };
13932 function liftMessage(payloadMessage, result) {
13933 const seed2 = payloadMessage ?? "";
13934 if (seed2 !== "") {
13935 return seed2;
13936 }
13937 if (typeof result === "string" && result !== "") {
13938 return result;
13939 }
13940 if (result && typeof result === "object" && "message" in result && typeof result.message === "string") {
13941 return result.message;
13942 }
13943 return "";
13944 }
13945 function serialiseOutcome(result) {
13946 if (result === void 0) {
13947 return { value: null };
13948 }
13949 if (typeof result === "object" && result !== null) {
13950 return result;
13951 }
13952 return { value: result };
13953 }
13954 function createAsk(deps2) {
13955 const postToSearch = async (body, signal) => {
13956 const config = deps2.config();
13957 const url = config.aiSearchUrl ?? "";
13958 const nonce = config.restNonce ?? "";
13959 if (!url || !nonce) {
13960 throw new Error(
13961 "[desktop-mode] wp.desktop.ai.ask: aiSearchUrl / restNonce missing from config. AI Copilot may not be enabled."
13962 );
13963 }
13964 try {
13965 return await trackedFetch$1(
13966 url,
13967 {
13968 method: "POST",
13969 credentials: "same-origin",
13970 headers: {
13971 "Content-Type": "application/json",
13972 "X-WP-Nonce": nonce
13973 },
13974 body: JSON.stringify(body),
13975 signal
13976 },
13977 { source: "desktop-mode/ai-ask" }
13978 );
13979 } catch (err) {
13980 if (isAbortError(err)) {
13981 throw err;
13982 }
13983 throw new Error(
13984 `[desktop-mode] wp.desktop.ai.ask: network error — ${String(
13985 err?.message ?? err
13986 )}`
13987 );
13988 }
13989 };
13990 const dispatchToolCall = async (payload, opts) => {
13991 const slug = payload.tool?.slug ?? "";
13992 const args = payload.tool?.args ?? "";
13993 const cmd = findCommand(slug);
13994 if (!cmd) {
13995 return {
13996 ok: false,
13997 response: {
13998 answer_type: "tool_call",
13999 message: `Command /${slug} was not registered on this page.`,
14000 entity: null,
14001 admin_links: null,
14002 toolCall: {
14003 slug,
14004 args,
14005 result: { error: "command_not_found" }
14006 },
14007 request_id: payload.request_id
14008 }
14009 };
14010 }
14011 const ctx = opts.commandContext ?? deps2.fallbackContext();
14012 let result;
14013 try {
14014 result = await Promise.resolve(cmd.run(args, ctx));
14015 } catch (err) {
14016 result = { error: String(err?.message ?? err) };
14017 }
14018 return { ok: true, slug, args, result };
14019 };
14020 const composeFollowUp = async (text, slug, args, result, sp, signal) => {
14021 const body = {
14022 query: text,
14023 follow_up: {
14024 tool: { slug, args },
14025 result: serialiseOutcome(result)
14026 }
14027 };
14028 if (sp) {
14029 body.system_prompt_text = sp.text;
14030 body.system_prompt_mode = sp.mode;
14031 }
14032 let res;
14033 try {
14034 res = await postToSearch(body, signal);
14035 } catch (err) {
14036 if (isAbortError(err)) {
14037 throw err;
14038 }
14039 return null;
14040 }
14041 if (!res.ok) {
14042 return null;
14043 }
14044 const payload = await res.json().catch(() => ({}));
14045 const message = typeof payload.message === "string" ? payload.message.trim() : "";
14046 return message !== "" ? payload.message ?? null : null;
14047 };
14048 return async function ask(query, opts = {}) {
14049 const text = (query ?? "").trim();
14050 if (text === "") {
14051 const hasMeaningfulOpts = opts.tools !== void 0 || opts.systemPrompt !== void 0 || opts.followUp === true || opts.resumeTool !== void 0 || opts.commandContext !== void 0;
14052 if (hasMeaningfulOpts) {
14053 throw new Error(
14054 "[desktop-mode] wp.desktop.ai.ask: empty query passed with non-default options — likely a caller bug. Provide a query or call without options."
14055 );
14056 }
14057 return {
14058 answer_type: "chat",
14059 message: "",
14060 entity: null,
14061 admin_links: null
14062 };
14063 }
14064 const commandTools = normaliseToolsOpt(opts.tools);
14065 const sp = normaliseSystemPrompt(opts.systemPrompt);
14066 const body = { query: text };
14067 if (opts.resumeTool) {
14068 body.resume_tool = opts.resumeTool;
14069 }
14070 if (typeof opts.startOffset === "number") {
14071 body.start_offset = opts.startOffset;
14072 }
14073 if (commandTools.length > 0) {
14074 body.command_tools = commandTools;
14075 }
14076 if (sp) {
14077 body.system_prompt_text = sp.text;
14078 body.system_prompt_mode = sp.mode;
14079 }
14080 const res = await postToSearch(body, opts.signal);
14081 if (!res.ok) {
14082 const detail = await res.json().catch(() => ({ message: res.statusText }));
14083 throw new Error(
14084 `[desktop-mode] wp.desktop.ai.ask: HTTP ${res.status} — ${detail.message ?? res.statusText}`
14085 );
14086 }
14087 const payload = await res.json();
14088 if (payload.answer_type !== "tool_call" || !payload.tool) {
14089 return {
14090 answer_type: payload.answer_type,
14091 message: payload.message ?? "",
14092 entity: payload.entity ?? null,
14093 admin_links: payload.admin_links ?? null,
14094 request_id: payload.request_id,
14095 continue: payload.continue ?? null
14096 };
14097 }
14098 const dispatch2 = await dispatchToolCall(payload, opts);
14099 if (!dispatch2.ok) {
14100 return dispatch2.response;
14101 }
14102 const { slug, args, result } = dispatch2;
14103 let message = liftMessage(payload.message, result);
14104 if (opts.followUp === true) {
14105 const composed = await composeFollowUp(
14106 text,
14107 slug,
14108 args,
14109 result,
14110 sp,
14111 opts.signal
14112 );
14113 if (composed !== null) {
14114 message = composed;
14115 }
14116 }
14117 return {
14118 answer_type: "tool_call",
14119 message,
14120 entity: null,
14121 admin_links: null,
14122 toolCall: { slug, args, result },
14123 request_id: payload.request_id
14124 };
14125 };
14126 }
14127 const EVENT_NAME = "desktop-mode-broadcast";
14128 const POSTMESSAGE_TYPE = "desktop-mode-broadcast";
14129 const ORIGIN = window.location.origin;
14130 let _manager = null;
14131 function attachBroadcastBus(manager) {
14132 _manager = manager;
14133 }
14134 function broadcast(topic, payload) {
14135 const filteredTopic = String(
14136 applyFilters("desktop-mode.broadcast.topic", topic, { payload }) ?? topic
14137 );
14138 const filteredPayload = applyFilters(
14139 "desktop-mode.broadcast.payload",
14140 payload,
14141 { topic: filteredTopic }
14142 );
14143 const detail = {
14144 topic: filteredTopic,
14145 payload: filteredPayload
14146 };
14147 document.dispatchEvent(new CustomEvent(EVENT_NAME, { detail }));
14148 doAction(HOOKS.BROADCAST, detail);
14149 activity.publish(
14150 filteredTopic,
14151 filteredPayload
14152 );
14153 if (!_manager) {
14154 return;
14155 }
14156 const message = {
14157 type: POSTMESSAGE_TYPE,
14158 topic: filteredTopic,
14159 payload: filteredPayload
14160 };
14161 for (const win of _manager._stack) {
14162 const target2 = win.iframe?.contentWindow;
14163 if (!target2) {
14164 continue;
14165 }
14166 try {
14167 target2.postMessage(message, ORIGIN);
14168 } catch (err) {
14169 }
14170 }
14171 }
14172 function subscribe$2(topic, cb) {
14173 const handler = (e) => {
14174 const detail = e.detail;
14175 if (!detail) {
14176 return;
14177 }
14178 if (topic !== "*" && detail.topic !== topic) {
14179 return;
14180 }
14181 try {
14182 cb(detail.payload, { topic: detail.topic });
14183 } catch (err) {
14184 doAction(HOOKS.SHELL_ERROR, {
14185 scope: "broadcast-subscriber",
14186 topic: detail.topic,
14187 error: err
14188 });
14189 }
14190 };
14191 document.addEventListener(EVENT_NAME, handler);
14192 return () => document.removeEventListener(EVENT_NAME, handler);
14193 }
14194 function installBroadcastReceiver() {
14195 window.addEventListener("message", (e) => {
14196 if (e.origin !== ORIGIN) {
14197 return;
14198 }
14199 const data = e.data;
14200 if (!data || data.type !== POSTMESSAGE_TYPE) {
14201 return;
14202 }
14203 if (data._fromParent) {
14204 return;
14205 }
14206 if (typeof data.topic !== "string") {
14207 return;
14208 }
14209 broadcast(data.topic, data.payload);
14210 });
14211 }
14212 const LOG_PREFIX = "[desktop-mode-bin badge]";
14213 function log(...args) {
14214 try {
14215 if (window.localStorage?.getItem("desktopModeBinDebug")) {
14216 console.info(LOG_PREFIX, ...args);
14217 }
14218 } catch {
14219 }
14220 }
14221 function warn(...args) {
14222 console.warn(LOG_PREFIX, ...args);
14223 }
14224 const TARGET_ID = "desktop-mode-recycle-bin";
14225 const HEARTBEAT_FIELD$1 = "desktop_mode_recycle_bin_seen_ts";
14226 function getDesktopApi() {
14227 return window.wp?.desktop;
14228 }
14229 const store$3 = createSharedStore(
14230 "desktop-mode/recycle-bin/badge",
14231 () => ({
14232 current: 0,
14233 seenTs: 0,
14234 started: false,
14235 countUrl: ""
14236 })
14237 );
14238 function setRecycleBinBadge(next) {
14239 const safe = Math.max(0, Math.floor(next));
14240 const prev = store$3.state.current;
14241 store$3.state.current = safe;
14242 log("setRecycleBinBadge", { prev, next: safe });
14243 paintBadge(safe);
14244 }
14245 function adjustRecycleBinBadge(delta) {
14246 setRecycleBinBadge(store$3.state.current + delta);
14247 }
14248 function _currentRecycleBinBadge() {
14249 return store$3.state.current;
14250 }
14251 function paintBadge(count) {
14252 const desktop = getDesktopApi();
14253 const active2 = isBinWindowActive();
14254 const visible = active2 ? 0 : count;
14255 log("paintBadge", { count, visible, active: active2 });
14256 desktop?.dock?.setBadge?.(TARGET_ID, visible);
14257 desktop?.taskbar?.setBadge?.(TARGET_ID, visible);
14258 desktop?.icons?.setBadge?.(TARGET_ID, visible);
14259 }
14260 function isBinWindowActive() {
14261 return !!getDesktopApi()?.windowManager?.isActive?.(TARGET_ID);
14262 }
14263 function startRecycleBinBadge(initialRaw, countUrl = "") {
14264 const initial = Number(initialRaw) || 0;
14265 const cfg = window.desktopModeConfig;
14266 const cfgCount = cfg?.recycleBinCount;
14267 const cfgUrl = cfg?.recycleBinCountUrl;
14268 const cfgDebug = cfg?.desktopModeBinDebug;
14269 log("startRecycleBinBadge entry", {
14270 initial,
14271 countUrl,
14272 alreadyStarted: store$3.state.started,
14273 cfgCount,
14274 cfgUrl,
14275 cfgDebug,
14276 readyState: document.readyState
14277 });
14278 const cfgCountNum = Number(cfgCount);
14279 const cfgCountIsHealthy = (typeof cfgCount === "number" || typeof cfgCount === "string") && Number.isFinite(cfgCountNum);
14280 if (!cfgCountIsHealthy) {
14281 warn(
14282 "desktopModeConfig.recycleBinCount is missing — PHP filter `desktop_mode_shell_config` did not deliver. Check your PHP error log for `[desktop-mode-bin debug]` lines.",
14283 { cfg }
14284 );
14285 }
14286 if (store$3.state.started) {
14287 setRecycleBinBadge(initial);
14288 return;
14289 }
14290 store$3.state.started = true;
14291 store$3.state.countUrl = countUrl;
14292 store$3.state.seenTs = Date.now();
14293 setRecycleBinBadge(initial);
14294 wireDockTileSignal();
14295 wireDesktopIconsSignal();
14296 wireBroadcastDeltas();
14297 wirePostMessageFastPath();
14298 wireHeartbeatProbe();
14299 wireWindowLifecycleSignals();
14300 }
14301 function wireWindowLifecycleSignals() {
14302 const ns = "desktop-mode/recycle-bin/badge-lifecycle";
14303 const repaint = (payload) => {
14304 const detail = payload;
14305 if (detail?.windowId !== TARGET_ID) {
14306 return;
14307 }
14308 paintBadge(store$3.state.current);
14309 };
14310 addAction(HOOKS.WINDOW_OPENED, ns, repaint);
14311 addAction(HOOKS.WINDOW_FOCUSED, ns, repaint);
14312 addAction(HOOKS.WINDOW_BLURRED, ns, repaint);
14313 addAction(HOOKS.WINDOW_MINIMIZED, ns, repaint);
14314 addAction(HOOKS.WINDOW_RESTORED, ns, repaint);
14315 addAction(HOOKS.WINDOW_CLOSED, ns, repaint);
14316 addAction(HOOKS.WINDOW_REOPENED, ns, repaint);
14317 }
14318 function wireDockTileSignal() {
14319 addAction(
14320 HOOKS.DOCK_ITEM_APPENDED,
14321 "desktop-mode/recycle-bin/badge",
14322 (payload) => {
14323 if (payload?.id === TARGET_ID) {
14324 paintBadge(store$3.state.current);
14325 }
14326 }
14327 );
14328 }
14329 function wireDesktopIconsSignal() {
14330 addAction(
14331 HOOKS.DESKTOP_ICONS_RENDERED,
14332 "desktop-mode/recycle-bin/badge",
14333 (payload) => {
14334 if (payload?.ids?.includes(TARGET_ID)) {
14335 paintBadge(store$3.state.current);
14336 }
14337 }
14338 );
14339 }
14340 function wireBroadcastDeltas() {
14341 const onDomain = (payload) => {
14342 const detail = payload;
14343 if (!detail) {
14344 return;
14345 }
14346 const ids = Array.isArray(detail.ids) ? detail.ids.length : 0;
14347 switch (detail.action) {
14348 case "trashed":
14349 adjustRecycleBinBadge(+ids);
14350 break;
14351 case "untrashed":
14352 case "deleted":
14353 adjustRecycleBinBadge(-ids);
14354 break;
14355 }
14356 };
14357 subscribe$2("desktop-mode.post.changed", onDomain);
14358 subscribe$2("desktop-mode.page.changed", onDomain);
14359 subscribe$2("desktop-mode.attachment.changed", onDomain);
14360 subscribe$2("desktop-mode.comment.changed", onDomain);
14361 subscribe$2("desktop-mode.placement.changed", onDomain);
14362 subscribe$2("desktop-mode.shortcut.changed", onDomain);
14363 subscribe$2("desktop-mode.folder.changed", onDomain);
14364 }
14365 function wirePostMessageFastPath() {
14366 const expectedOrigin = window.location.origin;
14367 window.addEventListener("message", (e) => {
14368 if (e.origin !== expectedOrigin) {
14369 return;
14370 }
14371 const data = e.data;
14372 if (!data || data.type !== "desktop-mode-recycle-bin-changed") {
14373 return;
14374 }
14375 const ts = typeof data.ts === "number" ? data.ts : Date.now();
14376 if (ts <= store$3.state.seenTs) {
14377 log("postMessage skipped (ts <= seenTs)", { ts, seenTs: store$3.state.seenTs });
14378 return;
14379 }
14380 log("postMessage triggers refetch", { ts, prevSeenTs: store$3.state.seenTs });
14381 store$3.state.seenTs = ts;
14382 void refetchCount();
14383 });
14384 }
14385 function wireHeartbeatProbe() {
14386 const $ = window.jQuery;
14387 if (!$) {
14388 warn("wireHeartbeatProbe: window.jQuery not available — heartbeat path disabled");
14389 return;
14390 }
14391 log("wireHeartbeatProbe: jQuery + heartbeat hooks attached");
14392 $(document).on("heartbeat-send", (...args) => {
14393 const data = args[1];
14394 if (data) {
14395 data[HEARTBEAT_FIELD$1] = store$3.state.seenTs;
14396 }
14397 });
14398 $(document).on("heartbeat-tick", (...args) => {
14399 const response = args[1];
14400 const block = response?.desktop_mode_recycle_bin;
14401 log("heartbeat-tick", { hasBlock: !!block, block });
14402 if (!block) {
14403 return;
14404 }
14405 if (typeof block.ts === "number" && block.ts > store$3.state.seenTs) {
14406 store$3.state.seenTs = block.ts;
14407 }
14408 if (typeof block.count === "number") {
14409 setRecycleBinBadge(block.count);
14410 }
14411 });
14412 }
14413 async function refetchCount() {
14414 if (!store$3.state.countUrl) {
14415 log("refetchCount: no countUrl, skip");
14416 return;
14417 }
14418 log("refetchCount: hitting", store$3.state.countUrl);
14419 try {
14420 const response = await fetch(store$3.state.countUrl, {
14421 credentials: "same-origin",
14422 headers: { Accept: "application/json" }
14423 });
14424 if (!response.ok) {
14425 warn("refetchCount: non-OK", response.status, response.statusText);
14426 return;
14427 }
14428 const json = await response.json();
14429 log("refetchCount: response", json);
14430 if (typeof json.count === "number") {
14431 setRecycleBinBadge(json.count);
14432 }
14433 } catch (err) {
14434 warn("refetchCount: fetch failed", err);
14435 }
14436 }
14437 const OS_SETTINGS_ID = "desktop-mode-os-settings";
14438 const RECYCLE_BIN_ID = "desktop-mode-recycle-bin";
14439 function registerBuiltInPeekRenderers(opts) {
14440 const wpHooks = getWpHooks();
14441 if (!wpHooks) {
14442 return;
14443 }
14444 wpHooks.addFilter(
14445 "desktop-mode.dock.peek-card-content",
14446 "desktop-mode/built-in-peek-renderers",
14447 (body, ctx) => {
14448 const context = ctx;
14449 const id = context.window.id;
14450 if (id === OS_SETTINGS_ID) {
14451 return renderOsSettings();
14452 }
14453 if (id === RECYCLE_BIN_ID) {
14454 return renderRecycleBin(context, opts.getRecycleBinCount);
14455 }
14456 return body;
14457 }
14458 );
14459 }
14460 function renderOsSettings(_ctx) {
14461 const root = document.createElement("span");
14462 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--os-settings";
14463 root.setAttribute("aria-hidden", "true");
14464 const hero = document.createElement("span");
14465 hero.className = "desktop-mode-dock-peek__os-hero dashicons dashicons-admin-generic";
14466 root.appendChild(hero);
14467 const subtitle = document.createElement("span");
14468 subtitle.className = "desktop-mode-dock-peek__os-subtitle";
14469 subtitle.textContent = __("System Preferences");
14470 root.appendChild(subtitle);
14471 const tabs = document.createElement("span");
14472 tabs.className = "desktop-mode-dock-peek__os-tabs";
14473 for (const cls of [
14474 "dashicons-art",
14475 "dashicons-admin-customizer",
14476 "dashicons-editor-help"
14477 ]) {
14478 const tab = document.createElement("span");
14479 tab.className = `desktop-mode-dock-peek__os-tab dashicons ${cls}`;
14480 tabs.appendChild(tab);
14481 }
14482 root.appendChild(tabs);
14483 return root;
14484 }
14485 function renderRecycleBin(_ctx, getCount) {
14486 const root = document.createElement("span");
14487 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--recycle-bin";
14488 root.setAttribute("aria-hidden", "true");
14489 const count = Math.max(0, Math.floor(getCount() || 0));
14490 root.dataset.empty = count === 0 ? "true" : "false";
14491 const stage = document.createElement("span");
14492 stage.className = "desktop-mode-dock-peek__bin-stage";
14493 const stack = document.createElement("span");
14494 stack.className = "desktop-mode-dock-peek__bin-stack";
14495 for (let i = 0; i < 3; i++) {
14496 const slip = document.createElement("span");
14497 slip.className = "desktop-mode-dock-peek__bin-slip";
14498 stack.appendChild(slip);
14499 }
14500 stage.appendChild(stack);
14501 const icon = document.createElement("span");
14502 icon.className = `desktop-mode-dock-peek__bin-icon dashicons ${count === 0 ? "dashicons-trash" : "dashicons-trash"}`;
14503 stage.appendChild(icon);
14504 root.appendChild(stage);
14505 const label = document.createElement("span");
14506 label.className = "desktop-mode-dock-peek__bin-label";
14507 if (count === 0) {
14508 label.textContent = __("Recycle Bin — empty");
14509 } else if (count === 1) {
14510 label.textContent = __("1 item");
14511 } else if (count > 99) {
14512 label.textContent = "99+ items";
14513 } else {
14514 label.textContent = `${count} items`;
14515 }
14516 root.appendChild(label);
14517 return root;
14518 }
14519 function getWpHooks() {
14520 const wp = window.wp;
14521 return wp?.hooks ?? null;
14522 }
14523 const BUG_REPORT_WINDOW_ID = "desktop-mode-bug-report";
14524 const REPO_OWNER = "WordPress";
14525 const REPO_NAME = "desktop-mode";
14526 const MAX_BODY_LENGTH = 6e3;
14527 function renderBugReport(body) {
14528 body.classList.add("desktop-mode-bug-report");
14529 body.replaceChildren();
14530 const form = document.createElement("form");
14531 form.className = "desktop-mode-bug-report__form";
14532 form.setAttribute("novalidate", "");
14533 const intro = document.createElement("p");
14534 intro.className = "desktop-mode-bug-report__intro";
14535 intro.textContent = __(
14536 "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."
14537 );
14538 form.appendChild(intro);
14539 form.appendChild(buildTypeField());
14540 form.appendChild(buildTextField("title", __("Title"), {
14541 placeholder: __("A short summary"),
14542 required: true
14543 }));
14544 form.appendChild(buildTextareaField("description", __("What happened? What did you expect?"), {
14545 placeholder: __("Describe the issue or the feature you have in mind."),
14546 rows: 5,
14547 required: true
14548 }));
14549 form.appendChild(buildTextareaField("steps", __("Steps to reproduce (bug only)"), {
14550 placeholder: __("One step per line"),
14551 rows: 4
14552 }));
14553 const meta = buildMetadataPreview();
14554 form.appendChild(meta);
14555 const actions = document.createElement("div");
14556 actions.className = "desktop-mode-bug-report__actions";
14557 const submit = document.createElement("button");
14558 submit.type = "submit";
14559 submit.className = "desktop-mode-bug-report__submit";
14560 submit.textContent = __("Open issue on GitHub");
14561 actions.appendChild(submit);
14562 const hint = document.createElement("span");
14563 hint.className = "desktop-mode-bug-report__hint";
14564 hint.textContent = __("You will review and submit on GitHub.");
14565 actions.appendChild(hint);
14566 form.appendChild(actions);
14567 form.addEventListener("submit", (e) => {
14568 e.preventDefault();
14569 const state2 = readFormState(form);
14570 if (!state2.title.trim() || !state2.description.trim()) {
14571 showInlineError(form, __("Title and description are both required."));
14572 return;
14573 }
14574 const url = buildGithubIssueUrl(state2);
14575 window.open(url, "_blank", "noopener");
14576 });
14577 body.appendChild(form);
14578 }
14579 function buildTypeField() {
14580 const wrap = document.createElement("div");
14581 wrap.className = "desktop-mode-bug-report__field desktop-mode-bug-report__field--type";
14582 const label = document.createElement("span");
14583 label.className = "desktop-mode-bug-report__label";
14584 label.textContent = __("Type");
14585 wrap.appendChild(label);
14586 const group = document.createElement("div");
14587 group.className = "desktop-mode-bug-report__radio-group";
14588 group.setAttribute("role", "radiogroup");
14589 const options = [
14590 { value: "bug", label: __("Bug"), checked: true },
14591 { value: "feature", label: __("Feature request") },
14592 { value: "question", label: __("Question") }
14593 ];
14594 for (const opt of options) {
14595 const radioLabel = document.createElement("label");
14596 radioLabel.className = "desktop-mode-bug-report__radio";
14597 const input = document.createElement("input");
14598 input.type = "radio";
14599 input.name = "type";
14600 input.value = opt.value;
14601 if (opt.checked) {
14602 input.checked = true;
14603 }
14604 radioLabel.appendChild(input);
14605 const text = document.createElement("span");
14606 text.textContent = opt.label;
14607 radioLabel.appendChild(text);
14608 group.appendChild(radioLabel);
14609 }
14610 wrap.appendChild(group);
14611 return wrap;
14612 }
14613 function buildTextField(name, labelText, opts = {}) {
14614 const wrap = document.createElement("div");
14615 wrap.className = "desktop-mode-bug-report__field";
14616 const label = document.createElement("label");
14617 label.className = "desktop-mode-bug-report__label";
14618 label.textContent = labelText;
14619 wrap.appendChild(label);
14620 const input = document.createElement("input");
14621 input.type = "text";
14622 input.name = name;
14623 input.className = "desktop-mode-bug-report__input";
14624 if (opts.placeholder) {
14625 input.placeholder = opts.placeholder;
14626 }
14627 if (opts.required) {
14628 input.setAttribute("aria-required", "true");
14629 }
14630 label.appendChild(input);
14631 return wrap;
14632 }
14633 function buildTextareaField(name, labelText, opts = {}) {
14634 const wrap = document.createElement("div");
14635 wrap.className = "desktop-mode-bug-report__field";
14636 const label = document.createElement("label");
14637 label.className = "desktop-mode-bug-report__label";
14638 label.textContent = labelText;
14639 wrap.appendChild(label);
14640 const textarea = document.createElement("textarea");
14641 textarea.name = name;
14642 textarea.className = "desktop-mode-bug-report__textarea";
14643 textarea.rows = opts.rows ?? 4;
14644 if (opts.placeholder) {
14645 textarea.placeholder = opts.placeholder;
14646 }
14647 if (opts.required) {
14648 textarea.setAttribute("aria-required", "true");
14649 }
14650 label.appendChild(textarea);
14651 return wrap;
14652 }
14653 function buildMetadataPreview() {
14654 const details = document.createElement("details");
14655 details.className = "desktop-mode-bug-report__metadata";
14656 const summary = document.createElement("summary");
14657 summary.textContent = __("Environment included with the report");
14658 details.appendChild(summary);
14659 const pre = document.createElement("pre");
14660 pre.className = "desktop-mode-bug-report__metadata-body";
14661 pre.textContent = formatMetadata(collectMetadata());
14662 details.appendChild(pre);
14663 return details;
14664 }
14665 function showInlineError(form, msg) {
14666 let banner = form.querySelector(".desktop-mode-bug-report__error");
14667 if (!banner) {
14668 banner = document.createElement("div");
14669 banner.className = "desktop-mode-bug-report__error";
14670 banner.setAttribute("role", "alert");
14671 form.prepend(banner);
14672 }
14673 banner.textContent = msg;
14674 }
14675 function readFormState(form) {
14676 const data = new FormData(form);
14677 return {
14678 type: data.get("type") ?? "bug",
14679 title: data.get("title") ?? "",
14680 description: data.get("description") ?? "",
14681 steps: data.get("steps") ?? ""
14682 };
14683 }
14684 function buildGithubIssueUrl(state2) {
14685 const labels = labelsForType(state2.type);
14686 const body = composeIssueBody(state2);
14687 const params = new URLSearchParams();
14688 params.set("title", state2.title.trim());
14689 params.set("body", body);
14690 if (labels.length) {
14691 params.set("labels", labels.join(","));
14692 }
14693 return `https://github.com/${REPO_OWNER}/${REPO_NAME}/issues/new?${params.toString()}`;
14694 }
14695 function labelsForType(type) {
14696 switch (type) {
14697 case "bug":
14698 return ["bug"];
14699 case "feature":
14700 return ["enhancement"];
14701 case "question":
14702 return ["question"];
14703 default:
14704 return [];
14705 }
14706 }
14707 function composeIssueBody(state2) {
14708 const parts = [];
14709 parts.push(state2.description.trim());
14710 if (state2.type === "bug" && state2.steps.trim()) {
14711 parts.push("");
14712 parts.push("## Steps to reproduce");
14713 parts.push("");
14714 parts.push(state2.steps.trim());
14715 }
14716 parts.push("");
14717 parts.push("<details><summary>Environment</summary>");
14718 parts.push("");
14719 parts.push("```");
14720 parts.push(formatMetadata(collectMetadata()));
14721 parts.push("```");
14722 parts.push("");
14723 parts.push("</details>");
14724 let out = parts.join("\n");
14725 if (out.length > MAX_BODY_LENGTH) {
14726 out = out.slice(0, MAX_BODY_LENGTH) + "\n\n…(truncated to fit GitHub URL length limit)";
14727 }
14728 return out;
14729 }
14730 function collectMetadata() {
14731 const cfg = window.wp?.desktop?.config;
14732 return {
14733 pluginVersion: cfg?.pluginVersion ?? "unknown",
14734 wordpressVersion: cfg?.wordpressVersion ?? "unknown",
14735 userAgent: navigator.userAgent,
14736 viewport: `${window.innerWidth}x${window.innerHeight}`,
14737 platform: navigator.platform || "unknown",
14738 currentUrl: window.location.href
14739 };
14740 }
14741 function formatMetadata(m) {
14742 return [
14743 `Plugin version: ${m.pluginVersion}`,
14744 `WordPress version: ${m.wordpressVersion}`,
14745 `User agent: ${m.userAgent}`,
14746 `Viewport: ${m.viewport}`,
14747 `Platform: ${m.platform}`,
14748 `Current URL: ${m.currentUrl}`
14749 ].join("\n");
14750 }
14751 let _config = null;
14752 let _state = {
14753 installHintDismissed: false,
14754 notificationsEnabled: false
14755 };
14756 const _listeners = /* @__PURE__ */ new Set();
14757 function initPwaState(config) {
14758 if (!config) {
14759 _config = null;
14760 return;
14761 }
14762 _config = config;
14763 _state = { ...config.state };
14764 notify$4();
14765 }
14766 function getPwaState() {
14767 return { ..._state };
14768 }
14769 function updatePwaState(patch) {
14770 _state = { ..._state, ...patch };
14771 notify$4();
14772 if (!_config) {
14773 return getPwaState();
14774 }
14775 const body = JSON.stringify(patch);
14776 const nonce = readRestNonce$2();
14777 void fetch(_config.stateUrl, {
14778 method: "POST",
14779 credentials: "same-origin",
14780 headers: {
14781 "Content-Type": "application/json",
14782 ...nonce ? { "X-WP-Nonce": nonce } : {}
14783 },
14784 body
14785 }).catch((err) => {
14786 if (typeof console !== "undefined") {
14787 console.warn("[desktop-mode] pwa-state write failed:", err);
14788 }
14789 });
14790 return getPwaState();
14791 }
14792 function subscribePwaState(cb) {
14793 _listeners.add(cb);
14794 return () => {
14795 _listeners.delete(cb);
14796 };
14797 }
14798 function notify$4() {
14799 const snapshot = getPwaState();
14800 for (const cb of Array.from(_listeners)) {
14801 try {
14802 cb(snapshot);
14803 } catch (err) {
14804 if (typeof console !== "undefined") {
14805 console.error(
14806 "[desktop-mode] pwa-state listener threw:",
14807 err
14808 );
14809 }
14810 }
14811 }
14812 }
14813 function readRestNonce$2() {
14814 const cfg = window.desktopModeConfig;
14815 return cfg?.restNonce ?? "";
14816 }
14817 const state = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
14818 __proto__: null,
14819 getPwaState,
14820 initPwaState,
14821 subscribePwaState,
14822 updatePwaState
14823 }, Symbol.toStringTag, { value: "Module" }));
14824 let _registration = null;
14825 let _registrationFailed = false;
14826 let _controllerChangeBound = false;
14827 let _reloadingForSwUpdate = false;
14828 let _status = "pending";
14829 function bindControllerChangeReload() {
14830 if (_controllerChangeBound) {
14831 return;
14832 }
14833 _controllerChangeBound = true;
14834 const hadInitialController = !!navigator.serviceWorker.controller;
14835 navigator.serviceWorker.addEventListener("controllerchange", () => {
14836 if (!hadInitialController) {
14837 return;
14838 }
14839 if (_reloadingForSwUpdate) {
14840 return;
14841 }
14842 if (wasRecentlyReloadedForSwUpdate()) {
14843 return;
14844 }
14845 markReloadedForSwUpdate();
14846 _reloadingForSwUpdate = true;
14847 setTimeout(() => window.location.reload(), 0);
14848 });
14849 }
14850 const SW_RELOAD_THROTTLE_KEY = "wpd-sw-reload-ts";
14851 const SW_RELOAD_THROTTLE_MS = 3e4;
14852 function wasRecentlyReloadedForSwUpdate() {
14853 try {
14854 const raw = sessionStorage.getItem(SW_RELOAD_THROTTLE_KEY);
14855 const last = raw ? Number.parseInt(raw, 10) : 0;
14856 if (!Number.isFinite(last) || last <= 0) {
14857 return false;
14858 }
14859 return Date.now() - last < SW_RELOAD_THROTTLE_MS;
14860 } catch {
14861 return false;
14862 }
14863 }
14864 function markReloadedForSwUpdate() {
14865 try {
14866 sessionStorage.setItem(SW_RELOAD_THROTTLE_KEY, String(Date.now()));
14867 } catch {
14868 }
14869 }
14870 async function registerServiceWorker(config, options = {}) {
14871 if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
14872 _status = "unsupported";
14873 return null;
14874 }
14875 if (!config?.swUrl) {
14876 _status = "unsupported";
14877 return null;
14878 }
14879 if (!window.isSecureContext) {
14880 _status = "unsupported";
14881 return null;
14882 }
14883 if (_registration || _registrationFailed) {
14884 return _registration;
14885 }
14886 if (!options.forceReplace) {
14887 const existing = await navigator.serviceWorker.getRegistrations().catch(() => []);
14888 const foreign = existing.find((reg) => {
14889 const url = reg.active?.scriptURL ?? reg.installing?.scriptURL ?? "";
14890 return url !== "" && url !== config.swUrl;
14891 });
14892 if (foreign) {
14893 _status = "foreign-sw";
14894 if (typeof console !== "undefined") {
14895 console.warn(
14896 "[desktop-mode] another service worker is already registered (" + foreign.scope + "); skipping desktop-mode SW. Set desktop_mode_pwa_force_replace_sw=true to override."
14897 );
14898 }
14899 return null;
14900 }
14901 }
14902 try {
14903 _registration = await navigator.serviceWorker.register(config.swUrl, {
14904 scope: "/",
14905 updateViaCache: "none"
14906 });
14907 _status = "registered";
14908 bindControllerChangeReload();
14909 return _registration;
14910 } catch (err) {
14911 _registrationFailed = true;
14912 _status = "failed";
14913 if (typeof console !== "undefined") {
14914 console.warn("[desktop-mode] SW registration failed:", err);
14915 }
14916 return null;
14917 }
14918 }
14919 function getSwRegistrationStatus() {
14920 return _status;
14921 }
14922 const PWA_INSTALL_TILE_ID = "desktop-mode-pwa-install";
14923 function isStandaloneDisplay() {
14924 if (typeof window === "undefined") {
14925 return false;
14926 }
14927 if (window.matchMedia?.("(display-mode: standalone)").matches) {
14928 return true;
14929 }
14930 const nav = window.navigator;
14931 return nav.standalone === true;
14932 }
14933 async function isLikelyInstalled() {
14934 if (isStandaloneDisplay()) {
14935 return true;
14936 }
14937 const nav = window.navigator;
14938 if (typeof nav.getInstalledRelatedApps !== "function") {
14939 return false;
14940 }
14941 try {
14942 const apps = await nav.getInstalledRelatedApps();
14943 return Array.isArray(apps) && apps.length > 0;
14944 } catch {
14945 return false;
14946 }
14947 }
14948 let _deferred = null;
14949 function installPwaInstallAffordance(siteName, showToast2) {
14950 if (typeof window === "undefined") {
14951 return;
14952 }
14953 window.removeEventListener(
14954 "beforeinstallprompt",
14955 _handleBeforeInstall
14956 );
14957 window.addEventListener(
14958 "beforeinstallprompt",
14959 _handleBeforeInstall
14960 );
14961 window.removeEventListener("appinstalled", _handleAppInstalled);
14962 window.addEventListener("appinstalled", _handleAppInstalled);
14963 function _handleBeforeInstall(ev) {
14964 ev.preventDefault();
14965 _deferred = ev;
14966 }
14967 function _handleAppInstalled() {
14968 _deferred = null;
14969 showToast2({
14970 message: sprintf(
14971 /* translators: %s: site name */
14972 __("Installed %s as an app."),
14973 siteName
14974 )
14975 });
14976 }
14977 }
14978 function getInstallTileDef(siteName, showToast2) {
14979 return {
14980 id: PWA_INSTALL_TILE_ID,
14981 title: sprintf(
14982 /* translators: %s: site name */
14983 __("Install %s as an app"),
14984 siteName
14985 ),
14986 // Dashicons class — the dock renderer prefers Dashicons
14987 // strings. `dashicons-download` is the closest match for
14988 // "install" in the WordPress glyph set without shipping
14989 // bespoke artwork.
14990 icon: "dashicons-download",
14991 onOpen: () => {
14992 void onTileClick(siteName, showToast2);
14993 }
14994 };
14995 }
14996 async function onTileClick(siteName, showToast2) {
14997 if (_deferred) {
14998 const event = _deferred;
14999 _deferred = null;
15000 try {
15001 await event.prompt();
15002 const choice = await event.userChoice;
15003 if (choice.outcome === "dismissed") {
15004 showToast2({
15005 message: __("Install cancelled.")
15006 });
15007 }
15008 } catch (err) {
15009 if (typeof console !== "undefined") {
15010 console.warn(
15011 "[desktop-mode] install prompt failed:",
15012 err
15013 );
15014 }
15015 }
15016 return;
15017 }
15018 if (await isLikelyInstalled()) {
15019 showToast2({
15020 message: sprintf(
15021 /* translators: %s: site name */
15022 __(
15023 "%s is already installed. Open it from your apps menu or home screen."
15024 ),
15025 siteName
15026 )
15027 });
15028 return;
15029 }
15030 if (getSwRegistrationStatus() === "foreign-sw") {
15031 showToast2({
15032 message: __(
15033 "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."
15034 )
15035 });
15036 return;
15037 }
15038 showToast2({
15039 message: __(
15040 "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."
15041 )
15042 });
15043 }
15044 async function promptInstall() {
15045 if (!_deferred) {
15046 return "unavailable";
15047 }
15048 const event = _deferred;
15049 _deferred = null;
15050 try {
15051 await event.prompt();
15052 const choice = await event.userChoice;
15053 return choice.outcome;
15054 } catch {
15055 return "unavailable";
15056 }
15057 }
15058 function undismissInstallHint() {
15059 Promise.resolve().then(() => state).then((m) => {
15060 m.updatePwaState({ installHintDismissed: false });
15061 });
15062 }
15063 function notify$3(options) {
15064 const intent = activity.filter(
15065 "desktop-mode/notification-requested",
15066 { ...options }
15067 );
15068 if (!intent || intent.cancel === true || !intent.title) {
15069 return () => void 0;
15070 }
15071 let dismissed = false;
15072 let dismissNative = null;
15073 let dismissToast = null;
15074 const dismiss = () => {
15075 if (dismissed) {
15076 return;
15077 }
15078 dismissed = true;
15079 if (dismissNative) {
15080 dismissNative();
15081 }
15082 if (dismissToast) {
15083 dismissToast();
15084 }
15085 };
15086 const fallback = () => {
15087 dismissToast = showToast({
15088 message: intent.body ? intent.title + " — " + intent.body : intent.title
15089 });
15090 activity.publish("desktop-mode/notification-shown", {
15091 ...intent,
15092 fallback: "toast"
15093 });
15094 };
15095 if (typeof window === "undefined" || typeof Notification === "undefined") {
15096 fallback();
15097 return dismiss;
15098 }
15099 const perm = Notification.permission;
15100 if (perm === "granted") {
15101 dismissNative = renderNative(intent);
15102 return dismiss;
15103 }
15104 if (perm === "denied") {
15105 fallback();
15106 return dismiss;
15107 }
15108 void Notification.requestPermission().then((result) => {
15109 if (dismissed) {
15110 return;
15111 }
15112 if (result === "granted") {
15113 updatePwaState({ notificationsEnabled: true });
15114 dismissNative = renderNative(intent);
15115 return;
15116 }
15117 fallback();
15118 });
15119 return dismiss;
15120 }
15121 function renderNative(intent) {
15122 let n = null;
15123 try {
15124 n = new Notification(intent.title, {
15125 body: intent.body,
15126 icon: intent.icon,
15127 tag: intent.tag,
15128 requireInteraction: intent.requireInteraction
15129 });
15130 } catch (err) {
15131 if (typeof console !== "undefined") {
15132 console.warn("[desktop-mode] Notification ctor threw:", err);
15133 }
15134 return () => void 0;
15135 }
15136 if (intent.onClick) {
15137 const handler = intent.onClick;
15138 n.onclick = () => {
15139 try {
15140 handler(n);
15141 } catch (hErr) {
15142 if (typeof console !== "undefined") {
15143 console.error(
15144 "[desktop-mode] notification onClick threw:",
15145 hErr
15146 );
15147 }
15148 }
15149 };
15150 }
15151 activity.publish("desktop-mode/notification-shown", {
15152 ...intent,
15153 fallback: null
15154 });
15155 return () => {
15156 if (n) {
15157 n.close();
15158 }
15159 };
15160 }
15161 async function requestNotificationPermission() {
15162 if (typeof Notification === "undefined") {
15163 return "unsupported";
15164 }
15165 if (Notification.permission !== "default") {
15166 return Notification.permission;
15167 }
15168 const result = await Notification.requestPermission();
15169 if (result === "granted") {
15170 updatePwaState({ notificationsEnabled: true });
15171 }
15172 return result;
15173 }
15174 function getNotificationPermission() {
15175 if (typeof Notification === "undefined") {
15176 return "unsupported";
15177 }
15178 return Notification.permission;
15179 }
15180 function bootstrapPwa(config, showToast2) {
15181 if (!config.pwa) {
15182 return;
15183 }
15184 initPwaState(config.pwa);
15185 installPwaInstallAffordance(
15186 config.pwa.appName || "WordPress",
15187 showToast2
15188 );
15189 void registerServiceWorker(config.pwa, {
15190 forceReplace: !!config.pwa.forceReplaceSw
15191 });
15192 }
15193 const DRAG_BRIDGE_EVENTS = {
15194 START: "desktop-mode-cross-frame-drag-start",
15195 END: "desktop-mode-cross-frame-drag-end"
15196 };
15197 function isStart(m) {
15198 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-start" && !!m.payload && typeof m.payload === "object";
15199 }
15200 function isEnd(m) {
15201 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-end";
15202 }
15203 function isPayloadRequest(m) {
15204 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-payload-request";
15205 }
15206 function normalizeLegacyPayload(payload) {
15207 const obj = payload;
15208 if (obj.kind !== void 0 && obj.kind !== null) {
15209 return payload;
15210 }
15211 if (typeof obj.id === "number" && typeof obj.url === "string" && typeof obj.mime === "string") {
15212 return {
15213 kind: "attachment",
15214 id: obj.id,
15215 url: obj.url,
15216 title: typeof obj.title === "string" ? obj.title : "",
15217 alt: typeof obj.alt === "string" ? obj.alt : "",
15218 mime: obj.mime,
15219 thumbnailUrl: typeof obj.thumbnailUrl === "string" ? obj.thumbnailUrl : void 0,
15220 sizes: obj.sizes && typeof obj.sizes === "object" ? obj.sizes : void 0
15221 };
15222 }
15223 return payload;
15224 }
15225 class DragBridge {
15226 constructor() {
15227 this._payload = null;
15228 this._onMessage = (e) => {
15229 if (e.origin !== this._origin) {
15230 return;
15231 }
15232 const msg = e.data;
15233 if (isStart(msg)) {
15234 this._startDrag(msg.payload);
15235 return;
15236 }
15237 if (isEnd(msg)) {
15238 this._endDrag();
15239 return;
15240 }
15241 if (isPayloadRequest(msg) && this._payload && e.source) {
15242 try {
15243 e.source.postMessage(
15244 { type: "desktop-mode-drag-payload", payload: this._payload },
15245 this._origin
15246 );
15247 } catch {
15248 }
15249 }
15250 };
15251 this._origin = window.location.origin;
15252 window.addEventListener("message", this._onMessage);
15253 }
15254 getPayload() {
15255 return this._payload;
15256 }
15257 isDragging() {
15258 return this._payload !== null;
15259 }
15260 start(payload) {
15261 if (this._payload === payload) {
15262 return;
15263 }
15264 this._startDrag(payload);
15265 }
15266 end() {
15267 this._endDrag();
15268 }
15269 _startDrag(payload) {
15270 const normalized = normalizeLegacyPayload(payload);
15271 this._payload = normalized;
15272 document.dispatchEvent(
15273 new CustomEvent(DRAG_BRIDGE_EVENTS.START, {
15274 detail: { payload: normalized }
15275 })
15276 );
15277 }
15278 _endDrag() {
15279 if (this._payload === null) {
15280 return;
15281 }
15282 const payload = this._payload;
15283 this._payload = null;
15284 document.dispatchEvent(
15285 new CustomEvent(DRAG_BRIDGE_EVENTS.END, { detail: { payload } })
15286 );
15287 }
15288 }
15289 class DropTargetRegistry {
15290 constructor() {
15291 this._targets = /* @__PURE__ */ new Map();
15292 this._byElement = /* @__PURE__ */ new Map();
15293 }
15294 register(target2) {
15295 const prev = this._targets.get(target2.id);
15296 if (prev) {
15297 this._byElement.delete(prev.element);
15298 }
15299 this._targets.set(target2.id, target2);
15300 this._byElement.set(target2.element, target2);
15301 return () => {
15302 const cur = this._targets.get(target2.id);
15303 if (cur === target2) {
15304 this._targets.delete(target2.id);
15305 this._byElement.delete(target2.element);
15306 }
15307 };
15308 }
15309 list() {
15310 return Array.from(this._targets.values());
15311 }
15312 clear() {
15313 this._targets.clear();
15314 this._byElement.clear();
15315 }
15316 /**
15317 * Find the deepest registered target whose element is `el` or an
15318 * ancestor of `el`. Walks the DOM tree once (O(depth)).
15319 *
15320 * Window claim boundary: if the walk crosses a `.desktop-mode-window`
15321 * element BEFORE finding a registered target, hit-testing stops
15322 * there and returns null. This is the rule that makes "drag over
15323 * a Gutenberg admin window" produce reject feedback instead of
15324 * silently routing the drop to the wallpaper canvas underneath.
15325 *
15326 * A window can opt INTO accepting drops by registering a target
15327 * on its own body (e.g. Recycle Bin's `[data-desktop-mode-recycle-bin-root]`):
15328 * since that element sits inside the window, the walk hits it
15329 * before reaching the window boundary and the body's target wins.
15330 */
15331 hitTest(el) {
15332 let cur = el;
15333 while (cur) {
15334 if (cur instanceof HTMLElement) {
15335 const t = this._byElement.get(cur);
15336 if (t) {
15337 return t;
15338 }
15339 if (cur.classList.contains("desktop-mode-window")) {
15340 return null;
15341 }
15342 }
15343 cur = cur.parentElement;
15344 }
15345 return null;
15346 }
15347 /**
15348 * Convenience: pick the target at viewport `(clientX, clientY)`.
15349 * Caller is responsible for hiding any obscuring ghost element
15350 * before calling — see `GhostHandle.withHidden()`.
15351 */
15352 hitTestPoint(clientX, clientY) {
15353 const el = document.elementFromPoint(clientX, clientY);
15354 const target2 = this.hitTest(el);
15355 return { target: target2, element: el, accepted: false };
15356 }
15357 }
15358 const GHOST_CLASS = "desktop-mode-drag-ghost";
15359 const GHOST_ACCEPT_CLASS = "desktop-mode-drag-ghost--accept";
15360 const GHOST_REJECT_CLASS = "desktop-mode-drag-ghost--reject";
15361 const HINT_CLASS = "desktop-mode-drag-hint";
15362 const HINT_ACCEPT_CLASS = "desktop-mode-drag-hint--accept";
15363 const HINT_REJECT_CLASS = "desktop-mode-drag-hint--reject";
15364 const HINT_NEUTRAL_CLASS = "desktop-mode-drag-hint--neutral";
15365 const HINT_OFFSET_X = 16;
15366 const HINT_OFFSET_Y = 18;
15367 function mountGhost(payload, clientX, clientY) {
15368 const ghost = buildGhost(payload);
15369 const offsetX = payload.ghost?.offsetX ?? defaultOffsetX(payload.source);
15370 const offsetY = payload.ghost?.offsetY ?? defaultOffsetY(payload.source);
15371 ghost.classList.add(GHOST_CLASS);
15372 ghost.setAttribute("aria-hidden", "true");
15373 ghost.style.position = "fixed";
15374 ghost.style.left = "0";
15375 ghost.style.top = "0";
15376 ghost.style.margin = "0";
15377 ghost.style.pointerEvents = "none";
15378 ghost.style.zIndex = "2147483647";
15379 ghost.style.willChange = "transform";
15380 document.body.appendChild(ghost);
15381 const labels = resolveHintLabels(payload);
15382 const hint = labels ? buildHintChip() : null;
15383 if (hint) {
15384 document.body.appendChild(hint);
15385 }
15386 const handle = {
15387 get element() {
15388 return ghost;
15389 },
15390 moveTo(cx, cy) {
15391 ghost.style.transform = `translate3d(${cx - offsetX}px, ${cy - offsetY}px, 0)`;
15392 if (hint) {
15393 hint.style.transform = `translate3d(${cx + HINT_OFFSET_X}px, ${cy + HINT_OFFSET_Y}px, 0)`;
15394 }
15395 },
15396 setMode(mode, overrides) {
15397 ghost.classList.remove(GHOST_ACCEPT_CLASS, GHOST_REJECT_CLASS);
15398 if (mode === "accept") {
15399 ghost.classList.add(GHOST_ACCEPT_CLASS);
15400 } else if (mode === "reject") {
15401 ghost.classList.add(GHOST_REJECT_CLASS);
15402 }
15403 if (hint && labels) {
15404 hint.classList.remove(
15405 HINT_ACCEPT_CLASS,
15406 HINT_REJECT_CLASS,
15407 HINT_NEUTRAL_CLASS
15408 );
15409 if (mode === "accept") {
15410 hint.classList.add(HINT_ACCEPT_CLASS);
15411 hint.textContent = overrides?.acceptLabel ?? labels.accept;
15412 } else if (mode === "reject") {
15413 hint.classList.add(HINT_REJECT_CLASS);
15414 hint.textContent = labels.reject;
15415 } else {
15416 hint.classList.add(HINT_NEUTRAL_CLASS);
15417 hint.textContent = labels.neutral;
15418 }
15419 hint.hidden = !hint.textContent;
15420 }
15421 },
15422 withHidden(fn) {
15423 const prevG = ghost.style.visibility;
15424 const prevH = hint?.style.visibility ?? "";
15425 ghost.style.visibility = "hidden";
15426 if (hint) {
15427 hint.style.visibility = "hidden";
15428 }
15429 try {
15430 return fn();
15431 } finally {
15432 ghost.style.visibility = prevG;
15433 if (hint) {
15434 hint.style.visibility = prevH;
15435 }
15436 }
15437 },
15438 dispose() {
15439 if (ghost.isConnected) {
15440 ghost.remove();
15441 }
15442 if (hint?.isConnected) {
15443 hint.remove();
15444 }
15445 }
15446 };
15447 handle.moveTo(clientX, clientY);
15448 handle.setMode("neutral");
15449 return handle;
15450 }
15451 function buildHintChip() {
15452 const chip = document.createElement("div");
15453 chip.className = HINT_CLASS;
15454 chip.setAttribute("aria-hidden", "true");
15455 chip.setAttribute("role", "presentation");
15456 chip.style.position = "fixed";
15457 chip.style.left = "0";
15458 chip.style.top = "0";
15459 chip.style.margin = "0";
15460 chip.style.pointerEvents = "none";
15461 chip.style.zIndex = "2147483647";
15462 chip.style.willChange = "transform";
15463 return chip;
15464 }
15465 function resolveHintLabels(payload) {
15466 const cfg = payload.ghost?.hint;
15467 if (cfg?.hidden) {
15468 return null;
15469 }
15470 return {
15471 accept: cfg?.accept ?? defaultAcceptLabel(payload),
15472 reject: cfg?.reject ?? defaultRejectLabel(),
15473 neutral: cfg?.neutral ?? defaultNeutralLabel(payload)
15474 };
15475 }
15476 function defaultAcceptLabel(payload) {
15477 if (payload.type === "shortcut") {
15478 return __("Drop here to create shortcut", "desktop-mode");
15479 }
15480 if (payload.type === "desktop-file") {
15481 return __("Drop here to move", "desktop-mode");
15482 }
15483 return __("Drop here", "desktop-mode");
15484 }
15485 function defaultRejectLabel(_payload) {
15486 return __("Can’t drop here", "desktop-mode");
15487 }
15488 function defaultNeutralLabel(payload) {
15489 if (payload.type === "shortcut") {
15490 return __(
15491 "Drop on the desktop or a folder",
15492 "desktop-mode"
15493 );
15494 }
15495 if (payload.type === "desktop-file") {
15496 return __("Drop in a folder", "desktop-mode");
15497 }
15498 return "";
15499 }
15500 function buildGhost(payload) {
15501 if (payload.ghost?.element) {
15502 return payload.ghost.element;
15503 }
15504 const clone = payload.source.cloneNode(true);
15505 clone.removeAttribute("id");
15506 const rect = payload.source.getBoundingClientRect();
15507 clone.style.width = `${rect.width}px`;
15508 clone.style.height = `${rect.height}px`;
15509 return clone;
15510 }
15511 function defaultOffsetX(source) {
15512 return source.offsetWidth / 2;
15513 }
15514 function defaultOffsetY(source) {
15515 return source.offsetHeight / 2;
15516 }
15517 let _installed$2 = false;
15518 function installRecovery(cancelActive) {
15519 if (_installed$2) {
15520 return;
15521 }
15522 _installed$2 = true;
15523 document.addEventListener("keydown", (e) => {
15524 if (e.key === "Escape") {
15525 cancelActive("escape");
15526 }
15527 });
15528 window.addEventListener("blur", () => {
15529 cancelActive("blur");
15530 });
15531 document.addEventListener("visibilitychange", () => {
15532 if (document.hidden) {
15533 cancelActive("visibility");
15534 }
15535 });
15536 }
15537 const DRAG_THRESHOLD_PX = 4;
15538 const DRAG_EVENTS = {
15539 START: "desktop-mode.drag.start",
15540 MOVE: "desktop-mode.drag.move",
15541 ENTER: "desktop-mode.drag.enter",
15542 LEAVE: "desktop-mode.drag.leave",
15543 REJECTED: "desktop-mode.drag.rejected",
15544 COMMIT: "desktop-mode.drag.commit",
15545 CANCEL: "desktop-mode.drag.cancel",
15546 END: "desktop-mode.drag.end"
15547 };
15548 const SOURCE_DRAGGING_CLASS = "desktop-mode-file-tile--dragging";
15549 const TARGET_DROP_ACTIVE_CLASS = "desktop-mode-file-tile--drop-target";
15550 const TRASH_DROP_ACTIVE_ATTR$1 = "data-desktop-mode-trash-drop-active";
15551 const FILES_DROP_ACTIVE_ATTR = "data-files-drop-active";
15552 const BODY_DRAGGING_ATTR = "data-desktop-mode-dragging";
15553 const BODY_DRAG_TYPE_ATTR = "data-desktop-mode-drag-type";
15554 const BODY_DRAG_MODE_ATTR = "data-desktop-mode-drag-mode";
15555 class DragManager {
15556 constructor() {
15557 this._registry = new DropTargetRegistry();
15558 this._active = null;
15559 this._docListenersAttached = false;
15560 this._lastLiftedEndAt = 0;
15561 this._onPointerMove = (e) => {
15562 const session = this._active;
15563 if (!session || session._pointerId !== e.pointerId) {
15564 return;
15565 }
15566 const dx = e.clientX - session._origin.clientX;
15567 const dy = e.clientY - session._origin.clientY;
15568 if (!session._lifted) {
15569 if (Math.abs(dx) < DRAG_THRESHOLD_PX && Math.abs(dy) < DRAG_THRESHOLD_PX) {
15570 return;
15571 }
15572 this._lift(session, e);
15573 }
15574 if (!session._ghost) {
15575 return;
15576 }
15577 session._ghost.moveTo(e.clientX, e.clientY);
15578 this._updateHover(session, e.clientX, e.clientY);
15579 dispatchOnDocument(DRAG_EVENTS.MOVE, {
15580 payload: session.payload,
15581 clientX: e.clientX,
15582 clientY: e.clientY
15583 });
15584 };
15585 this._onPointerUp = (e) => {
15586 const session = this._active;
15587 if (!session || session._pointerId !== e.pointerId) {
15588 return;
15589 }
15590 if (!session._lifted) {
15591 session._finished = true;
15592 this._active = null;
15593 try {
15594 session._callbacks.onClickOnly?.();
15595 } catch (err) {
15596 console.error("[desktop-mode] drag onClickOnly threw:", err);
15597 }
15598 return;
15599 }
15600 const hit = this._hitTestNow(session, e.clientX, e.clientY);
15601 if (hit && hit.accepted && hit.target) {
15602 this._commit(session, hit.target, e.clientX, e.clientY);
15603 return;
15604 }
15605 this._cancel(session, hit && hit.target ? "rejected" : "no-target");
15606 };
15607 this._onPointerCancel = (e) => {
15608 const session = this._active;
15609 if (!session || session._pointerId !== e.pointerId) {
15610 return;
15611 }
15612 this._cancel(session, "pointercancel");
15613 };
15614 }
15615 start(opts) {
15616 if (this._active) {
15617 return null;
15618 }
15619 if (opts.origin.button !== 0) {
15620 return null;
15621 }
15622 const session = {
15623 payload: opts.payload,
15624 isFinished: () => session._finished,
15625 cancel: (reason) => this._cancel(session, reason ?? "caller"),
15626 _origin: opts.origin,
15627 _pointerId: opts.origin.pointerId,
15628 _lifted: false,
15629 _finished: false,
15630 _callbacks: {
15631 onClickOnly: opts.onClickOnly,
15632 onCancel: opts.onCancel,
15633 onCommit: opts.onCommit
15634 },
15635 _ghost: null,
15636 _currentTarget: null,
15637 _currentAccepted: false
15638 };
15639 this._active = session;
15640 this._ensureDocListeners();
15641 installRecovery((reason) => {
15642 if (this._active) {
15643 this._cancel(this._active, reason);
15644 }
15645 });
15646 return session;
15647 }
15648 registerDropTarget(target2) {
15649 return this._registry.register(target2);
15650 }
15651 isDragging() {
15652 return this._active !== null && this._active._lifted;
15653 }
15654 /**
15655 * Whether a real (lifted) drag ended within `withinMs` of now.
15656 * Surfaces that bind plain `click` listeners use this to ignore
15657 * the synthesized click that fires after a drop. 500 ms is a
15658 * generous default — browsers fire the click within 10–50 ms of
15659 * pointerup, but plugins may chain post-drag work into a
15660 * `requestAnimationFrame` and call back into a click-driven API.
15661 *
15662 * @public
15663 * @since 0.18.x
15664 */
15665 recentlyEndedDrag(withinMs = 500) {
15666 if (this._lastLiftedEndAt === 0) {
15667 return false;
15668 }
15669 return Date.now() - this._lastLiftedEndAt < withinMs;
15670 }
15671 getActive() {
15672 return this._active;
15673 }
15674 debug() {
15675 return {
15676 findOrphans: () => findOrphans(),
15677 listTargets: () => this._registry.list()
15678 };
15679 }
15680 // -----------------------------------------------------------------
15681 // Internals
15682 // -----------------------------------------------------------------
15683 _ensureDocListeners() {
15684 if (this._docListenersAttached) {
15685 return;
15686 }
15687 this._docListenersAttached = true;
15688 document.addEventListener("pointermove", this._onPointerMove, true);
15689 document.addEventListener("pointerup", this._onPointerUp, true);
15690 document.addEventListener("pointercancel", this._onPointerCancel, true);
15691 }
15692 _lift(session, e) {
15693 session._lifted = true;
15694 session.payload.source.classList.add(SOURCE_DRAGGING_CLASS);
15695 session._ghost = mountGhost(session.payload, e.clientX, e.clientY);
15696 if (typeof document !== "undefined" && document.body) {
15697 document.body.setAttribute(BODY_DRAGGING_ATTR, "");
15698 document.body.setAttribute(
15699 BODY_DRAG_TYPE_ATTR,
15700 String(session.payload.type)
15701 );
15702 document.body.setAttribute(BODY_DRAG_MODE_ATTR, "neutral");
15703 }
15704 dispatchOnDocument(DRAG_EVENTS.START, { payload: session.payload });
15705 }
15706 _hitTestNow(session, clientX, clientY) {
15707 const run = () => {
15708 const el = document.elementFromPoint(clientX, clientY);
15709 const target2 = this._registry.hitTest(el);
15710 if (!target2) {
15711 return { target: null, accepted: false };
15712 }
15713 let accepted = false;
15714 try {
15715 accepted = target2.accept(session.payload);
15716 } catch (err) {
15717 console.error("[desktop-mode] drop target accept() threw:", target2.id, err);
15718 }
15719 return { target: target2, accepted };
15720 };
15721 if (session._ghost) {
15722 return session._ghost.withHidden(run);
15723 }
15724 return run();
15725 }
15726 _updateHover(session, clientX, clientY) {
15727 const next = this._hitTestNow(session, clientX, clientY);
15728 const prevTarget = session._currentTarget;
15729 if (next.target === prevTarget && next.accepted === session._currentAccepted) {
15730 return;
15731 }
15732 if (prevTarget) {
15733 fireLeave(prevTarget, session);
15734 }
15735 session._currentTarget = next.target;
15736 session._currentAccepted = next.accepted;
15737 let mode;
15738 if (next.target) {
15739 if (next.accepted) {
15740 fireEnter(next.target, session);
15741 session._ghost?.setMode("accept", {
15742 acceptLabel: next.target.acceptLabel
15743 });
15744 mode = "accept";
15745 } else {
15746 session._ghost?.setMode("reject");
15747 dispatchOnDocument(DRAG_EVENTS.REJECTED, {
15748 payload: session.payload,
15749 targetId: next.target.id
15750 });
15751 mode = "reject";
15752 }
15753 } else {
15754 session._ghost?.setMode("reject");
15755 mode = "reject";
15756 }
15757 if (typeof document !== "undefined" && document.body) {
15758 document.body.setAttribute(BODY_DRAG_MODE_ATTR, mode);
15759 }
15760 }
15761 _commit(session, target2, clientX, clientY) {
15762 session._finished = true;
15763 this._lastLiftedEndAt = Date.now();
15764 fireLeave(target2, session);
15765 this._cleanupDom(session);
15766 const prevActive = this._active;
15767 this._active = null;
15768 try {
15769 void target2.onDrop(session, { clientX, clientY });
15770 } catch (err) {
15771 console.error("[desktop-mode] drop target onDrop threw:", target2.id, err);
15772 }
15773 try {
15774 session._callbacks.onCommit?.(target2);
15775 } catch (err) {
15776 console.error("[desktop-mode] drag onCommit threw:", err);
15777 }
15778 dispatchOnDocument(DRAG_EVENTS.COMMIT, {
15779 payload: session.payload,
15780 targetId: target2.id
15781 });
15782 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason: "commit" });
15783 if (this._active === prevActive) {
15784 this._active = null;
15785 }
15786 }
15787 _cancel(session, reason) {
15788 if (session._finished) {
15789 return;
15790 }
15791 session._finished = true;
15792 if (session._lifted) {
15793 this._lastLiftedEndAt = Date.now();
15794 }
15795 if (session._currentTarget) {
15796 fireLeave(session._currentTarget, session);
15797 }
15798 this._cleanupDom(session);
15799 this._active = null;
15800 try {
15801 session._callbacks.onCancel?.(reason);
15802 } catch (err) {
15803 console.error("[desktop-mode] drag onCancel threw:", err);
15804 }
15805 dispatchOnDocument(DRAG_EVENTS.CANCEL, { payload: session.payload, reason });
15806 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason });
15807 }
15808 _cleanupDom(session) {
15809 try {
15810 session.payload.source.classList.remove(SOURCE_DRAGGING_CLASS);
15811 } catch {
15812 }
15813 session._ghost?.dispose();
15814 session._ghost = null;
15815 session._currentTarget = null;
15816 session._currentAccepted = false;
15817 if (typeof document !== "undefined" && document.body) {
15818 document.body.removeAttribute(BODY_DRAGGING_ATTR);
15819 document.body.removeAttribute(BODY_DRAG_TYPE_ATTR);
15820 document.body.removeAttribute(BODY_DRAG_MODE_ATTR);
15821 }
15822 scrubOrphans();
15823 }
15824 }
15825 function dispatchOnDocument(type, detail) {
15826 if (typeof document === "undefined") {
15827 return;
15828 }
15829 document.dispatchEvent(new CustomEvent(type, { detail }));
15830 }
15831 function fireEnter(target2, session) {
15832 try {
15833 target2.onEnter?.(session);
15834 } catch (err) {
15835 console.error("[desktop-mode] drop target onEnter threw:", target2.id, err);
15836 }
15837 dispatchOnDocument(DRAG_EVENTS.ENTER, {
15838 payload: session.payload,
15839 targetId: target2.id
15840 });
15841 }
15842 function fireLeave(target2, session) {
15843 try {
15844 target2.onLeave?.(session);
15845 } catch (err) {
15846 console.error("[desktop-mode] drop target onLeave threw:", target2.id, err);
15847 }
15848 dispatchOnDocument(DRAG_EVENTS.LEAVE, {
15849 payload: session.payload,
15850 targetId: target2.id
15851 });
15852 }
15853 function findOrphans() {
15854 if (typeof document === "undefined") {
15855 return [];
15856 }
15857 const out = [];
15858 for (const sel of [
15859 `.${SOURCE_DRAGGING_CLASS}`,
15860 `.${TARGET_DROP_ACTIVE_CLASS}`,
15861 `[${TRASH_DROP_ACTIVE_ATTR$1}]`,
15862 `[${FILES_DROP_ACTIVE_ATTR}]`
15863 ]) {
15864 document.querySelectorAll(sel).forEach((el) => out.push(el));
15865 }
15866 return out;
15867 }
15868 function scrubOrphans() {
15869 for (const el of findOrphans()) {
15870 el.classList.remove(SOURCE_DRAGGING_CLASS, TARGET_DROP_ACTIVE_CLASS);
15871 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR$1);
15872 el.removeAttribute(FILES_DROP_ACTIVE_ATTR);
15873 }
15874 }
15875 const TARGET_ID_PREFIX = "desktop-mode-iframe-drop-";
15876 const IFRAME_SELECTOR = "iframe.desktop-mode-window__iframe";
15877 const DROP_ACTIVE_ATTR = "data-desktop-mode-iframe-drop-active";
15878 let _installed$1 = false;
15879 let _dragManager = null;
15880 const _suppressedIframes = /* @__PURE__ */ new Map();
15881 const _activeRegistrations = /* @__PURE__ */ new Map();
15882 let _bridgeInterceptPayload = null;
15883 let _lastHoveredBridgeIframe = null;
15884 function suppressIframePointerEventsBridge() {
15885 const iframes = document.querySelectorAll(
15886 IFRAME_SELECTOR
15887 );
15888 iframes.forEach((iframe) => {
15889 if (_suppressedIframes.has(iframe)) {
15890 return;
15891 }
15892 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
15893 iframe.style.pointerEvents = "none";
15894 });
15895 }
15896 function restoreIframePointerEvents() {
15897 _suppressedIframes.forEach((prev, iframe) => {
15898 iframe.style.pointerEvents = prev;
15899 });
15900 _suppressedIframes.clear();
15901 }
15902 function findIframeAtCursor(clientX, clientY) {
15903 const el = document.elementFromPoint(clientX, clientY);
15904 if (!el) {
15905 return null;
15906 }
15907 const win = el.closest(".desktop-mode-window");
15908 if (!(win instanceof HTMLElement)) {
15909 return null;
15910 }
15911 const iframe = win.querySelector(IFRAME_SELECTOR);
15912 return iframe instanceof HTMLIFrameElement ? iframe : null;
15913 }
15914 const onBridgeDragOver = (e) => {
15915 if (!_bridgeInterceptPayload) {
15916 return;
15917 }
15918 e.preventDefault();
15919 if (e.dataTransfer) {
15920 e.dataTransfer.dropEffect = "copy";
15921 }
15922 const iframe = findIframeAtCursor(e.clientX, e.clientY);
15923 if (iframe === _lastHoveredBridgeIframe) {
15924 return;
15925 }
15926 if (_lastHoveredBridgeIframe) {
15927 postIntoIframe(_lastHoveredBridgeIframe, {
15928 type: "desktop-mode-drag-leave"
15929 });
15930 }
15931 _lastHoveredBridgeIframe = iframe;
15932 if (iframe) {
15933 postIntoIframe(iframe, {
15934 type: "desktop-mode-drag-over",
15935 payload: _bridgeInterceptPayload
15936 });
15937 }
15938 };
15939 const onBridgeDrop = (e) => {
15940 if (!_bridgeInterceptPayload) {
15941 return;
15942 }
15943 e.preventDefault();
15944 e.stopPropagation();
15945 if (typeof e.stopImmediatePropagation === "function") {
15946 e.stopImmediatePropagation();
15947 }
15948 const iframe = findIframeAtCursor(e.clientX, e.clientY);
15949 const payload = _bridgeInterceptPayload;
15950 stopBridgeIntercept();
15951 if (!iframe) {
15952 return;
15953 }
15954 const rect = iframe.getBoundingClientRect();
15955 postIntoIframe(iframe, {
15956 type: "desktop-mode-drop",
15957 payload,
15958 position: {
15959 x: e.clientX - rect.left,
15960 y: e.clientY - rect.top
15961 }
15962 });
15963 };
15964 const onBridgeDragEnd = () => {
15965 stopBridgeIntercept();
15966 };
15967 function startBridgeIntercept(payload) {
15968 if (_bridgeInterceptPayload) {
15969 _bridgeInterceptPayload = payload;
15970 return;
15971 }
15972 _bridgeInterceptPayload = payload;
15973 suppressIframePointerEventsBridge();
15974 document.addEventListener("dragover", onBridgeDragOver, true);
15975 document.addEventListener("drop", onBridgeDrop, true);
15976 document.addEventListener("dragend", onBridgeDragEnd, true);
15977 }
15978 function stopBridgeIntercept() {
15979 if (!_bridgeInterceptPayload) {
15980 return;
15981 }
15982 _bridgeInterceptPayload = null;
15983 if (_lastHoveredBridgeIframe) {
15984 postIntoIframe(_lastHoveredBridgeIframe, {
15985 type: "desktop-mode-drag-leave"
15986 });
15987 _lastHoveredBridgeIframe = null;
15988 }
15989 document.removeEventListener("dragover", onBridgeDragOver, true);
15990 document.removeEventListener("drop", onBridgeDrop, true);
15991 document.removeEventListener("dragend", onBridgeDragEnd, true);
15992 restoreIframePointerEvents();
15993 }
15994 function extractBridgePayload(payload) {
15995 if (!payload || typeof payload !== "object") {
15996 return void 0;
15997 }
15998 const obj = payload;
15999 if (obj.type !== "shortcut" && obj.type !== "desktop-file") {
16000 return void 0;
16001 }
16002 const data = obj.data;
16003 return data?.bridgePayload;
16004 }
16005 function postIntoIframe(iframe, msg) {
16006 const w = iframe.contentWindow;
16007 if (!w) {
16008 return;
16009 }
16010 try {
16011 w.postMessage(msg, window.location.origin);
16012 } catch {
16013 }
16014 }
16015 function registerDropTargetFor(dragManager, iframe, target2, windowId) {
16016 return dragManager.registerDropTarget({
16017 id: `${TARGET_ID_PREFIX}${windowId}`,
16018 element: target2,
16019 accept: (payload) => !!extractBridgePayload(payload),
16020 onEnter: (session) => {
16021 const bridge = extractBridgePayload(session.payload);
16022 if (!bridge) {
16023 return;
16024 }
16025 target2.setAttribute(DROP_ACTIVE_ATTR, "");
16026 postIntoIframe(iframe, {
16027 type: "desktop-mode-drag-over",
16028 payload: bridge
16029 });
16030 },
16031 onLeave: () => {
16032 target2.removeAttribute(DROP_ACTIVE_ATTR);
16033 postIntoIframe(iframe, { type: "desktop-mode-drag-leave" });
16034 },
16035 onDrop: (session, ev) => {
16036 target2.removeAttribute(DROP_ACTIVE_ATTR);
16037 const bridge = extractBridgePayload(session.payload);
16038 if (!bridge) {
16039 return;
16040 }
16041 const rect = iframe.getBoundingClientRect();
16042 postIntoIframe(iframe, {
16043 type: "desktop-mode-drop",
16044 payload: bridge,
16045 position: {
16046 x: ev.clientX - rect.left,
16047 y: ev.clientY - rect.top
16048 }
16049 });
16050 }
16051 });
16052 }
16053 function deriveWindowIdFromIframe(iframe) {
16054 let cur = iframe.parentElement;
16055 while (cur) {
16056 if (cur.id.startsWith("wp-window-")) {
16057 return cur.id.slice("wp-window-".length);
16058 }
16059 cur = cur.parentElement;
16060 }
16061 return `unknown-${Math.random().toString(36).slice(2, 10)}`;
16062 }
16063 function onDragStart(payload) {
16064 const dragManager = _dragManager;
16065 if (!dragManager) {
16066 return;
16067 }
16068 const iframes = document.querySelectorAll(IFRAME_SELECTOR);
16069 const isBridgeable = !!extractBridgePayload(payload);
16070 console.info(
16071 "[desktop-mode] drag-start: suppressing %d iframe(s); bridgeable=%s",
16072 iframes.length,
16073 isBridgeable,
16074 payload
16075 );
16076 iframes.forEach((iframe) => {
16077 if (!_suppressedIframes.has(iframe)) {
16078 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
16079 iframe.style.pointerEvents = "none";
16080 }
16081 if (!isBridgeable) {
16082 return;
16083 }
16084 if (_activeRegistrations.has(iframe)) {
16085 return;
16086 }
16087 const dropTargetEl = iframe.parentElement;
16088 if (!dropTargetEl) {
16089 return;
16090 }
16091 const windowId = deriveWindowIdFromIframe(iframe);
16092 const deregister = registerDropTargetFor(
16093 dragManager,
16094 iframe,
16095 dropTargetEl,
16096 windowId
16097 );
16098 _activeRegistrations.set(iframe, deregister);
16099 });
16100 }
16101 function onDragEnd() {
16102 _suppressedIframes.forEach((prev, iframe) => {
16103 iframe.style.pointerEvents = prev;
16104 });
16105 _suppressedIframes.clear();
16106 _activeRegistrations.forEach((deregister) => {
16107 try {
16108 deregister();
16109 } catch {
16110 }
16111 });
16112 _activeRegistrations.clear();
16113 }
16114 function installIframeDropTargets(dragManager) {
16115 if (_installed$1) {
16116 return;
16117 }
16118 _installed$1 = true;
16119 _dragManager = dragManager;
16120 document.addEventListener(DRAG_EVENTS.START, (e) => {
16121 const detail = e.detail;
16122 onDragStart(detail?.payload);
16123 });
16124 document.addEventListener(DRAG_EVENTS.END, () => {
16125 onDragEnd();
16126 });
16127 document.addEventListener(DRAG_BRIDGE_EVENTS.START, (e) => {
16128 const detail = e.detail;
16129 if (!detail?.payload) {
16130 return;
16131 }
16132 startBridgeIntercept(detail.payload);
16133 });
16134 document.addEventListener(DRAG_BRIDGE_EVENTS.END, () => {
16135 stopBridgeIntercept();
16136 });
16137 addAction(
16138 HOOKS.WINDOW_CLOSED,
16139 "desktop-mode/drag/iframe-drop-targets-window-close",
16140 () => {
16141 for (const [iframe] of Array.from(_suppressedIframes)) {
16142 if (!iframe.isConnected) {
16143 _suppressedIframes.delete(iframe);
16144 }
16145 }
16146 for (const [iframe, deregister] of Array.from(_activeRegistrations)) {
16147 if (!iframe.isConnected) {
16148 try {
16149 deregister();
16150 } catch {
16151 }
16152 _activeRegistrations.delete(iframe);
16153 }
16154 }
16155 }
16156 );
16157 window.__desktopModeIframeDropDebug = () => ({
16158 installed: _installed$1,
16159 iframesInDom: document.querySelectorAll(IFRAME_SELECTOR).length,
16160 suppressedCount: _suppressedIframes.size,
16161 registeredCount: _activeRegistrations.size,
16162 suppressedIframeIds: Array.from(_suppressedIframes.keys()).map(
16163 deriveWindowIdFromIframe
16164 )
16165 });
16166 }
16167 function collectOpenables() {
16168 const desktop = window.wp?.desktop;
16169 if (!desktop) {
16170 return [];
16171 }
16172 const wm = desktop.windowManager;
16173 const config = desktop.config;
16174 if (!wm || !config) {
16175 return [];
16176 }
16177 const items = [];
16178 const fromMenu = (item, group) => ({
16179 id: item.id,
16180 label: item.title,
16181 description: group,
16182 icon: item.icon,
16183 open: () => wm.open({
16184 id: item.id,
16185 baseId: item.id,
16186 url: item.url,
16187 title: item.title,
16188 icon: item.icon
16189 })
16190 });
16191 for (const item of config.dockItems ?? []) {
16192 items.push(fromMenu(item, "Admin menu"));
16193 }
16194 const filtered = applyFilters(
16195 "desktop-mode.open-command.items",
16196 items
16197 );
16198 return Array.isArray(filtered) ? filtered : items;
16199 }
16200 const openCommand = {
16201 slug: "open",
16202 label: "Open",
16203 description: "Open an admin page or registered window.",
16204 hint: "[window]",
16205 icon: "dashicons-external",
16206 /**
16207 * Suggest matching windows as the user types args. Simple
16208 * case-insensitive substring match against label AND id so
16209 * "add" finds "Add New Post" and "jorvy" finds Jorvy whether
16210 * the plugin listed it with a friendly label or the slug.
16211 */
16212 suggest(args) {
16213 const q = args.trim().toLowerCase();
16214 const list2 = collectOpenables();
16215 const hits = q === "" ? list2 : list2.filter(
16216 (w) => w.label.toLowerCase().includes(q) || w.id.toLowerCase().includes(q)
16217 );
16218 return hits.slice(0, 12).map((w) => ({
16219 value: w.label,
16220 label: w.label,
16221 description: w.description,
16222 icon: w.icon ?? "dashicons-external"
16223 }));
16224 },
16225 run(args, ctx) {
16226 const q = args.trim();
16227 if (!q) {
16228 return "Type the name of a window to open, for example `/open Posts`.";
16229 }
16230 const list2 = collectOpenables();
16231 const ql = q.toLowerCase();
16232 const match = list2.find((w) => w.label.toLowerCase() === ql || w.id.toLowerCase() === ql) ?? list2.find(
16233 (w) => w.label.toLowerCase().includes(ql) || w.id.toLowerCase().includes(ql)
16234 );
16235 if (!match) {
16236 return `No window matching **${q}** — try \`/open\` alone to see available options.`;
16237 }
16238 match.open();
16239 ctx.close();
16240 }
16241 };
16242 function registerBuiltInCommands() {
16243 registerCommand(openCommand);
16244 }
16245 const palettes = [];
16246 const listeners$2 = /* @__PURE__ */ new Set();
16247 function registerPalette(p) {
16248 if (!p || typeof p.id !== "string" || p.id === "") {
16249 return () => {
16250 };
16251 }
16252 if (typeof p.open !== "function" || typeof p.close !== "function" || typeof p.isOpen !== "function") {
16253 return () => {
16254 };
16255 }
16256 const idx = palettes.findIndex((x) => x.id === p.id);
16257 if (idx >= 0) {
16258 palettes[idx] = p;
16259 } else {
16260 palettes.push(p);
16261 }
16262 notify$2();
16263 return () => {
16264 const i = palettes.findIndex((x) => x.id === p.id);
16265 if (i >= 0) {
16266 palettes.splice(i, 1);
16267 notify$2();
16268 }
16269 };
16270 }
16271 function unregisterPalette(id) {
16272 const idx = palettes.findIndex((x) => x.id === id);
16273 if (idx >= 0) {
16274 palettes.splice(idx, 1);
16275 notify$2();
16276 }
16277 }
16278 function listPalettes() {
16279 return palettes.slice();
16280 }
16281 function notify$2() {
16282 for (const cb of Array.from(listeners$2)) {
16283 try {
16284 cb();
16285 } catch (err) {
16286 if (typeof console !== "undefined") {
16287 console.error("[desktop-mode] palette-registry listener threw:", err);
16288 }
16289 }
16290 }
16291 }
16292 function cyclePalettes() {
16293 if (palettes.length === 0) {
16294 return;
16295 }
16296 const cur = palettes.findIndex((p) => {
16297 try {
16298 return p.isOpen();
16299 } catch {
16300 return false;
16301 }
16302 });
16303 if (cur === -1) {
16304 try {
16305 palettes[0].open();
16306 } catch {
16307 }
16308 return;
16309 }
16310 try {
16311 palettes[cur].close();
16312 } catch {
16313 }
16314 const next = cur + 1;
16315 if (next < palettes.length) {
16316 try {
16317 palettes[next].open();
16318 } catch {
16319 }
16320 }
16321 }
16322 function openPaletteOnly(id) {
16323 const target2 = palettes.find((p) => p.id === id);
16324 if (!target2) {
16325 return;
16326 }
16327 for (const p of palettes) {
16328 if (p.id !== id) {
16329 try {
16330 if (p.isOpen()) {
16331 p.close();
16332 }
16333 } catch {
16334 }
16335 }
16336 }
16337 try {
16338 target2.open();
16339 } catch {
16340 }
16341 }
16342 let installed$1 = false;
16343 function installPaletteShortcut() {
16344 if (installed$1) {
16345 return;
16346 }
16347 installed$1 = true;
16348 document.addEventListener(
16349 "keydown",
16350 (e) => {
16351 if (!(e.metaKey || e.ctrlKey) || e.key !== "k") {
16352 return;
16353 }
16354 if (e.shiftKey || e.altKey) {
16355 return;
16356 }
16357 e.preventDefault();
16358 e.stopImmediatePropagation();
16359 cyclePalettes();
16360 },
16361 true
16362 );
16363 const origin = window.location.origin;
16364 window.addEventListener("message", (e) => {
16365 if (e.origin !== origin) {
16366 return;
16367 }
16368 const data = e.data;
16369 if (data && data.type === "desktop-mode-palette-cycle") {
16370 cyclePalettes();
16371 }
16372 });
16373 }
16374 const suppliers = /* @__PURE__ */ new Map();
16375 const subscribers = /* @__PURE__ */ new Map();
16376 let booted$2 = false;
16377 const heartbeat = {
16378 contribute(field, supplier) {
16379 suppliers.set(field, supplier);
16380 return () => {
16381 if (suppliers.get(field) === supplier) {
16382 suppliers.delete(field);
16383 }
16384 };
16385 },
16386 subscribe(field, cb) {
16387 let set = subscribers.get(field);
16388 if (!set) {
16389 set = /* @__PURE__ */ new Set();
16390 subscribers.set(field, set);
16391 }
16392 set.add(cb);
16393 return () => {
16394 set.delete(cb);
16395 };
16396 }
16397 };
16398 function bootHeartbeatBus() {
16399 if (booted$2) {
16400 return;
16401 }
16402 booted$2 = true;
16403 const $ = window.jQuery;
16404 if (!$) {
16405 console.warn(
16406 "[desktop-mode/heartbeat] jQuery missing — Heartbeat bus disabled."
16407 );
16408 return;
16409 }
16410 $(document).on("heartbeat-send", (...args) => {
16411 const data = args[1];
16412 if (!data) {
16413 return;
16414 }
16415 for (const [field, supplier] of suppliers) {
16416 try {
16417 data[field] = supplier();
16418 } catch (err) {
16419 console.error(
16420 `[desktop-mode/heartbeat] supplier for "${field}" threw:`,
16421 err
16422 );
16423 }
16424 }
16425 });
16426 $(document).on("heartbeat-tick", (...args) => {
16427 const response = args[1];
16428 if (!response) {
16429 return;
16430 }
16431 for (const [field, set] of subscribers) {
16432 const value = response[field];
16433 if (value === void 0) {
16434 continue;
16435 }
16436 for (const cb of set) {
16437 try {
16438 cb(value);
16439 } catch (err) {
16440 console.error(
16441 `[desktop-mode/heartbeat] subscriber for "${field}" threw:`,
16442 err
16443 );
16444 }
16445 }
16446 }
16447 });
16448 }
16449 const store$2 = createSharedStore(
16450 "desktop-mode/presence",
16451 () => ({ byUser: /* @__PURE__ */ new Map(), serverTimeMs: 0 })
16452 );
16453 const ACTIVE_THRESHOLD_MS = 5 * 60 * 1e3;
16454 let lastInputMs = Date.now();
16455 let booted$1 = false;
16456 function noteUserActivity() {
16457 lastInputMs = Date.now();
16458 }
16459 function applySnapshot(block) {
16460 if (!block || !block.snapshot) {
16461 return;
16462 }
16463 const previous = store$2.state.byUser;
16464 const next = new Map(previous);
16465 const transitions = [];
16466 for (const [rawId, raw] of Object.entries(block.snapshot)) {
16467 const userId = Number(rawId);
16468 if (!Number.isFinite(userId) || userId <= 0) {
16469 continue;
16470 }
16471 const status = raw?.status ?? "offline";
16472 const entry = {
16473 status,
16474 lastSeenMs: Number(raw?.lastSeenMs ?? 0) || 0,
16475 lastActiveMs: Number(raw?.lastActiveMs ?? 0) || 0
16476 };
16477 const old = previous.get(userId);
16478 next.set(userId, entry);
16479 if (!old || old.status !== entry.status) {
16480 transitions.push({
16481 userId,
16482 oldStatus: old ? old.status : null,
16483 newStatus: entry.status,
16484 entry
16485 });
16486 }
16487 }
16488 store$2.state.byUser = next;
16489 if (typeof block.serverTimeMs === "number") {
16490 store$2.state.serverTimeMs = block.serverTimeMs;
16491 }
16492 store$2.notify();
16493 for (const t of transitions) {
16494 const detail = {
16495 userId: t.userId,
16496 oldStatus: t.oldStatus,
16497 newStatus: t.newStatus,
16498 lastSeenMs: t.entry.lastSeenMs,
16499 lastActiveMs: t.entry.lastActiveMs
16500 };
16501 document.dispatchEvent(
16502 new CustomEvent("desktop-mode-presence-changed", { detail })
16503 );
16504 activity.publish("desktop-mode/presence-changed", detail);
16505 }
16506 activity.publish("desktop-mode/presence-snapshot-applied", {
16507 applied: Object.keys(block.snapshot).length,
16508 transitions: transitions.length
16509 });
16510 }
16511 function bootPresenceProbe() {
16512 if (booted$1) {
16513 return;
16514 }
16515 booted$1 = true;
16516 document.addEventListener("pointerdown", noteUserActivity, {
16517 capture: true,
16518 passive: true
16519 });
16520 document.addEventListener("keydown", noteUserActivity, {
16521 capture: true,
16522 passive: true
16523 });
16524 document.addEventListener("visibilitychange", () => {
16525 if (!document.hidden) {
16526 noteUserActivity();
16527 }
16528 });
16529 heartbeat.contribute("desktop_mode_presence_active", () => true);
16530 heartbeat.contribute(
16531 "desktop_mode_user_active",
16532 () => Date.now() - lastInputMs < ACTIVE_THRESHOLD_MS
16533 );
16534 heartbeat.subscribe("desktop_mode_presence", (block) => {
16535 applySnapshot(block);
16536 });
16537 }
16538 function getStatus(userId) {
16539 const entry = store$2.state.byUser.get(userId);
16540 return entry ? entry.status : "offline";
16541 }
16542 function getAll() {
16543 return new Map(store$2.state.byUser);
16544 }
16545 function getEntry(userId) {
16546 return store$2.state.byUser.get(userId) ?? null;
16547 }
16548 function subscribe$1(cb) {
16549 return store$2.subscribe((s) => cb(s));
16550 }
16551 function markActive() {
16552 noteUserActivity();
16553 }
16554 function applyPresenceBatch(updates) {
16555 if (!Array.isArray(updates) || updates.length === 0) {
16556 return;
16557 }
16558 const previous = store$2.state.byUser;
16559 const next = new Map(previous);
16560 const transitions = [];
16561 for (const u of updates) {
16562 const userId = Number(u.userId);
16563 if (!Number.isFinite(userId) || userId <= 0) {
16564 continue;
16565 }
16566 const old = previous.get(userId);
16567 const entry = {
16568 status: u.status,
16569 lastSeenMs: typeof u.lastSeenMs === "number" ? u.lastSeenMs : old?.lastSeenMs ?? 0,
16570 lastActiveMs: typeof u.lastActiveMs === "number" ? u.lastActiveMs : old?.lastActiveMs ?? 0
16571 };
16572 next.set(userId, entry);
16573 if (!old || old.status !== entry.status) {
16574 transitions.push({
16575 userId,
16576 oldStatus: old ? old.status : null,
16577 newStatus: entry.status,
16578 entry
16579 });
16580 }
16581 }
16582 if (transitions.length === 0 && next.size === previous.size) {
16583 return;
16584 }
16585 store$2.state.byUser = next;
16586 store$2.notify();
16587 for (const t of transitions) {
16588 const detail = {
16589 userId: t.userId,
16590 oldStatus: t.oldStatus,
16591 newStatus: t.newStatus,
16592 lastSeenMs: t.entry.lastSeenMs,
16593 lastActiveMs: t.entry.lastActiveMs
16594 };
16595 document.dispatchEvent(
16596 new CustomEvent("desktop-mode-presence-changed", { detail })
16597 );
16598 activity.publish("desktop-mode/presence-changed", detail);
16599 }
16600 activity.publish("desktop-mode/presence-snapshot-applied", {
16601 applied: updates.length,
16602 transitions: transitions.length
16603 });
16604 }
16605 const presenceApi = Object.freeze({
16606 getStatus,
16607 getAll,
16608 getEntry,
16609 subscribe: subscribe$1,
16610 markActive,
16611 applyBatch: applyPresenceBatch
16612 });
16613 const HEARTBEAT_FIELD = "desktop_mode_nonces";
16614 const targets = /* @__PURE__ */ new Map();
16615 let booted = false;
16616 function registerNonceTarget(action, updater) {
16617 if (typeof action !== "string" || action === "") {
16618 return () => {
16619 };
16620 }
16621 let set = targets.get(action);
16622 if (!set) {
16623 set = /* @__PURE__ */ new Set();
16624 targets.set(action, set);
16625 }
16626 set.add(updater);
16627 return () => {
16628 set.delete(updater);
16629 };
16630 }
16631 function bootNonceRefresh() {
16632 if (booted) {
16633 return;
16634 }
16635 booted = true;
16636 heartbeat.subscribe(HEARTBEAT_FIELD, (payload) => {
16637 if (!payload || typeof payload !== "object") {
16638 return;
16639 }
16640 for (const [action, value] of Object.entries(payload)) {
16641 if (typeof value !== "string" || value === "") {
16642 continue;
16643 }
16644 const set = targets.get(action);
16645 if (!set) {
16646 continue;
16647 }
16648 for (const updater of set) {
16649 try {
16650 updater(value);
16651 } catch (err) {
16652 console.error(
16653 `[desktop-mode/nonce-refresh] updater for "${action}" threw:`,
16654 err
16655 );
16656 }
16657 }
16658 }
16659 });
16660 registerShellAndPluginsWindowTargets();
16661 }
16662 function registerShellAndPluginsWindowTargets() {
16663 registerNonceTarget("wp_rest", updateAllRestNonces);
16664 registerNonceTarget("desktop-mode-plugins", (fresh) => {
16665 writeWindowConfigField("desktop-mode-plugins", "ajaxNonce", fresh);
16666 });
16667 registerNonceTarget("updates", (fresh) => {
16668 writeWindowConfigField("desktop-mode-plugins", "updatesNonce", fresh);
16669 });
16670 }
16671 function updateAllRestNonces(fresh) {
16672 const cfg = readShellConfig();
16673 if (cfg && typeof cfg.restNonce === "string") {
16674 cfg.restNonce = fresh;
16675 }
16676 const windowConfigs = readWindowConfigs();
16677 if (!windowConfigs) {
16678 return;
16679 }
16680 for (const blob of Object.values(windowConfigs)) {
16681 if (blob && typeof blob === "object" && typeof blob.restNonce === "string") {
16682 blob.restNonce = fresh;
16683 }
16684 }
16685 }
16686 function writeWindowConfigField(windowId, field, value) {
16687 const blobs = readWindowConfigs();
16688 const blob = blobs?.[windowId];
16689 if (blob && typeof blob === "object") {
16690 blob[field] = value;
16691 }
16692 }
16693 function readShellConfig() {
16694 if (typeof window === "undefined") {
16695 return void 0;
16696 }
16697 return window.desktopModeConfig;
16698 }
16699 function readWindowConfigs() {
16700 if (typeof window === "undefined") {
16701 return void 0;
16702 }
16703 return window.desktopModeWindowConfig;
16704 }
16705 const VIEWPORT_CLAMP_MARGIN = 12;
16706 function findDockEntryForUrl(url, config) {
16707 const windowId = deriveWindowId(url, config.adminUrl);
16708 return (config.dockItems || []).find(
16709 (i) => deriveWindowId(i.url, config.adminUrl) === windowId || (i.submenu || []).some(
16710 (s) => deriveWindowId(s.url, config.adminUrl) === windowId
16711 )
16712 );
16713 }
16714 function clampGeometryToViewport(win, rect) {
16715 const maxW = Math.max(200, rect.width - VIEWPORT_CLAMP_MARGIN * 2);
16716 const maxH = Math.max(200, rect.height - VIEWPORT_CLAMP_MARGIN * 2);
16717 const width = Math.min(win.width, maxW);
16718 const height = Math.min(win.height, maxH);
16719 const maxX = Math.max(0, rect.width - width - VIEWPORT_CLAMP_MARGIN);
16720 const maxY = Math.max(0, rect.height - height - VIEWPORT_CLAMP_MARGIN);
16721 const x = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.x, maxX));
16722 const y = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.y, maxY));
16723 return { x, y, width, height };
16724 }
16725 const INITIAL_ORIGIN$1 = window.location.origin;
16726 function bindTopWindowLinkInterceptor(manager, config) {
16727 document.addEventListener(
16728 "click",
16729 (e) => {
16730 if (e.defaultPrevented) {
16731 return;
16732 }
16733 if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
16734 return;
16735 }
16736 const target2 = e.target;
16737 const link = target2 && target2.closest ? target2.closest("a[href]") : null;
16738 if (!link) {
16739 return;
16740 }
16741 const anchor = link;
16742 const linkTarget = anchor.getAttribute("target");
16743 if (linkTarget && linkTarget !== "" && linkTarget !== "_self") {
16744 return;
16745 }
16746 if (anchor.hasAttribute("download")) {
16747 return;
16748 }
16749 const rawHref = anchor.getAttribute("href");
16750 if (!rawHref || rawHref.charAt(0) === "#") {
16751 return;
16752 }
16753 if (/^(mailto:|tel:|javascript:|data:)/i.test(rawHref)) {
16754 return;
16755 }
16756 let url;
16757 try {
16758 url = new URL(rawHref, window.location.href);
16759 } catch (err) {
16760 if (typeof console !== "undefined") {
16761 console.warn(
16762 "[desktop-mode] Couldn’t parse href; letting the browser handle the click:",
16763 rawHref,
16764 err
16765 );
16766 }
16767 return;
16768 }
16769 if (url.origin !== INITIAL_ORIGIN$1) {
16770 return;
16771 }
16772 let adminPath;
16773 try {
16774 adminPath = new URL(config.adminUrl).pathname;
16775 } catch (err) {
16776 if (typeof console !== "undefined") {
16777 console.error(
16778 "[desktop-mode] config.adminUrl is not a valid URL; falling back to /wp-admin/:",
16779 config.adminUrl,
16780 err
16781 );
16782 }
16783 adminPath = "/wp-admin/";
16784 }
16785 if (!url.pathname.startsWith(adminPath)) {
16786 return;
16787 }
16788 if (/\/(admin-post|admin-ajax)\.php$/.test(url.pathname)) {
16789 return;
16790 }
16791 if (url.searchParams.has("action") && url.searchParams.get("action") === "logout") {
16792 return;
16793 }
16794 if (url.searchParams.has("desktop_mode_classic")) {
16795 return;
16796 }
16797 e.preventDefault();
16798 e.stopPropagation();
16799 if (tryNativeUrlRemap(url.href)) {
16800 return;
16801 }
16802 const windowId = deriveWindowId(url.href, config.adminUrl);
16803 const dockEntry = findDockEntryForUrl(url.href, config);
16804 const fallbackTitle = (anchor.textContent || "").trim() || dockEntry?.title || "";
16805 const isAdminBarNew = !!anchor.closest("#wp-admin-bar-new-content");
16806 const openOpts = {
16807 id: windowId,
16808 baseId: windowId,
16809 multi: !!dockEntry?.multi || isAdminBarNew,
16810 url: url.href,
16811 parentUrl: dockEntry?.url ?? url.href,
16812 title: dockEntry?.title || fallbackTitle,
16813 icon: dockEntry?.icon || "dashicons-admin-generic",
16814 submenu: dockEntry?.submenu
16815 };
16816 if (isAdminBarNew) {
16817 void manager.openNew(openOpts);
16818 return;
16819 }
16820 void manager.open(openOpts);
16821 },
16822 true
16823 );
16824 }
16825 const REGISTRY_CHANGED_EVENT = "desktop-mode-registry-changed";
16826 function diffIds(prev, next) {
16827 const prevIds = /* @__PURE__ */ new Set();
16828 if (Array.isArray(prev)) {
16829 for (const item of prev) {
16830 if (item && typeof item.id === "string") {
16831 prevIds.add(item.id);
16832 }
16833 }
16834 }
16835 const nextIds = /* @__PURE__ */ new Set();
16836 for (const item of next) {
16837 if (item && typeof item.id === "string") {
16838 nextIds.add(item.id);
16839 }
16840 }
16841 const added = [];
16842 for (const id of nextIds) {
16843 if (!prevIds.has(id)) {
16844 added.push(id);
16845 }
16846 }
16847 const removed = [];
16848 for (const id of prevIds) {
16849 if (!nextIds.has(id)) {
16850 removed.push(id);
16851 }
16852 }
16853 return { added, removed };
16854 }
16855 function emitRegistryChanged(registry2, prev, next) {
16856 const { added, removed } = diffIds(prev, next);
16857 if (added.length === 0 && removed.length === 0) {
16858 return;
16859 }
16860 if (typeof document === "undefined") {
16861 return;
16862 }
16863 const detail = { registry: registry2, added, removed };
16864 document.dispatchEvent(
16865 new CustomEvent(REGISTRY_CHANGED_EVENT, { detail })
16866 );
16867 }
16868 function createApplyPayload(deps2) {
16869 const {
16870 applyDockItems,
16871 config,
16872 syncNativeWindows,
16873 syncServerWidgets,
16874 syncServerWallpapers,
16875 syncServerCommands,
16876 syncServerSettingsTabs,
16877 syncServerTitleBarButtons,
16878 syncServerDockRailRenderers,
16879 renderIcons
16880 } = deps2;
16881 return function applyPayload(payload) {
16882 const dockItems = payload.dockItems;
16883 const nativeWindows = payload.nativeWindows;
16884 const serverWidgets = payload.serverWidgets;
16885 const serverWallpapers = payload.serverWallpapers;
16886 const serverCommandScripts = payload.serverCommandScripts;
16887 const serverCommands = payload.serverCommands;
16888 const serverSettingsTabScripts = payload.serverSettingsTabScripts;
16889 const serverSettingsTabs = payload.serverSettingsTabs;
16890 const serverDockRailRendererScripts = payload.serverDockRailRendererScripts;
16891 const serverTitleBarButtonScripts = payload.serverTitleBarButtonScripts;
16892 const serverWindowNotices = payload.serverWindowNotices;
16893 const desktopIcons = payload.desktopIcons;
16894 if (!Array.isArray(dockItems) || dockItems.length === 0) {
16895 return;
16896 }
16897 const prevDockItems = config.dockItems;
16898 applyDockItems(dockItems);
16899 config.dockItems = dockItems;
16900 emitRegistryChanged(
16901 "dock-items",
16902 prevDockItems,
16903 dockItems
16904 );
16905 if (Array.isArray(nativeWindows)) {
16906 const prevNativeWindows = config.nativeWindows;
16907 void syncNativeWindows(
16908 nativeWindows
16909 );
16910 config.nativeWindows = nativeWindows;
16911 emitRegistryChanged(
16912 "native-windows",
16913 prevNativeWindows,
16914 nativeWindows
16915 );
16916 }
16917 if (Array.isArray(serverWidgets)) {
16918 void syncServerWidgets(
16919 serverWidgets
16920 );
16921 config.serverWidgets = serverWidgets;
16922 }
16923 if (Array.isArray(serverWallpapers)) {
16924 void syncServerWallpapers(
16925 serverWallpapers
16926 );
16927 config.serverWallpapers = serverWallpapers;
16928 }
16929 if (Array.isArray(serverCommandScripts)) {
16930 void syncServerCommands(
16931 serverCommandScripts,
16932 Array.isArray(serverCommands) ? serverCommands : void 0
16933 );
16934 config.serverCommandScripts = serverCommandScripts;
16935 if (Array.isArray(serverCommands)) {
16936 config.serverCommands = serverCommands;
16937 }
16938 }
16939 if (Array.isArray(serverSettingsTabScripts)) {
16940 void syncServerSettingsTabs(
16941 serverSettingsTabScripts,
16942 Array.isArray(serverSettingsTabs) ? serverSettingsTabs : void 0
16943 );
16944 config.serverSettingsTabScripts = serverSettingsTabScripts;
16945 if (Array.isArray(serverSettingsTabs)) {
16946 config.serverSettingsTabs = serverSettingsTabs;
16947 }
16948 }
16949 if (Array.isArray(serverTitleBarButtonScripts)) {
16950 void syncServerTitleBarButtons(
16951 serverTitleBarButtonScripts
16952 );
16953 config.serverTitleBarButtonScripts = serverTitleBarButtonScripts;
16954 }
16955 if (Array.isArray(serverDockRailRendererScripts)) {
16956 void syncServerDockRailRenderers(
16957 serverDockRailRendererScripts
16958 );
16959 config.serverDockRailRendererScripts = serverDockRailRendererScripts;
16960 }
16961 if (Array.isArray(serverWindowNotices)) {
16962 applyServerWindowNotices(
16963 serverWindowNotices
16964 );
16965 config.serverWindowNotices = serverWindowNotices;
16966 }
16967 if (Array.isArray(desktopIcons)) {
16968 const prevDesktopIcons = config.desktopIcons;
16969 renderIcons(desktopIcons);
16970 config.desktopIcons = desktopIcons;
16971 emitRegistryChanged(
16972 "desktop-icons",
16973 prevDesktopIcons,
16974 desktopIcons
16975 );
16976 }
16977 };
16978 }
16979 const MENU_REFRESH_TIMEOUT_MS = 8e3;
16980 function bindMenuRefresh(deps2) {
16981 const {
16982 layoutDispatcher,
16983 config,
16984 syncNativeWindows,
16985 syncServerWidgets,
16986 syncServerWallpapers,
16987 syncServerCommands,
16988 syncServerSettingsTabs,
16989 syncServerTitleBarButtons,
16990 syncServerDockRailRenderers,
16991 renderIcons
16992 } = deps2;
16993 const applyPayload = createApplyPayload({
16994 applyDockItems: (items) => layoutDispatcher?.applyDockItems(items),
16995 config,
16996 syncNativeWindows,
16997 syncServerWidgets,
16998 syncServerWallpapers,
16999 syncServerCommands,
17000 syncServerSettingsTabs,
17001 syncServerTitleBarButtons,
17002 syncServerDockRailRenderers,
17003 renderIcons
17004 });
17005 window.addEventListener("message", (e) => {
17006 if (e.origin !== INITIAL_ORIGIN$1) {
17007 return;
17008 }
17009 const data = e.data;
17010 if (!data || data.type !== "desktop-mode-plugins-changed") {
17011 return;
17012 }
17013 if (data.payload) {
17014 applyPayload(data.payload);
17015 }
17016 });
17017 const refresh = () => {
17018 if (!config.adminUrl) {
17019 return Promise.resolve();
17020 }
17021 const probeUrl = (() => {
17022 try {
17023 const url = new URL("admin.php", config.adminUrl);
17024 url.searchParams.set("desktop_mode_chromeless", "1");
17025 url.searchParams.set("desktop_mode_menu_refresh", "1");
17026 return url.toString();
17027 } catch (_err) {
17028 return null;
17029 }
17030 })();
17031 if (!probeUrl) {
17032 return Promise.resolve();
17033 }
17034 return new Promise((resolve2) => {
17035 const iframe = document.createElement("iframe");
17036 iframe.setAttribute("aria-hidden", "true");
17037 iframe.tabIndex = -1;
17038 iframe.style.cssText = "position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;border:0;opacity:0;pointer-events:none;";
17039 iframe.src = probeUrl;
17040 let done = false;
17041 const cleanup = () => {
17042 if (done) {
17043 return;
17044 }
17045 done = true;
17046 window.clearTimeout(timeoutId);
17047 window.removeEventListener("message", onMessage);
17048 if (iframe.parentNode) {
17049 iframe.parentNode.removeChild(iframe);
17050 }
17051 resolve2();
17052 };
17053 const onMessage = (e) => {
17054 if (e.source !== iframe.contentWindow) {
17055 return;
17056 }
17057 const data = e.data;
17058 if (!data || data.type !== "desktop-mode-plugins-changed") {
17059 return;
17060 }
17061 cleanup();
17062 };
17063 const timeoutId = window.setTimeout(() => {
17064 doAction(HOOKS.SHELL_ERROR, {
17065 scope: "menu-refresh",
17066 error: new Error("menu refresh probe timed out")
17067 });
17068 cleanup();
17069 }, MENU_REFRESH_TIMEOUT_MS);
17070 window.addEventListener("message", onMessage);
17071 document.body.appendChild(iframe);
17072 });
17073 };
17074 return refresh;
17075 }
17076 function hasRestorableSession(session) {
17077 if (!session) {
17078 return false;
17079 }
17080 if (Array.isArray(session.windows) && session.windows.length > 0) {
17081 return true;
17082 }
17083 if (typeof session.updated !== "number" || session.updated <= 0 || !Array.isArray(session.desktops) || session.desktops.length === 0) {
17084 return false;
17085 }
17086 if (session.desktops.length > 1) {
17087 return true;
17088 }
17089 const onlyDesktop = session.desktops[0];
17090 if (onlyDesktop?.id && onlyDesktop.id !== "desktop-1") {
17091 return true;
17092 }
17093 return !!session.activeDesktop && session.activeDesktop !== "desktop-1";
17094 }
17095 async function restoreSession(manager, config, desktopArea) {
17096 const rect = desktopArea.getBoundingClientRect();
17097 if (Array.isArray(config.session.desktops) && config.session.desktops.length > 0) {
17098 manager.seedDesktops(
17099 config.session.desktops,
17100 config.session.activeDesktop || config.session.desktops[0].id
17101 );
17102 }
17103 for (const win of config.session.windows) {
17104 const clamped = clampGeometryToViewport(win, rect);
17105 const dockEntry = findDockEntryForUrl(win.url, config);
17106 const opened = await manager.open({
17107 id: win.id,
17108 baseId: win.baseId || win.id,
17109 desktopId: win.desktopId,
17110 multi: !!dockEntry?.multi,
17111 url: win.url,
17112 // `dockEntry?.url` is the parent menu's landing page —
17113 // recover it so the synthetic "back to parent" tab in
17114 // the in-window strip points at the dock URL even when
17115 // the saved `win.url` is a sub-page (e.g. theme-install.php
17116 // under Appearance, or a deep wc-admin route under
17117 // WooCommerce). Without this the dedup check in
17118 // `dom.ts` sees the iframe URL match a submenu entry
17119 // and suppresses the parent tab — losing the only
17120 // affordance to navigate back.
17121 parentUrl: dockEntry?.url ?? win.url,
17122 title: win.title,
17123 icon: win.icon || "dashicons-admin-generic",
17124 x: clamped.x,
17125 y: clamped.y,
17126 width: clamped.width,
17127 height: clamped.height,
17128 initialState: win.state,
17129 submenu: dockEntry?.submenu
17130 });
17131 if (Array.isArray(win.externalTabs)) {
17132 for (const ext of win.externalTabs) {
17133 if (ext && typeof ext.url === "string" && ext.url !== "") {
17134 opened.addExternalTab(
17135 ext.url,
17136 typeof ext.label === "string" && ext.label !== "" ? ext.label : ext.url
17137 );
17138 }
17139 }
17140 }
17141 }
17142 if (config.session.focused) {
17143 const focused = manager.getById(config.session.focused);
17144 if (focused) {
17145 manager.focus(focused);
17146 }
17147 }
17148 }
17149 async function openCurrentPage(manager, config) {
17150 if (tryNativeUrlRemap(config.currentPage)) {
17151 return;
17152 }
17153 const windowId = deriveWindowId(config.currentPage, config.adminUrl);
17154 const dockEntry = findDockEntryForUrl(config.currentPage, config);
17155 await manager.open({
17156 id: windowId,
17157 baseId: windowId,
17158 multi: !!dockEntry?.multi,
17159 url: config.currentPage,
17160 parentUrl: dockEntry?.url ?? config.currentPage,
17161 title: config.currentTitle,
17162 icon: config.currentIcon,
17163 submenu: dockEntry?.submenu
17164 });
17165 }
17166 function shouldAutoOpenCurrentPage(inputs) {
17167 const suppress = inputs.fromPortal && !inputs.fromPortalIntent && (inputs.hasSession || !inputs.defaultEnabled || inputs.isNativeDefault);
17168 return !suppress;
17169 }
17170 function trackedFetch(manager, input, requestInit, opts) {
17171 const finalInit = injectRestNonce(input, requestInit);
17172 const promise = window.fetch(input, finalInit);
17173 if (opts?.silent) {
17174 return promise;
17175 }
17176 let target2 = opts?.window;
17177 if (!target2 && opts?.windowId) {
17178 target2 = manager.getById(opts.windowId) ?? null;
17179 }
17180 if (!target2) {
17181 target2 = manager.getFocused();
17182 }
17183 if (target2 && typeof target2.trackActivity === "function") {
17184 void target2.trackActivity(promise).catch(() => {
17185 });
17186 }
17187 return promise;
17188 }
17189 const SESSION_SAVE_DEBOUNCE_MS = 500;
17190 function createSessionSaver(manager, config) {
17191 let debounceTimer = null;
17192 let inFlight = false;
17193 const doSave = async () => {
17194 if (inFlight) {
17195 return;
17196 }
17197 const payload = manager.snapshot();
17198 inFlight = true;
17199 try {
17200 await trackedFetch(
17201 manager,
17202 config.sessionUrl,
17203 {
17204 method: "POST",
17205 credentials: "same-origin",
17206 headers: {
17207 "Content-Type": "application/json",
17208 "X-WP-Nonce": config.restNonce
17209 },
17210 body: JSON.stringify({ session: payload }),
17211 // Best-effort: we don't block the UI on persistence.
17212 keepalive: true
17213 },
17214 { silent: true }
17215 );
17216 } catch (err) {
17217 doAction(HOOKS.SHELL_ERROR, { scope: "session-save", error: err });
17218 } finally {
17219 inFlight = false;
17220 }
17221 };
17222 const flushImmediately = () => {
17223 if (debounceTimer !== null) {
17224 clearTimeout(debounceTimer);
17225 debounceTimer = null;
17226 }
17227 const payload = manager.snapshot();
17228 const body = new Blob(
17229 [JSON.stringify({ session: payload })],
17230 { type: "application/json" }
17231 );
17232 const beaconUrl = config.sessionUrl + (config.sessionUrl.includes("?") ? "&" : "?") + "_wpnonce=" + encodeURIComponent(config.restNonce);
17233 if (navigator.sendBeacon && navigator.sendBeacon(beaconUrl, body)) {
17234 return;
17235 }
17236 void doSave();
17237 };
17238 const schedule = () => {
17239 if (debounceTimer !== null) {
17240 clearTimeout(debounceTimer);
17241 }
17242 debounceTimer = window.setTimeout(() => {
17243 debounceTimer = null;
17244 void doSave();
17245 }, SESSION_SAVE_DEBOUNCE_MS);
17246 };
17247 window.addEventListener("pagehide", flushImmediately);
17248 document.addEventListener("visibilitychange", () => {
17249 if (document.visibilityState === "hidden") {
17250 flushImmediately();
17251 }
17252 });
17253 return schedule;
17254 }
17255 const SHELL_RESIZE_DEBOUNCE_MS = 120;
17256 function wireSessionEvents(save) {
17257 document.addEventListener("desktop-mode-window-opened", save);
17258 document.addEventListener("desktop-mode-window-closed", save);
17259 document.addEventListener("desktop-mode-window-focused", save);
17260 document.addEventListener("desktop-mode-window-changed", save);
17261 addAction(HOOKS.DESKTOP_CREATED, "desktop-mode/session-save", save);
17262 addAction(HOOKS.DESKTOP_CLOSED, "desktop-mode/session-save", save);
17263 addAction(HOOKS.DESKTOP_SWITCHED, "desktop-mode/session-save", save);
17264 }
17265 function bindShellLifecycle() {
17266 const shellEl = document.getElementById("desktop-mode-shell");
17267 let resizeTimer = null;
17268 const fireShellResize = () => {
17269 resizeTimer = null;
17270 const rect = shellEl ? shellEl.getBoundingClientRect() : null;
17271 doAction(HOOKS.SHELL_RESIZED, {
17272 width: rect ? Math.round(rect.width) : window.innerWidth,
17273 height: rect ? Math.round(rect.height) : window.innerHeight
17274 });
17275 };
17276 window.addEventListener("resize", () => {
17277 if (resizeTimer !== null) {
17278 window.clearTimeout(resizeTimer);
17279 }
17280 resizeTimer = window.setTimeout(
17281 fireShellResize,
17282 SHELL_RESIZE_DEBOUNCE_MS
17283 );
17284 });
17285 document.addEventListener("visibilitychange", () => {
17286 doAction(HOOKS.SHELL_VISIBILITY, {
17287 state: document.hidden ? "hidden" : "visible"
17288 });
17289 });
17290 }
17291 function applyTileClasses(baseClasses, item, ctx) {
17292 const fullCtx = {
17293 rail: ctx.rail ?? "dock",
17294 orientation: ctx.orientation,
17295 dockId: ctx.dockId,
17296 container: ctx.container ?? document.body,
17297 item,
17298 isSystem: ctx.isSystem
17299 };
17300 return applyFilters(
17301 HOOKS.DOCK_TILE_CLASS,
17302 baseClasses,
17303 fullCtx
17304 );
17305 }
17306 function applyTileElement(tile2, item, ctx) {
17307 const fullCtx = {
17308 rail: ctx.rail ?? "dock",
17309 orientation: ctx.orientation,
17310 dockId: ctx.dockId,
17311 container: ctx.container ?? document.body,
17312 item,
17313 isSystem: ctx.isSystem
17314 };
17315 return applyFilters(
17316 HOOKS.DOCK_TILE_ELEMENT,
17317 tile2,
17318 fullCtx
17319 );
17320 }
17321 function applyTileTooltip(label, item, ctx) {
17322 const fullCtx = {
17323 rail: ctx.rail ?? "dock",
17324 orientation: ctx.orientation,
17325 dockId: ctx.dockId,
17326 container: ctx.container ?? document.body,
17327 item,
17328 isSystem: ctx.isSystem
17329 };
17330 return applyFilters(
17331 HOOKS.DOCK_TILE_TOOLTIP,
17332 label,
17333 fullCtx
17334 );
17335 }
17336 function dispatchTileRendered(el, item, ctx) {
17337 const fullCtx = {
17338 rail: ctx.rail ?? "dock",
17339 orientation: ctx.orientation,
17340 dockId: ctx.dockId,
17341 container: ctx.container ?? document.body,
17342 item,
17343 isSystem: ctx.isSystem
17344 };
17345 doAction(HOOKS.DOCK_TILE_RENDERED, { ...fullCtx, el });
17346 }
17347 const DEFAULT_DOCK_SELECTOR = [
17348 ".desktop-mode-dock",
17349 "#desktop-mode-dock",
17350 "#desktop-mode-side-dock",
17351 ".desktop-mode-dock__tooltip",
17352 ".desktop-mode-dock-submenu"
17353 ].join(",");
17354 const customSelectors = /* @__PURE__ */ new Set();
17355 function isDockElement(target2) {
17356 if (!target2 || typeof target2.closest !== "function") {
17357 return false;
17358 }
17359 const el = target2;
17360 if (el.closest(DEFAULT_DOCK_SELECTOR)) {
17361 return true;
17362 }
17363 for (const selector of customSelectors) {
17364 if (el.closest(selector)) {
17365 return true;
17366 }
17367 }
17368 return false;
17369 }
17370 function registerDockSelector(selector) {
17371 if (typeof selector !== "string" || selector.trim() === "") {
17372 return () => void 0;
17373 }
17374 customSelectors.add(selector);
17375 return () => {
17376 customSelectors.delete(selector);
17377 };
17378 }
17379 const states = /* @__PURE__ */ new Map();
17380 const INITIAL_ORIGIN = window.location.origin;
17381 function ensureState(windowId) {
17382 let s = states.get(windowId);
17383 if (!s) {
17384 s = {
17385 headers: /* @__PURE__ */ new Map(),
17386 observers: /* @__PURE__ */ new Set(),
17387 observeCount: 0,
17388 loadHandler: null,
17389 loadHandlerTarget: null
17390 };
17391 states.set(windowId, s);
17392 }
17393 ensureLoadHandler(windowId, s);
17394 return s;
17395 }
17396 function ensureLoadHandler(windowId, s) {
17397 const iframe = findIframe(windowId);
17398 if (!iframe) {
17399 return;
17400 }
17401 if (s.loadHandlerTarget === iframe && s.loadHandler) {
17402 return;
17403 }
17404 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
17405 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
17406 }
17407 if (typeof iframe.addEventListener !== "function") {
17408 return;
17409 }
17410 const handler = () => {
17411 queueMicrotask(() => pushInstrumentation(windowId));
17412 };
17413 iframe.addEventListener("load", handler);
17414 s.loadHandler = handler;
17415 s.loadHandlerTarget = iframe;
17416 }
17417 function detachLoadHandler(s) {
17418 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
17419 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
17420 }
17421 s.loadHandler = null;
17422 s.loadHandlerTarget = null;
17423 }
17424 function findIframe(windowId) {
17425 const wpd = window.wp?.desktop?.windowManager;
17426 if (wpd && typeof wpd.getById === "function") {
17427 const win = wpd.getById(windowId);
17428 if (win?.iframe) {
17429 return win.iframe;
17430 }
17431 if (win?.element) {
17432 const synth = win.element.querySelector("iframe");
17433 if (synth) {
17434 return synth;
17435 }
17436 }
17437 }
17438 const fallback = document.getElementById(`wp-window-${windowId}`);
17439 return fallback?.querySelector("iframe") ?? null;
17440 }
17441 function snapshotHeaders(s) {
17442 const out = {};
17443 for (const [name, contributions] of s.headers) {
17444 const parts = [];
17445 for (const c of contributions) {
17446 let v;
17447 try {
17448 v = typeof c.value === "function" ? c.value() : c.value;
17449 } catch {
17450 continue;
17451 }
17452 if (typeof v === "string" && v !== "") {
17453 parts.push(v);
17454 }
17455 }
17456 if (parts.length > 0) {
17457 out[name] = parts.join(", ");
17458 }
17459 }
17460 return out;
17461 }
17462 function pushInstrumentation(windowId) {
17463 const iframe = findIframe(windowId);
17464 if (!iframe || !iframe.contentWindow) {
17465 return;
17466 }
17467 const s = states.get(windowId);
17468 const headers = s ? snapshotHeaders(s) : {};
17469 const observe = !!s && s.observeCount > 0;
17470 try {
17471 iframe.contentWindow.postMessage(
17472 {
17473 type: "desktop-mode-instrument-set",
17474 headers,
17475 observe
17476 },
17477 INITIAL_ORIGIN
17478 );
17479 } catch {
17480 }
17481 }
17482 addAction(HOOKS.IFRAME_READY, "desktop-mode/devtools/replay", (payload) => {
17483 const p = payload;
17484 if (p && typeof p.windowId === "string" && states.has(p.windowId)) {
17485 pushInstrumentation(p.windowId);
17486 }
17487 });
17488 addAction(
17489 HOOKS.IFRAME_NETWORK_COMPLETED,
17490 "desktop-mode/devtools/dispatch",
17491 (payload) => {
17492 const p = payload;
17493 if (!p || typeof p.windowId !== "string") {
17494 return;
17495 }
17496 const s = states.get(p.windowId);
17497 if (!s) {
17498 return;
17499 }
17500 for (const cb of s.observers) {
17501 try {
17502 cb(p);
17503 } catch {
17504 }
17505 }
17506 }
17507 );
17508 const sessions = /* @__PURE__ */ new Map();
17509 const POLL_INTERVAL_MS = 1e3;
17510 function pollOnce(sessionId, restUrl2, restNonce) {
17511 const sp = sessions.get(sessionId);
17512 if (!sp || sp.inflight) {
17513 return;
17514 }
17515 sp.inflight = true;
17516 const u = new URL(restUrl2 + "desktop-mode/v1/debug", window.location.origin);
17517 u.searchParams.set("sessionId", sessionId);
17518 u.searchParams.set("since", String(sp.cursor));
17519 for (const ch of sp.channels.keys()) {
17520 u.searchParams.append("channels[]", ch);
17521 }
17522 const url = u.toString();
17523 fetch(url, {
17524 credentials: "same-origin",
17525 headers: { "X-WP-Nonce": restNonce }
17526 }).then((r) => r.ok ? r.json() : { events: [], cursor: sp.cursor }).then((body) => {
17527 sp.inflight = false;
17528 if (!sessions.has(sessionId)) {
17529 return;
17530 }
17531 if (typeof body.cursor === "number") {
17532 sp.cursor = body.cursor;
17533 }
17534 for (const ev of body.events || []) {
17535 const bucket2 = sp.channels.get(ev.channel);
17536 if (!bucket2) {
17537 continue;
17538 }
17539 for (const cb of bucket2) {
17540 try {
17541 cb(ev);
17542 } catch {
17543 }
17544 }
17545 }
17546 }).catch(() => {
17547 sp.inflight = false;
17548 }).finally(() => {
17549 const stillThere = sessions.get(sessionId);
17550 if (stillThere && stillThere.channels.size > 0) {
17551 stillThere.timer = setTimeout(
17552 () => pollOnce(sessionId, restUrl2, restNonce),
17553 POLL_INTERVAL_MS
17554 );
17555 }
17556 });
17557 }
17558 function getRestEndpoint() {
17559 const cfg = window.desktopModeConfig;
17560 if (!cfg || !cfg.restUrl || !cfg.restNonce) {
17561 return null;
17562 }
17563 return { restUrl: cfg.restUrl, restNonce: cfg.restNonce };
17564 }
17565 function dispatchLocal(sessionId, ev) {
17566 const sp = sessions.get(sessionId);
17567 if (!sp) {
17568 return;
17569 }
17570 const bucket2 = sp.channels.get(ev.channel);
17571 if (!bucket2) {
17572 return;
17573 }
17574 for (const cb of bucket2) {
17575 try {
17576 cb(ev);
17577 } catch {
17578 }
17579 }
17580 }
17581 let _localEventCounter = 0;
17582 const debugBus = {
17583 startSession() {
17584 const cryptoApi = window.crypto;
17585 if (cryptoApi && typeof cryptoApi.randomUUID === "function") {
17586 return cryptoApi.randomUUID();
17587 }
17588 return "wpdbg-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
17589 },
17590 publish(sessionId, channel, payload) {
17591 dispatchLocal(sessionId, {
17592 id: ++_localEventCounter,
17593 t: Date.now(),
17594 channel,
17595 payload
17596 });
17597 },
17598 subscribe(sessionId, channel, cb) {
17599 let sp = sessions.get(sessionId);
17600 const startedFresh = !sp;
17601 if (!sp) {
17602 sp = {
17603 channels: /* @__PURE__ */ new Map(),
17604 cursor: 0,
17605 timer: null,
17606 inflight: false
17607 };
17608 sessions.set(sessionId, sp);
17609 }
17610 let bucket2 = sp.channels.get(channel);
17611 if (!bucket2) {
17612 bucket2 = /* @__PURE__ */ new Set();
17613 sp.channels.set(channel, bucket2);
17614 }
17615 bucket2.add(cb);
17616 if (startedFresh) {
17617 const ep = getRestEndpoint();
17618 if (ep) {
17619 pollOnce(sessionId, ep.restUrl, ep.restNonce);
17620 }
17621 }
17622 return () => {
17623 const cur = sessions.get(sessionId);
17624 if (!cur) {
17625 return;
17626 }
17627 const b = cur.channels.get(channel);
17628 if (b) {
17629 b.delete(cb);
17630 if (b.size === 0) {
17631 cur.channels.delete(channel);
17632 }
17633 }
17634 if (cur.channels.size === 0) {
17635 if (cur.timer) {
17636 clearTimeout(cur.timer);
17637 }
17638 sessions.delete(sessionId);
17639 }
17640 };
17641 }
17642 };
17643 const devtools = {
17644 addRequestHeader(windowId, name, value) {
17645 if (typeof windowId !== "string" || windowId === "") {
17646 return () => {
17647 };
17648 }
17649 if (typeof name !== "string" || name === "") {
17650 return () => {
17651 };
17652 }
17653 const s = ensureState(windowId);
17654 const contribution = { value };
17655 let bucket2 = s.headers.get(name);
17656 if (!bucket2) {
17657 bucket2 = [];
17658 s.headers.set(name, bucket2);
17659 }
17660 bucket2.push(contribution);
17661 pushInstrumentation(windowId);
17662 return () => {
17663 const cur = states.get(windowId);
17664 if (!cur) {
17665 return;
17666 }
17667 const b = cur.headers.get(name);
17668 if (!b) {
17669 return;
17670 }
17671 const i = b.indexOf(contribution);
17672 if (i >= 0) {
17673 b.splice(i, 1);
17674 }
17675 if (b.length === 0) {
17676 cur.headers.delete(name);
17677 }
17678 pushInstrumentation(windowId);
17679 gcWindowState(windowId);
17680 };
17681 },
17682 onRequest(windowId, cb, opts) {
17683 if (typeof windowId !== "string" || windowId === "") {
17684 return () => {
17685 };
17686 }
17687 if (typeof cb !== "function") {
17688 return () => {
17689 };
17690 }
17691 const s = ensureState(windowId);
17692 s.observers.add(cb);
17693 const wantsObserve = !!opts?.observe;
17694 if (wantsObserve) {
17695 s.observeCount++;
17696 pushInstrumentation(windowId);
17697 }
17698 return () => {
17699 const cur = states.get(windowId);
17700 if (!cur) {
17701 return;
17702 }
17703 cur.observers.delete(cb);
17704 if (wantsObserve) {
17705 cur.observeCount = Math.max(0, cur.observeCount - 1);
17706 pushInstrumentation(windowId);
17707 }
17708 gcWindowState(windowId);
17709 };
17710 },
17711 reloadWithDebugSession(windowId, sessionId, opts) {
17712 if (typeof windowId !== "string" || windowId === "" || typeof sessionId !== "string" || sessionId === "") {
17713 return null;
17714 }
17715 const iframe = findIframe(windowId);
17716 if (!iframe) {
17717 return null;
17718 }
17719 const headerName = opts?.headerName || "X-WP-Debug-Session";
17720 const queryArg = opts?.queryArg || "wp_debug_session";
17721 const stopHeader = devtools.addRequestHeader(windowId, headerName, sessionId);
17722 try {
17723 const currentSrc = iframe.getAttribute("src") || iframe.src || "";
17724 const u = new URL(currentSrc, window.location.origin);
17725 u.searchParams.set(queryArg, sessionId);
17726 iframe.src = u.toString();
17727 } catch {
17728 }
17729 return {
17730 dispose: () => {
17731 stopHeader();
17732 }
17733 };
17734 },
17735 debug: debugBus
17736 };
17737 function gcWindowState(windowId) {
17738 const s = states.get(windowId);
17739 if (!s) {
17740 return;
17741 }
17742 if (s.headers.size === 0 && s.observers.size === 0) {
17743 detachLoadHandler(s);
17744 states.delete(windowId);
17745 }
17746 }
17747 async function wpdConfirm(options) {
17748 await ensureShellOverlaysLoaded(shellOverlaysBundleUrl());
17749 return new Promise((resolve2) => {
17750 const dialog2 = document.createElement("wpd-confirm-dialog");
17751 dialog2.setAttribute("open", "");
17752 if (options.title) {
17753 dialog2.setAttribute("title", options.title);
17754 }
17755 dialog2.setAttribute("message", options.message);
17756 if (options.confirmLabel) {
17757 dialog2.setAttribute("confirm-label", options.confirmLabel);
17758 }
17759 if (options.cancelLabel) {
17760 dialog2.setAttribute("cancel-label", options.cancelLabel);
17761 }
17762 if (options.danger) {
17763 dialog2.setAttribute("danger", "");
17764 }
17765 if (options.hideCancel) {
17766 dialog2.setAttribute("hide-cancel", "");
17767 }
17768 if (options.dismissable) {
17769 dialog2.setAttribute("dismissable", "");
17770 }
17771 const cleanup = (ok) => {
17772 dialog2.remove();
17773 resolve2(ok);
17774 };
17775 dialog2.addEventListener("wpd-confirm", () => cleanup(true));
17776 dialog2.addEventListener("wpd-cancel", () => cleanup(false));
17777 document.body.appendChild(dialog2);
17778 const inner = dialog2.shadowRoot?.querySelector(".dialog");
17779 (inner ?? dialog2).focus?.();
17780 });
17781 }
17782 function collectWallpaperSurfaces(manager) {
17783 const seed2 = [];
17784 for (const w of manager.getVisibleRects()) {
17785 if (w.state === "minimized") {
17786 continue;
17787 }
17788 if (w.element.offsetParent === null) {
17789 continue;
17790 }
17791 const r = w.element.getBoundingClientRect();
17792 seed2.push({
17793 id: `window:${w.windowId}`,
17794 kind: "window",
17795 rect: rectFromDom(r),
17796 face: "top",
17797 element: w.element
17798 });
17799 }
17800 const shellEl = document.getElementById("desktop-mode-shell");
17801 if (shellEl) {
17802 const r = shellEl.getBoundingClientRect();
17803 seed2.push({
17804 id: "shell:floor",
17805 kind: "shell",
17806 rect: {
17807 x: r.left,
17808 y: r.bottom - 1,
17809 width: r.width,
17810 height: 1
17811 },
17812 face: "top",
17813 element: shellEl
17814 });
17815 }
17816 const dockEls = document.querySelectorAll(
17817 ".desktop-mode-dock"
17818 );
17819 let dockIndex = 0;
17820 for (const dockEl of Array.from(dockEls)) {
17821 const r = dockEl.getBoundingClientRect();
17822 if (r.width <= 0 || r.height <= 0) {
17823 continue;
17824 }
17825 const placement = dockEl.getAttribute("data-desktop-mode-dock-placement") ?? "bottom";
17826 const id = dockIndex === 0 ? "dock:edge" : `dock:edge:${dockIndex}`;
17827 dockIndex++;
17828 if (placement === "bottom") {
17829 seed2.push({
17830 id,
17831 kind: "dock",
17832 rect: { x: r.left, y: r.top, width: r.width, height: 1 },
17833 face: "top",
17834 element: dockEl
17835 });
17836 } else if (placement === "right") {
17837 seed2.push({
17838 id,
17839 kind: "dock",
17840 rect: { x: r.left, y: r.top, width: 1, height: r.height },
17841 face: "left",
17842 element: dockEl
17843 });
17844 } else {
17845 seed2.push({
17846 id,
17847 kind: "dock",
17848 rect: {
17849 x: r.right - 1,
17850 y: r.top,
17851 width: 1,
17852 height: r.height
17853 },
17854 face: "right",
17855 element: dockEl
17856 });
17857 }
17858 }
17859 const widgetCards = document.querySelectorAll(
17860 ".desktop-mode-widgets__card"
17861 );
17862 let widgetIndex = 0;
17863 widgetCards.forEach((card) => {
17864 const r = card.getBoundingClientRect();
17865 if (r.width === 0 && r.height === 0) {
17866 return;
17867 }
17868 const id = card.dataset.widgetId ?? String(widgetIndex++);
17869 seed2.push({
17870 id: `widget:${id}`,
17871 kind: "widget",
17872 rect: rectFromDom(r),
17873 face: "top",
17874 element: card
17875 });
17876 });
17877 const filtered = applyFilters(HOOKS.WALLPAPER_SURFACES, seed2);
17878 return Array.isArray(filtered) ? filtered : seed2;
17879 }
17880 function rectFromDom(r) {
17881 return {
17882 x: r.left,
17883 y: r.top,
17884 width: r.width,
17885 height: r.height
17886 };
17887 }
17888 const NODE_KEY_PROP = "__desktop_modeKeyedListKey";
17889 const NODE_DATA_PROP = "__desktop_modeKeyedListData";
17890 function getHostState(host) {
17891 const cached = host.__desktop_modeKeyedList;
17892 if (cached) {
17893 return cached;
17894 }
17895 const fresh = { byKey: /* @__PURE__ */ new Map() };
17896 host.__desktop_modeKeyedList = fresh;
17897 return fresh;
17898 }
17899 function renderKeyedList(host, items, opts) {
17900 const state2 = getHostState(host);
17901 const prev = state2.byKey;
17902 const next = /* @__PURE__ */ new Map();
17903 const ordered = [];
17904 const seenKeys = /* @__PURE__ */ new Set();
17905 for (const item of items) {
17906 const key = String(opts.keyOf(item));
17907 if (seenKeys.has(key)) {
17908 console.warn(
17909 "[desktop-mode/keyed-list] duplicate key — only the last item with this key will render:",
17910 key
17911 );
17912 }
17913 seenKeys.add(key);
17914 const reused = prev.get(key);
17915 if (reused) {
17916 const prevData = reused.data;
17917 opts.updateItem?.(reused.el, item, prevData);
17918 reused.data = item;
17919 next.set(key, reused);
17920 ordered.push(reused.el);
17921 continue;
17922 }
17923 const el = opts.buildItem(item);
17924 el[NODE_KEY_PROP] = key;
17925 el[NODE_DATA_PROP] = item;
17926 next.set(key, { el, data: item });
17927 ordered.push(el);
17928 }
17929 for (const [key, entry] of prev) {
17930 if (!next.has(key)) {
17931 entry.el.remove();
17932 }
17933 }
17934 for (let i = 0; i < ordered.length; i++) {
17935 const desired = ordered[i];
17936 const live = host.children[i];
17937 if (live === desired) {
17938 continue;
17939 }
17940 host.insertBefore(desired, live ?? null);
17941 }
17942 state2.byKey = next;
17943 }
17944 function clearKeyedList(host) {
17945 const cached = host.__desktop_modeKeyedList;
17946 if (!cached) {
17947 return;
17948 }
17949 for (const entry of cached.byKey.values()) {
17950 entry.el.remove();
17951 }
17952 cached.byKey.clear();
17953 delete host.__desktop_modeKeyedList;
17954 }
17955 function createInfiniteList(options) {
17956 const {
17957 root,
17958 fetchPage,
17959 getId,
17960 renderItem,
17961 rootMargin = "200px",
17962 initialCursor = null,
17963 onLoadingChange = () => void 0,
17964 onError = (err) => {
17965 if (typeof console !== "undefined") {
17966 console.error("[desktop-mode] createInfiniteList:", err);
17967 }
17968 }
17969 } = options;
17970 let sentinel = options.sentinel ?? null;
17971 if (!sentinel) {
17972 sentinel = document.createElement("div");
17973 sentinel.dataset.wpdInfiniteListSentinel = "";
17974 sentinel.style.height = "1px";
17975 root.appendChild(sentinel);
17976 }
17977 const seen = /* @__PURE__ */ new Set();
17978 let cursor = initialCursor;
17979 let hasMoreInternal = true;
17980 let loading = false;
17981 let controller = null;
17982 let renderedCount = 0;
17983 let destroyed = false;
17984 let observer = null;
17985 const setLoading = (next) => {
17986 if (loading === next) {
17987 return;
17988 }
17989 loading = next;
17990 try {
17991 onLoadingChange(next);
17992 } catch (err) {
17993 onError(err);
17994 }
17995 };
17996 const detachObserver = () => {
17997 if (observer) {
17998 observer.disconnect();
17999 observer = null;
18000 }
18001 };
18002 const ensureObserver = () => {
18003 if (observer || !sentinel || destroyed) {
18004 return;
18005 }
18006 observer = new IntersectionObserver(
18007 (entries) => {
18008 for (const entry of entries) {
18009 if (entry.isIntersecting) {
18010 void loadMore();
18011 }
18012 }
18013 },
18014 { rootMargin }
18015 );
18016 observer.observe(sentinel);
18017 };
18018 const loadMore = async () => {
18019 if (destroyed || loading || !hasMoreInternal) {
18020 return;
18021 }
18022 setLoading(true);
18023 controller = new AbortController();
18024 const localController = controller;
18025 try {
18026 const page = await fetchPage(cursor, localController.signal);
18027 if (destroyed || localController !== controller) {
18028 return;
18029 }
18030 let appended = 0;
18031 const frag = document.createDocumentFragment();
18032 for (const item of page.items ?? []) {
18033 const key = String(getId(item));
18034 if (seen.has(key)) {
18035 continue;
18036 }
18037 seen.add(key);
18038 const el = renderItem(item, renderedCount + appended);
18039 frag.appendChild(el);
18040 appended++;
18041 }
18042 if (appended > 0) {
18043 if (sentinel && sentinel.parentNode === root) {
18044 root.insertBefore(frag, sentinel);
18045 } else {
18046 root.appendChild(frag);
18047 }
18048 renderedCount += appended;
18049 }
18050 cursor = page.nextCursor ?? null;
18051 if (!cursor) {
18052 hasMoreInternal = false;
18053 detachObserver();
18054 }
18055 } catch (err) {
18056 if (err?.name === "AbortError") {
18057 return;
18058 }
18059 onError(err);
18060 } finally {
18061 if (localController === controller) {
18062 setLoading(false);
18063 controller = null;
18064 }
18065 }
18066 };
18067 const reset = () => {
18068 if (destroyed) {
18069 return;
18070 }
18071 controller?.abort();
18072 controller = null;
18073 seen.clear();
18074 cursor = initialCursor;
18075 hasMoreInternal = true;
18076 renderedCount = 0;
18077 const sentinelInRoot = sentinel && sentinel.parentNode === root;
18078 while (root.firstChild) {
18079 root.removeChild(root.firstChild);
18080 }
18081 if (sentinelInRoot && sentinel) {
18082 root.appendChild(sentinel);
18083 }
18084 setLoading(false);
18085 ensureObserver();
18086 void loadMore();
18087 };
18088 const destroy = () => {
18089 if (destroyed) {
18090 return;
18091 }
18092 destroyed = true;
18093 detachObserver();
18094 controller?.abort();
18095 controller = null;
18096 if (!options.sentinel && sentinel && sentinel.parentNode === root) {
18097 root.removeChild(sentinel);
18098 }
18099 sentinel = null;
18100 setLoading(false);
18101 };
18102 ensureObserver();
18103 void loadMore();
18104 return {
18105 reset,
18106 loadMore,
18107 hasMore: () => hasMoreInternal,
18108 isLoading: () => loading,
18109 destroy
18110 };
18111 }
18112 const POPUP_DEFAULT_WIDTH = 520;
18113 const POPUP_DEFAULT_HEIGHT = 720;
18114 const POPUP_CLOSE_POLL_MS = 500;
18115 function startOAuth(service, options = {}) {
18116 if (typeof service !== "string" || service === "") {
18117 return Promise.reject(
18118 new Error("[desktop-mode] startOAuth requires a non-empty service slug.")
18119 );
18120 }
18121 const restRoot2 = readRestRoot$1();
18122 const restNonce = readRestNonce$1();
18123 return trackedFetch$1(
18124 joinRestUrl(restRoot2, "desktop-mode/v1/oauth/start"),
18125 {
18126 method: "POST",
18127 headers: {
18128 "Content-Type": "application/json",
18129 "X-WP-Nonce": restNonce ?? ""
18130 },
18131 body: JSON.stringify({ service })
18132 },
18133 { source: "desktop-mode/oauth-start" }
18134 ).then(async (res) => {
18135 if (!res.ok) {
18136 const text = await res.text().catch(() => "");
18137 throw new Error(
18138 `[desktop-mode] OAuth start failed (${res.status}): ${text}`
18139 );
18140 }
18141 return await res.json();
18142 }).then((startBody) => openPopupAndWait(startBody, service, options));
18143 }
18144 function openPopupAndWait(body, service, options) {
18145 return new Promise((resolve2, reject) => {
18146 const width = options.width ?? POPUP_DEFAULT_WIDTH;
18147 const height = options.height ?? POPUP_DEFAULT_HEIGHT;
18148 const left = Math.max(0, Math.floor((window.screen.width - width) / 2));
18149 const top = Math.max(0, Math.floor((window.screen.height - height) / 2));
18150 const features = [
18151 `width=${width}`,
18152 `height=${height}`,
18153 `left=${left}`,
18154 `top=${top}`,
18155 "menubar=no",
18156 "toolbar=no",
18157 "location=yes",
18158 "status=no",
18159 "resizable=yes",
18160 "scrollbars=yes"
18161 ].join(",");
18162 const popup = window.open(
18163 body.authorize_url,
18164 `desktop-mode-oauth-${service}`,
18165 features
18166 );
18167 if (!popup) {
18168 reject(
18169 new Error(
18170 "[desktop-mode] OAuth popup blocked. Tell users to allow popups for this site."
18171 )
18172 );
18173 return;
18174 }
18175 const expectedOrigin = window.location.origin;
18176 let pollTimer = null;
18177 let detached = false;
18178 const cleanup = () => {
18179 if (detached) {
18180 return;
18181 }
18182 detached = true;
18183 window.removeEventListener("message", onMessage);
18184 if (pollTimer !== null) {
18185 window.clearInterval(pollTimer);
18186 pollTimer = null;
18187 }
18188 };
18189 const onMessage = (e) => {
18190 if (e.origin !== expectedOrigin) {
18191 return;
18192 }
18193 const data = e.data;
18194 if (!data || data.type !== "desktop-mode-oauth-callback") {
18195 return;
18196 }
18197 const payload = data.payload;
18198 cleanup();
18199 if (payload && payload.ok) {
18200 resolve2(payload);
18201 } else {
18202 const reason = payload?.reason ?? "unknown";
18203 const message = payload?.message ?? "OAuth flow failed";
18204 const err = new Error(
18205 `[desktop-mode] startOAuth(${service}) failed: ${reason} — ${message}`
18206 );
18207 err.cause = payload;
18208 reject(err);
18209 }
18210 };
18211 window.addEventListener("message", onMessage);
18212 pollTimer = window.setInterval(() => {
18213 if (popup.closed) {
18214 cleanup();
18215 reject(
18216 new Error(
18217 `[desktop-mode] startOAuth(${service}) cancelled — popup closed before completing.`
18218 )
18219 );
18220 }
18221 }, POPUP_CLOSE_POLL_MS);
18222 });
18223 }
18224 function readDesktopConfig() {
18225 return window.desktopModeConfig ?? {};
18226 }
18227 function readRestRoot$1() {
18228 const root = readDesktopConfig().restRoot;
18229 if (typeof root === "string" && root !== "") {
18230 return root;
18231 }
18232 return `${window.location.origin}/wp-json/`;
18233 }
18234 function readRestNonce$1() {
18235 const nonce = readDesktopConfig().restNonce;
18236 return typeof nonce === "string" && nonce !== "" ? nonce : null;
18237 }
18238 const RESERVED_NAMESPACE_KEYS = /* @__PURE__ */ new Set([
18239 "windowManager",
18240 "dock",
18241 "taskbar",
18242 "icons",
18243 "saveSession",
18244 "hooks",
18245 "HOOKS",
18246 "isActive",
18247 "registerWallpaper",
18248 "registerWidget",
18249 "widgetLayer",
18250 "widgets",
18251 "registerSystemTile",
18252 "registerWindow",
18253 "openWindow",
18254 "cloneTemplate",
18255 "onWindow",
18256 "loadVendorScript",
18257 "getWallpaperSurfaces",
18258 "registerModule",
18259 "loadModules",
18260 "whenReady",
18261 "ready",
18262 "isReady",
18263 "setDefaultWindow",
18264 "refreshMenu",
18265 "config",
18266 "ai",
18267 "dragBridge",
18268 "dragManager",
18269 "registerCommand",
18270 "unregisterCommand",
18271 "listCommands",
18272 "registerDestructiveAdminAction",
18273 "unregisterDestructiveAdminAction",
18274 "listDestructiveAdminActions",
18275 "registerSettingsTab",
18276 "unregisterSettingsTab",
18277 "listSettingsTabs",
18278 "registerDockRailRenderer",
18279 "unregisterDockRailRenderer",
18280 "listDockRailRenderers",
18281 "openOsSettings",
18282 "getOsSettings",
18283 "subscribeOsSettings",
18284 "updateOsSettings",
18285 "deriveWindowId",
18286 "listSystemTiles",
18287 "getSystemTile",
18288 "getMenuItems",
18289 "renderIcon",
18290 "applyTileClasses",
18291 "applyTileElement",
18292 "applyTileTooltip",
18293 "dispatchTileRendered",
18294 "isDockElement",
18295 "registerDockSelector",
18296 "registerTitleBarButton",
18297 "unregisterTitleBarButton",
18298 "listTitleBarButtons",
18299 "registerWindowTheme",
18300 "unregisterWindowTheme",
18301 "listWindowThemes",
18302 "applyWindowTheme",
18303 "registerWindowControl",
18304 "unregisterWindowControl",
18305 "listWindowControls",
18306 "applyWindowControls",
18307 "registerWindowSlot",
18308 "unregisterWindowSlot",
18309 "listWindowSlots",
18310 "applyWindowSlot",
18311 "registerWindowNotice",
18312 "unregisterWindowNotice",
18313 "listWindowNotices",
18314 "dismissWindowNotice",
18315 "undismissWindowNotice",
18316 "registerWindowChrome",
18317 "unregisterWindowChrome",
18318 "listWindowChromes",
18319 "applyWindowChrome",
18320 "connect",
18321 "broadcast",
18322 "subscribe",
18323 "registerPalette",
18324 "unregisterPalette",
18325 "listPalettes",
18326 "openPalette",
18327 "devtools",
18328 "createSharedStore",
18329 "presence",
18330 "activity",
18331 "heartbeat",
18332 "showToast",
18333 "renderKeyedList",
18334 "clearKeyedList",
18335 "registerNamespace",
18336 "notify",
18337 "pwa",
18338 "getWindowConfig",
18339 "debug",
18340 "fetch"
18341 ]);
18342 function buildPublicApi(deps2) {
18343 const {
18344 manager,
18345 dock,
18346 layoutDispatcher,
18347 osSettings,
18348 iconsApi: iconsApi2,
18349 filesApi: filesApi2,
18350 saveSession,
18351 widgetLayer,
18352 registerWindow,
18353 openWindowById,
18354 openNewWindowById,
18355 placeSystemTile,
18356 setDefaultWindow,
18357 refreshMenu,
18358 openOsSettings,
18359 aiAssistant,
18360 dragBridge,
18361 dragManager,
18362 connect,
18363 getConnection,
18364 config
18365 } = deps2;
18366 const desktopApi = {
18367 windowManager: manager,
18368 dock,
18369 sideDock: layoutDispatcher?.getSide() ?? null,
18370 desktopLayout: osSettings.getOsSettingsSnapshot().desktopLayout,
18371 icons: iconsApi2,
18372 files: filesApi2,
18373 confirm: wpdConfirm,
18374 saveSession,
18375 hooks: rawHooks(),
18376 HOOKS,
18377 isActive: () => !!document.getElementById("desktop-mode-shell"),
18378 registerWallpaper: (def) => {
18379 register$2(def);
18380 osSettings.apply();
18381 },
18382 registerWidget: (def) => {
18383 register(def);
18384 },
18385 widgetLayer,
18386 widgets: {
18387 redock: (id) => {
18388 widgetLayer?.redock(id);
18389 }
18390 },
18391 loadVendorScript,
18392 getWallpaperSurfaces: () => collectWallpaperSurfaces(manager),
18393 registerWindow,
18394 openWindow: openWindowById,
18395 openNewWindow: openNewWindowById,
18396 fetch: (input, requestInit, opts) => trackedFetch(manager, input, requestInit, opts),
18397 repaintLoadingOverlays,
18398 cloneTemplate,
18399 onWindow,
18400 createInfiniteList,
18401 startOAuth,
18402 registerSystemTile: (item) => {
18403 placeSystemTile(item);
18404 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: item.id });
18405 },
18406 registerModule,
18407 loadModules,
18408 whenReady,
18409 ready: whenReady,
18410 isReady,
18411 setDefaultWindow,
18412 refreshMenu,
18413 config,
18414 ai: aiAssistant,
18415 dragBridge,
18416 dragManager,
18417 registerCommand,
18418 unregisterCommand,
18419 listCommands,
18420 registerDestructiveAdminAction,
18421 unregisterDestructiveAdminAction,
18422 listDestructiveAdminActions,
18423 registerSettingsTab,
18424 unregisterSettingsTab,
18425 listSettingsTabs,
18426 registerDockRailRenderer: register$1,
18427 unregisterDockRailRenderer: unregister$1,
18428 listDockRailRenderers: list,
18429 openOsSettings,
18430 getOsSettings: () => osSettings.getOsSettingsSnapshot(),
18431 subscribeOsSettings: (cb) => osSettings.subscribeOsSettings(cb),
18432 updateOsSettings: (patch, opts = {}) => {
18433 if (typeof patch.wallpaper === "string") {
18434 osSettings.state.wallpaper = patch.wallpaper;
18435 }
18436 if (typeof patch.accent === "string") {
18437 osSettings.state.accent = patch.accent;
18438 }
18439 if (typeof patch.dockSize === "string") {
18440 osSettings.state.dockSize = patch.dockSize;
18441 }
18442 if (typeof patch.desktopLayout === "string") {
18443 osSettings.state.desktopLayout = patch.desktopLayout;
18444 }
18445 if (typeof patch.dockRailRenderer === "string") {
18446 osSettings.state.dockRailRenderer = patch.dockRailRenderer;
18447 }
18448 if (patch.ai && typeof patch.ai === "object") {
18449 osSettings.state.ai = { ...osSettings.state.ai, ...patch.ai };
18450 }
18451 if (typeof patch.nativePostsEnabled === "boolean") {
18452 osSettings.state.nativePostsEnabled = patch.nativePostsEnabled;
18453 }
18454 if (typeof patch.nativePagesEnabled === "boolean") {
18455 osSettings.state.nativePagesEnabled = patch.nativePagesEnabled;
18456 }
18457 if (typeof patch.nativeUsersEnabled === "boolean") {
18458 osSettings.state.nativeUsersEnabled = patch.nativeUsersEnabled;
18459 }
18460 if (typeof patch.nativePluginsEnabled === "boolean") {
18461 osSettings.state.nativePluginsEnabled = patch.nativePluginsEnabled;
18462 }
18463 if (typeof patch.nativeCommentsEnabled === "boolean") {
18464 osSettings.state.nativeCommentsEnabled = patch.nativeCommentsEnabled;
18465 }
18466 if (typeof patch.foldersSharingEnabled === "boolean") {
18467 osSettings.state.foldersSharingEnabled = patch.foldersSharingEnabled;
18468 }
18469 if (Array.isArray(patch.nativePostsHiddenColumns)) {
18470 osSettings.state.nativePostsHiddenColumns = patch.nativePostsHiddenColumns.filter(
18471 (v) => typeof v === "string" && v !== ""
18472 ).slice(0, 32);
18473 }
18474 if (patch.itemVisibility && typeof patch.itemVisibility === "object") {
18475 const allowed = ["both", "dock", "desktop", "hidden"];
18476 const next = {};
18477 for (const [k, v] of Object.entries(
18478 patch.itemVisibility
18479 )) {
18480 if (typeof k !== "string" || k === "") {
18481 continue;
18482 }
18483 if (typeof v !== "string" || !allowed.includes(v)) {
18484 continue;
18485 }
18486 next[k] = v;
18487 }
18488 osSettings.state.itemVisibility = next;
18489 }
18490 if (Array.isArray(patch.dockOrder)) {
18491 osSettings.state.dockOrder = patch.dockOrder.filter(
18492 (v) => typeof v === "string" && v !== ""
18493 ).slice(0, 256);
18494 }
18495 if (patch.dockPromotedPositions && typeof patch.dockPromotedPositions === "object") {
18496 const MAX_COORD = 1e5;
18497 const next = {};
18498 for (const [k, v] of Object.entries(
18499 patch.dockPromotedPositions
18500 )) {
18501 if (typeof k !== "string" || k === "") {
18502 continue;
18503 }
18504 if (!v || typeof v !== "object") {
18505 continue;
18506 }
18507 const pos = v;
18508 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) {
18509 continue;
18510 }
18511 next[k] = { x: pos.x, y: pos.y };
18512 if (Object.keys(next).length >= 256) {
18513 break;
18514 }
18515 }
18516 osSettings.state.dockPromotedPositions = next;
18517 }
18518 osSettings.save(opts);
18519 if (patch.itemVisibility || patch.dockOrder) {
18520 layoutDispatcher?.refresh();
18521 }
18522 },
18523 deriveWindowId: (url, overrideAdminUrl) => deriveWindowId(url, overrideAdminUrl ?? config.adminUrl),
18524 listSystemTiles: () => layoutDispatcher?.listSystemTiles() ?? [],
18525 getSystemTile: (id) => layoutDispatcher?.getSystemTile(id) ?? null,
18526 getMenuItems: () => layoutDispatcher?.getMenuItems() ?? [],
18527 renderIcon,
18528 applyTileClasses,
18529 applyTileElement,
18530 applyTileTooltip,
18531 dispatchTileRendered,
18532 isDockElement,
18533 registerDockSelector,
18534 registerTitleBarButton,
18535 unregisterTitleBarButton,
18536 listTitleBarButtons,
18537 registerWindowTheme,
18538 unregisterWindowTheme,
18539 listWindowThemes,
18540 applyWindowTheme: (windowId, override) => {
18541 const win = manager.getById(windowId);
18542 if (!win) {
18543 return;
18544 }
18545 win.setAppearanceTheme(override);
18546 },
18547 registerWindowControl,
18548 unregisterWindowControl,
18549 listWindowControls,
18550 applyWindowControls: (windowId, override) => {
18551 const win = manager.getById(windowId);
18552 if (!win) {
18553 return;
18554 }
18555 win.setAppearanceControls(override);
18556 },
18557 registerWindowSlot,
18558 unregisterWindowSlot,
18559 listWindowSlots,
18560 applyWindowSlot: (windowId, slot, slotConfig) => {
18561 const win = manager.getById(windowId);
18562 if (!win) {
18563 return;
18564 }
18565 win.setAppearanceSlot(slot, slotConfig);
18566 },
18567 registerWindowNotice,
18568 unregisterWindowNotice,
18569 listWindowNotices,
18570 dismissWindowNotice,
18571 undismissWindowNotice,
18572 registerWindowChrome,
18573 unregisterWindowChrome,
18574 listWindowChromes,
18575 applyWindowChrome: (windowId, chromeId) => {
18576 const win = manager.getById(windowId);
18577 if (!win) {
18578 return;
18579 }
18580 win.setAppearanceChrome(chromeId);
18581 },
18582 connect,
18583 getConnection,
18584 broadcast,
18585 subscribe: subscribe$2,
18586 registerPalette,
18587 unregisterPalette,
18588 listPalettes,
18589 openPalette: openPaletteOnly,
18590 devtools,
18591 createSharedStore,
18592 presence: presenceApi,
18593 activity,
18594 heartbeat,
18595 showToast,
18596 notify: notify$3,
18597 pwa: {
18598 promptInstall,
18599 undismissInstallHint,
18600 getState: getPwaState,
18601 subscribe: subscribePwaState,
18602 requestNotificationPermission,
18603 getNotificationPermission
18604 },
18605 renderKeyedList,
18606 clearKeyedList,
18607 registerNamespace: (name, api) => {
18608 if (typeof name !== "string" || name === "") {
18609 console.warn(
18610 "[desktop-mode] registerNamespace: name must be a non-empty string"
18611 );
18612 return;
18613 }
18614 if (!api || typeof api !== "object") {
18615 console.warn(
18616 `[desktop-mode] registerNamespace("${name}"): api must be an object`
18617 );
18618 return;
18619 }
18620 if (RESERVED_NAMESPACE_KEYS.has(name)) {
18621 console.warn(
18622 `[desktop-mode] registerNamespace("${name}"): name is reserved by the shell — pick a plugin-specific key`
18623 );
18624 return;
18625 }
18626 desktopApi[name] = api;
18627 },
18628 getWindowConfig: (id) => {
18629 const store2 = window.desktopModeWindowConfig;
18630 if (!store2 || typeof store2 !== "object") {
18631 return void 0;
18632 }
18633 const value = store2[id];
18634 return value === void 0 ? void 0 : value;
18635 },
18636 debug: {
18637 window: (id) => {
18638 const entry = (config.nativeWindows ?? []).find(
18639 (e) => e.id === id
18640 );
18641 if (!entry) {
18642 return null;
18643 }
18644 const url = entry.scriptUrl || "";
18645 let loadPath = "unknown";
18646 let tagInDom = false;
18647 if (url) {
18648 const lazyTag = document.querySelector(
18649 `script[data-desktop-mode-vendor="${url.replace(/"/g, '\\"')}"]`
18650 );
18651 if (lazyTag) {
18652 loadPath = "lazy";
18653 tagInDom = true;
18654 } else {
18655 const eagerTag = Array.from(
18656 document.querySelectorAll(
18657 "script[src]"
18658 )
18659 ).find((s) => s.src === url);
18660 if (eagerTag) {
18661 loadPath = "eager";
18662 tagInDom = true;
18663 }
18664 }
18665 }
18666 const cfgStore = window.desktopModeWindowConfig;
18667 const configPresent = !!(cfgStore && typeof cfgStore === "object" && Object.prototype.hasOwnProperty.call(cfgStore, id));
18668 return {
18669 id,
18670 scriptHandle: entry.scriptHandle || "",
18671 scriptUrl: url,
18672 loadPath,
18673 tagInDom,
18674 configPresent,
18675 extras: {
18676 hasTranslations: !!entry.scriptTranslations,
18677 l10nCount: (entry.scriptL10n ?? []).length,
18678 beforeCount: (entry.scriptBefore ?? []).length,
18679 afterCount: (entry.scriptAfter ?? []).length
18680 }
18681 };
18682 }
18683 }
18684 };
18685 return desktopApi;
18686 }
18687 function installPublicApi(api) {
18688 if (!window.wp) {
18689 window.wp = {};
18690 }
18691 if (!window.wp.desktop) {
18692 window.wp.desktop = api;
18693 return;
18694 }
18695 Object.assign(
18696 window.wp.desktop,
18697 api
18698 );
18699 }
18700 const store$1 = createSharedStore("desktop-mode/layout", () => ({
18701 // Default mirrors the OsSettingsSnapshot default; the shell
18702 // re-publishes the persisted value as soon as it boots.
18703 layout: "classic"
18704 }));
18705 function setCurrentLayout(layout) {
18706 if (store$1.state.layout === layout) {
18707 return;
18708 }
18709 store$1.state.layout = layout;
18710 store$1.notify();
18711 }
18712 class DesktopFile {
18713 constructor(shape) {
18714 this.shape = shape;
18715 }
18716 /** Title shown under the tile. Defaults to `shape.title`. */
18717 title() {
18718 return this.shape.title;
18719 }
18720 /** Dashicon class or data URI. Defaults to `shape.icon`. */
18721 icon() {
18722 return this.shape.icon;
18723 }
18724 /** Optional preview-image URL. Defaults to `shape.previewUrl`. */
18725 previewUrl() {
18726 return this.shape.previewUrl;
18727 }
18728 /** Reference (id, URL, …). */
18729 ref() {
18730 return this.shape.ref;
18731 }
18732 /** Whether the underlying entity still exists. */
18733 exists() {
18734 return this.shape.exists;
18735 }
18736 }
18737 class DefaultDesktopFile extends DesktopFile {
18738 constructor(shape, typeSlug) {
18739 super(shape);
18740 this.typeSlug = typeSlug;
18741 }
18742 type() {
18743 return this.typeSlug;
18744 }
18745 }
18746 const seed$1 = /* @__PURE__ */ new Map();
18747 const listeners$1 = /* @__PURE__ */ new Set();
18748 function registerType(def) {
18749 if (!def.type) {
18750 throw new Error("[desktop-mode] registerType: `type` is required.");
18751 }
18752 if (!def.label) {
18753 throw new Error("[desktop-mode] registerType: `label` is required.");
18754 }
18755 seed$1.set(def.type, {
18756 type: def.type,
18757 label: def.label,
18758 sort: typeof def.sort === "number" ? def.sort : 100,
18759 DesktopFile: def.DesktopFile
18760 });
18761 doAction("desktop-mode.files.type-registered", def.type, def);
18762 notify$1();
18763 }
18764 function unregisterType(typeSlug) {
18765 if (seed$1.delete(typeSlug)) {
18766 doAction("desktop-mode.files.type-unregistered", typeSlug);
18767 notify$1();
18768 }
18769 }
18770 function getType(typeSlug) {
18771 const entry = seed$1.get(typeSlug);
18772 return entry ? entry : null;
18773 }
18774 function getTypes() {
18775 const list2 = Array.from(seed$1.values()).slice();
18776 const filtered = applyFilters(
18777 "desktop-mode.files.types",
18778 list2
18779 );
18780 const arr = Array.isArray(filtered) ? filtered : list2;
18781 arr.sort((a, b) => {
18782 if (a.sort !== b.sort) {
18783 return a.sort - b.sort;
18784 }
18785 return a.label.localeCompare(b.label);
18786 });
18787 return arr;
18788 }
18789 function resolve(shape) {
18790 const entry = seed$1.get(shape.type);
18791 if (entry?.DesktopFile) {
18792 return new entry.DesktopFile(shape);
18793 }
18794 return new DefaultDesktopFile(shape, shape.type);
18795 }
18796 function subscribe(cb) {
18797 listeners$1.add(cb);
18798 return () => listeners$1.delete(cb);
18799 }
18800 function notify$1() {
18801 for (const cb of listeners$1) {
18802 try {
18803 cb();
18804 } catch (err) {
18805 console.error("[desktop-mode] files registry subscriber threw:", err);
18806 }
18807 }
18808 }
18809 const seed = /* @__PURE__ */ new Map();
18810 const listeners = /* @__PURE__ */ new Set();
18811 let userAssociations = {};
18812 function setUserAssociations(map) {
18813 userAssociations = { ...map };
18814 notify();
18815 }
18816 function getUserAssociations() {
18817 return { ...userAssociations };
18818 }
18819 function registerOpener(def) {
18820 if (!def.id) {
18821 throw new Error("[desktop-mode] registerOpener: `id` is required.");
18822 }
18823 if (!def.label) {
18824 throw new Error("[desktop-mode] registerOpener: `label` is required.");
18825 }
18826 if (!Array.isArray(def.types) || def.types.length === 0) {
18827 throw new Error("[desktop-mode] registerOpener: `types` must be a non-empty array.");
18828 }
18829 if (!def.handler || typeof def.handler !== "object") {
18830 throw new Error("[desktop-mode] registerOpener: `handler` is required.");
18831 }
18832 seed.set(def.id, {
18833 id: def.id,
18834 label: def.label,
18835 types: def.types.slice(),
18836 isDefault: !!def.isDefault,
18837 sort: typeof def.sort === "number" ? def.sort : 100,
18838 handler: def.handler
18839 });
18840 doAction("desktop-mode.files.opener-registered", def.id, def);
18841 notify();
18842 }
18843 function unregisterOpener(id) {
18844 if (seed.delete(id)) {
18845 doAction("desktop-mode.files.opener-unregistered", id);
18846 notify();
18847 }
18848 }
18849 function getOpener(id) {
18850 return seed.get(id) ?? null;
18851 }
18852 function getOpeners() {
18853 const list2 = Array.from(seed.values()).slice();
18854 const filtered = applyFilters(
18855 "desktop-mode.files.openers",
18856 list2
18857 );
18858 const arr = Array.isArray(filtered) ? filtered : list2;
18859 arr.sort((a, b) => {
18860 const sa = typeof a.sort === "number" ? a.sort : 100;
18861 const sb = typeof b.sort === "number" ? b.sort : 100;
18862 if (sa !== sb) {
18863 return sa - sb;
18864 }
18865 return a.label.localeCompare(b.label);
18866 });
18867 return arr;
18868 }
18869 function getOpenersForType(type) {
18870 return getOpeners().filter((e) => e.types.includes(type));
18871 }
18872 function resolveOpener(type) {
18873 const candidates = getOpenersForType(type);
18874 if (candidates.length === 0) {
18875 return null;
18876 }
18877 const override = userAssociations[type];
18878 let resolved = null;
18879 if (override) {
18880 resolved = candidates.find((e) => e.id === override) ?? null;
18881 }
18882 if (!resolved) {
18883 resolved = candidates.find((e) => e.isDefault) ?? null;
18884 }
18885 if (!resolved) {
18886 resolved = candidates[0];
18887 }
18888 const filtered = applyFilters(
18889 "desktop-mode.files.resolve-opener",
18890 resolved,
18891 type
18892 );
18893 return filtered ?? null;
18894 }
18895 function subscribeOpeners(cb) {
18896 listeners.add(cb);
18897 return () => listeners.delete(cb);
18898 }
18899 function notify() {
18900 for (const cb of listeners) {
18901 try {
18902 cb();
18903 } catch (err) {
18904 console.error("[desktop-mode] openers subscriber threw:", err);
18905 }
18906 }
18907 }
18908 let deps$1 = null;
18909 function installOpenDeps(next) {
18910 deps$1 = next;
18911 }
18912 async function openFile(file, ctx) {
18913 if (!deps$1) {
18914 console.warn(
18915 "[desktop-mode] wp.desktop.files.open() called before the shell installed open deps. The file will not open."
18916 );
18917 return false;
18918 }
18919 const opener = resolveOpener(file.type());
18920 if (!opener) {
18921 doAction("desktop-mode.files.open-failed", {
18922 reason: "no-opener",
18923 type: file.type(),
18924 ref: file.ref()
18925 });
18926 return false;
18927 }
18928 doAction("desktop-mode.files.opening", { file, openerId: opener.id });
18929 try {
18930 const handler = opener.handler;
18931 if (handler.kind === "url") {
18932 const url = await handler.url(file);
18933 if (!url) {
18934 return false;
18935 }
18936 const id = handler.windowId ? handler.windowId(file) : deps$1.deriveWindowId(url);
18937 const title = handler.title ? handler.title(file) : file.title();
18938 const icon = file.icon();
18939 const opened = deps$1.openUrl({ id, url, title, icon });
18940 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "url" });
18941 return opened;
18942 }
18943 if (handler.kind === "window") {
18944 const config = handler.config ? handler.config(file) : void 0;
18945 const opened = deps$1.openNativeWindow(handler.windowId, config);
18946 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "window" });
18947 return opened;
18948 }
18949 await handler.open(file, ctx);
18950 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "js" });
18951 return true;
18952 } catch (err) {
18953 doAction("desktop-mode.files.open-failed", {
18954 reason: "handler-threw",
18955 type: file.type(),
18956 ref: file.ref(),
18957 openerId: opener.id,
18958 error: err
18959 });
18960 console.error("[desktop-mode] file opener threw:", err);
18961 return false;
18962 }
18963 }
18964 function registerBuiltInFileTypes() {
18965 registerType({ type: "shortcut", label: "Plugin shortcut", sort: 1 });
18966 registerType({ type: "folder", label: "Folder", sort: 5 });
18967 registerType({ type: "post", label: "Post", sort: 10 });
18968 registerType({ type: "attachment", label: "Media", sort: 20 });
18969 registerType({ type: "user", label: "User", sort: 30 });
18970 registerType({ type: "term", label: "Taxonomy term", sort: 40 });
18971 registerType({ type: "comment", label: "Comment", sort: 50 });
18972 registerType({ type: "bookmark", label: "Bookmark", sort: 60 });
18973 registerType({ type: "link", label: "Web link", sort: 70 });
18974 registerType({ type: "embed", label: "Embedded web window", sort: 80 });
18975 }
18976 let deps = null;
18977 function installRestDeps(next) {
18978 deps = next;
18979 }
18980 function ensureDeps() {
18981 if (!deps) {
18982 throw new Error("[desktop-mode] files REST client called before installRestDeps().");
18983 }
18984 return deps;
18985 }
18986 class FilesConflictError extends Error {
18987 constructor(detail) {
18988 super(
18989 `Row was changed by ${detail.actor.name || "another session"} (parent="${detail.current.parentName}")`
18990 );
18991 this.name = "FilesConflictError";
18992 this.status = 409;
18993 this.detail = detail;
18994 }
18995 }
18996 async function call(path, init2) {
18997 const { baseUrl, nonce } = ensureDeps();
18998 const url = joinRestUrl(baseUrl, path);
18999 const headers = new Headers(init2.headers ?? {});
19000 headers.set("X-WP-Nonce", nonce);
19001 if (init2.body && !headers.has("Content-Type")) {
19002 headers.set("Content-Type", "application/json");
19003 }
19004 const res = await trackedFetch$1(
19005 url,
19006 { ...init2, headers, credentials: "same-origin" },
19007 { source: "desktop-mode/files" }
19008 );
19009 const text = await res.text();
19010 let body = null;
19011 let parseError = null;
19012 if (text) {
19013 try {
19014 body = JSON.parse(text);
19015 } catch (e) {
19016 body = null;
19017 parseError = e;
19018 }
19019 }
19020 if (!res.ok) {
19021 if (res.status === 409) {
19022 const data = body?.data?.data ?? body?.data;
19023 if (data && typeof data === "object") {
19024 throw new FilesConflictError(data);
19025 }
19026 }
19027 const err = body;
19028 throw new Error(
19029 `[desktop-mode] files REST ${res.status}: ${err?.code ?? ""} ${err?.message ?? ""}`.trim()
19030 );
19031 }
19032 if (null === body) {
19033 if (parseError && text) {
19034 const head = text.slice(0, 120).replace(/\s+/g, " ");
19035 throw new Error(
19036 `[desktop-mode] files REST ${res.status} returned non-JSON body — ${parseError.message}. First 120 chars: ${head}`
19037 );
19038 }
19039 throw new Error(
19040 `[desktop-mode] files REST ${res.status}: empty or unparseable body.`
19041 );
19042 }
19043 return body;
19044 }
19045 function listPlacements(folderId = 0) {
19046 return call(
19047 `/placements?folder=${encodeURIComponent(String(folderId))}`,
19048 { method: "GET" }
19049 );
19050 }
19051 function createPlacement(body) {
19052 return call("/placements", {
19053 method: "POST",
19054 body: JSON.stringify(body)
19055 });
19056 }
19057 function updatePlacement(id, body, ifMatchMs) {
19058 const headers = {};
19059 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
19060 headers["If-Match"] = String(ifMatchMs);
19061 }
19062 return call(`/placements/${id}`, {
19063 method: "PATCH",
19064 body: JSON.stringify(body),
19065 headers
19066 });
19067 }
19068 function deletePlacement(id) {
19069 return call(`/placements/${id}`, { method: "DELETE" });
19070 }
19071 async function restoreTrashedItem(id, type) {
19072 const { baseUrl, nonce } = ensureDeps();
19073 const root = baseUrl.replace(/\/files\/?$/, "");
19074 const url = `${root}/recycle-bin/restore`;
19075 const res = await trackedFetch$1(
19076 url,
19077 {
19078 method: "POST",
19079 headers: {
19080 "Content-Type": "application/json",
19081 "X-WP-Nonce": nonce
19082 },
19083 credentials: "same-origin",
19084 body: JSON.stringify({ items: [{ id, type }] })
19085 },
19086 { source: "desktop-mode/files" }
19087 );
19088 if (!res.ok) {
19089 throw new Error(`[desktop-mode] restore ${res.status}`);
19090 }
19091 return await res.json();
19092 }
19093 function listFolders() {
19094 return call("/folders", { method: "GET" });
19095 }
19096 function createFolder(body) {
19097 return call("/folders", {
19098 method: "POST",
19099 body: JSON.stringify(body)
19100 });
19101 }
19102 function updateFolder(id, body, ifMatchMs) {
19103 const headers = {};
19104 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
19105 headers["If-Match"] = String(ifMatchMs);
19106 }
19107 return call(`/folders/${id}`, {
19108 method: "PATCH",
19109 body: JSON.stringify(body),
19110 headers
19111 });
19112 }
19113 function deleteFolder(id) {
19114 return call(`/folders/${id}`, { method: "DELETE" });
19115 }
19116 function saveAssociations(associations) {
19117 return call("/associations", {
19118 method: "PUT",
19119 body: JSON.stringify({ associations })
19120 });
19121 }
19122 function listShares(folderId) {
19123 return call(`/folders/${folderId}/shares`, { method: "GET" });
19124 }
19125 function inviteShare(folderId, body) {
19126 return call(`/folders/${folderId}/shares`, {
19127 method: "POST",
19128 body: JSON.stringify(body)
19129 });
19130 }
19131 function updateShareCapability(folderId, shareId, capability) {
19132 return call(`/folders/${folderId}/shares/${shareId}`, {
19133 method: "PATCH",
19134 body: JSON.stringify({ capability })
19135 });
19136 }
19137 function revokeShare(folderId, shareId) {
19138 return call(`/folders/${folderId}/shares/${shareId}`, {
19139 method: "DELETE"
19140 });
19141 }
19142 function acceptShare(folderId, shareId) {
19143 return call(`/folders/${folderId}/shares/${shareId}/accept`, {
19144 method: "POST"
19145 });
19146 }
19147 function denyShare(folderId, shareId) {
19148 return call(`/folders/${folderId}/shares/${shareId}/deny`, {
19149 method: "POST"
19150 });
19151 }
19152 function leaveShare(folderId) {
19153 return call(`/folders/${folderId}/leave`, {
19154 method: "POST"
19155 });
19156 }
19157 function purgeFolderSharingTables() {
19158 return call(
19159 "/folder-sharing-tables/purge",
19160 { method: "POST" }
19161 );
19162 }
19163 const filesRest = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
19164 __proto__: null,
19165 FilesConflictError,
19166 acceptShare,
19167 createFolder,
19168 createPlacement,
19169 deleteFolder,
19170 deletePlacement,
19171 denyShare,
19172 installRestDeps,
19173 inviteShare,
19174 leaveShare,
19175 listFolders,
19176 listPlacements,
19177 listShares,
19178 purgeFolderSharingTables,
19179 restoreTrashedItem,
19180 revokeShare,
19181 saveAssociations,
19182 updateFolder,
19183 updatePlacement,
19184 updateShareCapability
19185 }, Symbol.toStringTag, { value: "Module" }));
19186 const STORE_KEY = "desktop-mode/files";
19187 function getFilesStore() {
19188 return createSharedStore(STORE_KEY, () => ({
19189 placementsByFolder: /* @__PURE__ */ new Map(),
19190 folders: /* @__PURE__ */ new Map(),
19191 hydratedFolders: /* @__PURE__ */ new Set()
19192 }));
19193 }
19194 function fireChanged(detail) {
19195 if (typeof document === "undefined") {
19196 return;
19197 }
19198 document.dispatchEvent(
19199 new CustomEvent("desktop-mode-files-changed", {
19200 detail: { source: "local", ...detail }
19201 })
19202 );
19203 }
19204 function setFolderPlacements(folderId, placements) {
19205 const store2 = getFilesStore();
19206 const next = new Map(store2.state.placementsByFolder);
19207 next.set(folderId, placements.slice());
19208 const hydrated = new Set(store2.state.hydratedFolders);
19209 hydrated.add(folderId);
19210 store2.state = { ...store2.state, placementsByFolder: next, hydratedFolders: hydrated };
19211 store2.notify();
19212 fireChanged({ kind: "placements-set", folderId });
19213 }
19214 function upsertPlacement(placement, source = "local") {
19215 if (!placement || typeof placement.id !== "number") {
19216 console.warn(
19217 "[desktop-mode] upsertPlacement called with a non-placement value; ignoring.",
19218 placement
19219 );
19220 return;
19221 }
19222 const store2 = getFilesStore();
19223 const next = new Map(store2.state.placementsByFolder);
19224 for (const [folderId, list2] of next) {
19225 const idx2 = list2.findIndex((p) => p && p.id === placement.id);
19226 if (idx2 >= 0 && folderId !== placement.parentId) {
19227 const copy = list2.filter(Boolean);
19228 const removeAt = copy.findIndex((p) => p.id === placement.id);
19229 if (removeAt >= 0) {
19230 copy.splice(removeAt, 1);
19231 }
19232 next.set(folderId, copy);
19233 }
19234 }
19235 const rawTarget = next.get(placement.parentId)?.slice() ?? [];
19236 const target2 = rawTarget.filter(Boolean);
19237 const idx = target2.findIndex((p) => p.id === placement.id);
19238 if (idx >= 0) {
19239 target2[idx] = placement;
19240 } else {
19241 target2.push(placement);
19242 }
19243 next.set(placement.parentId, target2);
19244 store2.state = { ...store2.state, placementsByFolder: next };
19245 store2.notify();
19246 fireChanged({ kind: "placement-upserted", placementId: placement.id, folderId: placement.parentId, source });
19247 }
19248 function removePlacement(placementId, source = "local") {
19249 const store2 = getFilesStore();
19250 const next = new Map(store2.state.placementsByFolder);
19251 let touchedFolder;
19252 for (const [folderId, list2] of next) {
19253 const idx = list2.findIndex((p) => p && p.id === placementId);
19254 if (idx >= 0) {
19255 const copy = list2.filter(Boolean).filter(
19256 (p) => p.id !== placementId
19257 );
19258 next.set(folderId, copy);
19259 touchedFolder = folderId;
19260 }
19261 }
19262 if (touchedFolder === void 0) {
19263 return;
19264 }
19265 store2.state = { ...store2.state, placementsByFolder: next };
19266 store2.notify();
19267 fireChanged({ kind: "placement-removed", placementId, folderId: touchedFolder, source });
19268 }
19269 function setFolders(folders) {
19270 const store2 = getFilesStore();
19271 const next = /* @__PURE__ */ new Map();
19272 for (const f of folders) {
19273 next.set(f.id, f);
19274 }
19275 store2.state = { ...store2.state, folders: next };
19276 store2.notify();
19277 fireChanged({ kind: "folders-set" });
19278 }
19279 function upsertFolder(folder, source = "local") {
19280 const store2 = getFilesStore();
19281 const next = new Map(store2.state.folders);
19282 next.set(folder.id, folder);
19283 store2.state = { ...store2.state, folders: next };
19284 store2.notify();
19285 fireChanged({ kind: "folder-upserted", folderRowId: folder.id, source });
19286 }
19287 function removeFolder(folderId, source = "local") {
19288 const store2 = getFilesStore();
19289 const folders = new Map(store2.state.folders);
19290 folders.delete(folderId);
19291 const placements = new Map(store2.state.placementsByFolder);
19292 placements.delete(folderId);
19293 store2.state = { ...store2.state, folders, placementsByFolder: placements };
19294 store2.notify();
19295 fireChanged({ kind: "folder-removed", folderRowId: folderId, source });
19296 }
19297 function subscribeFilesStore(cb) {
19298 const store2 = getFilesStore();
19299 const off = store2.subscribe(cb);
19300 return off;
19301 }
19302 function getFilesState() {
19303 return getFilesStore().getState();
19304 }
19305 const store = {
19306 getState: getFilesState,
19307 subscribe: subscribeFilesStore,
19308 setFolderPlacements,
19309 upsertPlacement,
19310 upsertFolder,
19311 removePlacement,
19312 removeFolder
19313 };
19314 const styles$5 = css`:host{display:inline-block}`;
19315 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 )}`;
19316 const _WpdRibbon = class _WpdRibbon extends Component {
19317 render() {
19318 return html`<span class="banner" part="banner"><slot></slot></span>`;
19319 }
19320 };
19321 _WpdRibbon.props = ["placement", "tone"];
19322 _WpdRibbon.styles = [styles$4];
19323 _WpdRibbon.help = {
19324 title: "Ribbon",
19325 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.",
19326 status: "experimental",
19327 since: "0.20.0",
19328 props: [
19329 {
19330 name: "placement",
19331 type: '"top-end" | "top-start" | "bottom-end" | "bottom-start"',
19332 description: "Which corner of the parent the ribbon hugs. Defaults to `top-end` (logical right in LTR, left in RTL)."
19333 },
19334 {
19335 name: "tone",
19336 type: '"primary" | "success" | "warning" | "danger" | "info" | "neutral"',
19337 description: "Background color tone. Defaults to `primary` (the admin theme accent)."
19338 }
19339 ],
19340 slots: [{ name: "(default)", description: "Ribbon label text. Keep short." }],
19341 cssProps: [
19342 { name: "--wpd-ribbon-size", default: "90px", description: "Square clipping window edge." },
19343 { name: "--wpd-ribbon-banner-width", default: "140px", description: "Width of the rotated strip." },
19344 { name: "--wpd-ribbon-banner-offset", default: "20px", description: "Distance from corner to strip center." },
19345 { name: "--wpd-ribbon-banner-pull", default: "-36px", description: "How far the strip overhangs the clip edge." },
19346 { name: "--wpd-ribbon-bg", default: "var(--wp-admin-theme-color, #2271b1)" },
19347 { name: "--wpd-ribbon-fg", default: "#fff" },
19348 { name: "--wpd-ribbon-shadow", default: "0 2px 4px rgba(0,0,0,0.2)" },
19349 { name: "--wpd-ribbon-padding", default: "4px 0" },
19350 { name: "--wpd-ribbon-font", default: "700 10px/1.4 system-ui" },
19351 { name: "--wpd-ribbon-tracking", default: "0.06em" },
19352 { name: "--wpd-ribbon-z", default: "2" }
19353 ],
19354 example: html`
19355 <div
19356 style="position: relative; width: 240px; height: 120px;
19357 border: 1px solid #ccc; border-radius: 8px;
19358 padding: 16px; box-sizing: border-box;"
19359 >
19360 <wpd-ribbon>Featured</wpd-ribbon>
19361 Card body…
19362 </div>
19363 `
19364 };
19365 let WpdRibbon = _WpdRibbon;
19366 defineComponent("wpd-ribbon", WpdRibbon);
19367 const TILE_CLASS = "desktop-mode-file-tile";
19368 const STATUS_LABEL = {
19369 draft: "Draft",
19370 pending: "Pending",
19371 private: "Private",
19372 future: "Scheduled"
19373 };
19374 function statusRibbonsEnabled() {
19375 const get2 = window.wp?.desktop?.getOsSettings;
19376 if (typeof get2 !== "function") {
19377 return true;
19378 }
19379 try {
19380 return get2()?.showPostStatusRibbons !== false;
19381 } catch {
19382 return true;
19383 }
19384 }
19385 function getDragManager$1() {
19386 const api = window.wp?.desktop?.dragManager;
19387 return api ?? null;
19388 }
19389 const REACTIVE_PROPS = [
19390 "type",
19391 "ref",
19392 "label",
19393 "icon",
19394 "thumbnail",
19395 "kind",
19396 "status",
19397 "selected",
19398 "missing",
19399 "access-gated",
19400 "drag-kind",
19401 "drag-title",
19402 "drag-icon"
19403 ];
19404 const _WpdTile = class _WpdTile extends Component {
19405 constructor() {
19406 super(...arguments);
19407 this._pointerdownHandler = null;
19408 this._keydownHandler = null;
19409 }
19410 connectedCallback() {
19411 super.connectedCallback();
19412 if (!this._keydownHandler) {
19413 this._keydownHandler = (e) => {
19414 if (e.key === "Enter" || e.key === " ") {
19415 e.preventDefault();
19416 this.click();
19417 }
19418 };
19419 this.addEventListener("keydown", this._keydownHandler);
19420 }
19421 this._paint();
19422 }
19423 disconnectedCallback() {
19424 if (this._pointerdownHandler) {
19425 this.removeEventListener(
19426 "pointerdown",
19427 this._pointerdownHandler
19428 );
19429 this._pointerdownHandler = null;
19430 }
19431 if (this._keydownHandler) {
19432 this.removeEventListener(
19433 "keydown",
19434 this._keydownHandler
19435 );
19436 this._keydownHandler = null;
19437 }
19438 }
19439 /**
19440 * Bypass the templated render loop. Lit-html's `render(template,
19441 * root)` would wipe the host's light-DOM children every tick —
19442 * including the visual / label / ribbon `_paint()` just
19443 * inserted. We override `requestUpdate` directly so attribute
19444 * changes call `_paint` (idempotent) without lit-html getting
19445 * involved.
19446 */
19447 requestUpdate() {
19448 if (!this.isConnected) {
19449 return;
19450 }
19451 this._paint();
19452 }
19453 render() {
19454 return html``;
19455 }
19456 _paint() {
19457 const type = this.getAttribute("type") ?? "";
19458 const ref = this.getAttribute("ref") ?? "";
19459 const label = this.getAttribute("label") ?? "";
19460 const icon = this.getAttribute("icon") ?? "";
19461 const thumbnail = this.getAttribute("thumbnail") ?? "";
19462 const kind = this.getAttribute("kind") ?? "entry";
19463 const status = this.getAttribute("status") ?? "";
19464 const selected = this.hasAttribute("selected");
19465 const missing = this.hasAttribute("missing");
19466 const accessGated = this.hasAttribute("access-gated");
19467 const ownedClasses = [
19468 TILE_CLASS,
19469 `${TILE_CLASS}--folder`,
19470 `${TILE_CLASS}--missing`,
19471 `${TILE_CLASS}--access-gated`,
19472 `${TILE_CLASS}--selected`
19473 ];
19474 for (const c of ownedClasses) {
19475 this.classList.remove(c);
19476 }
19477 this.classList.add(TILE_CLASS);
19478 if (kind === "folder") {
19479 this.classList.add(`${TILE_CLASS}--folder`);
19480 }
19481 if (missing) {
19482 this.classList.add(`${TILE_CLASS}--missing`);
19483 }
19484 if (accessGated) {
19485 this.classList.add(`${TILE_CLASS}--access-gated`);
19486 }
19487 if (selected) {
19488 this.classList.add(`${TILE_CLASS}--selected`);
19489 }
19490 this.dataset.fileType = type;
19491 this.dataset.fileRef = ref;
19492 if (kind) {
19493 this.dataset.role = kind;
19494 }
19495 this.setAttribute("role", "listitem");
19496 this.setAttribute("aria-label", label);
19497 if (!this.hasAttribute("tabindex")) {
19498 this.setAttribute("tabindex", "0");
19499 }
19500 const accessGatedTitle = "You don’t have permission to open this — ask the folder owner for access.";
19501 if (accessGated) {
19502 this.title = accessGatedTitle;
19503 this.setAttribute("aria-disabled", "true");
19504 } else {
19505 this.removeAttribute("aria-disabled");
19506 if (this.title === accessGatedTitle) {
19507 this.removeAttribute("title");
19508 }
19509 }
19510 const SLOTS = [
19511 `${TILE_CLASS}__visual`,
19512 `${TILE_CLASS}__label`,
19513 `${TILE_CLASS}__lock`
19514 ];
19515 for (const cls of SLOTS) {
19516 this.querySelectorAll(`:scope > .${cls}`).forEach(
19517 (n) => n.remove()
19518 );
19519 }
19520 this.querySelectorAll(":scope > wpd-ribbon").forEach(
19521 (n) => n.remove()
19522 );
19523 const visual = document.createElement("span");
19524 visual.className = `${TILE_CLASS}__visual`;
19525 if (thumbnail) {
19526 const img = document.createElement("img");
19527 img.src = thumbnail;
19528 img.alt = "";
19529 img.loading = "lazy";
19530 img.decoding = "async";
19531 img.className = `${TILE_CLASS}__preview`;
19532 img.draggable = false;
19533 visual.appendChild(img);
19534 } else if (icon) {
19535 const iconNode = renderIcon(icon, {
19536 title: label,
19537 className: `${TILE_CLASS}__icon`
19538 });
19539 visual.appendChild(iconNode);
19540 }
19541 this.appendChild(visual);
19542 const labelNode = document.createElement("span");
19543 labelNode.className = `${TILE_CLASS}__label`;
19544 labelNode.textContent = label;
19545 this.appendChild(labelNode);
19546 if (accessGated) {
19547 const lock = document.createElement("span");
19548 lock.className = `${TILE_CLASS}__lock dashicons dashicons-lock`;
19549 lock.setAttribute("aria-hidden", "true");
19550 this.appendChild(lock);
19551 }
19552 if (status && status !== "publish" && STATUS_LABEL[status] && statusRibbonsEnabled()) {
19553 const ribbon = document.createElement("wpd-ribbon");
19554 ribbon.setAttribute("placement", "top-end");
19555 ribbon.setAttribute("tone", ribbonToneFor(status));
19556 ribbon.textContent = STATUS_LABEL[status];
19557 this.appendChild(ribbon);
19558 }
19559 applyTileEntryStagger(this);
19560 doAction("desktop-mode.tile.rendered", { tile: this });
19561 this._wireDragOut();
19562 }
19563 _wireDragOut() {
19564 if (this._pointerdownHandler) {
19565 this.removeEventListener(
19566 "pointerdown",
19567 this._pointerdownHandler
19568 );
19569 this._pointerdownHandler = null;
19570 }
19571 const dragKind = this.getAttribute("drag-kind");
19572 if (!dragKind) {
19573 return;
19574 }
19575 const handler = (e) => {
19576 if (e.button !== 0) {
19577 return;
19578 }
19579 const dragManager = getDragManager$1();
19580 if (!dragManager) {
19581 return;
19582 }
19583 const ref = this.getAttribute("ref") ?? "";
19584 const title = this.getAttribute("drag-title") ?? this.getAttribute("label") ?? void 0;
19585 const icon = this.getAttribute("drag-icon") ?? this.getAttribute("icon") ?? void 0;
19586 const rect = this.getBoundingClientRect();
19587 dragManager.start({
19588 payload: {
19589 type: "shortcut",
19590 source: this,
19591 data: {
19592 kind: dragKind,
19593 ref,
19594 title,
19595 icon
19596 },
19597 ghost: {
19598 offsetX: e.clientX - rect.left,
19599 offsetY: e.clientY - rect.top
19600 }
19601 },
19602 origin: e
19603 });
19604 };
19605 this._pointerdownHandler = handler;
19606 this.addEventListener("pointerdown", handler);
19607 }
19608 };
19609 _WpdTile.shadow = false;
19610 _WpdTile.props = REACTIVE_PROPS;
19611 _WpdTile.styles = [styles$5];
19612 _WpdTile.help = {
19613 title: "Tile",
19614 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.",
19615 status: "experimental",
19616 since: "0.21.0",
19617 props: [
19618 { name: "type", type: "string" },
19619 { name: "ref", type: "string" },
19620 { name: "label", type: "string" },
19621 { name: "icon", type: "string", description: "Dashicon class / URL / data URI. Ignored when `thumbnail` is set." },
19622 { name: "thumbnail", type: "string", description: "Preview image URL. Renders as `<img>` and wins over `icon`." },
19623 { name: "kind", type: "`entry` | `folder`" },
19624 { name: "status", type: "`draft` | `pending` | `private` | `future` | `publish`" },
19625 { name: "selected", type: "boolean" },
19626 { name: "missing", type: "boolean" },
19627 { name: "access-gated", type: "boolean" },
19628 { name: "drag-kind", type: "string", description: "When set, the component wires pointerdown → DragManager." },
19629 { name: "drag-title", type: "string" },
19630 { name: "drag-icon", type: "string" }
19631 ]
19632 };
19633 let WpdTile = _WpdTile;
19634 function ribbonToneFor(status) {
19635 switch (status) {
19636 case "draft":
19637 return "warning";
19638 case "pending":
19639 return "info";
19640 case "private":
19641 return "danger";
19642 case "future":
19643 return "primary";
19644 default:
19645 return "primary";
19646 }
19647 }
19648 defineComponent("wpd-tile", WpdTile);
19649 function buildTileFromSpec(spec) {
19650 const tile2 = document.createElement("wpd-tile");
19651 tile2.setAttribute("type", spec.type);
19652 tile2.setAttribute("ref", spec.ref);
19653 tile2.setAttribute("label", spec.label);
19654 if (spec.icon) {
19655 tile2.setAttribute("icon", spec.icon);
19656 }
19657 if (spec.thumbnail) {
19658 tile2.setAttribute("thumbnail", spec.thumbnail);
19659 }
19660 if (spec.role) {
19661 tile2.setAttribute("kind", spec.role);
19662 }
19663 if (spec.status) {
19664 tile2.setAttribute("status", spec.status);
19665 }
19666 if (spec.missing) {
19667 tile2.setAttribute("missing", "");
19668 }
19669 if (spec.accessGated) {
19670 tile2.setAttribute("access-gated", "");
19671 }
19672 if (spec.dataset) {
19673 for (const [key, raw] of Object.entries(spec.dataset)) {
19674 if (raw === void 0 || raw === null) {
19675 continue;
19676 }
19677 tile2.dataset[key] = String(raw);
19678 }
19679 }
19680 if (Array.isArray(spec.extraClasses)) {
19681 for (const c of spec.extraClasses) {
19682 if (c) {
19683 tile2.classList.add(c);
19684 }
19685 }
19686 }
19687 const classFiltered = applyFilters(
19688 "desktop-mode.tile.class",
19689 tile2.className,
19690 spec
19691 );
19692 if (classFiltered && classFiltered !== tile2.className) {
19693 tile2.className = classFiltered;
19694 }
19695 if (typeof spec.x === "number" && typeof spec.y === "number") {
19696 tile2.style.position = "absolute";
19697 tile2.style.left = `${spec.x}px`;
19698 tile2.style.top = `${spec.y}px`;
19699 }
19700 return tile2;
19701 }
19702 function placementToSpec(placement, folderId) {
19703 const file = resolve(placement.file);
19704 const previewUrl = file.previewUrl();
19705 const metaName = placement.meta && typeof placement.meta.name === "string" ? placement.meta.name.trim() : "";
19706 const label = metaName !== "" ? metaName : file.title();
19707 const metaIconUrl = placement.meta && typeof placement.meta.iconUrl === "string" ? placement.meta.iconUrl.trim() : "";
19708 return {
19709 type: placement.file.type,
19710 ref: placement.file.ref,
19711 label,
19712 // Preview wins over icon (matches the previous behavior).
19713 thumbnail: previewUrl || void 0,
19714 icon: previewUrl ? void 0 : metaIconUrl || file.icon(),
19715 x: placement.x,
19716 y: placement.y,
19717 dataset: {
19718 placementId: placement.id,
19719 folderId
19720 },
19721 meta: placement.meta,
19722 missing: !placement.file.exists,
19723 accessGated: Boolean(placement.accessGated),
19724 ariaLabel: label
19725 };
19726 }
19727 function buildTile(placement, folderId) {
19728 const file = resolve(placement.file);
19729 const tile2 = buildTileFromSpec(placementToSpec(placement, folderId));
19730 const classFiltered = applyFilters(
19731 "desktop-mode.files.tile-class",
19732 TILE_CLASS,
19733 placement
19734 );
19735 if (classFiltered && classFiltered !== TILE_CLASS) {
19736 tile2.className = classFiltered;
19737 }
19738 const extra = applyFilters(
19739 "desktop-mode.files.tile-element",
19740 null,
19741 placement
19742 );
19743 if (extra instanceof Element) {
19744 tile2.appendChild(extra);
19745 }
19746 tile2.addEventListener("dblclick", (e) => {
19747 e.preventDefault();
19748 e.stopPropagation();
19749 if (placement.accessGated) {
19750 showToast({
19751 message: `You don’t have permission to open "${placement.file.title || file.title()}". Ask the folder owner if you need access to this item.`,
19752 duration: 6e3
19753 });
19754 return;
19755 }
19756 void openFile(file, {
19757 placement: {
19758 id: placement.id,
19759 x: placement.x,
19760 y: placement.y,
19761 meta: placement.meta
19762 }
19763 });
19764 });
19765 doAction("desktop-mode.files.tile-rendered", { tile: tile2, placement });
19766 return tile2;
19767 }
19768 function setTilePosition(tile2, x, y) {
19769 tile2.style.left = `${x}px`;
19770 tile2.style.top = `${y}px`;
19771 }
19772 function attachDismissable(host, options) {
19773 const onAway = (e) => {
19774 if (e.target instanceof Node && host.contains(e.target)) {
19775 return;
19776 }
19777 if (e.target instanceof Node) {
19778 for (const sel of options.siblingSelectors ?? []) {
19779 const matches = Array.from(
19780 document.querySelectorAll(sel)
19781 );
19782 for (const m of matches) {
19783 if (m.contains(e.target)) {
19784 return;
19785 }
19786 }
19787 }
19788 }
19789 if (options.excludeOutsideTarget && e.target instanceof Node && options.excludeOutsideTarget.contains(e.target)) {
19790 return;
19791 }
19792 options.close();
19793 };
19794 const onKey = (e) => {
19795 if (e.key === "Escape") {
19796 options.close();
19797 }
19798 };
19799 document.addEventListener("mousedown", onAway, { capture: true });
19800 document.addEventListener("keydown", onKey);
19801 return () => {
19802 document.removeEventListener("mousedown", onAway, { capture: true });
19803 document.removeEventListener("keydown", onKey);
19804 };
19805 }
19806 const MENU_CLASS$2 = "desktop-mode-wallpaper-menu";
19807 let activeMenu$2 = null;
19808 function closeTileMenu() {
19809 if (!activeMenu$2) {
19810 return;
19811 }
19812 activeMenu$2.dispatchEvent(new CustomEvent("tile-menu-closed"));
19813 activeMenu$2.remove();
19814 activeMenu$2 = null;
19815 doAction("desktop-mode.files.tile-menu.closed", {});
19816 }
19817 let openGeneration$1 = 0;
19818 function openTileMenu(pos, opts) {
19819 closeTileMenu();
19820 const myGen = ++openGeneration$1;
19821 openWithShellOverlays(
19822 () => myGen === openGeneration$1,
19823 () => openTileMenuImmediate(pos, opts)
19824 );
19825 }
19826 function openTileMenuImmediate(pos, { placement, items }) {
19827 const list2 = applyFilters(
19828 "desktop-mode.files.tile-menu",
19829 items.slice(),
19830 placement
19831 );
19832 const sorted = (Array.isArray(list2) ? list2 : items).slice().sort((a, b) => {
19833 const sa = typeof a.sort === "number" ? a.sort : 100;
19834 const sb = typeof b.sort === "number" ? b.sort : 100;
19835 if (sa !== sb) {
19836 return sa - sb;
19837 }
19838 return a.label.localeCompare(b.label);
19839 });
19840 if (sorted.length === 0) {
19841 return;
19842 }
19843 const menu = document.createElement("wpd-context-menu");
19844 menu.setAttribute("open", "");
19845 menu.classList.add(MENU_CLASS$2);
19846 menu.dataset.placementId = String(placement.id);
19847 menu.style.left = `${pos.x}px`;
19848 menu.style.top = `${pos.y}px`;
19849 const itemById = /* @__PURE__ */ new Map();
19850 for (const item of sorted) {
19851 itemById.set(item.id, item);
19852 const opt = document.createElement("wpd-context-menu-option");
19853 opt.dataset.menuItemId = item.id;
19854 opt.setAttribute("value", item.id);
19855 if (item.danger) {
19856 opt.setAttribute("danger", "");
19857 }
19858 if (item.disabled) {
19859 opt.setAttribute("disabled", "");
19860 }
19861 if (item.icon) {
19862 opt.setAttribute("icon", sanitizeClass$2(item.icon));
19863 }
19864 opt.textContent = item.label;
19865 menu.appendChild(opt);
19866 }
19867 menu.addEventListener("wpd-context-menu-pick", (e) => {
19868 const detail = e.detail;
19869 const item = itemById.get(detail.id);
19870 if (!item) {
19871 return;
19872 }
19873 closeTileMenu();
19874 void item.onClick(new MouseEvent("click"));
19875 });
19876 document.body.appendChild(menu);
19877 activeMenu$2 = menu;
19878 const rect = menu.getBoundingClientRect();
19879 if (rect.right > window.innerWidth) {
19880 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
19881 }
19882 if (rect.bottom > window.innerHeight) {
19883 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
19884 }
19885 const detach = attachDismissable(menu, {
19886 close: () => closeTileMenu()
19887 });
19888 menu.addEventListener("tile-menu-closed", detach);
19889 doAction("desktop-mode.files.tile-menu.opened", {
19890 placementId: placement.id,
19891 items: sorted.map((i) => i.id)
19892 });
19893 }
19894 function sanitizeClass$2(raw) {
19895 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
19896 }
19897 const ROOT_CLASS$3 = "desktop-mode-create-folder-dialog";
19898 let active$1 = null;
19899 function closeCreateFolderDialog() {
19900 if (!active$1) {
19901 return;
19902 }
19903 active$1.dispatchEvent(new CustomEvent("create-folder-dialog-closed"));
19904 active$1.remove();
19905 active$1 = null;
19906 doAction("desktop-mode.files.create-folder.closed", {});
19907 }
19908 function openCreateFolderDialog(options) {
19909 closeCreateFolderDialog();
19910 const decision = applyFilters(
19911 "desktop-mode.files.create-folder.dialog",
19912 null,
19913 options
19914 );
19915 if (decision === false) {
19916 return;
19917 }
19918 const initial = (options.initialName ?? "Untitled folder").trim();
19919 const overlay = document.createElement("div");
19920 overlay.className = `${ROOT_CLASS$3}__overlay`;
19921 overlay.setAttribute("role", "presentation");
19922 const dialog2 = document.createElement("div");
19923 dialog2.className = ROOT_CLASS$3;
19924 dialog2.setAttribute("role", "dialog");
19925 dialog2.setAttribute("aria-modal", "true");
19926 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS$3}-title`);
19927 const title = document.createElement("h2");
19928 title.id = `${ROOT_CLASS$3}-title`;
19929 title.className = `${ROOT_CLASS$3}__title`;
19930 title.textContent = options.title ?? "New folder";
19931 dialog2.appendChild(title);
19932 const label = document.createElement("label");
19933 label.className = `${ROOT_CLASS$3}__label`;
19934 label.htmlFor = `${ROOT_CLASS$3}-input`;
19935 label.textContent = options.label ?? "Folder name";
19936 dialog2.appendChild(label);
19937 const input = document.createElement("input");
19938 input.type = "text";
19939 input.id = `${ROOT_CLASS$3}-input`;
19940 input.className = `${ROOT_CLASS$3}__input`;
19941 input.value = initial;
19942 input.setAttribute("autocomplete", "off");
19943 input.setAttribute("spellcheck", "false");
19944 dialog2.appendChild(input);
19945 const error = document.createElement("p");
19946 error.className = `${ROOT_CLASS$3}__error`;
19947 error.hidden = true;
19948 error.setAttribute("role", "alert");
19949 dialog2.appendChild(error);
19950 const actions = document.createElement("div");
19951 actions.className = `${ROOT_CLASS$3}__actions`;
19952 const cancel = document.createElement("button");
19953 cancel.type = "button";
19954 cancel.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--secondary`;
19955 cancel.textContent = "Cancel";
19956 const submit = document.createElement("button");
19957 submit.type = "button";
19958 submit.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--primary`;
19959 submit.textContent = options.submitLabel ?? "Create";
19960 actions.appendChild(cancel);
19961 actions.appendChild(submit);
19962 dialog2.appendChild(actions);
19963 overlay.appendChild(dialog2);
19964 document.body.appendChild(overlay);
19965 active$1 = overlay;
19966 input.focus();
19967 input.select();
19968 doAction("desktop-mode.files.create-folder.opened", {});
19969 const setBusy = (busy) => {
19970 input.disabled = busy;
19971 cancel.disabled = busy;
19972 submit.disabled = busy;
19973 dialog2.classList.toggle(`${ROOT_CLASS$3}--busy`, busy);
19974 };
19975 const showError = (msg) => {
19976 error.textContent = msg;
19977 error.hidden = false;
19978 };
19979 const doCancel = () => {
19980 closeCreateFolderDialog();
19981 options.onCancel?.();
19982 };
19983 const doSubmit = async () => {
19984 const name = input.value.trim();
19985 if (!name) {
19986 showError("Please enter a name.");
19987 input.focus();
19988 return;
19989 }
19990 error.hidden = true;
19991 setBusy(true);
19992 try {
19993 await options.onSubmit(name);
19994 closeCreateFolderDialog();
19995 } catch (err) {
19996 setBusy(false);
19997 showError(
19998 err instanceof Error ? err.message : "Could not create the folder."
19999 );
20000 input.focus();
20001 input.select();
20002 }
20003 };
20004 cancel.addEventListener("click", () => doCancel());
20005 submit.addEventListener("click", () => void doSubmit());
20006 overlay.addEventListener("click", (e) => {
20007 if (e.target === overlay) {
20008 doCancel();
20009 }
20010 });
20011 const onKey = (e) => {
20012 if (e.key === "Escape") {
20013 e.preventDefault();
20014 doCancel();
20015 } else if (e.key === "Enter" && !e.isComposing) {
20016 e.preventDefault();
20017 void doSubmit();
20018 }
20019 };
20020 dialog2.addEventListener("keydown", onKey);
20021 overlay.addEventListener("create-folder-dialog-closed", () => {
20022 dialog2.removeEventListener("keydown", onKey);
20023 });
20024 }
20025 const GRID_PADDING = 16;
20026 const GRID_CELL_W = 96;
20027 const GRID_CELL_H = 110;
20028 function pointToCell(x, y) {
20029 const col = Math.max(0, Math.round((x - GRID_PADDING) / GRID_CELL_W));
20030 const row = Math.max(0, Math.round((y - GRID_PADDING) / GRID_CELL_H));
20031 return cellToPos(col, row);
20032 }
20033 function cellToPos(col, row) {
20034 return {
20035 col,
20036 row,
20037 x: GRID_PADDING + col * GRID_CELL_W,
20038 y: GRID_PADDING + row * GRID_CELL_H
20039 };
20040 }
20041 function snapToEmptyCell(x, y, occupied, host) {
20042 const target2 = pointToCell(x, y);
20043 if (!occupied.has(cellKey(target2.col, target2.row))) {
20044 return target2;
20045 }
20046 const maxRows = host ? Math.max(1, Math.floor((host.clientHeight - GRID_PADDING) / GRID_CELL_H)) : 999;
20047 for (let col = 0; col < 999; col++) {
20048 for (let row = 0; row < maxRows; row++) {
20049 if (!occupied.has(cellKey(col, row))) {
20050 return cellToPos(col, row);
20051 }
20052 }
20053 }
20054 return target2;
20055 }
20056 function nextRowMajorCell(occupied, host) {
20057 const cols = host ? Math.max(
20058 1,
20059 Math.floor((host.clientWidth - GRID_PADDING) / GRID_CELL_W)
20060 ) : 4;
20061 const maxCols = Math.max(1, cols);
20062 for (let row = 0; row < 999; row++) {
20063 for (let col = 0; col < maxCols; col++) {
20064 if (!occupied.has(cellKey(col, row))) {
20065 return cellToPos(col, row);
20066 }
20067 }
20068 }
20069 return cellToPos(0, 0);
20070 }
20071 function buildOccupiedSet(placements, excludeId) {
20072 const out = /* @__PURE__ */ new Set();
20073 for (const p of placements) {
20074 const cell = pointToCell(p.x, p.y);
20075 out.add(cellKey(cell.col, cell.row));
20076 }
20077 return out;
20078 }
20079 function cellKey(col, row) {
20080 return `${col},${row}`;
20081 }
20082 function isConflict(err) {
20083 return err instanceof FilesConflictError;
20084 }
20085 function buildReason(err) {
20086 const actor = err.detail.actor.name || "Someone else";
20087 const where = err.detail.current.parentName || "another folder";
20088 if (err.detail.reason === "trashed") {
20089 return "This item is in the recycle bin.";
20090 }
20091 if (err.detail.reason === "forbidden") {
20092 return "You no longer have access.";
20093 }
20094 if (err.detail.reason === "gone") {
20095 return "This item was deleted.";
20096 }
20097 return `${actor} moved this to "${where}".`;
20098 }
20099 function showConflictToast(err) {
20100 const reason = buildReason(err);
20101 const targetParentId = err.detail.current.parentId;
20102 let action;
20103 if (targetParentId > 0) {
20104 action = {
20105 label: "View folder",
20106 onClick: () => {
20107 const winId = `desktop-mode-folder-${targetParentId}`;
20108 const mgr = window.desktopMode?.windowManager;
20109 if (mgr?.focus) {
20110 const w = mgr.focus(winId);
20111 if (w) {
20112 return;
20113 }
20114 }
20115 if (mgr?.open) {
20116 void mgr.open(winId);
20117 }
20118 }
20119 };
20120 }
20121 showToast({
20122 message: reason,
20123 action,
20124 duration: 7e3
20125 });
20126 }
20127 function broadcastFilesChange(kind, action, ids) {
20128 const api = window.wp?.desktop;
20129 api?.broadcast?.(`desktop-mode.${kind}.changed`, {
20130 source: "desktop-files",
20131 action,
20132 ids
20133 });
20134 }
20135 function showTrashErrorToast(err) {
20136 const api = window.wp?.desktop;
20137 if (!api?.showToast) {
20138 return;
20139 }
20140 const raw = err instanceof Error ? err.message : String(err);
20141 const friendly = raw.replace(/^\[desktop-mode\][^:]*:\s*/, "").replace(/^desktop_mode_files_[a-z_]+\s*/, "");
20142 api.showToast({
20143 message: friendly || "Could not move this item to the recycle bin.",
20144 duration: 5e3
20145 });
20146 }
20147 function showTrashedToast(message, onUndo) {
20148 const api = window.wp?.desktop;
20149 if (!api?.showToast) {
20150 return;
20151 }
20152 api.showToast({
20153 message,
20154 duration: 6e3,
20155 action: {
20156 label: "Undo",
20157 onClick: onUndo
20158 }
20159 });
20160 }
20161 async function trashPlacementWithUndo(placement) {
20162 const placementId = placement.id;
20163 const parentId = placement.parentId;
20164 const title = placement.file?.title ?? "Item";
20165 const kind = placement.file?.type === "shortcut" ? "shortcut" : "placement";
20166 store.removePlacement(placementId);
20167 try {
20168 await deletePlacement(placementId);
20169 broadcastFilesChange(kind, "trashed", [placementId]);
20170 showTrashedToast(`"${title}" moved to Trash`, async () => {
20171 try {
20172 await restoreTrashedItem(placementId, "placement");
20173 const res = await listPlacements(parentId);
20174 store.setFolderPlacements(parentId, res.placements);
20175 broadcastFilesChange(kind, "untrashed", [placementId]);
20176 } catch (err) {
20177 console.error("[desktop-mode] restore failed:", err);
20178 }
20179 });
20180 } catch (err) {
20181 console.error("[desktop-mode] deletePlacement failed:", err);
20182 showTrashErrorToast(err);
20183 void listPlacements(parentId).then((res) => {
20184 store.setFolderPlacements(parentId, res.placements);
20185 });
20186 }
20187 }
20188 async function trashFolderWithUndo(placement) {
20189 const folderId = parseInt(placement.file.ref, 10);
20190 if (!folderId) {
20191 return;
20192 }
20193 const placementId = placement.id;
20194 const parentId = placement.parentId;
20195 const title = placement.file?.title ?? "Folder";
20196 store.removePlacement(placementId);
20197 store.removeFolder(folderId);
20198 try {
20199 await deleteFolder(folderId);
20200 broadcastFilesChange("folder", "trashed", [folderId]);
20201 showTrashedToast(`"${title}" moved to Trash`, async () => {
20202 try {
20203 await restoreTrashedItem(folderId, "folder");
20204 const res = await listPlacements(parentId);
20205 store.setFolderPlacements(parentId, res.placements);
20206 broadcastFilesChange("folder", "untrashed", [folderId]);
20207 } catch (err) {
20208 console.error("[desktop-mode] restore folder failed:", err);
20209 }
20210 });
20211 } catch (err) {
20212 console.error("[desktop-mode] deleteFolder failed:", err);
20213 showTrashErrorToast(err);
20214 void listPlacements(parentId).then((res) => {
20215 store.setFolderPlacements(parentId, res.placements);
20216 });
20217 }
20218 }
20219 function trashByFileType(placement) {
20220 if (placement.file?.type === "folder") {
20221 return trashFolderWithUndo(placement);
20222 }
20223 return trashPlacementWithUndo(placement);
20224 }
20225 function buildBridgePayloadFromPlacement(placement) {
20226 const file = placement.file;
20227 if (!file) {
20228 return void 0;
20229 }
20230 const id = parseInt(String(file.ref ?? ""), 10);
20231 if (!Number.isFinite(id) || id <= 0) {
20232 return void 0;
20233 }
20234 const title = String(file.title ?? "");
20235 if (file.type === "attachment") {
20236 const url = String(file.sourceUrl ?? file.previewUrl ?? "");
20237 return {
20238 kind: "attachment",
20239 id,
20240 url,
20241 title,
20242 alt: String(file.alt ?? ""),
20243 mime: String(file.mime ?? ""),
20244 thumbnailUrl: file.previewUrl ? String(file.previewUrl) : void 0
20245 };
20246 }
20247 if (file.type === "post") {
20248 return {
20249 kind: "post",
20250 id,
20251 postType: String(file.postType ?? "post"),
20252 url: String(file.link ?? ""),
20253 title
20254 };
20255 }
20256 if (file.type === "user") {
20257 return {
20258 kind: "user",
20259 id,
20260 url: String(file.link ?? ""),
20261 title
20262 };
20263 }
20264 return void 0;
20265 }
20266 function getDragManager() {
20267 const api = window.wp?.desktop?.dragManager;
20268 return api ?? null;
20269 }
20270 const LAYER_CLASS = "desktop-mode-files-layer";
20271 function mountFilesLayer(host, folderId = 0) {
20272 const container = document.createElement("div");
20273 container.className = LAYER_CLASS;
20274 container.setAttribute("role", "list");
20275 container.dataset.folderId = String(folderId);
20276 host.appendChild(container);
20277 let lastFingerprint = "";
20278 let selectedId = null;
20279 const selectionListeners = /* @__PURE__ */ new Set();
20280 const notifySelection = (placement) => {
20281 for (const cb of selectionListeners) {
20282 try {
20283 cb(placement);
20284 } catch (err) {
20285 console.error(
20286 "[desktop-mode] files: selection listener threw:",
20287 err
20288 );
20289 }
20290 }
20291 };
20292 const setSelected = (placement) => {
20293 const newId = placement ? placement.id : null;
20294 if (newId === selectedId) {
20295 return;
20296 }
20297 container.querySelectorAll(`.${TILE_CLASS}--selected`).forEach((n) => n.removeAttribute("selected"));
20298 if (placement) {
20299 const tile2 = container.querySelector(
20300 `[data-placement-id="${placement.id}"]`
20301 );
20302 tile2?.setAttribute("selected", "");
20303 }
20304 selectedId = newId;
20305 notifySelection(placement);
20306 };
20307 const computeLayout = (list2) => {
20308 const pinnedSlots = /* @__PURE__ */ new Map();
20309 const occupiedCells = /* @__PURE__ */ new Set();
20310 let pinnedIdx = 0;
20311 for (const placement of list2) {
20312 if (!isPinned(placement)) {
20313 continue;
20314 }
20315 const slot = cellToPos(0, pinnedIdx);
20316 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
20317 occupiedCells.add(cellKey(slot.col, slot.row));
20318 pinnedIdx += 1;
20319 }
20320 const displaced = /* @__PURE__ */ new Map();
20321 for (const placement of list2) {
20322 if (pinnedSlots.has(placement.id)) {
20323 continue;
20324 }
20325 const target2 = pointToCell(placement.x, placement.y);
20326 const key = cellKey(target2.col, target2.row);
20327 if (!occupiedCells.has(key)) {
20328 occupiedCells.add(key);
20329 continue;
20330 }
20331 const free = snapToEmptyCell(
20332 placement.x,
20333 placement.y,
20334 occupiedCells,
20335 host
20336 );
20337 occupiedCells.add(cellKey(free.col, free.row));
20338 displaced.set(placement.id, { x: free.x, y: free.y });
20339 }
20340 return { pinnedSlots, displaced };
20341 };
20342 const applyTilePosition = (tile2, placement, pinnedSlots, displaced) => {
20343 const pinned = pinnedSlots.get(placement.id);
20344 const moved = displaced.get(placement.id);
20345 if (pinned) {
20346 setTilePosition(tile2, pinned.x, pinned.y);
20347 } else if (moved) {
20348 setTilePosition(tile2, moved.x, moved.y);
20349 } else {
20350 setTilePosition(tile2, placement.x, placement.y);
20351 }
20352 };
20353 const wireTile = (placement, pinnedSlots, displaced) => {
20354 const tile2 = buildTile(placement, folderId);
20355 const pinnedSlot = pinnedSlots.get(placement.id);
20356 if (pinnedSlot) {
20357 setTilePosition(tile2, pinnedSlot.x, pinnedSlot.y);
20358 tile2.classList.add(`${TILE_CLASS}--pinned`);
20359 attachContextMenu(tile2, placement);
20360 attachSelectOnClick(tile2, placement);
20361 if (shouldRejectTileDrops(placement)) {
20362 const dragManager = getDragManager();
20363 if (dragManager) {
20364 const deregister = dragManager.registerDropTarget({
20365 id: `desktop-mode-files-tile-${placement.id}-reject`,
20366 element: tile2,
20367 accept: () => false,
20368 onDrop: () => {
20369 }
20370 });
20371 tileRejectDeregisters.set(placement.id, deregister);
20372 }
20373 }
20374 return tile2;
20375 }
20376 const moved = displaced.get(placement.id);
20377 if (moved) {
20378 setTilePosition(tile2, moved.x, moved.y);
20379 }
20380 attachTileDrag(tile2, placement, folderId);
20381 attachContextMenu(tile2, placement);
20382 attachSelectOnClick(tile2, placement);
20383 if (placement.file.type === "folder") {
20384 const targetFolderId = parseInt(placement.file.ref, 10);
20385 if (targetFolderId > 0) {
20386 const dragManager = getDragManager();
20387 if (dragManager) {
20388 const deregister = registerFolderDropTarget(
20389 dragManager,
20390 tile2,
20391 targetFolderId
20392 );
20393 folderDropDeregisters.set(placement.id, deregister);
20394 }
20395 }
20396 } else if (shouldRejectTileDrops(placement)) {
20397 const dragManager = getDragManager();
20398 if (dragManager) {
20399 const deregister = dragManager.registerDropTarget({
20400 id: `desktop-mode-files-tile-${placement.id}-reject`,
20401 element: tile2,
20402 accept: () => false,
20403 onDrop: () => {
20404 }
20405 });
20406 tileRejectDeregisters.set(placement.id, deregister);
20407 }
20408 }
20409 return tile2;
20410 };
20411 const tryPatchIncremental = (list2) => {
20412 const existing = /* @__PURE__ */ new Map();
20413 for (const tile2 of container.querySelectorAll(
20414 "[data-placement-id]"
20415 )) {
20416 const raw = tile2.dataset.placementId ?? "";
20417 const id = parseInt(raw, 10);
20418 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
20419 return false;
20420 }
20421 existing.set(id, tile2);
20422 }
20423 const wantIds = /* @__PURE__ */ new Set();
20424 for (const placement of list2) {
20425 wantIds.add(placement.id);
20426 }
20427 for (const placement of list2) {
20428 const tile2 = existing.get(placement.id);
20429 if (!tile2) {
20430 continue;
20431 }
20432 if (tile2.dataset.fileType !== placement.file.type) {
20433 return false;
20434 }
20435 if (tile2.dataset.fileRef !== placement.file.ref) {
20436 return false;
20437 }
20438 const wasPinned = tile2.classList.contains(
20439 `${TILE_CLASS}--pinned`
20440 );
20441 if (wasPinned !== isPinned(placement)) {
20442 return false;
20443 }
20444 }
20445 for (const [id, tile2] of existing) {
20446 if (wantIds.has(id)) {
20447 continue;
20448 }
20449 const folderDereg = folderDropDeregisters.get(id);
20450 if (folderDereg) {
20451 try {
20452 folderDereg();
20453 } catch {
20454 }
20455 folderDropDeregisters.delete(id);
20456 }
20457 const rejectDereg = tileRejectDeregisters.get(id);
20458 if (rejectDereg) {
20459 try {
20460 rejectDereg();
20461 } catch {
20462 }
20463 tileRejectDeregisters.delete(id);
20464 }
20465 tile2.remove();
20466 }
20467 const { pinnedSlots, displaced } = computeLayout(list2);
20468 for (const placement of list2) {
20469 const tile2 = existing.get(placement.id);
20470 if (tile2) {
20471 applyTilePosition(tile2, placement, pinnedSlots, displaced);
20472 continue;
20473 }
20474 container.appendChild(
20475 wireTile(placement, pinnedSlots, displaced)
20476 );
20477 }
20478 if (selectedId !== null && !container.querySelector(
20479 `[data-placement-id="${selectedId}"]`
20480 )) {
20481 selectedId = null;
20482 notifySelection(null);
20483 }
20484 doAction("desktop-mode.files.grid-rendered", {
20485 folderId,
20486 count: list2.length
20487 });
20488 return true;
20489 };
20490 const repaint = (state2) => {
20491 const raw = state2.placementsByFolder.get(folderId) ?? [];
20492 const list2 = raw.slice().sort((a, b) => {
20493 const ap = isPinned(a) ? 0 : 1;
20494 const bp = isPinned(b) ? 0 : 1;
20495 return ap - bp;
20496 });
20497 const fp = fingerprint(list2);
20498 if (fp === lastFingerprint) {
20499 return;
20500 }
20501 lastFingerprint = fp;
20502 if (tryPatchPositions(list2, container, host)) {
20503 return;
20504 }
20505 if (tryPatchIncremental(list2)) {
20506 return;
20507 }
20508 container.replaceChildren();
20509 for (const [, deregister] of folderDropDeregisters) {
20510 try {
20511 deregister();
20512 } catch {
20513 }
20514 }
20515 folderDropDeregisters.clear();
20516 for (const [, deregister] of tileRejectDeregisters) {
20517 try {
20518 deregister();
20519 } catch {
20520 }
20521 }
20522 tileRejectDeregisters.clear();
20523 const { pinnedSlots, displaced } = computeLayout(list2);
20524 for (const placement of list2) {
20525 container.appendChild(
20526 wireTile(placement, pinnedSlots, displaced)
20527 );
20528 }
20529 if (selectedId !== null && !container.querySelector(`[data-placement-id="${selectedId}"]`)) {
20530 selectedId = null;
20531 notifySelection(null);
20532 } else if (selectedId !== null) {
20533 const tile2 = container.querySelector(
20534 `[data-placement-id="${selectedId}"]`
20535 );
20536 tile2?.setAttribute("selected", "");
20537 }
20538 doAction("desktop-mode.files.grid-rendered", {
20539 folderId,
20540 count: list2.length
20541 });
20542 };
20543 const dropTargetDeregisters = [];
20544 const folderDropDeregisters = /* @__PURE__ */ new Map();
20545 const tileRejectDeregisters = /* @__PURE__ */ new Map();
20546 let dropPreviewEl = null;
20547 let dropPreviewMoveHandler = null;
20548 const installCanvasDropPreview = (session) => {
20549 if (dropPreviewEl) {
20550 return;
20551 }
20552 if (session.payload.type !== "desktop-file") {
20553 return;
20554 }
20555 const previewEl = document.createElement("div");
20556 previewEl.className = "desktop-mode-files-drop-preview";
20557 previewEl.setAttribute("aria-hidden", "true");
20558 container.appendChild(previewEl);
20559 dropPreviewEl = previewEl;
20560 const ghost = session.payload.ghost;
20561 const offsetX = ghost?.offsetX ?? 0;
20562 const offsetY = ghost?.offsetY ?? 0;
20563 const data = session.payload.data;
20564 const movingId = data?.placement?.id;
20565 const updatePreview = (clientX, clientY) => {
20566 const rect = container.getBoundingClientRect();
20567 const rawX = Math.max(0, clientX - rect.left - offsetX);
20568 const rawY = Math.max(0, clientY - rect.top - offsetY);
20569 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
20570 const occupied = buildVisualOccupiedSet(peers, movingId);
20571 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
20572 previewEl.style.transform = `translate3d(${cell.x}px, ${cell.y}px, 0)`;
20573 };
20574 const sourceRect = session.payload.source.getBoundingClientRect();
20575 updatePreview(
20576 sourceRect.left + offsetX,
20577 sourceRect.top + offsetY
20578 );
20579 const moveHandler = (ev) => {
20580 updatePreview(ev.clientX, ev.clientY);
20581 };
20582 document.addEventListener("pointermove", moveHandler);
20583 dropPreviewMoveHandler = moveHandler;
20584 };
20585 const teardownCanvasDropPreview = () => {
20586 if (dropPreviewMoveHandler) {
20587 document.removeEventListener("pointermove", dropPreviewMoveHandler);
20588 dropPreviewMoveHandler = null;
20589 }
20590 if (dropPreviewEl) {
20591 dropPreviewEl.remove();
20592 dropPreviewEl = null;
20593 }
20594 };
20595 const canvasDropTarget = {
20596 id: `desktop-mode-files-canvas-${folderId}`,
20597 element: host,
20598 accept: (payload) => {
20599 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
20600 return false;
20601 }
20602 if (folderId > 0 && payload.type === "desktop-file") {
20603 const data = payload.data;
20604 if (data.placement.file?.type === "folder") {
20605 const movingFolderId = parseInt(data.placement.file.ref, 10);
20606 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, folderId)) {
20607 return false;
20608 }
20609 }
20610 }
20611 return true;
20612 },
20613 onEnter: (session) => {
20614 host.setAttribute("data-files-drop-active", "");
20615 installCanvasDropPreview(session);
20616 },
20617 onLeave: () => {
20618 host.removeAttribute("data-files-drop-active");
20619 teardownCanvasDropPreview();
20620 },
20621 onDrop: (session, ev) => {
20622 host.removeAttribute("data-files-drop-active");
20623 teardownCanvasDropPreview();
20624 const rect = container.getBoundingClientRect();
20625 const ghost = session.payload.ghost;
20626 const offsetX = ghost?.offsetX ?? 0;
20627 const offsetY = ghost?.offsetY ?? 0;
20628 const rawX = Math.max(0, ev.clientX - rect.left - offsetX);
20629 const rawY = Math.max(0, ev.clientY - rect.top - offsetY);
20630 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
20631 if (session.payload.type === "desktop-file") {
20632 const data = session.payload.data;
20633 const occupied = buildVisualOccupiedSet(peers, data.placement.id);
20634 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
20635 const next = {
20636 ...data.placement,
20637 x: cell.x,
20638 y: cell.y,
20639 parentId: folderId
20640 };
20641 store.upsertPlacement(next);
20642 doAction("desktop-mode.files.tile-manually-placed", {
20643 folderId,
20644 placementId: data.placement.id
20645 });
20646 if (isSyntheticPlacement(data.placement)) {
20647 const dockItemId = readSynthSource(data.placement);
20648 if (dockItemId) {
20649 persistDockPromotedPosition(
20650 dockItemId,
20651 cell.x,
20652 cell.y
20653 );
20654 }
20655 return;
20656 }
20657 void updatePlacement(
20658 data.placement.id,
20659 {
20660 x: cell.x,
20661 y: cell.y,
20662 parentId: folderId
20663 },
20664 data.placement.updatedAtMs
20665 ).then((server) => {
20666 store.upsertPlacement(server, "remote");
20667 }).catch((err) => {
20668 if (isConflict(err)) {
20669 showConflictToast(err);
20670 } else {
20671 console.error(
20672 "[desktop-mode] files: drag persist failed",
20673 err
20674 );
20675 }
20676 store.upsertPlacement(data.placement);
20677 });
20678 return;
20679 }
20680 if (session.payload.type === "shortcut") {
20681 const data = session.payload.data;
20682 const occupied = buildVisualOccupiedSet(peers);
20683 const cell = nextRowMajorCell(occupied, host);
20684 void createPlacement({
20685 parentId: folderId,
20686 type: data.kind,
20687 ref: data.ref,
20688 x: cell.x,
20689 y: cell.y
20690 }).then((placement) => {
20691 store.upsertPlacement(placement);
20692 doAction("desktop-mode.files.shortcut-dropped", {
20693 folderId,
20694 placement
20695 });
20696 }).catch((err) => {
20697 console.error(
20698 "[desktop-mode] shortcut drop failed:",
20699 err
20700 );
20701 });
20702 }
20703 }
20704 };
20705 const dragManagerForLayer = getDragManager();
20706 if (dragManagerForLayer) {
20707 dropTargetDeregisters.push(
20708 dragManagerForLayer.registerDropTarget(canvasDropTarget)
20709 );
20710 }
20711 const onCanvasClick = (e) => {
20712 if (e.target instanceof Element && e.target.closest(`.${TILE_CLASS}`)) {
20713 return;
20714 }
20715 setSelected(null);
20716 };
20717 host.addEventListener("click", onCanvasClick);
20718 function attachSelectOnClick(tile2, placement) {
20719 tile2.addEventListener("click", (e) => {
20720 e.stopPropagation();
20721 setSelected(placement);
20722 });
20723 }
20724 repaint(store.getState());
20725 const off = store.subscribe(repaint);
20726 let resolveHydrated = () => void 0;
20727 const hydrated = new Promise((resolve2) => {
20728 resolveHydrated = resolve2;
20729 });
20730 if (!store.getState().hydratedFolders.has(folderId)) {
20731 void listPlacements(folderId).then((res) => {
20732 store.setFolderPlacements(folderId, res.placements);
20733 }).catch((err) => {
20734 console.error("[desktop-mode] files: failed to hydrate folder", folderId, err);
20735 }).finally(() => {
20736 resolveHydrated();
20737 });
20738 } else {
20739 queueMicrotask(resolveHydrated);
20740 }
20741 const colsForWidth = () => {
20742 const w = host.clientWidth > 0 ? host.clientWidth : 4 * GRID_CELL_W;
20743 return Math.max(1, Math.floor((w - GRID_PADDING) / GRID_CELL_W));
20744 };
20745 const sortPlacements = (list2, mode) => {
20746 const sorted = list2.slice();
20747 switch (mode) {
20748 case "name-asc":
20749 sorted.sort(
20750 (a, b) => a.file.title.localeCompare(b.file.title)
20751 );
20752 break;
20753 case "name-desc":
20754 sorted.sort(
20755 (a, b) => b.file.title.localeCompare(a.file.title)
20756 );
20757 break;
20758 case "date-asc":
20759 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
20760 break;
20761 case "date-desc":
20762 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
20763 break;
20764 }
20765 return sorted;
20766 };
20767 const sort = (mode) => {
20768 const live = store.getState().placementsByFolder.get(folderId);
20769 if (!live || live.length === 0) {
20770 return;
20771 }
20772 const pinned = live.filter((p) => isPinned(p));
20773 const draggable = live.filter((p) => !isPinned(p));
20774 const sorted = sortPlacements(draggable, mode);
20775 const cols = colsForWidth();
20776 const occupied = /* @__PURE__ */ new Set();
20777 for (let i = 0; i < pinned.length; i += 1) {
20778 occupied.add(cellKey(0, i));
20779 }
20780 let idx = 0;
20781 const nextCell = () => {
20782 while (true) {
20783 const row = Math.floor(idx / cols);
20784 const col = idx % cols;
20785 idx += 1;
20786 if (!occupied.has(cellKey(col, row))) {
20787 return { col, row };
20788 }
20789 }
20790 };
20791 sorted.forEach((p, i) => {
20792 const cell = nextCell();
20793 const x = GRID_PADDING + cell.col * GRID_CELL_W;
20794 const y = GRID_PADDING + cell.row * GRID_CELL_H;
20795 const next = {
20796 ...p,
20797 x,
20798 y,
20799 sortOrder: i
20800 };
20801 store.upsertPlacement(next);
20802 if (isSyntheticPlacement(p)) {
20803 return;
20804 }
20805 void updatePlacement(p.id, { x, y, sortOrder: i }).catch((err) => {
20806 console.error(
20807 "[desktop-mode] files: sort persist failed",
20808 err
20809 );
20810 });
20811 });
20812 };
20813 const reflow = () => {
20814 const live = store.getState().placementsByFolder.get(folderId);
20815 if (!live || live.length === 0) {
20816 return;
20817 }
20818 const w = host.clientWidth > 0 ? host.clientWidth : Infinity;
20819 const overflowing = live.some((p) => {
20820 const right = p.x + GRID_CELL_W;
20821 return right > w;
20822 });
20823 if (!overflowing) {
20824 return;
20825 }
20826 const cols = colsForWidth();
20827 const pinned = live.filter((p) => isPinned(p));
20828 const draggable = live.filter((p) => !isPinned(p));
20829 const occupied = /* @__PURE__ */ new Set();
20830 for (let i = 0; i < pinned.length; i += 1) {
20831 occupied.add(cellKey(0, i));
20832 }
20833 let idx = 0;
20834 const nextCell = () => {
20835 while (true) {
20836 const row = Math.floor(idx / cols);
20837 const col = idx % cols;
20838 idx += 1;
20839 if (!occupied.has(cellKey(col, row))) {
20840 return { col, row };
20841 }
20842 }
20843 };
20844 for (const p of draggable) {
20845 const cell = nextCell();
20846 const x = GRID_PADDING + cell.col * GRID_CELL_W;
20847 const y = GRID_PADDING + cell.row * GRID_CELL_H;
20848 const tile2 = container.querySelector(
20849 `[data-placement-id="${p.id}"]`
20850 );
20851 if (tile2) {
20852 setTilePosition(tile2, x, y);
20853 }
20854 }
20855 };
20856 let lastWidth = host.clientWidth;
20857 let resizeObserver = null;
20858 if (typeof ResizeObserver !== "undefined") {
20859 resizeObserver = new ResizeObserver(() => {
20860 const w = host.clientWidth;
20861 if (w === lastWidth) {
20862 return;
20863 }
20864 lastWidth = w;
20865 reflow();
20866 });
20867 resizeObserver.observe(host);
20868 }
20869 return {
20870 host,
20871 folderId,
20872 onSelectionChange(cb) {
20873 selectionListeners.add(cb);
20874 return () => {
20875 selectionListeners.delete(cb);
20876 };
20877 },
20878 sort,
20879 reflow,
20880 hydrated,
20881 dispose() {
20882 off();
20883 resizeObserver?.disconnect();
20884 resizeObserver = null;
20885 for (const deregister of dropTargetDeregisters) {
20886 try {
20887 deregister();
20888 } catch {
20889 }
20890 }
20891 dropTargetDeregisters.length = 0;
20892 for (const deregister of folderDropDeregisters.values()) {
20893 try {
20894 deregister();
20895 } catch {
20896 }
20897 }
20898 folderDropDeregisters.clear();
20899 for (const deregister of tileRejectDeregisters.values()) {
20900 try {
20901 deregister();
20902 } catch {
20903 }
20904 }
20905 tileRejectDeregisters.clear();
20906 host.removeEventListener("click", onCanvasClick);
20907 selectionListeners.clear();
20908 container.remove();
20909 }
20910 };
20911 }
20912 function fingerprint(list2) {
20913 if (list2.length === 0) {
20914 return "0";
20915 }
20916 const parts = [];
20917 for (const p of list2) {
20918 parts.push(
20919 `${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}`
20920 );
20921 }
20922 return parts.join("|");
20923 }
20924 function isPinned(placement) {
20925 return Boolean(placement.file.pinned);
20926 }
20927 function readSynthSource(placement) {
20928 const meta = placement.meta;
20929 if (!meta || typeof meta !== "object") {
20930 return null;
20931 }
20932 const v = meta.__synthFromDockItem;
20933 return typeof v === "string" && v !== "" ? v : null;
20934 }
20935 function isSyntheticPlacement(placement) {
20936 return placement.id <= 0 || readSynthSource(placement) !== null;
20937 }
20938 const RECYCLE_BIN_REF = "desktop-mode-recycle-bin";
20939 function shouldRejectTileDrops(placement) {
20940 if (placement.file?.type === "folder") {
20941 return false;
20942 }
20943 if (placement.file?.ref === RECYCLE_BIN_REF) {
20944 return false;
20945 }
20946 return true;
20947 }
20948 function buildVisualOccupiedSet(placements, excludeId) {
20949 const sorted = placements.slice().sort((a, b) => {
20950 const ap = isPinned(a) ? 0 : 1;
20951 const bp = isPinned(b) ? 0 : 1;
20952 return ap - bp;
20953 });
20954 const set = /* @__PURE__ */ new Set();
20955 let pinnedIdx = 0;
20956 for (const p of sorted) {
20957 if (excludeId !== void 0 && p.id === excludeId) {
20958 continue;
20959 }
20960 if (isPinned(p)) {
20961 set.add(cellKey(0, pinnedIdx));
20962 pinnedIdx += 1;
20963 } else {
20964 const cell = pointToCell(p.x, p.y);
20965 set.add(cellKey(cell.col, cell.row));
20966 }
20967 }
20968 return set;
20969 }
20970 function wouldCreateFolderCycle(movingFolderId, targetParentId) {
20971 if (targetParentId <= 0 || movingFolderId <= 0) {
20972 return false;
20973 }
20974 if (movingFolderId === targetParentId) {
20975 return true;
20976 }
20977 const parentByFolderId = /* @__PURE__ */ new Map();
20978 const state2 = store.getState();
20979 for (const bucket2 of state2.placementsByFolder.values()) {
20980 for (const p of bucket2) {
20981 if (p.file?.type !== "folder") {
20982 continue;
20983 }
20984 const fid = parseInt(p.file.ref, 10);
20985 if (Number.isNaN(fid) || fid <= 0) {
20986 continue;
20987 }
20988 if (!parentByFolderId.has(fid)) {
20989 parentByFolderId.set(fid, p.parentId);
20990 }
20991 }
20992 }
20993 const visited = /* @__PURE__ */ new Set();
20994 let cursor = targetParentId;
20995 let maxDepth = 256;
20996 while (cursor > 0 && maxDepth-- > 0) {
20997 if (cursor === movingFolderId) {
20998 return true;
20999 }
21000 if (visited.has(cursor)) {
21001 return true;
21002 }
21003 visited.add(cursor);
21004 const next = parentByFolderId.get(cursor);
21005 if (next === void 0) {
21006 return false;
21007 }
21008 cursor = next;
21009 }
21010 return false;
21011 }
21012 function persistDockPromotedPosition(dockItemId, x, y) {
21013 const api = window.wp?.desktop;
21014 if (!api?.getOsSettings || !api?.updateOsSettings) {
21015 return;
21016 }
21017 const current = api.getOsSettings().dockPromotedPositions ?? {};
21018 api.updateOsSettings({
21019 dockPromotedPositions: {
21020 ...current,
21021 [dockItemId]: { x, y }
21022 }
21023 });
21024 }
21025 function tryPatchPositions(list2, container, host) {
21026 const tiles = Array.from(
21027 container.querySelectorAll("[data-placement-id]")
21028 );
21029 if (tiles.length !== list2.length) {
21030 return false;
21031 }
21032 const byId = /* @__PURE__ */ new Map();
21033 for (const tile2 of tiles) {
21034 const raw = tile2.dataset.placementId ?? "";
21035 const id = parseInt(raw, 10);
21036 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
21037 return false;
21038 }
21039 byId.set(id, tile2);
21040 }
21041 for (const placement of list2) {
21042 const tile2 = byId.get(placement.id);
21043 if (!tile2) {
21044 return false;
21045 }
21046 if (tile2.dataset.fileType !== placement.file.type) {
21047 return false;
21048 }
21049 if (tile2.dataset.fileRef !== placement.file.ref) {
21050 return false;
21051 }
21052 const wasPinned = tile2.classList.contains(`${TILE_CLASS}--pinned`);
21053 if (wasPinned !== isPinned(placement)) {
21054 return false;
21055 }
21056 }
21057 const pinnedSlots = /* @__PURE__ */ new Map();
21058 const occupiedCells = /* @__PURE__ */ new Set();
21059 let pinnedIdx = 0;
21060 for (const placement of list2) {
21061 if (!isPinned(placement)) {
21062 continue;
21063 }
21064 const slot = cellToPos(0, pinnedIdx);
21065 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
21066 occupiedCells.add(cellKey(slot.col, slot.row));
21067 pinnedIdx += 1;
21068 }
21069 const displaced = /* @__PURE__ */ new Map();
21070 for (const placement of list2) {
21071 if (pinnedSlots.has(placement.id)) {
21072 continue;
21073 }
21074 const target2 = pointToCell(placement.x, placement.y);
21075 const key = cellKey(target2.col, target2.row);
21076 if (!occupiedCells.has(key)) {
21077 occupiedCells.add(key);
21078 continue;
21079 }
21080 const free = snapToEmptyCell(
21081 placement.x,
21082 placement.y,
21083 occupiedCells,
21084 host
21085 );
21086 occupiedCells.add(cellKey(free.col, free.row));
21087 displaced.set(placement.id, { x: free.x, y: free.y });
21088 }
21089 for (const placement of list2) {
21090 const tile2 = byId.get(placement.id);
21091 if (!tile2) {
21092 continue;
21093 }
21094 const pinned = pinnedSlots.get(placement.id);
21095 const disp = displaced.get(placement.id);
21096 if (pinned) {
21097 setTilePosition(tile2, pinned.x, pinned.y);
21098 } else if (disp) {
21099 setTilePosition(tile2, disp.x, disp.y);
21100 } else {
21101 setTilePosition(tile2, placement.x, placement.y);
21102 }
21103 }
21104 return true;
21105 }
21106 function hidePromotedDockItem(dockItemId) {
21107 const api = window.wp?.desktop;
21108 if (!api?.getOsSettings || !api?.updateOsSettings) {
21109 return;
21110 }
21111 const current = api.getOsSettings().itemVisibility ?? {};
21112 const next = { ...current, [dockItemId]: "dock" };
21113 api.updateOsSettings({ itemVisibility: next });
21114 }
21115 function registerFolderDropTarget(dragManager, tile2, targetFolderId, currentFolderId) {
21116 const target2 = {
21117 id: `desktop-mode-files-folder-${targetFolderId}-tile-${tile2.dataset.placementId ?? "?"}`,
21118 element: tile2,
21119 accept: (payload) => {
21120 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
21121 return false;
21122 }
21123 if (payload.type === "desktop-file") {
21124 const data = payload.data;
21125 if (data.placement.file.type === "folder" && parseInt(data.placement.file.ref, 10) === targetFolderId) {
21126 return false;
21127 }
21128 if (data.placement.parentId === targetFolderId) {
21129 return false;
21130 }
21131 if (isSyntheticPlacement(data.placement)) {
21132 return false;
21133 }
21134 if (data.placement.file.type === "folder") {
21135 const movingFolderId = parseInt(data.placement.file.ref, 10);
21136 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, targetFolderId)) {
21137 return false;
21138 }
21139 }
21140 }
21141 return true;
21142 },
21143 onEnter: () => {
21144 tile2.classList.add(`${TILE_CLASS}--drop-target`);
21145 },
21146 onLeave: () => {
21147 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
21148 },
21149 onDrop: (session) => {
21150 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
21151 if (session.payload.type === "desktop-file") {
21152 const data = session.payload.data;
21153 const next = {
21154 ...data.placement,
21155 parentId: targetFolderId
21156 };
21157 store.upsertPlacement(next);
21158 void updatePlacement(
21159 data.placement.id,
21160 { parentId: targetFolderId },
21161 data.placement.updatedAtMs
21162 ).then((server) => {
21163 store.upsertPlacement(server, "remote");
21164 }).catch((err) => {
21165 if (isConflict(err)) {
21166 showConflictToast(err);
21167 } else {
21168 console.error(
21169 "[desktop-mode] files: move-into-folder persist failed",
21170 err
21171 );
21172 }
21173 store.upsertPlacement(data.placement);
21174 });
21175 return;
21176 }
21177 if (session.payload.type === "shortcut") {
21178 const data = session.payload.data;
21179 const peers = store.getState().placementsByFolder.get(targetFolderId) ?? [];
21180 const cell = nextRowMajorCell(buildVisualOccupiedSet(peers));
21181 void createPlacement({
21182 parentId: targetFolderId,
21183 type: data.kind,
21184 ref: data.ref,
21185 x: cell.x,
21186 y: cell.y
21187 }).then((placement) => {
21188 store.upsertPlacement(placement);
21189 doAction("desktop-mode.files.shortcut-dropped", {
21190 folderId: targetFolderId,
21191 placement
21192 });
21193 }).catch((err) => {
21194 console.error(
21195 "[desktop-mode] shortcut drop into folder failed:",
21196 err
21197 );
21198 });
21199 }
21200 }
21201 };
21202 return dragManager.registerDropTarget(target2);
21203 }
21204 function attachTileDrag(tile2, placement, folderId) {
21205 tile2.addEventListener("pointerdown", (e) => {
21206 if (e.button !== 0) {
21207 return;
21208 }
21209 const dragManager = getDragManager();
21210 if (!dragManager) {
21211 return;
21212 }
21213 const liveBucket = store.getState().placementsByFolder.get(folderId);
21214 const livePlacement = liveBucket?.find((p) => p.id === placement.id) ?? placement;
21215 parseFloat(tile2.style.left) || livePlacement.x;
21216 parseFloat(tile2.style.top) || livePlacement.y;
21217 dragManager.start({
21218 payload: {
21219 type: "desktop-file",
21220 source: tile2,
21221 data: {
21222 placement: livePlacement,
21223 sourceFolderId: folderId,
21224 // Synthesize a cross-frame bridge payload from the
21225 // placement's file shape so a wallpaper-placed
21226 // shortcut can be dropped into an open Gutenberg
21227 // iframe and inserted as the matching block. The
21228 // PHP serialize() methods (`Desktop_Mode_Post_File`,
21229 // `Desktop_Mode_User_File`, `Desktop_Mode_Attachment_File`)
21230 // surface the URL fields this needs.
21231 bridgePayload: buildBridgePayloadFromPlacement(livePlacement)
21232 },
21233 ghost: {
21234 offsetX: e.clientX - tile2.getBoundingClientRect().left,
21235 offsetY: e.clientY - tile2.getBoundingClientRect().top
21236 }
21237 },
21238 origin: e
21239 // `onClickOnly` intentionally empty — a tile click is
21240 // handled by the dedicated `attachSelectOnClick` listener
21241 // below, which fires from the regular `click` event after
21242 // a sub-threshold pointerup. The manager won't fire a
21243 // `click` itself; the browser does.
21244 });
21245 });
21246 }
21247 function attachContextMenu(tile2, placement) {
21248 tile2.addEventListener("contextmenu", (e) => {
21249 e.preventDefault();
21250 e.stopPropagation();
21251 const items = [
21252 {
21253 id: "open",
21254 label: "Open",
21255 icon: "dashicons-external",
21256 sort: 10,
21257 onClick: () => {
21258 const file = resolve(placement.file);
21259 void openFile(file);
21260 }
21261 }
21262 ];
21263 if (placement.file.type === "post") {
21264 items.push({
21265 id: "navigate-into",
21266 label: "Navigate into",
21267 icon: "dashicons-category",
21268 sort: 20,
21269 onClick: () => {
21270 const postId = parseInt(placement.file.ref, 10);
21271 if (!postId) {
21272 return;
21273 }
21274 const api = window.wp?.desktop?.myWordpress;
21275 const postType = typeof placement.file.postType === "string" ? placement.file.postType : "post";
21276 const entityId = postType === "page" ? "pages" : "posts";
21277 api?.openDetail({
21278 entityId,
21279 postId,
21280 postTitle: placement.file.title || `#${postId}`
21281 });
21282 }
21283 });
21284 }
21285 const isFolder = placement.file.type === "folder";
21286 if (isFolder) {
21287 items.push({
21288 id: "rename-folder",
21289 label: "Rename…",
21290 icon: "dashicons-edit",
21291 sort: 30,
21292 onClick: () => {
21293 const folderId = parseInt(placement.file.ref, 10);
21294 if (!folderId) {
21295 return;
21296 }
21297 openCreateFolderDialog({
21298 title: "Rename folder",
21299 label: "New name",
21300 submitLabel: "Rename",
21301 initialName: placement.file.title,
21302 onSubmit: async (name) => {
21303 const trimmed = name.trim();
21304 if (!trimmed || trimmed === placement.file.title) {
21305 return;
21306 }
21307 const previousTitle = placement.file.title;
21308 const optimistic = {
21309 ...placement,
21310 file: { ...placement.file, title: trimmed }
21311 };
21312 store.upsertPlacement(optimistic);
21313 try {
21314 const folderUpdatedAtMs = store.getState().folders.get(folderId)?.updatedAtMs ?? 0;
21315 const updated = await updateFolder(
21316 folderId,
21317 { name: trimmed },
21318 folderUpdatedAtMs
21319 );
21320 store.upsertFolder(updated);
21321 const refreshed = await listPlacements(
21322 placement.parentId
21323 );
21324 store.setFolderPlacements(
21325 placement.parentId,
21326 refreshed.placements
21327 );
21328 } catch (err) {
21329 console.error(
21330 "[desktop-mode] rename folder failed:",
21331 err
21332 );
21333 store.upsertPlacement({
21334 ...placement,
21335 file: {
21336 ...placement.file,
21337 title: previousTitle
21338 }
21339 });
21340 }
21341 }
21342 });
21343 }
21344 });
21345 if (placement.canTrash !== false) {
21346 items.push({
21347 id: "delete-folder",
21348 label: "Move folder to Trash",
21349 icon: "dashicons-trash",
21350 sort: 90,
21351 danger: true,
21352 onClick: () => trashFolderWithUndo(placement)
21353 });
21354 }
21355 } else {
21356 const synthFromDockItem = readSynthSource(placement);
21357 const isRegisteredIcon = placement.file.type === "shortcut";
21358 if (synthFromDockItem || isRegisteredIcon) {
21359 const hideId = synthFromDockItem ?? placement.file.ref;
21360 items.push({
21361 id: "hide-from-desktop",
21362 label: "Hide from desktop",
21363 icon: "dashicons-hidden",
21364 sort: 90,
21365 onClick: () => hidePromotedDockItem(hideId)
21366 });
21367 } else if (placement.canTrash !== false) {
21368 items.push({
21369 id: "remove",
21370 label: "Move to Trash",
21371 icon: "dashicons-trash",
21372 sort: 90,
21373 danger: true,
21374 onClick: () => trashPlacementWithUndo(placement)
21375 });
21376 }
21377 }
21378 openTileMenu({ x: e.clientX, y: e.clientY }, { placement, items });
21379 });
21380 }
21381 const STATUS_BAR_CLASS = "desktop-mode-folder-status-bar";
21382 const ROOT_CLASS$2 = STATUS_BAR_CLASS;
21383 function mountFolderStatusBar(host, folderId) {
21384 const bar = document.createElement("div");
21385 bar.className = ROOT_CLASS$2;
21386 bar.setAttribute("role", "status");
21387 bar.dataset.folderId = String(folderId);
21388 host.appendChild(bar);
21389 const repaint = () => {
21390 const list2 = getFilesState().placementsByFolder.get(folderId) ?? [];
21391 const folders = list2.filter((p) => p.file.type === "folder").length;
21392 const files = list2.length - folders;
21393 const ctx = {
21394 folderId,
21395 totals: { files, folders, total: list2.length }
21396 };
21397 const segments = computeSegments(ctx);
21398 render(bar, segments);
21399 };
21400 repaint();
21401 const off = subscribeFilesStore(() => repaint());
21402 return {
21403 dispose() {
21404 off();
21405 bar.remove();
21406 }
21407 };
21408 }
21409 function computeSegments(ctx) {
21410 const { folders, files } = ctx.totals;
21411 const builtIns = [
21412 {
21413 id: "count",
21414 label: pluralize(files, "file", "files") + (folders > 0 ? `, ${pluralize(folders, "folder", "folders")}` : ""),
21415 align: "start",
21416 sort: 10
21417 }
21418 ];
21419 const filtered = applyFilters(
21420 "desktop-mode.files.folder-window.status-bar",
21421 builtIns,
21422 ctx
21423 );
21424 return Array.isArray(filtered) ? filtered : builtIns;
21425 }
21426 function render(bar, segments) {
21427 const sort = (a, b) => {
21428 const sa = typeof a.sort === "number" ? a.sort : 100;
21429 const sb = typeof b.sort === "number" ? b.sort : 100;
21430 if (sa !== sb) {
21431 return sa - sb;
21432 }
21433 return a.label.localeCompare(b.label);
21434 };
21435 const start = segments.filter((s) => (s.align ?? "start") === "start").sort(sort);
21436 const end = segments.filter((s) => s.align === "end").sort(sort);
21437 bar.replaceChildren();
21438 bar.appendChild(buildCluster("start", start));
21439 bar.appendChild(buildCluster("end", end));
21440 }
21441 function buildCluster(align, segs) {
21442 const cluster = document.createElement("div");
21443 cluster.className = `${ROOT_CLASS$2}__cluster ${ROOT_CLASS$2}__cluster--${align}`;
21444 for (const seg of segs) {
21445 cluster.appendChild(buildSegment(seg));
21446 }
21447 return cluster;
21448 }
21449 function buildSegment(seg) {
21450 const interactive = typeof seg.onClick === "function";
21451 const el = document.createElement(interactive ? "button" : "span");
21452 el.className = `${ROOT_CLASS$2}__segment`;
21453 el.dataset.segmentId = seg.id;
21454 if (interactive) {
21455 el.type = "button";
21456 el.addEventListener("click", (e) => seg.onClick(e));
21457 }
21458 if (seg.icon) {
21459 const icon = document.createElement("span");
21460 icon.className = `${ROOT_CLASS$2}__icon dashicons ${seg.icon.replace(/[^a-zA-Z0-9_-]/g, "")}`;
21461 icon.setAttribute("aria-hidden", "true");
21462 el.appendChild(icon);
21463 }
21464 const label = document.createElement("span");
21465 label.className = `${ROOT_CLASS$2}__label`;
21466 label.textContent = seg.label;
21467 el.appendChild(label);
21468 return el;
21469 }
21470 function pluralize(n, singular, plural) {
21471 return `${n} ${n === 1 ? singular : plural}`;
21472 }
21473 const MENU_CLASS$1 = "desktop-mode-icon-canvas-menu";
21474 let activeMenu$1 = null;
21475 let activeFlyout = null;
21476 let activeCanvas = null;
21477 let outsideHandler = null;
21478 let escHandler = null;
21479 function attachIconCanvasMenu(canvas, deps2) {
21480 deps2.openOnBackgroundClick !== false;
21481 const onContextMenu = (e) => {
21482 if (isInsideTile(e.target) || isInsideMenu(e.target)) {
21483 return;
21484 }
21485 e.preventDefault();
21486 toggle(e.clientX, e.clientY);
21487 };
21488 let toggleGen = 0;
21489 const toggle = (x, y) => {
21490 if (activeCanvas === canvas && activeMenu$1) {
21491 closeMenu();
21492 return;
21493 }
21494 const items = buildItems(deps2);
21495 const filtered = applyFilters(
21496 "desktop-mode.icon-canvas.menu",
21497 items,
21498 deps2.scope
21499 );
21500 const finalItems = Array.isArray(filtered) ? filtered : items;
21501 const myGen = ++toggleGen;
21502 openWithShellOverlays(
21503 () => myGen === toggleGen,
21504 () => openMenu(finalItems, { x, y }, canvas)
21505 );
21506 };
21507 canvas.addEventListener("contextmenu", onContextMenu);
21508 return {
21509 dispose: () => {
21510 canvas.removeEventListener("contextmenu", onContextMenu);
21511 closeMenu();
21512 }
21513 };
21514 }
21515 function isInsideTile(target2) {
21516 if (!(target2 instanceof Element)) {
21517 return false;
21518 }
21519 return target2.closest(".desktop-mode-file-tile") !== null;
21520 }
21521 function isInsideMenu(target2) {
21522 if (!(target2 instanceof Element)) {
21523 return false;
21524 }
21525 return target2.closest(`.${MENU_CLASS$1}`) !== null;
21526 }
21527 function buildItems(deps2) {
21528 const sortItem = {
21529 id: "sort-by",
21530 label: __("Sort by", "desktop-mode"),
21531 icon: "dashicons-sort",
21532 sort: 10,
21533 children: [
21534 {
21535 id: "sort-name-asc",
21536 label: __("Name (A → Z)", "desktop-mode"),
21537 sort: 10,
21538 onClick: () => deps2.onSort("name-asc")
21539 },
21540 {
21541 id: "sort-name-desc",
21542 label: __("Name (Z → A)", "desktop-mode"),
21543 sort: 20,
21544 onClick: () => deps2.onSort("name-desc")
21545 },
21546 {
21547 id: "sort-date-desc",
21548 label: __("Newest first", "desktop-mode"),
21549 sort: 30,
21550 onClick: () => deps2.onSort("date-desc")
21551 },
21552 {
21553 id: "sort-date-asc",
21554 label: __("Oldest first", "desktop-mode"),
21555 sort: 40,
21556 onClick: () => deps2.onSort("date-asc")
21557 }
21558 ]
21559 };
21560 const items = [sortItem];
21561 if (Array.isArray(deps2.extraItems)) {
21562 items.push(...deps2.extraItems);
21563 }
21564 return items;
21565 }
21566 function sortItems(items) {
21567 return items.slice().sort((a, b) => {
21568 const sa = typeof a.sort === "number" ? a.sort : 100;
21569 const sb = typeof b.sort === "number" ? b.sort : 100;
21570 if (sa !== sb) {
21571 return sa - sb;
21572 }
21573 return a.label.localeCompare(b.label);
21574 });
21575 }
21576 function openMenu(items, pos, canvas) {
21577 closeMenu();
21578 if (items.length === 0) {
21579 return;
21580 }
21581 activeCanvas = canvas;
21582 const sorted = sortItems(items);
21583 const menu = document.createElement("wpd-context-menu");
21584 menu.setAttribute("open", "");
21585 menu.classList.add(MENU_CLASS$1);
21586 menu.style.left = `${pos.x}px`;
21587 menu.style.top = `${pos.y}px`;
21588 const itemById = /* @__PURE__ */ new Map();
21589 for (const item of sorted) {
21590 itemById.set(item.id, item);
21591 const opt = appendOption(menu, item);
21592 if (hasChildren(item)) {
21593 opt.addEventListener("mouseenter", () => {
21594 openFlyout(item, opt);
21595 });
21596 }
21597 }
21598 menu.addEventListener("wpd-context-menu-pick", (e) => {
21599 const detail = e.detail;
21600 const item = itemById.get(detail.id);
21601 if (!item) {
21602 return;
21603 }
21604 if (hasChildren(item)) {
21605 e.stopPropagation();
21606 const anchor = menu.querySelector(
21607 `[data-menu-item-id="${item.id}"]`
21608 );
21609 if (anchor) {
21610 openFlyout(item, anchor);
21611 }
21612 return;
21613 }
21614 closeMenu();
21615 item.onClick?.();
21616 });
21617 document.body.appendChild(menu);
21618 activeMenu$1 = menu;
21619 clampToViewport(menu);
21620 queueMicrotask(() => {
21621 outsideHandler = (e) => {
21622 if (isInsideMenu(e.target)) {
21623 return;
21624 }
21625 closeMenu();
21626 };
21627 escHandler = (e) => {
21628 if (e.key === "Escape") {
21629 closeMenu();
21630 }
21631 };
21632 document.addEventListener("mousedown", outsideHandler);
21633 document.addEventListener("keydown", escHandler);
21634 });
21635 }
21636 function appendOption(host, item) {
21637 const opt = document.createElement("wpd-context-menu-option");
21638 opt.dataset.menuItemId = item.id;
21639 opt.setAttribute("value", item.id);
21640 if (item.heading) {
21641 opt.setAttribute("heading", "");
21642 }
21643 if (item.disabled) {
21644 opt.setAttribute("disabled", "");
21645 }
21646 if (item.icon) {
21647 opt.setAttribute("icon", sanitizeClass$1(item.icon));
21648 }
21649 if (hasChildren(item)) {
21650 opt.setAttribute("has-children", "");
21651 }
21652 opt.textContent = item.label;
21653 host.appendChild(opt);
21654 return opt;
21655 }
21656 function openFlyout(parent, anchor) {
21657 closeFlyout();
21658 if (!hasChildren(parent)) {
21659 return;
21660 }
21661 const fly = document.createElement("wpd-context-menu");
21662 fly.setAttribute("open", "");
21663 fly.classList.add(MENU_CLASS$1, `${MENU_CLASS$1}--flyout`);
21664 const childById = /* @__PURE__ */ new Map();
21665 for (const child of sortItems(parent.children ?? [])) {
21666 childById.set(child.id, child);
21667 appendOption(fly, child);
21668 }
21669 fly.addEventListener("wpd-context-menu-pick", (e) => {
21670 const detail = e.detail;
21671 const child = childById.get(detail.id);
21672 if (!child) {
21673 return;
21674 }
21675 e.stopPropagation();
21676 closeMenu();
21677 child.onClick?.();
21678 });
21679 document.body.appendChild(fly);
21680 activeFlyout = fly;
21681 positionFlyout(fly, anchor);
21682 }
21683 function positionFlyout(fly, anchor) {
21684 const ar = anchor.getBoundingClientRect();
21685 fly.style.position = "fixed";
21686 fly.style.left = `${ar.right}px`;
21687 fly.style.top = `${ar.top}px`;
21688 const fr = fly.getBoundingClientRect();
21689 if (fr.right > window.innerWidth) {
21690 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
21691 }
21692 if (fr.bottom > window.innerHeight) {
21693 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
21694 }
21695 }
21696 function clampToViewport(menu) {
21697 const rect = menu.getBoundingClientRect();
21698 if (rect.right > window.innerWidth) {
21699 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
21700 }
21701 if (rect.bottom > window.innerHeight) {
21702 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
21703 }
21704 }
21705 function hasChildren(item) {
21706 return Array.isArray(item.children) && item.children.length > 0;
21707 }
21708 function closeFlyout() {
21709 if (activeFlyout) {
21710 activeFlyout.remove();
21711 activeFlyout = null;
21712 }
21713 }
21714 function closeMenu() {
21715 closeFlyout();
21716 if (activeMenu$1) {
21717 activeMenu$1.remove();
21718 activeMenu$1 = null;
21719 }
21720 activeCanvas = null;
21721 if (outsideHandler) {
21722 document.removeEventListener("mousedown", outsideHandler);
21723 outsideHandler = null;
21724 }
21725 if (escHandler) {
21726 document.removeEventListener("keydown", escHandler);
21727 escHandler = null;
21728 }
21729 }
21730 function sanitizeClass$1(raw) {
21731 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
21732 }
21733 const ROOT_CLASS$1 = "desktop-mode-breadcrumbs";
21734 function renderBreadcrumbs(host, segments, opts = {}) {
21735 host.replaceChildren();
21736 host.classList.add(ROOT_CLASS$1);
21737 if (opts.onBack) {
21738 const back = document.createElement("button");
21739 back.type = "button";
21740 back.className = `${ROOT_CLASS$1}__back`;
21741 back.setAttribute("aria-label", __("Back", "desktop-mode"));
21742 back.title = __("Back", "desktop-mode");
21743 const arrow = document.createElement("span");
21744 arrow.className = "dashicons dashicons-arrow-left-alt2";
21745 arrow.setAttribute("aria-hidden", "true");
21746 back.appendChild(arrow);
21747 if (opts.backDisabled) {
21748 back.disabled = true;
21749 }
21750 const onBack = opts.onBack;
21751 back.addEventListener("click", () => {
21752 if (back.disabled) {
21753 return;
21754 }
21755 onBack();
21756 });
21757 host.appendChild(back);
21758 }
21759 const nav = document.createElement("nav");
21760 nav.className = `${ROOT_CLASS$1}__crumbs`;
21761 nav.setAttribute("aria-label", __("Breadcrumb", "desktop-mode"));
21762 segments.forEach((seg, idx) => {
21763 if (idx > 0) {
21764 const sep = document.createElement("span");
21765 sep.className = `${ROOT_CLASS$1}__sep`;
21766 sep.setAttribute("aria-hidden", "true");
21767 sep.textContent = "›";
21768 nav.appendChild(sep);
21769 }
21770 if (!seg.onClick) {
21771 const here = document.createElement("span");
21772 here.className = `${ROOT_CLASS$1}__crumb ${ROOT_CLASS$1}__crumb--current`;
21773 here.setAttribute("aria-current", "page");
21774 here.textContent = seg.label;
21775 nav.appendChild(here);
21776 return;
21777 }
21778 const btn = document.createElement("button");
21779 btn.type = "button";
21780 btn.className = `${ROOT_CLASS$1}__crumb`;
21781 btn.textContent = seg.label;
21782 const onClick = seg.onClick;
21783 btn.addEventListener("click", () => {
21784 onClick();
21785 });
21786 nav.appendChild(btn);
21787 });
21788 host.appendChild(nav);
21789 }
21790 async function getJson(url, init2 = {}) {
21791 const response = await trackedFetch$1(url, {
21792 credentials: "same-origin",
21793 headers: {
21794 Accept: "application/json",
21795 "X-WP-Nonce": readRestNonce(),
21796 ...init2.headers ?? {}
21797 },
21798 ...init2
21799 });
21800 if (!response.ok) {
21801 throw new Error(`${response.status} ${response.statusText}`);
21802 }
21803 return await response.json();
21804 }
21805 function readRestNonce() {
21806 const cfg = window.wp?.desktop?.config;
21807 return cfg?.restNonce ?? "";
21808 }
21809 function readRestRoot() {
21810 const cfg = window.wp?.desktop?.config;
21811 if (cfg?.restUrl) {
21812 return cfg.restUrl.endsWith("/") ? cfg.restUrl : cfg.restUrl + "/";
21813 }
21814 return `${window.location.origin}/wp-json/`;
21815 }
21816 function restUrl(path) {
21817 return joinRestUrl(readRestRoot(), path);
21818 }
21819 function renderPlacementPreview(placement, host) {
21820 const filtered = applyFilters(
21821 "desktop-mode.files.preview",
21822 null,
21823 placement
21824 );
21825 if (filtered instanceof HTMLElement) {
21826 host.replaceChildren(filtered);
21827 return;
21828 }
21829 if (placement.accessGated) {
21830 host.replaceChildren(renderAccessGated(placement));
21831 return;
21832 }
21833 host.replaceChildren(renderLoading());
21834 void renderByType(placement).then((node) => {
21835 host.replaceChildren(node);
21836 }).catch((err) => {
21837 host.replaceChildren(renderError(err));
21838 });
21839 }
21840 function renderAccessGated(placement) {
21841 const wrap = document.createElement("div");
21842 wrap.className = "desktop-mode-files__access-gated";
21843 const ring = document.createElement("div");
21844 ring.className = "desktop-mode-files__access-gated-ring";
21845 const glyph = document.createElement("span");
21846 glyph.className = "dashicons dashicons-lock desktop-mode-files__access-gated-glyph";
21847 glyph.setAttribute("aria-hidden", "true");
21848 ring.appendChild(glyph);
21849 wrap.appendChild(ring);
21850 const title = document.createElement("h2");
21851 title.className = "desktop-mode-files__access-gated-title";
21852 title.textContent = "No permission to view";
21853 wrap.appendChild(title);
21854 const sub = document.createElement("p");
21855 sub.className = "desktop-mode-files__access-gated-sub";
21856 const target2 = placement.file.title || placement.file.type;
21857 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.`;
21858 wrap.appendChild(sub);
21859 const hint = document.createElement("p");
21860 hint.className = "desktop-mode-files__access-gated-hint";
21861 hint.textContent = "Ask the owner to grant access on the underlying item, or to remove it from the shared folder.";
21862 wrap.appendChild(hint);
21863 return wrap;
21864 }
21865 async function renderByType(placement) {
21866 const file = placement.file;
21867 switch (file.type) {
21868 case "post":
21869 return renderPostPreview(file.ref, file);
21870 case "folder":
21871 return renderFolderPreview(file);
21872 case "shortcut":
21873 return renderShortcutPreview(file);
21874 case "attachment":
21875 return renderAttachmentPreview(file.ref, file);
21876 case "user":
21877 return renderUserSummary(file.ref, file);
21878 case "term":
21879 return renderTermSummary(file);
21880 case "comment":
21881 return renderCommentSummary(file.ref, file);
21882 case "bookmark":
21883 return renderBookmarkPreview(file);
21884 default:
21885 return renderGenericPreview(file);
21886 }
21887 }
21888 async function renderPostPreview(ref, file) {
21889 const id = parseInt(ref, 10);
21890 if (!id) {
21891 return renderGenericPreview(file);
21892 }
21893 let data = null;
21894 for (const path of ["wp/v2/posts", "wp/v2/pages"]) {
21895 try {
21896 data = await getJson(
21897 restUrl(
21898 `${path}/${id}?_fields=id,title,content,date,link,status`
21899 )
21900 );
21901 break;
21902 } catch {
21903 }
21904 }
21905 if (!data) {
21906 return renderGenericPreview(file);
21907 }
21908 const wrap = articleShell();
21909 const h = document.createElement("h2");
21910 h.className = "desktop-mode-my-wordpress__article-title";
21911 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
21912 wrap.appendChild(h);
21913 const meta = document.createElement("p");
21914 meta.className = "desktop-mode-my-wordpress__article-meta";
21915 const parts = [];
21916 parts.push(formatDate(data.date));
21917 if (data.status && data.status !== "publish") {
21918 parts.push(data.status);
21919 }
21920 meta.textContent = parts.join(" · ");
21921 wrap.appendChild(meta);
21922 if (data.content?.rendered) {
21923 const body = document.createElement("div");
21924 body.className = "desktop-mode-my-wordpress__article-content";
21925 body.innerHTML = data.content.rendered;
21926 wrap.appendChild(body);
21927 }
21928 const footer = document.createElement("footer");
21929 footer.className = "desktop-mode-my-wordpress__article-footer";
21930 const myWordpressApi = window.wp?.desktop?.myWordpress;
21931 if (myWordpressApi) {
21932 const exploreBtn = document.createElement("wpd-button");
21933 exploreBtn.setAttribute("variant", "secondary");
21934 exploreBtn.textContent = __("Explore details", "desktop-mode");
21935 exploreBtn.title = __(
21936 "See author, comments, categories, tags, attached media, and revisions for this entry.",
21937 "desktop-mode"
21938 );
21939 exploreBtn.addEventListener("click", () => {
21940 const postType = typeof file.postType === "string" ? file.postType : "post";
21941 myWordpressApi.openDetail({
21942 entityId: postType === "page" ? "pages" : "posts",
21943 postId: id,
21944 postTitle: stripTags(data.title.rendered) || `#${id}`
21945 });
21946 });
21947 footer.appendChild(exploreBtn);
21948 }
21949 const editBtn = document.createElement("wpd-button");
21950 editBtn.setAttribute("variant", "primary");
21951 editBtn.textContent = __("Open in editor", "desktop-mode");
21952 editBtn.addEventListener("click", () => {
21953 const adminUrl = window.wp?.desktop?.config?.adminUrl;
21954 if (!adminUrl) {
21955 return;
21956 }
21957 const editUrl = `${adminUrl}post.php?post=${id}&action=edit`;
21958 const wm = window.wp?.desktop?.windowManager;
21959 const postType = typeof file.postType === "string" ? file.postType : "post";
21960 const entityId = postType === "page" ? "pages" : "posts";
21961 wm?.open({
21962 id: `${entityId}-edit-${id}`,
21963 url: editUrl,
21964 title: stripTags(data.title.rendered),
21965 icon: file.icon
21966 });
21967 });
21968 footer.appendChild(editBtn);
21969 wrap.appendChild(footer);
21970 return wrap;
21971 }
21972 async function renderUserSummary(ref, file) {
21973 const id = parseInt(ref, 10);
21974 if (!id) {
21975 return renderGenericPreview(file);
21976 }
21977 let data = null;
21978 try {
21979 data = await getJson(
21980 restUrl(`desktop-mode/v1/user-stats/${id}`)
21981 );
21982 } catch {
21983 return renderGenericPreview(file);
21984 }
21985 const wrap = articleShell("desktop-mode-my-wordpress__user");
21986 const header = document.createElement("header");
21987 header.className = "desktop-mode-my-wordpress__user-header";
21988 if (data.profile.avatarUrl) {
21989 const img = document.createElement("img");
21990 img.className = "desktop-mode-my-wordpress__user-avatar";
21991 img.src = data.profile.avatarUrl;
21992 img.alt = "";
21993 header.appendChild(img);
21994 }
21995 const head = document.createElement("div");
21996 head.className = "desktop-mode-my-wordpress__user-headline";
21997 const h = document.createElement("h2");
21998 h.className = "desktop-mode-my-wordpress__article-title";
21999 h.textContent = data.profile.name || file.title || `#${id}`;
22000 head.appendChild(h);
22001 if (data.profile.roleLabels && data.profile.roleLabels.length > 0) {
22002 const roles = document.createElement("div");
22003 roles.className = "desktop-mode-my-wordpress__user-roles";
22004 for (const r of data.profile.roleLabels) {
22005 const badge = document.createElement("span");
22006 badge.className = "desktop-mode-my-wordpress__user-role";
22007 badge.textContent = r;
22008 roles.appendChild(badge);
22009 }
22010 head.appendChild(roles);
22011 }
22012 header.appendChild(head);
22013 wrap.appendChild(header);
22014 if (data.profile.description) {
22015 const bio = document.createElement("div");
22016 bio.className = "desktop-mode-my-wordpress__user-bio";
22017 bio.textContent = data.profile.description;
22018 wrap.appendChild(bio);
22019 }
22020 const cards = document.createElement("div");
22021 cards.className = "desktop-mode-my-wordpress__user-stats";
22022 cards.appendChild(
22023 statCard(
22024 data.counts.posts.total.toLocaleString(),
22025 __("Posts", "desktop-mode")
22026 )
22027 );
22028 cards.appendChild(
22029 statCard(
22030 data.counts.pages.total.toLocaleString(),
22031 __("Pages", "desktop-mode")
22032 )
22033 );
22034 cards.appendChild(
22035 statCard(
22036 data.counts.commentsReceived.toLocaleString(),
22037 __("Comments received", "desktop-mode")
22038 )
22039 );
22040 wrap.appendChild(cards);
22041 return wrap;
22042 }
22043 async function renderTermSummary(file) {
22044 const id = parseInt(file.ref, 10);
22045 const taxonomy = typeof file.taxonomy === "string" && file.taxonomy ? file.taxonomy : "category";
22046 if (!id) {
22047 return renderGenericPreview(file);
22048 }
22049 let data = null;
22050 try {
22051 data = await getJson(
22052 restUrl(`desktop-mode/v1/term-stats/${taxonomy}/${id}`)
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 = data.profile.name || 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.profile.taxonomyLabel || data.profile.taxonomy;
22065 wrap.appendChild(meta);
22066 if (data.profile.description) {
22067 const desc = document.createElement("div");
22068 desc.className = "desktop-mode-my-wordpress__article-content";
22069 desc.innerHTML = data.profile.description;
22070 wrap.appendChild(desc);
22071 }
22072 const cards = document.createElement("div");
22073 cards.className = "desktop-mode-my-wordpress__user-stats";
22074 cards.appendChild(
22075 statCard(
22076 data.counts.posts.total.toLocaleString(),
22077 __("Posts", "desktop-mode")
22078 )
22079 );
22080 cards.appendChild(
22081 statCard(
22082 data.counts.commentsReceived.toLocaleString(),
22083 __("Comments", "desktop-mode")
22084 )
22085 );
22086 cards.appendChild(
22087 statCard(
22088 data.counts.distinctAuthors.toLocaleString(),
22089 __("Authors", "desktop-mode")
22090 )
22091 );
22092 wrap.appendChild(cards);
22093 return wrap;
22094 }
22095 async function renderCommentSummary(ref, file) {
22096 const id = parseInt(ref, 10);
22097 if (!id) {
22098 return renderGenericPreview(file);
22099 }
22100 let data = null;
22101 try {
22102 data = await getJson(
22103 restUrl(`desktop-mode/v1/comment-stats/${id}`)
22104 );
22105 } catch {
22106 return renderGenericPreview(file);
22107 }
22108 const wrap = articleShell();
22109 const header = document.createElement("header");
22110 header.className = "desktop-mode-my-wordpress__user-header";
22111 if (data.author.avatarUrl) {
22112 const img = document.createElement("img");
22113 img.className = "desktop-mode-my-wordpress__user-avatar";
22114 img.src = data.author.avatarUrl;
22115 img.alt = "";
22116 header.appendChild(img);
22117 }
22118 const head = document.createElement("div");
22119 head.className = "desktop-mode-my-wordpress__user-headline";
22120 const h = document.createElement("h2");
22121 h.className = "desktop-mode-my-wordpress__article-title";
22122 h.textContent = data.author.name;
22123 head.appendChild(h);
22124 const sub = document.createElement("p");
22125 sub.className = "desktop-mode-my-wordpress__article-meta";
22126 sub.textContent = `${formatDate(data.comment.date)} · ${data.comment.status}`;
22127 head.appendChild(sub);
22128 header.appendChild(head);
22129 wrap.appendChild(header);
22130 const body = document.createElement("div");
22131 body.className = "desktop-mode-my-wordpress__article-content";
22132 body.innerHTML = data.comment.rendered;
22133 wrap.appendChild(body);
22134 if (data.post) {
22135 const card = document.createElement("div");
22136 card.className = "desktop-mode-my-wordpress__comment-post";
22137 const link = document.createElement("a");
22138 link.className = "desktop-mode-my-wordpress__comment-post-title";
22139 link.href = data.post.link;
22140 link.target = "_blank";
22141 link.rel = "noopener noreferrer";
22142 link.textContent = data.post.title;
22143 card.appendChild(link);
22144 wrap.appendChild(card);
22145 }
22146 return wrap;
22147 }
22148 async function renderAttachmentPreview(ref, file) {
22149 const id = parseInt(ref, 10);
22150 if (!id) {
22151 return renderGenericPreview(file);
22152 }
22153 let data = null;
22154 try {
22155 data = await getJson(
22156 restUrl(
22157 `wp/v2/media/${id}?_fields=id,title,source_url,mime_type,alt_text,media_details`
22158 )
22159 );
22160 } catch {
22161 return renderGenericPreview(file);
22162 }
22163 const wrap = articleShell();
22164 const h = document.createElement("h2");
22165 h.className = "desktop-mode-my-wordpress__article-title";
22166 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
22167 wrap.appendChild(h);
22168 const meta = document.createElement("p");
22169 meta.className = "desktop-mode-my-wordpress__article-meta";
22170 meta.textContent = data.mime_type;
22171 wrap.appendChild(meta);
22172 if (data.mime_type.startsWith("image/")) {
22173 const img = document.createElement("img");
22174 img.className = "desktop-mode-my-wordpress__article-hero";
22175 const sizes = data.media_details?.sizes;
22176 img.src = sizes?.large?.source_url ?? sizes?.medium?.source_url ?? data.source_url;
22177 img.alt = data.alt_text ?? "";
22178 wrap.appendChild(img);
22179 } else {
22180 const p = document.createElement("p");
22181 const a = document.createElement("a");
22182 a.href = data.source_url;
22183 a.textContent = data.source_url;
22184 a.target = "_blank";
22185 a.rel = "noopener noreferrer";
22186 p.appendChild(a);
22187 wrap.appendChild(p);
22188 }
22189 return wrap;
22190 }
22191 function renderFolderPreview(file) {
22192 const wrap = articleShell();
22193 const h = document.createElement("h2");
22194 h.className = "desktop-mode-my-wordpress__article-title";
22195 h.textContent = file.title || __("(folder)", "desktop-mode");
22196 wrap.appendChild(h);
22197 const meta = document.createElement("p");
22198 meta.className = "desktop-mode-my-wordpress__article-meta";
22199 meta.textContent = __("Double-click to open.", "desktop-mode");
22200 wrap.appendChild(meta);
22201 return wrap;
22202 }
22203 function renderShortcutPreview(file) {
22204 const wrap = articleShell();
22205 const h = document.createElement("h2");
22206 h.className = "desktop-mode-my-wordpress__article-title";
22207 h.textContent = file.title || __("Shortcut", "desktop-mode");
22208 wrap.appendChild(h);
22209 const meta = document.createElement("p");
22210 meta.className = "desktop-mode-my-wordpress__article-meta";
22211 meta.textContent = __("Plugin shortcut. Double-click to open.", "desktop-mode");
22212 wrap.appendChild(meta);
22213 return wrap;
22214 }
22215 function renderBookmarkPreview(file) {
22216 const wrap = articleShell();
22217 const h = document.createElement("h2");
22218 h.className = "desktop-mode-my-wordpress__article-title";
22219 h.textContent = file.title || __("Bookmark", "desktop-mode");
22220 wrap.appendChild(h);
22221 const url = typeof file.url === "string" ? file.url : "";
22222 if (url) {
22223 const a = document.createElement("a");
22224 a.href = url;
22225 a.textContent = url;
22226 a.target = "_blank";
22227 a.rel = "noopener noreferrer";
22228 wrap.appendChild(a);
22229 }
22230 return wrap;
22231 }
22232 function renderGenericPreview(file) {
22233 const wrap = articleShell();
22234 const h = document.createElement("h2");
22235 h.className = "desktop-mode-my-wordpress__article-title";
22236 h.textContent = file.title || file.type;
22237 wrap.appendChild(h);
22238 const meta = document.createElement("p");
22239 meta.className = "desktop-mode-my-wordpress__article-meta";
22240 meta.textContent = sprintf(
22241 // translators: %s is a file-type slug.
22242 __("Type: %s", "desktop-mode"),
22243 file.type
22244 );
22245 wrap.appendChild(meta);
22246 if (!file.exists) {
22247 const warn2 = document.createElement("p");
22248 warn2.className = "desktop-mode-my-wordpress__article-meta";
22249 warn2.textContent = __(
22250 "The underlying entity is no longer available.",
22251 "desktop-mode"
22252 );
22253 wrap.appendChild(warn2);
22254 }
22255 return wrap;
22256 }
22257 function articleShell(extraClass = "") {
22258 const article = document.createElement("article");
22259 article.className = "desktop-mode-my-wordpress__article" + (extraClass ? " " + extraClass : "");
22260 return article;
22261 }
22262 function statCard(value, label) {
22263 const card = document.createElement("div");
22264 card.className = "desktop-mode-my-wordpress__user-stat";
22265 const v = document.createElement("span");
22266 v.className = "desktop-mode-my-wordpress__user-stat-value";
22267 v.textContent = value;
22268 card.appendChild(v);
22269 const l = document.createElement("span");
22270 l.className = "desktop-mode-my-wordpress__user-stat-label";
22271 l.textContent = label;
22272 card.appendChild(l);
22273 return card;
22274 }
22275 function renderLoading() {
22276 const wrap = document.createElement("div");
22277 wrap.className = "desktop-mode-my-wordpress__preview-loading";
22278 const spinner = document.createElement("wpd-spinner");
22279 wrap.appendChild(spinner);
22280 return wrap;
22281 }
22282 function renderError(err) {
22283 const wrap = document.createElement("div");
22284 wrap.className = "desktop-mode-my-wordpress__error";
22285 wrap.textContent = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
22286 return wrap;
22287 }
22288 function stripTags(html2) {
22289 const div = document.createElement("div");
22290 div.innerHTML = html2;
22291 return (div.textContent ?? "").trim();
22292 }
22293 function formatDate(iso) {
22294 if (!iso) {
22295 return "";
22296 }
22297 try {
22298 return new Date(iso).toLocaleString();
22299 } catch {
22300 return iso;
22301 }
22302 }
22303 function renderPreviewEmpty() {
22304 const wrap = document.createElement("div");
22305 wrap.className = "desktop-mode-my-wordpress__preview-empty";
22306 wrap.textContent = __(
22307 "Select an item to preview it here.",
22308 "desktop-mode"
22309 );
22310 return wrap;
22311 }
22312 const ID_PREFIX = "desktop-mode-embed-";
22313 const DEFAULT_W = 800;
22314 const DEFAULT_H = 600;
22315 const MIN_W = 360;
22316 const MIN_H = 240;
22317 const PADDING = 16;
22318 const lastPersisted = /* @__PURE__ */ new Map();
22319 function openEmbedWindow(file, ctx) {
22320 const url = file.ref();
22321 if (!url) {
22322 return;
22323 }
22324 const wm = window.wp?.desktop?.windowManager;
22325 if (!wm) {
22326 return;
22327 }
22328 const placement = ctx?.placement;
22329 const meta = placement?.meta ?? null;
22330 const windowId = placement ? `${ID_PREFIX}${placement.id}` : `${ID_PREFIX}anon-${hash(url)}`;
22331 const customName = meta?.name?.trim() ?? "";
22332 const title = customName !== "" ? customName : file.title();
22333 const cfg = {
22334 id: windowId,
22335 baseId: windowId,
22336 url,
22337 title,
22338 icon: file.icon(),
22339 minWidth: MIN_W,
22340 minHeight: MIN_H
22341 };
22342 const saved = meta?.window;
22343 const area = document.getElementById("desktop-mode-area");
22344 const aw = area?.clientWidth ?? window.innerWidth;
22345 const ah = area?.clientHeight ?? window.innerHeight;
22346 if (saved && Number.isFinite(saved.width) && Number.isFinite(saved.height)) {
22347 const { x, y, width, height } = clampGeometry(saved, aw, ah);
22348 cfg.x = x;
22349 cfg.y = y;
22350 cfg.width = width;
22351 cfg.height = height;
22352 } else {
22353 cfg.width = Math.min(DEFAULT_W, Math.max(MIN_W, aw - PADDING * 2));
22354 cfg.height = Math.min(DEFAULT_H, Math.max(MIN_H, ah - PADDING * 2));
22355 }
22356 if (placement) {
22357 if (saved) {
22358 lastPersisted.set(windowId, { ...saved });
22359 }
22360 }
22361 wm.open(cfg);
22362 }
22363 let installed = false;
22364 function installEmbedPersistence() {
22365 if (installed) {
22366 return;
22367 }
22368 installed = true;
22369 const onChange = (payload) => {
22370 const p = payload;
22371 const id = p?.windowId;
22372 if (!id || !id.startsWith(ID_PREFIX)) {
22373 return;
22374 }
22375 const placementIdStr = id.slice(ID_PREFIX.length);
22376 const placementId = parseInt(placementIdStr, 10);
22377 if (!placementId) {
22378 return;
22379 }
22380 const wm = window.wp?.desktop?.windowManager;
22381 const win = wm?.getById?.(id);
22382 const el = win?.element;
22383 if (!el) {
22384 return;
22385 }
22386 const next = {
22387 x: el.offsetLeft,
22388 y: el.offsetTop,
22389 width: el.offsetWidth,
22390 height: el.offsetHeight
22391 };
22392 const prev = lastPersisted.get(id);
22393 if (prev && prev.x === next.x && prev.y === next.y && prev.width === next.width && prev.height === next.height) {
22394 return;
22395 }
22396 lastPersisted.set(id, next);
22397 void persist(placementId, next);
22398 };
22399 addAction(HOOKS.WINDOW_DRAG_END, "desktop-mode-embed-persist", onChange);
22400 addAction(HOOKS.WINDOW_RESIZE_END, "desktop-mode-embed-persist", onChange);
22401 }
22402 async function persist(placementId, geo) {
22403 try {
22404 const list2 = await listPlacements(0);
22405 const row = list2.placements.find((p) => p.id === placementId);
22406 const prevMeta = row?.meta ?? {};
22407 const nextMeta = {
22408 ...prevMeta,
22409 window: geo
22410 };
22411 await updatePlacement(placementId, { meta: nextMeta });
22412 } catch (err) {
22413 console.warn("[desktop-mode] embed window persist failed:", err);
22414 }
22415 }
22416 function clampGeometry(g, areaW, areaH) {
22417 const width = Math.max(MIN_W, Math.min(g.width, areaW - PADDING));
22418 const height = Math.max(MIN_H, Math.min(g.height, areaH - PADDING));
22419 const x = Math.max(0, Math.min(g.x, Math.max(0, areaW - width)));
22420 const y = Math.max(0, Math.min(g.y, Math.max(0, areaH - height)));
22421 return { x, y, width, height };
22422 }
22423 function hash(s) {
22424 let h = 0;
22425 for (let i = 0; i < s.length; i++) {
22426 h = (Math.imul(h, 31) + s.charCodeAt(i)) % 2147483647;
22427 }
22428 return Math.abs(h).toString(36);
22429 }
22430 function adminBase() {
22431 const cfg = window.wp?.desktop?.config;
22432 const url = cfg?.adminUrl ?? "/wp-admin/";
22433 return url.endsWith("/") ? url : `${url}/`;
22434 }
22435 function registerBuiltInFileOpeners() {
22436 registerOpener({
22437 id: "wp-post-editor",
22438 label: "Block Editor",
22439 types: ["post"],
22440 isDefault: true,
22441 sort: 10,
22442 handler: {
22443 kind: "url",
22444 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
22445 }
22446 });
22447 registerOpener({
22448 id: "wp-media-editor",
22449 label: "Media editor",
22450 types: ["attachment"],
22451 isDefault: true,
22452 sort: 10,
22453 handler: {
22454 kind: "url",
22455 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
22456 }
22457 });
22458 registerOpener({
22459 id: "wp-user-profile",
22460 label: "User profile",
22461 types: ["user"],
22462 isDefault: true,
22463 sort: 10,
22464 handler: {
22465 kind: "url",
22466 url: (file) => `${adminBase()}user-edit.php?user_id=${encodeURIComponent(file.ref())}`
22467 }
22468 });
22469 registerOpener({
22470 id: "wp-term-editor",
22471 label: "Term editor",
22472 types: ["term"],
22473 isDefault: true,
22474 sort: 10,
22475 handler: {
22476 kind: "url",
22477 url: (file) => {
22478 const [taxonomy, termId] = file.ref().split(":");
22479 return `${adminBase()}term.php?taxonomy=${encodeURIComponent(taxonomy ?? "")}&tag_ID=${encodeURIComponent(termId ?? "")}`;
22480 }
22481 }
22482 });
22483 registerOpener({
22484 id: "wp-comment-editor",
22485 label: "Comment editor",
22486 types: ["comment"],
22487 isDefault: true,
22488 sort: 10,
22489 handler: {
22490 kind: "url",
22491 url: (file) => `${adminBase()}comment.php?action=editcomment&c=${encodeURIComponent(file.ref())}`
22492 }
22493 });
22494 registerOpener({
22495 id: "desktop-mode-folder-window",
22496 label: "Open folder",
22497 types: ["folder"],
22498 isDefault: true,
22499 sort: 10,
22500 handler: {
22501 kind: "js",
22502 open: (file) => {
22503 const folderId = parseInt(file.ref(), 10);
22504 if (!folderId) {
22505 return;
22506 }
22507 const wm = window.wp?.desktop?.windowManager;
22508 if (!wm) {
22509 return;
22510 }
22511 const id = `desktop-mode-folder-${folderId}`;
22512 const folderRow = store.getState().folders.get(folderId);
22513 const viewerId2 = Number(window.desktopModeConfig?.currentUserId ?? 0);
22514 const isRecipient = !!folderRow && folderRow.ownerId > 0 && folderRow.ownerId !== viewerId2;
22515 const baseTitle = file.title();
22516 const titleWithCue = isRecipient ? `${baseTitle} · Shared` : baseTitle;
22517 wm.open({
22518 id,
22519 baseId: id,
22520 url: `#folder-${folderId}`,
22521 title: titleWithCue,
22522 icon: file.icon(),
22523 native: true,
22524 render: (body) => {
22525 body.replaceChildren();
22526 body.classList.add("desktop-mode-folder-window");
22527 const routes = [
22528 { folderId, title: file.title() }
22529 ];
22530 let currentDispose = null;
22531 const breadcrumbsHost = document.createElement("header");
22532 body.appendChild(breadcrumbsHost);
22533 const bodyHost = document.createElement("div");
22534 bodyHost.style.cssText = "flex:1 1 auto;min-height:0;display:flex;flex-direction:column;";
22535 body.appendChild(bodyHost);
22536 const paintBreadcrumbs = () => {
22537 const segments = routes.map(
22538 (route, idx) => {
22539 const isCurrent = idx === routes.length - 1;
22540 if (isCurrent) {
22541 return { label: route.title };
22542 }
22543 return {
22544 label: route.title,
22545 onClick: () => {
22546 routes.length = idx + 1;
22547 mountCurrent();
22548 }
22549 };
22550 }
22551 );
22552 renderBreadcrumbs(breadcrumbsHost, segments, {
22553 onBack: () => {
22554 if (routes.length <= 1) {
22555 return;
22556 }
22557 routes.pop();
22558 mountCurrent();
22559 },
22560 backDisabled: routes.length <= 1
22561 });
22562 };
22563 const mountCurrent = () => {
22564 currentDispose?.();
22565 currentDispose = null;
22566 bodyHost.replaceChildren();
22567 const split = document.createElement("div");
22568 split.className = "desktop-mode-folder-window__split";
22569 bodyHost.appendChild(split);
22570 const layerHost = document.createElement("div");
22571 layerHost.className = "desktop-mode-folder-window__layer";
22572 split.appendChild(layerHost);
22573 const previewPane = document.createElement("div");
22574 previewPane.className = "desktop-mode-folder-window__preview";
22575 previewPane.appendChild(renderPreviewEmpty());
22576 split.appendChild(previewPane);
22577 const route = routes[routes.length - 1];
22578 const layer = mountFilesLayer(
22579 layerHost,
22580 route.folderId
22581 );
22582 const offSelection = layer.onSelectionChange(
22583 (placement) => {
22584 if (!placement) {
22585 previewPane.replaceChildren(
22586 renderPreviewEmpty()
22587 );
22588 return;
22589 }
22590 renderPlacementPreview(
22591 placement,
22592 previewPane
22593 );
22594 }
22595 );
22596 const dblClickHandler = (e) => {
22597 if (!(e.target instanceof Element)) {
22598 return;
22599 }
22600 const tile2 = e.target.closest(
22601 ".desktop-mode-file-tile"
22602 );
22603 if (!tile2) {
22604 return;
22605 }
22606 if (tile2.dataset.fileType !== "folder") {
22607 return;
22608 }
22609 const subId = parseInt(
22610 tile2.dataset.fileRef ?? "",
22611 10
22612 );
22613 if (!subId) {
22614 return;
22615 }
22616 e.preventDefault();
22617 e.stopPropagation();
22618 const subTitle = tile2.querySelector(
22619 ".desktop-mode-file-tile__label"
22620 )?.textContent ?? `#${subId}`;
22621 routes.push({
22622 folderId: subId,
22623 title: subTitle
22624 });
22625 mountCurrent();
22626 };
22627 layerHost.addEventListener(
22628 "dblclick",
22629 dblClickHandler,
22630 true
22631 );
22632 const menu = attachIconCanvasMenu(layerHost, {
22633 scope: `desktop-mode-folder:${route.folderId}`,
22634 onSort: (mode) => layer.sort(mode),
22635 extraItems: [
22636 {
22637 id: "new-folder",
22638 label: "New folder",
22639 icon: "dashicons-portfolio",
22640 sort: 5,
22641 onClick: () => {
22642 openCreateFolderDialog({
22643 onSubmit: async (name) => {
22644 const folder = await createFolder({
22645 name
22646 });
22647 const peers = store.getState().placementsByFolder.get(
22648 route.folderId
22649 ) ?? [];
22650 const occupied = buildOccupiedSet(peers);
22651 const cell = snapToEmptyCell(
22652 GRID_PADDING,
22653 GRID_PADDING,
22654 occupied,
22655 layerHost
22656 );
22657 const placement = await createPlacement({
22658 type: "folder",
22659 ref: String(folder.id),
22660 parentId: route.folderId,
22661 x: cell.x,
22662 y: cell.y
22663 });
22664 store.upsertFolder(folder);
22665 store.upsertPlacement(
22666 placement
22667 );
22668 }
22669 });
22670 }
22671 }
22672 ]
22673 });
22674 const status = mountFolderStatusBar(
22675 bodyHost,
22676 route.folderId
22677 );
22678 currentDispose = () => {
22679 offSelection();
22680 menu.dispose();
22681 status.dispose();
22682 layerHost.removeEventListener(
22683 "dblclick",
22684 dblClickHandler,
22685 true
22686 );
22687 layer.dispose();
22688 };
22689 paintBreadcrumbs();
22690 };
22691 mountCurrent();
22692 },
22693 width: 720,
22694 height: 480,
22695 minWidth: 360,
22696 minHeight: 240
22697 });
22698 }
22699 }
22700 });
22701 registerOpener({
22702 id: "desktop-mode-shortcut-opener",
22703 label: "Open shortcut",
22704 types: ["shortcut"],
22705 isDefault: true,
22706 sort: 10,
22707 handler: {
22708 kind: "js",
22709 open: (file) => {
22710 const extras = file.shape;
22711 const wp = window.wp?.desktop;
22712 if (!wp) {
22713 return;
22714 }
22715 if (extras.shortcutWindow && wp.openWindow) {
22716 wp.openWindow(extras.shortcutWindow);
22717 return;
22718 }
22719 if (extras.shortcutUrl && wp.windowManager) {
22720 try {
22721 const u = new URL(extras.shortcutUrl, window.location.origin);
22722 if (u.origin !== window.location.origin) {
22723 window.open(u.toString(), "_blank", "noopener,noreferrer");
22724 return;
22725 }
22726 const adminUrl = wp.config?.adminUrl;
22727 const id = adminUrl ? deriveWindowId(u.toString(), adminUrl) : `desktop-icon-${file.ref()}`;
22728 wp.windowManager.open({
22729 id,
22730 baseId: id,
22731 url: u.toString(),
22732 title: file.title(),
22733 icon: file.icon()
22734 });
22735 } catch {
22736 }
22737 }
22738 }
22739 }
22740 });
22741 registerOpener({
22742 id: "browser-navigate",
22743 label: "Open in browser",
22744 types: ["bookmark"],
22745 isDefault: true,
22746 sort: 10,
22747 handler: {
22748 kind: "js",
22749 open: (file) => {
22750 const url = file.ref();
22751 if (!url) {
22752 return;
22753 }
22754 window.open(url, "_blank", "noopener,noreferrer");
22755 }
22756 }
22757 });
22758 registerOpener({
22759 id: "desktop-mode-link-opener",
22760 label: "Open in browser",
22761 types: ["link"],
22762 isDefault: true,
22763 sort: 10,
22764 handler: {
22765 kind: "js",
22766 open: (file) => {
22767 const url = file.ref();
22768 if (!url) {
22769 return;
22770 }
22771 window.open(url, "_blank", "noopener,noreferrer");
22772 }
22773 }
22774 });
22775 registerOpener({
22776 id: "desktop-mode-embed-opener",
22777 label: "Open as window",
22778 types: ["embed"],
22779 isDefault: true,
22780 sort: 10,
22781 handler: {
22782 kind: "js",
22783 open: (file, ctx) => {
22784 openEmbedWindow(file, ctx);
22785 }
22786 }
22787 });
22788 }
22789 const TAB_ID = "desktop-mode-file-associations";
22790 function registerFileAssociationsTab() {
22791 registerSettingsTab({
22792 id: TAB_ID,
22793 label: "File Associations",
22794 order: 50,
22795 render(body) {
22796 renderTab(body);
22797 }
22798 });
22799 }
22800 function renderTab(body) {
22801 body.replaceChildren();
22802 const types = getTypes();
22803 if (types.length === 0) {
22804 const empty = document.createElement("p");
22805 empty.className = "desktop-mode-file-associations__empty";
22806 empty.textContent = "No file types are registered.";
22807 body.appendChild(empty);
22808 return;
22809 }
22810 const intro = document.createElement("p");
22811 intro.className = "desktop-mode-file-associations__intro";
22812 intro.textContent = "Pick which app opens each kind of file when you double-click it on the desktop.";
22813 body.appendChild(intro);
22814 const associations = getUserAssociations();
22815 const list2 = document.createElement("div");
22816 list2.className = "desktop-mode-file-associations__list";
22817 list2.setAttribute("role", "list");
22818 for (const type of types) {
22819 list2.appendChild(buildRow(type.type, type.label, associations));
22820 }
22821 body.appendChild(list2);
22822 }
22823 function buildRow(typeSlug, typeLabel, associations) {
22824 const row = document.createElement("div");
22825 row.className = "desktop-mode-file-associations__row";
22826 row.setAttribute("role", "listitem");
22827 row.dataset.fileType = typeSlug;
22828 const label = document.createElement("label");
22829 label.className = "desktop-mode-file-associations__label";
22830 label.textContent = typeLabel;
22831 row.appendChild(label);
22832 const candidates = getOpenersForType(typeSlug);
22833 if (candidates.length === 0) {
22834 const empty = document.createElement("span");
22835 empty.className = "desktop-mode-file-associations__none";
22836 empty.textContent = "No app available";
22837 row.appendChild(empty);
22838 return row;
22839 }
22840 const resolved = resolveOpener(typeSlug);
22841 const currentId = associations[typeSlug] ?? resolved?.id ?? "";
22842 const select = document.createElement("wpd-select");
22843 select.setAttribute("value", currentId);
22844 select.setAttribute("aria-label", `Default app for ${typeLabel}`);
22845 select.className = "desktop-mode-file-associations__select";
22846 label.htmlFor = `assoc-${typeSlug}`;
22847 select.id = `assoc-${typeSlug}`;
22848 for (const o of candidates) {
22849 const opt = document.createElement("wpd-option");
22850 opt.setAttribute("value", o.id);
22851 opt.textContent = o.isDefault ? `${o.label} (default)` : o.label;
22852 select.appendChild(opt);
22853 }
22854 select.addEventListener("wpd-pick", (e) => {
22855 const next = e.detail?.value;
22856 if (!next) {
22857 return;
22858 }
22859 const merged = { ...getUserAssociations(), [typeSlug]: next };
22860 setUserAssociations(merged);
22861 void saveAssociations(merged).catch((err) => {
22862 console.error("[desktop-mode] saveAssociations failed:", err);
22863 });
22864 });
22865 row.appendChild(select);
22866 return row;
22867 }
22868 let _store$1 = null;
22869 function sharesStore() {
22870 if (!_store$1) {
22871 _store$1 = createSharedStore("desktop-files/shares", () => ({
22872 byFolder: /* @__PURE__ */ new Map(),
22873 pending: [],
22874 sharesVersion: 0,
22875 deniedFolders: /* @__PURE__ */ new Set()
22876 }));
22877 }
22878 return _store$1;
22879 }
22880 function setSharesForFolder(folderId, shares) {
22881 const s = sharesStore();
22882 s.state.byFolder.set(folderId, shares);
22883 s.notify();
22884 }
22885 function upsertShare(share) {
22886 if (!share || typeof share.folderId !== "number") {
22887 return;
22888 }
22889 const s = sharesStore();
22890 const existing = s.state.byFolder.get(share.folderId) ?? [];
22891 const next = existing.filter((r) => r.id !== share.id);
22892 next.push(share);
22893 s.state.byFolder.set(share.folderId, next);
22894 s.notify();
22895 }
22896 function removeShare(folderId, shareId) {
22897 const s = sharesStore();
22898 const existing = s.state.byFolder.get(folderId) ?? [];
22899 s.state.byFolder.set(
22900 folderId,
22901 existing.filter((r) => r.id !== shareId)
22902 );
22903 s.notify();
22904 }
22905 function inviteEquals(a, b) {
22906 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;
22907 }
22908 function ingestPendingInvites(invites) {
22909 const s = sharesStore();
22910 const existingById = new Map(s.state.pending.map((p) => [p.id, p]));
22911 let mutated = false;
22912 for (const inv of invites) {
22913 if (s.state.deniedFolders.has(inv.folderId)) {
22914 continue;
22915 }
22916 const existing = existingById.get(inv.id);
22917 if (existing) {
22918 if (inviteEquals(existing, inv)) {
22919 continue;
22920 }
22921 s.state.pending = s.state.pending.map((p) => p.id === inv.id ? inv : p);
22922 } else {
22923 s.state.pending.push(inv);
22924 }
22925 if (inv.invitedAtMs > s.state.sharesVersion) {
22926 s.state.sharesVersion = inv.invitedAtMs;
22927 }
22928 mutated = true;
22929 }
22930 if (mutated) {
22931 s.notify();
22932 }
22933 }
22934 function dropPending(shareId, opts = {}) {
22935 const s = sharesStore();
22936 s.state.pending = s.state.pending.filter((p) => p.id !== shareId);
22937 if (opts.denied && typeof opts.folderId === "number") {
22938 s.state.deniedFolders.add(opts.folderId);
22939 }
22940 s.notify();
22941 }
22942 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}`;
22943 const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
22944 const _WpdModal = class _WpdModal extends Component {
22945 constructor() {
22946 super(...arguments);
22947 this._prevFocus = null;
22948 this._onKey = (e) => {
22949 if (e.key === "Escape" && !this.hasAttribute("mandatory")) {
22950 e.preventDefault();
22951 this._cancel();
22952 return;
22953 }
22954 if (e.key === "Tab") {
22955 const f = this._focusables();
22956 if (f.length === 0) {
22957 return;
22958 }
22959 const first = f[0];
22960 const last = f[f.length - 1];
22961 const doc = this.ownerDocument;
22962 const fallback = doc ? doc.activeElement : null;
22963 const active2 = e.composedPath()[0] || fallback;
22964 if (e.shiftKey && active2 === first) {
22965 e.preventDefault();
22966 last.focus();
22967 } else if (!e.shiftKey && active2 === last) {
22968 e.preventDefault();
22969 first.focus();
22970 }
22971 }
22972 };
22973 this._onBackdrop = (e) => {
22974 if (this.hasAttribute("mandatory")) {
22975 return;
22976 }
22977 const path = e.composedPath();
22978 const original = path.length > 0 ? path[0] : e.target;
22979 if (original === this) {
22980 this._cancel();
22981 }
22982 };
22983 }
22984 connectedCallback() {
22985 super.connectedCallback();
22986 this.setAttribute("role", "dialog");
22987 this.setAttribute("aria-modal", "true");
22988 this.addEventListener("keydown", this._onKey);
22989 this.addEventListener("click", this._onBackdrop);
22990 }
22991 disconnectedCallback() {
22992 this.removeEventListener("keydown", this._onKey);
22993 this.removeEventListener("click", this._onBackdrop);
22994 }
22995 attributeChangedCallback(name, oldValue, newValue) {
22996 super.attributeChangedCallback?.(name, oldValue, newValue);
22997 if (name === "open") {
22998 if (newValue !== null) {
22999 const doc = this.ownerDocument;
23000 this._prevFocus = doc ? doc.activeElement : null;
23001 queueMicrotask(() => this._focusFirst());
23002 } else if (this._prevFocus) {
23003 try {
23004 this._prevFocus.focus();
23005 } catch (e) {
23006 }
23007 this._prevFocus = null;
23008 }
23009 }
23010 }
23011 showModal() {
23012 this.setAttribute("open", "");
23013 }
23014 hideModal() {
23015 this.removeAttribute("open");
23016 }
23017 _focusables() {
23018 const root = this.shadowRoot;
23019 if (!root) {
23020 return [];
23021 }
23022 const slotted = Array.from(this.querySelectorAll(FOCUSABLE));
23023 const inShadow = Array.from(root.querySelectorAll(FOCUSABLE));
23024 return [...slotted, ...inShadow].filter((el) => el.offsetParent !== null || el.tagName === "BUTTON");
23025 }
23026 _focusFirst() {
23027 const f = this._focusables();
23028 if (f.length > 0) {
23029 f[0].focus();
23030 } else {
23031 const inner = this.shadowRoot?.querySelector(".dialog");
23032 inner?.focus?.();
23033 }
23034 }
23035 _cancel() {
23036 const ev = new CustomEvent("wpd-modal-cancel", {
23037 bubbles: true,
23038 cancelable: true,
23039 composed: true
23040 });
23041 const allowed = this.dispatchEvent(ev);
23042 if (allowed) {
23043 this.hideModal();
23044 }
23045 }
23046 render() {
23047 const title = this.getAttribute("title") ?? "";
23048 const mandatory = this.hasAttribute("mandatory");
23049 return html`
23050 <div class="dialog" tabindex="-1">
23051 ${title ? html`
23052 <div class="header">
23053 <h2 class="title">${title}</h2>
23054 <div class="header-actions">
23055 <slot name="header-actions"></slot>
23056 ${mandatory ? html`` : html`<button
23057 type="button"
23058 class="close"
23059 aria-label="Close"
23060 @click=${() => this._cancel()}
23061 >×</button>`}
23062 </div>
23063 </div>
23064 ` : html``}
23065 <div class="body">
23066 <slot></slot>
23067 </div>
23068 <div class="footer">
23069 <slot name="footer"></slot>
23070 </div>
23071 </div>
23072 `;
23073 }
23074 };
23075 _WpdModal.props = ["open", "title", "size", "mandatory"];
23076 _WpdModal.styles = [modalStyles];
23077 _WpdModal.help = {
23078 title: "Modal overlay",
23079 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.",
23080 status: "experimental",
23081 since: "0.18.0",
23082 props: [
23083 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
23084 { name: "title", type: "string", description: "Heading shown at the top of the dialog." },
23085 { name: "size", type: "'sm' | 'md' | 'lg'", default: "md", description: "Width preset." },
23086 {
23087 name: "mandatory",
23088 type: "boolean attribute",
23089 description: "Disables ESC, click-outside and the close button."
23090 }
23091 ],
23092 slots: [
23093 { name: "(default)", description: "Body content." },
23094 { name: "footer", description: "Footer button row, right-aligned." },
23095 { name: "header-actions", description: "Extra actions next to the close button." }
23096 ],
23097 events: [
23098 {
23099 name: "wpd-modal-cancel",
23100 description: "Fires when the user dismisses the modal (ESC, click-outside, close button). Cancelable; calling `preventDefault()` keeps the modal open."
23101 }
23102 ]
23103 };
23104 let WpdModal = _WpdModal;
23105 defineComponent("wpd-modal", WpdModal);
23106 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}`;
23107 const _WpdUserSearch = class _WpdUserSearch extends Component {
23108 constructor() {
23109 super(...arguments);
23110 this._timer = null;
23111 this._abort = null;
23112 this._results = [];
23113 this._query = "";
23114 this._open = false;
23115 this._phase = "idle";
23116 this._error = "";
23117 this._dropdownStyle = "";
23118 this._onScrollOrResize = () => void 0;
23119 this._onInput = (e) => {
23120 const value = e.target.value;
23121 this._query = value;
23122 this._scheduleSearch(value);
23123 };
23124 this._onFocus = () => {
23125 if (this._results.length === 0 && this._phase === "idle") {
23126 this._scheduleSearch(this._query);
23127 return;
23128 }
23129 this._open = true;
23130 this._positionDropdown();
23131 this.requestUpdate();
23132 };
23133 this._onBlur = () => {
23134 setTimeout(() => {
23135 this._open = false;
23136 this.requestUpdate();
23137 }, 150);
23138 };
23139 this._pick = (user) => {
23140 this.emit("wpd-user-pick", { user });
23141 this._results = [];
23142 this._open = false;
23143 this._phase = "idle";
23144 this._query = "";
23145 const input = this.shadowRoot?.querySelector(".input");
23146 if (input) {
23147 input.value = "";
23148 }
23149 this.requestUpdate();
23150 };
23151 }
23152 connectedCallback() {
23153 super.connectedCallback();
23154 this._onScrollOrResize = () => {
23155 if (this._open) {
23156 this._positionDropdown();
23157 this.requestUpdate();
23158 }
23159 };
23160 window.addEventListener("resize", this._onScrollOrResize);
23161 window.addEventListener("scroll", this._onScrollOrResize, true);
23162 }
23163 disconnectedCallback() {
23164 if (this._timer) {
23165 clearTimeout(this._timer);
23166 }
23167 if (this._abort) {
23168 this._abort.abort();
23169 }
23170 window.removeEventListener("resize", this._onScrollOrResize);
23171 window.removeEventListener("scroll", this._onScrollOrResize, true);
23172 }
23173 _endpoint() {
23174 const attr = this.getAttribute("endpoint");
23175 if (attr) {
23176 return attr;
23177 }
23178 return window.desktopModeConfig?.filesUsersSearchUrl || "";
23179 }
23180 _scheduleSearch(q) {
23181 if (this._timer) {
23182 clearTimeout(this._timer);
23183 }
23184 this._phase = "loading";
23185 this._open = true;
23186 this._positionDropdown();
23187 this.requestUpdate();
23188 this._timer = setTimeout(() => this._runSearch(q), 200);
23189 }
23190 async _runSearch(q) {
23191 const url = this._endpoint();
23192 if (!url) {
23193 this._phase = "error";
23194 this._error = "Search endpoint is not configured.";
23195 this._results = [];
23196 this._open = true;
23197 this.requestUpdate();
23198 return;
23199 }
23200 if (this._abort) {
23201 this._abort.abort();
23202 }
23203 const ctrl = new AbortController();
23204 this._abort = ctrl;
23205 const exclude = this.getAttribute("exclude") || "";
23206 const full = url + "?q=" + encodeURIComponent(q) + "&exclude=" + encodeURIComponent(exclude);
23207 try {
23208 const init2 = {
23209 signal: ctrl.signal,
23210 credentials: "same-origin"
23211 };
23212 const res = await trackedFetch$1(full, init2, {
23213 source: "desktop-mode/files-user-search",
23214 silent: true
23215 });
23216 if (!res.ok) {
23217 throw new Error(`HTTP ${res.status}`);
23218 }
23219 const json = await res.json();
23220 this._results = json && Array.isArray(json.users) ? json.users : [];
23221 this._phase = "ready";
23222 this._error = "";
23223 this._open = true;
23224 } catch (e) {
23225 if (e.name === "AbortError") {
23226 return;
23227 }
23228 this._results = [];
23229 this._phase = "error";
23230 this._error = e.message || "Search failed.";
23231 this._open = true;
23232 }
23233 this._positionDropdown();
23234 this.requestUpdate();
23235 }
23236 _positionDropdown() {
23237 const input = this.shadowRoot?.querySelector(".input");
23238 if (!input) {
23239 return;
23240 }
23241 const rect = input.getBoundingClientRect();
23242 const top = rect.bottom + 4;
23243 const left = rect.left;
23244 const width = rect.width;
23245 const viewportH = window.innerHeight;
23246 const spaceBelow = viewportH - rect.bottom;
23247 const spaceAbove = rect.top;
23248 const maxHeight = Math.max(120, Math.min(280, Math.max(spaceBelow, spaceAbove) - 16));
23249 if (spaceBelow < 200 && spaceAbove > spaceBelow) {
23250 this._dropdownStyle = [
23251 "position:fixed",
23252 `left:${left}px`,
23253 `top:${rect.top - 4 - maxHeight}px`,
23254 `width:${width}px`,
23255 `max-height:${maxHeight}px`
23256 ].join(";");
23257 } else {
23258 this._dropdownStyle = [
23259 "position:fixed",
23260 `left:${left}px`,
23261 `top:${top}px`,
23262 `width:${width}px`,
23263 `max-height:${maxHeight}px`
23264 ].join(";");
23265 }
23266 }
23267 _dropdownContent() {
23268 if (this._phase === "loading") {
23269 return html`<div class="empty">Searching…</div>`;
23270 }
23271 if (this._phase === "error") {
23272 return html`<div class="empty error">${this._error}</div>`;
23273 }
23274 if (this._results.length === 0) {
23275 const message = this._query ? "No matches." : "No users available.";
23276 return html`<div class="empty">${message}</div>`;
23277 }
23278 return this._results.map(
23279 (u) => html`
23280 <button
23281 type="button"
23282 class="item"
23283 role="option"
23284 @mousedown=${(e) => e.preventDefault()}
23285 @click=${() => this._pick(u)}
23286 >
23287 <img class="avatar" src=${u.avatarUrl} alt="" />
23288 <div>
23289 <div class="name">${u.name}</div>
23290 <div class="slug">${u.slug}</div>
23291 </div>
23292 </button>
23293 `
23294 );
23295 }
23296 render() {
23297 const placeholder = this.getAttribute("placeholder") || "Search users…";
23298 return html`
23299 <input
23300 class="input"
23301 type="search"
23302 placeholder=${placeholder}
23303 autocomplete="off"
23304 @input=${this._onInput}
23305 @focus=${this._onFocus}
23306 @blur=${this._onBlur}
23307 .value=${this._query}
23308 />
23309 ${this._open ? html`
23310 <div class="dropdown" role="listbox" style=${this._dropdownStyle}>
23311 ${this._dropdownContent()}
23312 </div>
23313 ` : html``}
23314 `;
23315 }
23316 };
23317 _WpdUserSearch.props = ["placeholder", "exclude", "endpoint"];
23318 _WpdUserSearch.styles = [userSearchStyles];
23319 _WpdUserSearch.help = {
23320 title: "User autocomplete",
23321 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.",
23322 status: "experimental",
23323 since: "0.18.0",
23324 props: [
23325 { name: "placeholder", type: "string", description: "Input placeholder text." },
23326 {
23327 name: "exclude",
23328 type: "csv user ids",
23329 description: "Already-picked user ids to suppress in results."
23330 },
23331 {
23332 name: "endpoint",
23333 type: "URL",
23334 description: "Override the search URL (defaults to desktopModeConfig.filesUsersSearchUrl)."
23335 }
23336 ],
23337 events: [
23338 { name: "wpd-user-pick", description: "Emitted on pick. Detail: `{ user: SearchUser }`." }
23339 ]
23340 };
23341 let WpdUserSearch = _WpdUserSearch;
23342 defineComponent("wpd-user-search", WpdUserSearch);
23343 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}`;
23344 const _WpdRolePicker = class _WpdRolePicker extends Component {
23345 constructor() {
23346 super(...arguments);
23347 this._onToggle = (slug) => {
23348 const selected = !this._selectedSet().has(slug);
23349 this.emit("wpd-role-toggle", { slug, selected });
23350 };
23351 }
23352 _selectedSet() {
23353 const raw = this.getAttribute("selected") || "";
23354 return new Set(
23355 raw.split(",").map((s) => s.trim()).filter((s) => s !== "")
23356 );
23357 }
23358 _roles() {
23359 const attr = this.getAttribute("roles");
23360 if (attr) {
23361 try {
23362 const parsed = JSON.parse(attr);
23363 if (Array.isArray(parsed)) {
23364 return parsed;
23365 }
23366 } catch (e) {
23367 }
23368 }
23369 return window.desktopModeConfig?.shareEligibleRoles || [];
23370 }
23371 render() {
23372 const roles = this._roles();
23373 if (roles.length === 0) {
23374 return html`<span class="empty">No eligible roles.</span>`;
23375 }
23376 const set = this._selectedSet();
23377 return html`
23378 ${roles.map((r) => {
23379 const isSelected = set.has(r.slug);
23380 return html`
23381 <button
23382 type="button"
23383 class="chip"
23384 aria-pressed=${isSelected ? "true" : "false"}
23385 @click=${() => this._onToggle(r.slug)}
23386 >${r.name}</button>
23387 `;
23388 })}
23389 `;
23390 }
23391 };
23392 _WpdRolePicker.props = ["selected", "roles"];
23393 _WpdRolePicker.styles = [rolePickerStyles];
23394 _WpdRolePicker.help = {
23395 title: "Role picker",
23396 summary: "Chip multi-select for WordPress roles. Reads eligible roles from desktopModeConfig.shareEligibleRoles; emits wpd-role-toggle { slug, selected } on every change.",
23397 status: "experimental",
23398 since: "0.18.0",
23399 props: [
23400 {
23401 name: "selected",
23402 type: "csv role slugs",
23403 description: "Comma-separated role slugs that are currently selected."
23404 },
23405 {
23406 name: "roles",
23407 type: "JSON",
23408 description: "Override the source of eligible roles (defaults to the global config)."
23409 }
23410 ],
23411 events: [
23412 {
23413 name: "wpd-role-toggle",
23414 description: "Emitted on every click. Detail: `{ slug, selected }`."
23415 }
23416 ]
23417 };
23418 let WpdRolePicker = _WpdRolePicker;
23419 defineComponent("wpd-role-picker", WpdRolePicker);
23420 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}`;
23421 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}`;
23422 const _WpdSegment = class _WpdSegment extends Component {
23423 render() {
23424 this.setAttribute("role", "radio");
23425 return html`
23426 <button type="button" @click=${() => this._onPick()}>
23427 <slot></slot>
23428 </button>
23429 `;
23430 }
23431 _onPick() {
23432 this.emit("wpd-segment-pick", {
23433 value: this.value
23434 });
23435 }
23436 };
23437 _WpdSegment.props = ["value"];
23438 _WpdSegment.styles = [segmentStyles];
23439 _WpdSegment.help = {
23440 title: "Segment",
23441 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
23442 status: "stable",
23443 since: "0.9.0",
23444 props: [
23445 {
23446 name: "value",
23447 type: "string",
23448 description: "Identifier this segment contributes to the parent group selection."
23449 }
23450 ],
23451 slots: [
23452 { name: "(default)", description: "Visible segment label." }
23453 ],
23454 events: [
23455 {
23456 name: "wpd-segment-pick",
23457 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
23458 detail: "{ value: string }"
23459 }
23460 ]
23461 };
23462 let WpdSegment = _WpdSegment;
23463 defineComponent("wpd-segment", WpdSegment);
23464 const _WpdSegmented = class _WpdSegmented extends Component {
23465 connectedCallback() {
23466 super.connectedCallback();
23467 this.addEventListener("wpd-segment-pick", (e) => {
23468 const detail = e.detail;
23469 e.stopPropagation();
23470 this.value = detail.value;
23471 this.emit("wpd-pick", { value: detail.value });
23472 });
23473 }
23474 /**
23475 * Declarative item-list setter. Replaces the existing
23476 * `<wpd-segment>` children with a fresh set built from a
23477 * `{ value, label }` array; preserves the current selection
23478 * when the value still matches an entry, otherwise falls back
23479 * to the first item.
23480 *
23481 * Collapses the pre-0.11 imperative dance (clear children,
23482 * `createElement`, set `textContent`, `appendChild`, then
23483 * `setAttribute('value', …)` on the group — order matters) to
23484 * a single assignment:
23485 *
23486 * ```js
23487 * segmented.items = [
23488 * { value: 'm', label: 'm' },
23489 * { value: 'km', label: 'km' },
23490 * ];
23491 * ```
23492 *
23493 * @since 0.11.0
23494 */
23495 set items(list2) {
23496 const existing = this.querySelectorAll(":scope > wpd-segment");
23497 for (const el of Array.from(existing)) {
23498 el.remove();
23499 }
23500 for (const item of list2) {
23501 const seg = document.createElement("wpd-segment");
23502 seg.setAttribute("value", item.value);
23503 seg.textContent = item.label;
23504 this.appendChild(seg);
23505 }
23506 const current = this.value;
23507 const stillValid = current !== null && list2.some((i) => i.value === current);
23508 if (!stillValid && list2.length > 0) {
23509 this.value = list2[0].value;
23510 } else {
23511 this.requestUpdate();
23512 }
23513 }
23514 render() {
23515 const label = this.label || "";
23516 if (label) {
23517 this.setAttribute("aria-label", label);
23518 }
23519 this.setAttribute("role", "radiogroup");
23520 const current = this.value;
23521 queueMicrotask(() => {
23522 const segs = this.querySelectorAll("wpd-segment");
23523 for (const seg of Array.from(segs)) {
23524 const v = seg.getAttribute("value");
23525 seg.setAttribute(
23526 "aria-checked",
23527 v === current ? "true" : "false"
23528 );
23529 }
23530 });
23531 return html`<slot></slot>`;
23532 }
23533 };
23534 _WpdSegmented.props = ["value", "label"];
23535 _WpdSegmented.styles = [segmentedStyles];
23536 _WpdSegmented.help = {
23537 title: "Segmented",
23538 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
23539 status: "stable",
23540 since: "0.9.0",
23541 props: [
23542 {
23543 name: "value",
23544 type: "string",
23545 description: "Currently selected segment value. Mirrored onto child aria-checked."
23546 },
23547 {
23548 name: "label",
23549 type: "string",
23550 description: "aria-label for the radiogroup."
23551 }
23552 ],
23553 slots: [
23554 { name: "(default)", description: '<wpd-segment value="…"> children.' }
23555 ],
23556 events: [
23557 {
23558 name: "wpd-pick",
23559 description: "Fires when the selected segment changes.",
23560 detail: "{ value: string }"
23561 }
23562 ],
23563 cssProps: [
23564 { name: "--desktop-mode-window-bg", description: "Pill background." },
23565 { name: "--desktop-mode-text", description: "Active label colour." },
23566 { name: "--desktop-mode-muted", description: "Inactive label colour." }
23567 ],
23568 example: html`
23569 <wpd-segmented value="md" label="Dock size">
23570 <wpd-segment value="sm">Small</wpd-segment>
23571 <wpd-segment value="md">Medium</wpd-segment>
23572 <wpd-segment value="lg">Large</wpd-segment>
23573 </wpd-segmented>
23574 `
23575 };
23576 let WpdSegmented = _WpdSegmented;
23577 defineComponent("wpd-segmented", WpdSegmented);
23578 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}`;
23579 const _WpdButton = class _WpdButton extends Component {
23580 render() {
23581 const disabled = this.disabled !== null;
23582 const type = this.type || "button";
23583 return html`
23584 <button part="button" type=${type} ?disabled=${disabled}>
23585 <slot></slot>
23586 </button>
23587 `;
23588 }
23589 };
23590 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
23591 _WpdButton.styles = [styles$3];
23592 _WpdButton.help = {
23593 title: "Button",
23594 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
23595 status: "stable",
23596 since: "0.9.0",
23597 props: [
23598 {
23599 name: "variant",
23600 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
23601 default: "ghost",
23602 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
23603 },
23604 {
23605 name: "disabled",
23606 type: "boolean attribute",
23607 description: "Disable pointer + keyboard interaction and dim the chrome."
23608 },
23609 {
23610 name: "type",
23611 type: "'button' | 'submit' | 'reset'",
23612 default: "button",
23613 description: "Forwarded to the underlying native <button>."
23614 },
23615 {
23616 name: "busy",
23617 type: "boolean attribute",
23618 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
23619 },
23620 {
23621 name: "fill-cell",
23622 type: "boolean attribute",
23623 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
23624 }
23625 ],
23626 slots: [{ name: "(default)", description: "Button label." }],
23627 parts: [{ name: "button", description: "Underlying <button> element." }],
23628 cssProps: [
23629 { name: "--wpd-button-bg", description: "Background color." },
23630 { name: "--wpd-button-fg", description: "Text color." },
23631 { name: "--wpd-button-border", description: "Border shorthand." },
23632 { name: "--wpd-button-border-radius", default: "6px" },
23633 { name: "--wpd-button-padding", default: "6px 12px" },
23634 {
23635 name: "--wpd-button-min-height",
23636 description: "Minimum height when fill-cell is set."
23637 }
23638 ],
23639 example: html`
23640 <wpd-cluster gap="8">
23641 <wpd-button variant="primary">Primary</wpd-button>
23642 <wpd-button variant="secondary">Secondary</wpd-button>
23643 <wpd-button variant="ghost">Ghost</wpd-button>
23644 <wpd-button variant="danger">Danger</wpd-button>
23645 <wpd-button variant="link">Link</wpd-button>
23646 </wpd-cluster>
23647 `
23648 };
23649 let WpdButton = _WpdButton;
23650 defineComponent("wpd-button", WpdButton);
23651 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}`;
23652 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}}`;
23653 const _WpdToastContainer = class _WpdToastContainer extends Component {
23654 connectedCallback() {
23655 super.connectedCallback();
23656 this.setAttribute("aria-live", "polite");
23657 }
23658 render() {
23659 return html`<slot></slot>`;
23660 }
23661 };
23662 _WpdToastContainer.styles = [containerStyles];
23663 _WpdToastContainer.help = {
23664 title: "Toast container",
23665 summary: "Singleton stack beneath <body> that hosts transient <wpd-toast> notifications in the top-right. Created lazily by showToast(); authors rarely place one themselves.",
23666 status: "stable",
23667 since: "0.9.0",
23668 slots: [
23669 { name: "(default)", description: "<wpd-toast> children, stacked vertically." }
23670 ],
23671 cssProps: [
23672 { name: "--desktop-mode-z-fullscreen", description: "z-index base — toasts sit above fullscreen windows." }
23673 ],
23674 example: html`
23675 <wpd-toast-container>
23676 <wpd-toast state="in">Settings saved.</wpd-toast>
23677 <wpd-toast state="in" action="Undo">Theme changed.</wpd-toast>
23678 </wpd-toast-container>
23679 `
23680 };
23681 let WpdToastContainer = _WpdToastContainer;
23682 defineComponent("wpd-toast-container", WpdToastContainer);
23683 const _WpdToast = class _WpdToast extends Component {
23684 connectedCallback() {
23685 super.connectedCallback();
23686 if (!this.hasAttribute("role")) {
23687 this.setAttribute("role", "status");
23688 }
23689 }
23690 render() {
23691 const action = this.action || "";
23692 return html`
23693 <span class="wpd-toast__label"><slot></slot></span>
23694 <button
23695 type="button"
23696 ?hidden=${!action}
23697 @click=${(e) => this._onAction(e)}
23698 >
23699 ${action}
23700 </button>
23701 `;
23702 }
23703 _onAction(e) {
23704 e.preventDefault();
23705 e.stopPropagation();
23706 this.emit("wpd-toast-action", {});
23707 }
23708 };
23709 _WpdToast.props = ["action", "state"];
23710 _WpdToast.styles = [toastStyles];
23711 _WpdToast.help = {
23712 title: "Toast",
23713 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.',
23714 status: "stable",
23715 since: "0.9.0",
23716 props: [
23717 {
23718 name: "action",
23719 type: "string",
23720 description: "Optional action button label. When set, a button renders on the right and emits wpd-toast-action on click."
23721 },
23722 {
23723 name: "state",
23724 type: "'in' | 'out'",
23725 description: 'Drives the CSS fade transition. Set to "in" when rendered, flip to "out" before removal.'
23726 }
23727 ],
23728 slots: [
23729 { name: "(default)", description: "Message text." }
23730 ],
23731 events: [
23732 {
23733 name: "wpd-toast-action",
23734 description: "Fires when the action button is clicked.",
23735 detail: "{}"
23736 }
23737 ],
23738 example: html`
23739 <wpd-toast state="in" action="Undo">Post moved to trash.</wpd-toast>
23740 `
23741 };
23742 let WpdToast = _WpdToast;
23743 defineComponent("wpd-toast", WpdToast);
23744 function buildCapSegmented(initial, onChange) {
23745 const segmented = document.createElement("wpd-segmented");
23746 segmented.setAttribute("value", initial);
23747 segmented.setAttribute("label", "Capability");
23748 segmented.style.setProperty("--wpd-segmented-bg", "rgba(255,255,255,0.06)");
23749 segmented.style.setProperty(
23750 "--desktop-mode-window-bg",
23751 "var(--wp-admin-theme-color, #2271b1)"
23752 );
23753 segmented.style.setProperty("--desktop-mode-text", "#fff");
23754 segmented.style.setProperty("--desktop-mode-muted", "rgba(255,255,255,0.65)");
23755 const segRead = document.createElement("wpd-segment");
23756 segRead.setAttribute("value", "read");
23757 segRead.textContent = "Read";
23758 segmented.appendChild(segRead);
23759 const segWrite = document.createElement("wpd-segment");
23760 segWrite.setAttribute("value", "write");
23761 segWrite.textContent = "Read + Write";
23762 segmented.appendChild(segWrite);
23763 segmented.addEventListener("wpd-pick", (e) => {
23764 const detail = e.detail;
23765 onChange(detail.value);
23766 });
23767 return segmented;
23768 }
23769 function buildIconButton(label, onClick, opts = {}) {
23770 const btn = document.createElement("wpd-button");
23771 btn.setAttribute("variant", "ghost");
23772 btn.setAttribute("aria-label", opts.danger ? "Remove" : "Dismiss");
23773 btn.textContent = label;
23774 const fg = opts.danger ? "#ff8080" : "rgba(255,255,255,0.75)";
23775 const border = opts.danger ? "1px solid rgba(255,128,128,0.45)" : "1px solid rgba(255,255,255,0.18)";
23776 btn.style.setProperty("--wpd-button-fg", fg);
23777 btn.style.setProperty("--wpd-button-border", border);
23778 btn.style.setProperty("--wpd-button-padding", "6px 12px");
23779 btn.style.setProperty("--wpd-button-border-radius", "7px");
23780 btn.style.setProperty("--wpd-button-min-height", "34px");
23781 btn.style.minWidth = "34px";
23782 btn.style.fontSize = "18px";
23783 btn.style.lineHeight = "1";
23784 btn.addEventListener("click", onClick);
23785 return btn;
23786 }
23787 async function openShareSettingsModal(opts) {
23788 const modal = document.createElement("wpd-modal");
23789 modal.setAttribute("open", "");
23790 modal.setAttribute("size", "lg");
23791 modal.setAttribute("title", `Share "${opts.folderName}"`);
23792 document.body.appendChild(modal);
23793 let shares = [];
23794 let pendingPicks = [];
23795 const renderBody = () => {
23796 modal.innerHTML = "";
23797 const owner = document.createElement("div");
23798 owner.style.cssText = "opacity:0.7;margin-bottom:14px;font-size:12px;";
23799 owner.textContent = opts.ownerName ? `Owner: ${opts.ownerName} — cannot be changed` : "Owner cannot be changed";
23800 modal.appendChild(owner);
23801 const addPeople = document.createElement("div");
23802 addPeople.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
23803 const addPeopleLabel = document.createElement("div");
23804 addPeopleLabel.textContent = "Add people";
23805 addPeopleLabel.style.cssText = "font-weight:600;";
23806 addPeople.appendChild(addPeopleLabel);
23807 const userSearch = document.createElement("wpd-user-search");
23808 const excludedUserIds = shares.filter((s) => s.principalType === "user").map((s) => s.principalRef).concat(pendingPicks.filter((p) => p.kind === "user").map((p) => p.ref));
23809 userSearch.setAttribute("exclude", excludedUserIds.join(","));
23810 userSearch.setAttribute("placeholder", "Search users…");
23811 userSearch.addEventListener("wpd-user-pick", (e) => {
23812 const detail = e.detail;
23813 pendingPicks.push({
23814 kind: "user",
23815 ref: String(detail.user.id),
23816 label: detail.user.name,
23817 cap: "read"
23818 });
23819 renderBody();
23820 });
23821 addPeople.appendChild(userSearch);
23822 modal.appendChild(addPeople);
23823 const addRoles = document.createElement("div");
23824 addRoles.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
23825 const addRolesLabel = document.createElement("div");
23826 addRolesLabel.textContent = "Add roles";
23827 addRolesLabel.style.cssText = "font-weight:600;";
23828 addRoles.appendChild(addRolesLabel);
23829 const rolePicker = document.createElement("wpd-role-picker");
23830 const grantedRoles = shares.filter((s) => s.principalType === "role").map((s) => s.principalRef);
23831 const pickedRoles = pendingPicks.filter((p) => p.kind === "role").map((p) => p.ref);
23832 rolePicker.setAttribute("selected", [...grantedRoles, ...pickedRoles].join(","));
23833 rolePicker.addEventListener("wpd-role-toggle", (e) => {
23834 const detail = e.detail;
23835 const existing = shares.find(
23836 (s) => s.principalType === "role" && s.principalRef === detail.slug
23837 );
23838 if (existing) {
23839 if (!detail.selected) {
23840 void revoke(existing);
23841 }
23842 return;
23843 }
23844 if (detail.selected) {
23845 const eligible = (window.desktopModeConfig?.shareEligibleRoles ?? []).find(
23846 (r) => r.slug === detail.slug
23847 );
23848 pendingPicks.push({
23849 kind: "role",
23850 ref: detail.slug,
23851 label: eligible ? eligible.name : detail.slug,
23852 cap: "read"
23853 });
23854 } else {
23855 pendingPicks = pendingPicks.filter(
23856 (p) => !(p.kind === "role" && p.ref === detail.slug)
23857 );
23858 }
23859 renderBody();
23860 });
23861 addRoles.appendChild(rolePicker);
23862 modal.appendChild(addRoles);
23863 if (pendingPicks.length > 0) {
23864 const pendingBlock = document.createElement("div");
23865 pendingBlock.style.cssText = "border:1px dashed rgba(255,255,255,0.18);border-radius:8px;padding:10px;margin-bottom:14px;";
23866 const pendingTitle = document.createElement("div");
23867 pendingTitle.textContent = "New invites (not sent yet)";
23868 pendingTitle.style.cssText = "font-weight:600;margin-bottom:6px;font-size:12px;";
23869 pendingBlock.appendChild(pendingTitle);
23870 for (const pick of pendingPicks) {
23871 const row = document.createElement("div");
23872 row.style.cssText = "display:flex;align-items:center;gap:8px;padding:4px 0;font-size:13px;";
23873 const tag = document.createElement("span");
23874 tag.textContent = pick.kind === "role" ? `Role: ${pick.label}` : pick.label;
23875 tag.style.flex = "1";
23876 row.appendChild(tag);
23877 const capSeg = buildCapSegmented(pick.cap, (next) => {
23878 pick.cap = next;
23879 });
23880 row.appendChild(capSeg);
23881 const removeBtn = buildIconButton("×", () => {
23882 pendingPicks = pendingPicks.filter(
23883 (p) => !(p.kind === pick.kind && p.ref === pick.ref)
23884 );
23885 renderBody();
23886 });
23887 row.appendChild(removeBtn);
23888 pendingBlock.appendChild(row);
23889 }
23890 const sendBtn = document.createElement("wpd-button");
23891 sendBtn.setAttribute("variant", "primary");
23892 sendBtn.textContent = `Send ${pendingPicks.length} invite${pendingPicks.length === 1 ? "" : "s"}`;
23893 sendBtn.style.marginTop = "8px";
23894 sendBtn.addEventListener("click", async () => {
23895 if (pendingPicks.length === 0) {
23896 return;
23897 }
23898 sendBtn.setAttribute("busy", "");
23899 sendBtn.setAttribute("disabled", "");
23900 const snapshot = pendingPicks.slice();
23901 let succeeded = 0;
23902 let firstError = null;
23903 for (const pick of snapshot) {
23904 try {
23905 await inviteShare(opts.folderId, {
23906 principalType: pick.kind,
23907 principalRef: pick.ref,
23908 capability: pick.cap
23909 });
23910 succeeded++;
23911 } catch (err) {
23912 firstError = err;
23913 break;
23914 }
23915 }
23916 if (succeeded > 0) {
23917 pendingPicks = pendingPicks.slice(succeeded);
23918 }
23919 try {
23920 await refresh();
23921 } catch (_e) {
23922 }
23923 if (firstError) {
23924 showToast({
23925 message: `Could not send invites: ${firstError.message}`
23926 });
23927 } else {
23928 showToast({
23929 message: 1 === succeeded ? "Invite sent." : `${succeeded} invites sent.`
23930 });
23931 }
23932 sendBtn.removeAttribute("busy");
23933 sendBtn.removeAttribute("disabled");
23934 renderBody();
23935 });
23936 pendingBlock.appendChild(sendBtn);
23937 modal.appendChild(pendingBlock);
23938 }
23939 const listTitle = document.createElement("div");
23940 listTitle.textContent = "Who has access";
23941 listTitle.style.cssText = "font-weight:600;margin:8px 0 6px;";
23942 modal.appendChild(listTitle);
23943 if (shares.length === 0) {
23944 const empty = document.createElement("div");
23945 empty.textContent = "Only you can see this folder.";
23946 empty.style.cssText = "opacity:0.6;font-size:12px;";
23947 modal.appendChild(empty);
23948 } else {
23949 for (const s of shares) {
23950 const row = document.createElement("div");
23951 row.style.cssText = "display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.04);";
23952 const label = document.createElement("div");
23953 label.style.flex = "1";
23954 label.textContent = s.principalType === "role" ? `Role: ${s.displayName}` : s.displayName;
23955 if (s.state === "pending") {
23956 const tag = document.createElement("span");
23957 tag.textContent = " · pending";
23958 tag.style.cssText = "opacity:0.6;font-size:12px;";
23959 label.appendChild(tag);
23960 } else if (s.state === "denied") {
23961 const tag = document.createElement("span");
23962 tag.textContent = " · denied";
23963 tag.style.cssText = "color:#d63638;font-size:12px;";
23964 label.appendChild(tag);
23965 }
23966 row.appendChild(label);
23967 const cap = s.capability === "write" ? "write" : "read";
23968 const capSeg = buildCapSegmented(cap, (next) => {
23969 void changeCap(s, next);
23970 });
23971 row.appendChild(capSeg);
23972 const removeBtn = buildIconButton(
23973 "×",
23974 () => {
23975 void revoke(s);
23976 },
23977 { danger: true }
23978 );
23979 row.appendChild(removeBtn);
23980 modal.appendChild(row);
23981 }
23982 }
23983 const footer = document.createElement("div");
23984 footer.setAttribute("slot", "footer");
23985 footer.style.display = "flex";
23986 footer.style.justifyContent = "flex-end";
23987 footer.style.gap = "10px";
23988 footer.style.flexWrap = "wrap";
23989 const doneBtn = document.createElement("wpd-button");
23990 doneBtn.setAttribute("variant", "secondary");
23991 doneBtn.textContent = "Done";
23992 doneBtn.addEventListener("click", () => modal.remove());
23993 footer.appendChild(doneBtn);
23994 modal.appendChild(footer);
23995 };
23996 const refresh = async () => {
23997 try {
23998 const res = await listShares(opts.folderId);
23999 shares = res.shares;
24000 setSharesForFolder(opts.folderId, shares);
24001 } catch (err) {
24002 showToast({
24003 message: `Could not load shares: ${err.message}`
24004 });
24005 }
24006 renderBody();
24007 };
24008 const revoke = async (s) => {
24009 try {
24010 await revokeShare(opts.folderId, s.id);
24011 removeShare(opts.folderId, s.id);
24012 await refresh();
24013 showToast({ message: "Access revoked." });
24014 } catch (err) {
24015 showToast({
24016 message: `Could not revoke: ${err.message}`
24017 });
24018 }
24019 };
24020 const changeCap = async (s, cap) => {
24021 try {
24022 const next = await updateShareCapability(opts.folderId, s.id, cap);
24023 upsertShare(next);
24024 await refresh();
24025 } catch (err) {
24026 showToast({
24027 message: `Could not update capability: ${err.message}`
24028 });
24029 }
24030 };
24031 modal.addEventListener("wpd-modal-cancel", () => modal.remove());
24032 renderBody();
24033 await refresh();
24034 }
24035 function openPendingInviteModal(invite) {
24036 return new Promise((resolve2) => {
24037 const modal = document.createElement("wpd-modal");
24038 modal.setAttribute("open", "");
24039 modal.setAttribute("title", invite.folderName ? `${invite.ownerName ?? "Someone"} shared "${invite.folderName}" with you` : "Folder shared with you");
24040 const body = document.createElement("div");
24041 const capLabel = invite.capability === "write" ? "Read + Write" : "Read";
24042 body.innerHTML = `
24043 <p style="margin: 0 0 12px;">Accept the invite to add this folder to your desktop.</p>
24044 <p style="margin: 0; opacity: 0.75;">Access level: <strong>${capLabel}</strong></p>
24045 `;
24046 modal.appendChild(body);
24047 const footer = document.createElement("div");
24048 footer.setAttribute("slot", "footer");
24049 footer.style.display = "flex";
24050 footer.style.justifyContent = "flex-end";
24051 footer.style.gap = "10px";
24052 footer.style.flexWrap = "wrap";
24053 const laterBtn = document.createElement("wpd-button");
24054 laterBtn.setAttribute("variant", "secondary");
24055 laterBtn.textContent = "Decide later";
24056 laterBtn.addEventListener("click", () => {
24057 modal.remove();
24058 resolve2("dismissed");
24059 });
24060 const denyBtn = document.createElement("wpd-button");
24061 denyBtn.setAttribute("variant", "danger");
24062 denyBtn.textContent = "Deny";
24063 denyBtn.addEventListener("click", async () => {
24064 denyBtn.setAttribute("busy", "");
24065 denyBtn.setAttribute("disabled", "");
24066 try {
24067 await denyShare(invite.folderId, invite.id);
24068 sharesStore().state.deniedFolders.add(invite.folderId);
24069 sharesStore().notify();
24070 modal.remove();
24071 resolve2("denied");
24072 } catch (err) {
24073 showToast({
24074 message: `Could not deny: ${err.message}`
24075 });
24076 denyBtn.removeAttribute("busy");
24077 denyBtn.removeAttribute("disabled");
24078 }
24079 });
24080 const acceptBtn = document.createElement("wpd-button");
24081 acceptBtn.setAttribute("variant", "primary");
24082 acceptBtn.textContent = "Accept";
24083 acceptBtn.addEventListener("click", async () => {
24084 acceptBtn.setAttribute("busy", "");
24085 acceptBtn.setAttribute("disabled", "");
24086 try {
24087 await acceptShare(invite.folderId, invite.id);
24088 try {
24089 const res = await listPlacements(0);
24090 setFolderPlacements(0, res.placements);
24091 } catch (_e) {
24092 }
24093 modal.remove();
24094 resolve2("accepted");
24095 } catch (err) {
24096 showToast({
24097 message: `Could not accept: ${err.message}`
24098 });
24099 acceptBtn.removeAttribute("busy");
24100 acceptBtn.removeAttribute("disabled");
24101 }
24102 });
24103 footer.appendChild(laterBtn);
24104 footer.appendChild(denyBtn);
24105 footer.appendChild(acceptBtn);
24106 modal.appendChild(footer);
24107 modal.addEventListener("wpd-modal-cancel", () => {
24108 modal.remove();
24109 resolve2("dismissed");
24110 });
24111 document.body.appendChild(modal);
24112 });
24113 }
24114 function viewerId() {
24115 return Number(window.desktopModeConfig?.currentUserId ?? 0);
24116 }
24117 function sharingEnabled$1() {
24118 const settings = window.wp?.desktop?.getOsSettings?.();
24119 if (!settings) {
24120 return true;
24121 }
24122 return settings.foldersSharingEnabled !== false;
24123 }
24124 function folderOwnerId(folderId) {
24125 const folder = getFilesState().folders.get(folderId);
24126 return folder ? Number(folder.ownerId) : 0;
24127 }
24128 function folderIdFromBaseId(baseId) {
24129 if (typeof baseId !== "string") {
24130 return null;
24131 }
24132 const m = /^desktop-mode-folder-(\d+)$/.exec(baseId);
24133 return m ? Number(m[1]) : null;
24134 }
24135 function placementFolderId(placement) {
24136 if (placement.file.type !== "folder") {
24137 return null;
24138 }
24139 const ref = Number(placement.file.ref);
24140 if (!Number.isFinite(ref) || ref <= 0) {
24141 return null;
24142 }
24143 return ref;
24144 }
24145 function placementOwnerId(placement) {
24146 return Number(placement.file.ownerId ?? 0);
24147 }
24148 function installShareMenuItems() {
24149 addFilter(
24150 "desktop-mode.files.tile-menu",
24151 "desktop-mode/folder-share",
24152 (items, placement) => {
24153 if (!sharingEnabled$1()) {
24154 return items;
24155 }
24156 const folderId = placementFolderId(placement);
24157 if (folderId === null) {
24158 return items;
24159 }
24160 const ownerId = folderOwnerId(folderId) || placementOwnerId(placement);
24161 const viewer = viewerId();
24162 if (ownerId === viewer) {
24163 const shared = !!placement.file.shareSummary?.shared;
24164 const label = shared ? "Manage sharing…" : "Share folder…";
24165 items.push({
24166 id: "desktop-mode/folder-share",
24167 label,
24168 icon: "dashicons-share",
24169 sort: 30,
24170 onClick: () => {
24171 void openShareSettingsModal({
24172 folderId,
24173 folderName: placement.file.title || `Folder ${folderId}`
24174 });
24175 }
24176 });
24177 } else if (ownerId > 0) {
24178 items.push({
24179 id: "desktop-mode/folder-leave",
24180 label: "Leave shared folder",
24181 icon: "dashicons-exit",
24182 sort: 80,
24183 danger: true,
24184 onClick: async () => {
24185 const ok = await wpdConfirm$1({
24186 title: "Leave this folder?",
24187 message: "The folder will be removed from your desktop. The original and its contents are not deleted; the owner keeps them.",
24188 confirmLabel: "Leave",
24189 danger: true
24190 });
24191 if (!ok) {
24192 return;
24193 }
24194 try {
24195 await leaveShare(folderId);
24196 removePlacement(placement.id);
24197 try {
24198 const res = await listPlacements(0);
24199 setFolderPlacements(0, res.placements);
24200 } catch (_e) {
24201 }
24202 const winId = `desktop-mode-folder-${folderId}`;
24203 const mgr = window.desktopMode?.windowManager;
24204 mgr?.close?.(winId);
24205 showToast({ message: "You left the shared folder." });
24206 } catch (err) {
24207 showToast({
24208 message: `Could not leave: ${err.message}`
24209 });
24210 }
24211 }
24212 });
24213 }
24214 return items;
24215 }
24216 );
24217 registerTitleBarButton({
24218 id: "desktop-mode/folder-share",
24219 label: "Share folder",
24220 icon: "dashicons-share",
24221 placement: "right",
24222 order: 50,
24223 match: (w) => {
24224 if (!sharingEnabled$1()) {
24225 return false;
24226 }
24227 const base = w.config.baseId ?? w.id;
24228 const folderId = folderIdFromBaseId(base);
24229 if (folderId === null) {
24230 return false;
24231 }
24232 return folderOwnerId(folderId) === viewerId();
24233 },
24234 onClick: (w) => {
24235 const base = w.config.baseId ?? w.id;
24236 const folderId = folderIdFromBaseId(base);
24237 if (folderId === null) {
24238 return;
24239 }
24240 void openShareSettingsModal({
24241 folderId,
24242 folderName: w.config.title || `Folder ${folderId}`
24243 });
24244 }
24245 });
24246 addAction(
24247 "desktop-mode.files.tile-rendered",
24248 "desktop-mode/folder-share",
24249 (payload) => {
24250 const { tile: tile2, placement } = payload;
24251 if (placement.file.type !== "folder") {
24252 return;
24253 }
24254 const summary = placement.file.shareSummary;
24255 if (!summary?.shared) {
24256 return;
24257 }
24258 if (tile2.querySelector(".desktop-mode-file-tile__share-badge")) {
24259 return;
24260 }
24261 const badge = document.createElement("span");
24262 badge.className = "desktop-mode-file-tile__share-badge dashicons dashicons-share";
24263 badge.setAttribute("aria-label", "Shared folder");
24264 badge.title = "Shared folder";
24265 badge.style.cssText = [
24266 "position:absolute",
24267 "top:6px",
24268 "inset-inline-end:6px",
24269 "background:rgba(0,0,0,0.55)",
24270 "color:#fff",
24271 "border-radius:50%",
24272 "width:18px",
24273 "height:18px",
24274 "font-size:12px",
24275 "line-height:18px",
24276 "text-align:center",
24277 "pointer-events:none"
24278 ].join(";");
24279 tile2.appendChild(badge);
24280 }
24281 );
24282 }
24283 const prompted = /* @__PURE__ */ new Set();
24284 function sharingEnabled() {
24285 const settings = window.wp?.desktop?.getOsSettings?.();
24286 if (!settings) {
24287 return true;
24288 }
24289 return settings.foldersSharingEnabled !== false;
24290 }
24291 function installShareInviteBanner() {
24292 const store2 = sharesStore();
24293 const handle = (state2) => {
24294 if (!sharingEnabled()) {
24295 return;
24296 }
24297 for (const invite of state2.pending) {
24298 if (prompted.has(invite.id)) {
24299 continue;
24300 }
24301 prompted.add(invite.id);
24302 void openPendingInviteModal({
24303 id: invite.id,
24304 folderId: invite.folderId,
24305 folderName: invite.folderName,
24306 ownerName: invite.ownerName,
24307 capability: invite.capability
24308 }).then((decision) => {
24309 if (decision === "accepted") {
24310 dropPending(invite.id);
24311 } else if (decision === "denied") {
24312 dropPending(invite.id, { denied: true, folderId: invite.folderId });
24313 }
24314 });
24315 }
24316 };
24317 store2.subscribe(handle);
24318 handle(store2.state);
24319 }
24320 registerBuiltInFileTypes();
24321 registerBuiltInFileOpeners();
24322 installEmbedPersistence();
24323 registerFileAssociationsTab();
24324 installShareMenuItems();
24325 const seededPending = window.desktopModeConfig?.serverPendingShares;
24326 if (Array.isArray(seededPending) && seededPending.length > 0) {
24327 ingestPendingInvites(seededPending);
24328 }
24329 installShareInviteBanner();
24330 const filesApi = {
24331 DesktopFile,
24332 registerType,
24333 unregisterType,
24334 getType,
24335 getTypes,
24336 resolve,
24337 subscribe,
24338 registerOpener,
24339 unregisterOpener,
24340 getOpener,
24341 getOpeners,
24342 getOpenersForType,
24343 resolveOpener,
24344 subscribeOpeners,
24345 getUserAssociations,
24346 open: openFile,
24347 rest: filesRest,
24348 store: {
24349 get: getFilesStore,
24350 getState: getFilesState,
24351 subscribe: subscribeFilesStore,
24352 setFolderPlacements,
24353 upsertPlacement,
24354 removePlacement,
24355 setFolders,
24356 upsertFolder,
24357 removeFolder
24358 }
24359 };
24360 const SYNTH_META_KEY = "__synthFromDockItem";
24361 function hashToNegativeId(s) {
24362 let h = 0;
24363 for (let i = 0; i < s.length; i++) {
24364 h = (h * 31 + s.charCodeAt(i)) % 2147483647;
24365 }
24366 return -(h + 1);
24367 }
24368 function buildSyntheticPlacement(item, persistedPositions) {
24369 const saved = persistedPositions[item.id];
24370 return {
24371 id: hashToNegativeId(item.id),
24372 parentId: 0,
24373 x: saved ? saved.x : 0,
24374 y: saved ? saved.y : 0,
24375 sortOrder: 9999,
24376 updatedAtMs: Date.now(),
24377 meta: { [SYNTH_META_KEY]: item.id },
24378 file: {
24379 type: "shortcut",
24380 ref: `dock-promoted:${item.id}`,
24381 title: item.title,
24382 icon: item.icon,
24383 previewUrl: "",
24384 exists: true,
24385 // The shortcut opener (built-in-openers.ts) reads these
24386 // off the file shape — `shortcutUrl` is what a dock-item
24387 // promotion naturally has.
24388 shortcutUrl: item.url
24389 }
24390 };
24391 }
24392 function readDockItems() {
24393 const api = window.wp?.desktop;
24394 if (api?.getMenuItems) {
24395 const items = api.getMenuItems();
24396 return items.map((i) => ({
24397 id: i.id,
24398 title: i.title,
24399 icon: i.icon,
24400 url: i.url,
24401 badge: i.badge ?? 0,
24402 submenu: i.submenu ?? []
24403 }));
24404 }
24405 const cfg = window.desktopModeConfig;
24406 return cfg?.dockItems ?? [];
24407 }
24408 function readServerIcons() {
24409 const cfg = window.desktopModeConfig;
24410 return cfg?.desktopIcons ?? [];
24411 }
24412 let reentrant = false;
24413 const removedServerPlacementsByRef = /* @__PURE__ */ new Map();
24414 function syncShortcutsWithVisibility(visibility, positions = {}) {
24415 if (reentrant) {
24416 return;
24417 }
24418 reentrant = true;
24419 try {
24420 const dockItems = readDockItems();
24421 const serverIcons = readServerIcons();
24422 const state2 = filesApi.store.getState();
24423 const root = state2.placementsByFolder.get(0) ?? [];
24424 const currentSynth = /* @__PURE__ */ new Map();
24425 for (const p of root) {
24426 const sourceId = (p.meta ?? null) && typeof p.meta === "object" ? p.meta[SYNTH_META_KEY] : null;
24427 if (typeof sourceId === "string") {
24428 currentSynth.set(sourceId, p);
24429 }
24430 }
24431 const realByRef = /* @__PURE__ */ new Map();
24432 const registeredIconIds = new Set(
24433 serverIcons.map((i) => i.id)
24434 );
24435 for (const p of root) {
24436 const ref = p?.file?.ref;
24437 if (typeof ref === "string" && registeredIconIds.has(ref)) {
24438 realByRef.set(ref, p);
24439 }
24440 }
24441 const desiredSynth = /* @__PURE__ */ new Set();
24442 for (const item of dockItems) {
24443 const placement = visibility[item.id];
24444 if (placement === "desktop" || placement === "both") {
24445 desiredSynth.add(item.id);
24446 if (!currentSynth.has(item.id)) {
24447 filesApi.store.upsertPlacement(
24448 buildSyntheticPlacement(item, positions)
24449 );
24450 }
24451 }
24452 }
24453 for (const [sourceId, p] of currentSynth) {
24454 if (!desiredSynth.has(sourceId)) {
24455 filesApi.store.removePlacement(p.id);
24456 }
24457 }
24458 for (const icon of serverIcons) {
24459 const placement = visibility[icon.id];
24460 const inStore = realByRef.get(icon.id);
24461 if (placement === "dock" || placement === "hidden") {
24462 if (inStore) {
24463 removedServerPlacementsByRef.set(icon.id, inStore);
24464 filesApi.store.removePlacement(inStore.id);
24465 }
24466 continue;
24467 }
24468 if (!inStore) {
24469 const cached = removedServerPlacementsByRef.get(icon.id);
24470 if (cached) {
24471 filesApi.store.upsertPlacement(cached);
24472 removedServerPlacementsByRef.delete(icon.id);
24473 }
24474 }
24475 }
24476 } finally {
24477 reentrant = false;
24478 }
24479 }
24480 function installShortcutsSync(getVisibility, getPositions = () => ({})) {
24481 queueMicrotask(
24482 () => syncShortcutsWithVisibility(getVisibility(), getPositions())
24483 );
24484 const off = filesApi.store.subscribe(() => {
24485 syncShortcutsWithVisibility(getVisibility(), getPositions());
24486 });
24487 return off;
24488 }
24489 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%}`;
24490 const DEFAULT_EVENT = "desktop-mode-os-settings-save-lifecycle";
24491 const DEFAULT_AUTO_CLEAR_SAVED_MS = 2200;
24492 const DEFAULT_AUTO_CLEAR_FAILED_MS = 6e3;
24493 const _WpdSaveStatus = class _WpdSaveStatus extends Component {
24494 constructor() {
24495 super(...arguments);
24496 this._autoTimer = null;
24497 this._docListener = null;
24498 }
24499 connectedCallback() {
24500 super.connectedCallback();
24501 if (this.auto !== null) {
24502 this._installAutoListener();
24503 }
24504 }
24505 disconnectedCallback() {
24506 this._removeAutoListener();
24507 if (this._autoTimer !== null) {
24508 window.clearTimeout(this._autoTimer);
24509 this._autoTimer = null;
24510 }
24511 }
24512 attributeChangedCallback(name, oldValue, newValue) {
24513 super.attributeChangedCallback(name, oldValue, newValue);
24514 if (name === "auto" || name === "event") {
24515 this._removeAutoListener();
24516 if (this.auto !== null) {
24517 this._installAutoListener();
24518 }
24519 }
24520 if (name === "phase") {
24521 this._scheduleAutoClear();
24522 const detail = {
24523 phase: this.phase ?? "idle",
24524 error: this.error ?? void 0
24525 };
24526 this.emit("wpd-save-status-change", detail);
24527 }
24528 }
24529 render() {
24530 const phase = this.phase ?? "idle";
24531 const mode = this.mode ?? "dot";
24532 const error = this.error ?? "";
24533 const title = error || this._labelForPhase(phase);
24534 if (title) {
24535 this.setAttribute("title", title);
24536 } else {
24537 this.removeAttribute("title");
24538 }
24539 this.setAttribute("aria-live", phase === "failed" ? "assertive" : "polite");
24540 this.setAttribute("role", phase === "failed" ? "alert" : "status");
24541 return html`
24542 <span class="wpd-save-status">
24543 <span class="wpd-save-status__indicator" aria-hidden="true">
24544 <span class="wpd-save-status__glyph">${this._renderGlyph(phase)}</span>
24545 </span>
24546 ${mode === "pill" ? html`<span class="wpd-save-status__label"
24547 >${this._labelForPhase(phase)}</span
24548 >` : html``}
24549 </span>
24550 `;
24551 }
24552 _renderGlyph(phase) {
24553 if (phase === "saved") {
24554 return _iconCheck();
24555 }
24556 if (phase === "failed") {
24557 return _iconBang();
24558 }
24559 return "";
24560 }
24561 _labelForPhase(phase) {
24562 switch (phase) {
24563 case "pending":
24564 case "saving":
24565 return this["saving-label"] ?? "Saving…";
24566 case "saved":
24567 return this["saved-label"] ?? "Saved";
24568 case "failed": {
24569 const err = this.error ?? "";
24570 return err || "Couldn’t save";
24571 }
24572 default:
24573 return this["idle-label"] ?? "";
24574 }
24575 }
24576 _installAutoListener() {
24577 const eventName = this.event || DEFAULT_EVENT;
24578 this._docListener = (e) => {
24579 const detail = e.detail;
24580 if (!detail || typeof detail.phase !== "string") {
24581 return;
24582 }
24583 this.phase = detail.phase;
24584 if (detail.error) {
24585 this.error = detail.error;
24586 } else if (detail.phase !== "failed" && this.error) {
24587 this.removeAttribute("error");
24588 }
24589 };
24590 document.addEventListener(eventName, this._docListener);
24591 }
24592 _removeAutoListener() {
24593 if (!this._docListener) {
24594 return;
24595 }
24596 const eventName = this.event || DEFAULT_EVENT;
24597 document.removeEventListener(eventName, this._docListener);
24598 this._docListener = null;
24599 }
24600 _scheduleAutoClear() {
24601 if (this._autoTimer !== null) {
24602 window.clearTimeout(this._autoTimer);
24603 this._autoTimer = null;
24604 }
24605 const phase = this.phase ?? "idle";
24606 const ms = this._autoClearMsFor(phase);
24607 if (ms <= 0) {
24608 return;
24609 }
24610 this._autoTimer = window.setTimeout(() => {
24611 this._autoTimer = null;
24612 this.phase = "idle";
24613 }, ms);
24614 }
24615 _autoClearMsFor(phase) {
24616 if (phase === "saved") {
24617 const raw = this["auto-clear-saved-ms"];
24618 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_SAVED_MS;
24619 }
24620 if (phase === "failed") {
24621 const raw = this["auto-clear-failed-ms"];
24622 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_FAILED_MS;
24623 }
24624 return 0;
24625 }
24626 };
24627 _WpdSaveStatus.props = [
24628 "phase",
24629 "mode",
24630 "animation",
24631 "auto",
24632 "event",
24633 "error",
24634 "saving-label",
24635 "saved-label",
24636 "idle-label",
24637 "auto-clear-saved-ms",
24638 "auto-clear-failed-ms"
24639 ];
24640 _WpdSaveStatus.styles = [styles$2];
24641 _WpdSaveStatus.help = {
24642 title: "Save status",
24643 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.',
24644 status: "experimental",
24645 since: "0.8.0",
24646 props: [
24647 {
24648 name: "phase",
24649 type: "'idle' | 'pending' | 'saving' | 'saved' | 'failed'",
24650 default: "idle",
24651 description: "Current lifecycle phase. Set manually for one-off integrations, or rely on `auto` to populate it from a CustomEvent."
24652 },
24653 {
24654 name: "mode",
24655 type: "'dot' | 'icon' | 'pill'",
24656 default: "dot",
24657 description: "Layout. `dot` is the smallest (10×10 colored dot); `icon` adds a glyph inside on saved/failed; `pill` adds an inline label."
24658 },
24659 {
24660 name: "animation",
24661 type: "'pulse' | 'modem'",
24662 default: "pulse",
24663 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."
24664 },
24665 {
24666 name: "auto",
24667 type: "boolean attribute",
24668 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="…"`.'
24669 },
24670 {
24671 name: "event",
24672 type: "string",
24673 default: "desktop-mode-os-settings-save-lifecycle",
24674 description: "CustomEvent name to listen on when `auto` is set."
24675 },
24676 {
24677 name: "error",
24678 type: "string",
24679 description: "Error message shown in `pill` mode and exposed as the host title attribute (so dot/icon modes still surface the message via tooltip)."
24680 },
24681 {
24682 name: "saving-label",
24683 type: "string",
24684 default: "Saving…",
24685 description: "Pill-mode label shown during `pending` / `saving`."
24686 },
24687 {
24688 name: "saved-label",
24689 type: "string",
24690 default: "Saved",
24691 description: "Pill-mode label shown during `saved`."
24692 },
24693 {
24694 name: "idle-label",
24695 type: "string",
24696 description: 'Optional pill-mode label shown during `idle` (e.g. "All changes saved"). When unset, the pill collapses to invisible while idle.'
24697 },
24698 {
24699 name: "auto-clear-saved-ms",
24700 type: "integer",
24701 default: "2200",
24702 description: "How long the `saved` phase stays visible before auto-fading back to `idle`."
24703 },
24704 {
24705 name: "auto-clear-failed-ms",
24706 type: "integer",
24707 default: "6000",
24708 description: "How long the `failed` phase stays visible before auto-fading back to `idle`."
24709 }
24710 ],
24711 events: [
24712 {
24713 name: "wpd-save-status-change",
24714 description: "Fires when the phase changes (manually or via auto-listen).",
24715 detail: "{ phase, error }"
24716 }
24717 ],
24718 cssProps: [
24719 {
24720 name: "--wpd-save-status-bg",
24721 description: "Indicator background color (saving/pending phase)."
24722 },
24723 {
24724 name: "--wpd-save-status-saved-bg",
24725 description: "Indicator background on saved."
24726 },
24727 {
24728 name: "--wpd-save-status-failed-bg",
24729 description: "Indicator background on failed."
24730 },
24731 {
24732 name: "--wpd-save-status-pill-bg",
24733 description: "Pill background (mode=pill)."
24734 },
24735 {
24736 name: "--wpd-save-status-pill-fg",
24737 description: "Pill foreground (mode=pill)."
24738 }
24739 ],
24740 example: html`
24741 <wpd-cluster gap="12">
24742 <wpd-save-status phase="pending"></wpd-save-status>
24743 <wpd-save-status phase="saving"></wpd-save-status>
24744 <wpd-save-status phase="saved"></wpd-save-status>
24745 <wpd-save-status phase="failed"></wpd-save-status>
24746 <wpd-save-status mode="pill" phase="saving"></wpd-save-status>
24747 <wpd-save-status mode="pill" phase="saved"></wpd-save-status>
24748 <wpd-save-status mode="pill" phase="failed" error="Network error."></wpd-save-status>
24749 </wpd-cluster>
24750 `
24751 };
24752 let WpdSaveStatus = _WpdSaveStatus;
24753 defineComponent("wpd-save-status", WpdSaveStatus);
24754 function _iconCheck() {
24755 return html`
24756 <svg
24757 viewBox="0 0 12 12"
24758 aria-hidden="true"
24759 focusable="false"
24760 fill="none"
24761 stroke="currentColor"
24762 stroke-width="2"
24763 stroke-linecap="round"
24764 stroke-linejoin="round"
24765 >
24766 <path d="M2.5 6 L5 8.5 L9.5 4" />
24767 </svg>
24768 `;
24769 }
24770 function _iconBang() {
24771 return html`
24772 <svg
24773 viewBox="0 0 12 12"
24774 aria-hidden="true"
24775 focusable="false"
24776 fill="currentColor"
24777 >
24778 <path
24779 d="M5 2 H7 V7 H5 z M5 8.5 H7 V10.5 H5 z"
24780 />
24781 </svg>
24782 `;
24783 }
24784 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}`;
24785 const _WpdTextarea = class _WpdTextarea extends Component {
24786 constructor() {
24787 super(...arguments);
24788 this._textareaEl = null;
24789 }
24790 connectedCallback() {
24791 super.connectedCallback();
24792 ensureAutoId(this);
24793 }
24794 render() {
24795 const label = this._attr("label") || "";
24796 const value = this._attr("value") ?? "";
24797 const placeholder = this._attr("placeholder") || "";
24798 const disabled = this._boolAttr("disabled");
24799 const readonly = this._boolAttr("readonly");
24800 const ariaLabel = this._attr("aria-label") || label;
24801 const name = this._attr("name") || "";
24802 const rows = Number(this._attr("rows")) || 3;
24803 const maxLength = this._attr("maxlength");
24804 const minLength = this._attr("minlength");
24805 const invalid = this._boolAttr("invalid");
24806 const hostId = this.id || "wpd-unnamed";
24807 const fieldId = `${hostId}__field`;
24808 return html`
24809 ${label ? html`<label class="wpd-textarea__label" for=${fieldId}>${label}</label>` : html``}
24810 <textarea
24811 id=${fieldId}
24812 part="textarea"
24813 .value=${value}
24814 placeholder=${placeholder}
24815 ?disabled=${disabled}
24816 ?readonly=${readonly}
24817 rows=${rows}
24818 maxlength=${maxLength ?? ""}
24819 minlength=${minLength ?? ""}
24820 name=${name}
24821 aria-invalid=${invalid ? "true" : "false"}
24822 aria-label=${ariaLabel || ""}
24823 @input=${(e) => this._onInput(e)}
24824 @change=${(e) => this._onChange(e)}
24825 @keydown=${(e) => this._onKeyDown(e)}
24826 ></textarea>
24827 `;
24828 }
24829 _attr(name) {
24830 return this.getAttribute(name);
24831 }
24832 _boolAttr(name) {
24833 return this.getAttribute(name) !== null;
24834 }
24835 _onInput(e) {
24836 const ta = e.target;
24837 this._textareaEl = ta;
24838 this.setAttribute("value", ta.value);
24839 this.emit("wpd-input-change", { value: ta.value });
24840 if (this._boolAttr("auto-grow")) {
24841 this._autosize(ta);
24842 }
24843 }
24844 _onChange(e) {
24845 const ta = e.target;
24846 this.emit("wpd-input-commit", { value: ta.value });
24847 }
24848 _onKeyDown(e) {
24849 if (!this._boolAttr("submit-on-enter")) {
24850 return;
24851 }
24852 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
24853 e.preventDefault();
24854 const ta = e.target;
24855 this.emit("wpd-submit", { value: ta.value });
24856 }
24857 }
24858 /**
24859 * Grow the textarea height to fit content, capped at `max-rows`.
24860 * Resets to scroll-height each input then clamps; cheap because
24861 * the browser caches layout.
24862 */
24863 _autosize(ta) {
24864 const maxRows = Number(this._attr("max-rows")) || 8;
24865 const cs = window.getComputedStyle(ta);
24866 const fontSize = parseFloat(cs.fontSize) || 13;
24867 const lineHeightRaw = cs.lineHeight;
24868 const lineHeight = lineHeightRaw === "normal" ? fontSize * 1.45 : parseFloat(lineHeightRaw) || fontSize * 1.45;
24869 const paddingTop = parseFloat(cs.paddingTop) || 0;
24870 const paddingBottom = parseFloat(cs.paddingBottom) || 0;
24871 const max = lineHeight * maxRows + paddingTop + paddingBottom;
24872 ta.style.height = "auto";
24873 const next = Math.min(ta.scrollHeight, max);
24874 ta.style.height = `${next}px`;
24875 }
24876 /** Public helper for callers that programmatically set `.value` and want autosize to re-run. */
24877 refreshAutosize() {
24878 if (this._textareaEl && this._boolAttr("auto-grow")) {
24879 this._autosize(this._textareaEl);
24880 }
24881 }
24882 /** Imperatively focus the underlying textarea. */
24883 focusInput() {
24884 const root = this.shadowRoot ?? this;
24885 const ta = root.querySelector("textarea");
24886 ta?.focus();
24887 }
24888 /** Imperatively clear the value. */
24889 clear() {
24890 this.setAttribute("value", "");
24891 const root = this.shadowRoot ?? this;
24892 const ta = root.querySelector("textarea");
24893 if (ta) {
24894 ta.value = "";
24895 if (this._boolAttr("auto-grow")) {
24896 this._autosize(ta);
24897 }
24898 }
24899 }
24900 };
24901 _WpdTextarea.props = [
24902 "label",
24903 "value",
24904 "placeholder",
24905 "disabled",
24906 "readonly",
24907 "ariaLabel",
24908 "name",
24909 "rows",
24910 "maxlength",
24911 "minlength",
24912 "invalid",
24913 "autoGrow",
24914 "maxRows",
24915 "submitOnEnter"
24916 ];
24917 _WpdTextarea.styles = [textareaStyles];
24918 _WpdTextarea.help = {
24919 title: "Textarea",
24920 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).",
24921 status: "stable",
24922 since: "0.22.0",
24923 props: [
24924 { name: "label", type: "string", description: "Visible label above the textarea." },
24925 { name: "value", type: "string", description: "Current value; reflected two-way." },
24926 { name: "placeholder", type: "string", description: "Native placeholder." },
24927 { name: "disabled", type: "boolean attribute" },
24928 { name: "readonly", type: "boolean attribute" },
24929 { name: "aria-label", type: "string", description: "Accessible label when no visible label is rendered." },
24930 { name: "name", type: "string", description: "Forwarded to native textarea for form submission." },
24931 { name: "rows", type: "integer (string)", default: "3", description: "Initial visible row count." },
24932 { name: "maxlength", type: "integer (string)" },
24933 { name: "minlength", type: "integer (string)" },
24934 { name: "invalid", type: "boolean attribute", description: "Sets aria-invalid + error styling." },
24935 { name: "auto-grow", type: "boolean attribute", description: "Grows up to max-rows as the user types." },
24936 { name: "max-rows", type: "integer (string)", default: "8" },
24937 {
24938 name: "submit-on-enter",
24939 type: "boolean attribute",
24940 description: "Enter fires wpd-submit; Shift+Enter inserts a newline."
24941 }
24942 ],
24943 events: [
24944 { name: "wpd-input-change", description: "Fires on every keystroke.", detail: "{ value: string }" },
24945 { name: "wpd-input-commit", description: "Fires on blur / native change.", detail: "{ value: string }" },
24946 {
24947 name: "wpd-submit",
24948 description: "Fires on Enter (without Shift) when submit-on-enter is set.",
24949 detail: "{ value: string }"
24950 }
24951 ],
24952 example: html`
24953 <wpd-textarea label="Message" rows="3" auto-grow max-rows="8" submit-on-enter></wpd-textarea>
24954 `
24955 };
24956 let WpdTextarea = _WpdTextarea;
24957 defineComponent("wpd-textarea", WpdTextarea);
24958 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}`;
24959 const ICONS = {
24960 minimize: '<path d="M3 6h6" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>',
24961 maximize: '<rect x="3" y="3" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.25" fill="none"/>',
24962 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"/>',
24963 "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"/>',
24964 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"/>',
24965 reload: (
24966 // Filled icon scaled from a 512×512 source into the 12×12 viewBox
24967 // shared with the other title-bar glyphs. The wrapping `<g>` does
24968 // the math; the inner path is dropped in unmodified so its
24969 // authoring tool can be re-edited and copy-pasted again.
24970 // `scale(0.021)` ≈ 90% of full fit, with `translate(0.6)` to
24971 // keep the result centered inside the 12×12 viewBox so the
24972 // glyph reads slightly smaller than min/max/close — closer to
24973 // the visual weight of the other title-bar buttons.
24974 '<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>'
24975 ),
24976 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"/>',
24977 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"/>'
24978 };
24979 const _WpdWindowButton = class _WpdWindowButton extends Component {
24980 constructor() {
24981 super(...arguments);
24982 this._activateWired = false;
24983 }
24984 render() {
24985 const iconKey = this.icon || "";
24986 const svgInner = ICONS[iconKey] || "";
24987 return html`
24988 <button type="button">
24989 <svg
24990 width="14"
24991 height="14"
24992 viewBox="0 0 12 12"
24993 aria-hidden="true"
24994 focusable="false"
24995 ></svg>
24996 <slot></slot>
24997 </button>
24998 <span data-svg-buffer style="display:none">${svgInner}</span>
24999 `;
25000 }
25001 /**
25002 * After each render, copy the raw SVG markup into the actual
25003 * `<svg>` element. The templater only writes text into slots,
25004 * so we stash the intended markup in a hidden buffer and
25005 * `innerHTML = ` the svg once here — a one-shot post-render
25006 * hook that keeps the declarative template honest.
25007 *
25008 * Also wires up the `wpd-button-activate` CustomEvent that
25009 * fires exactly once per gesture — the canonical contract
25010 * for plugin-registered title-bar buttons. Plugin authors who
25011 * use `addEventListener( 'click', cb )` directly still get
25012 * what they expect (the title bar's drag-handler now excludes
25013 * chrome buttons by class so static clicks land normally),
25014 * but `wpd-button-activate` is the documented surface that
25015 * documents the once-per-gesture contract explicitly. See
25016 * the class-level docblock for rationale.
25017 */
25018 connectedCallback() {
25019 super.connectedCallback();
25020 queueMicrotask(() => this._paintSvg());
25021 queueMicrotask(() => this._wireActivateEvent());
25022 }
25023 attributeChangedCallback(name, oldValue, newValue) {
25024 super.attributeChangedCallback(name, oldValue, newValue);
25025 queueMicrotask(() => this._paintSvg());
25026 }
25027 _paintSvg() {
25028 const root = this.shadowRoot;
25029 if (!root) {
25030 return;
25031 }
25032 const svg = root.querySelector("svg");
25033 const buffer = root.querySelector("[data-svg-buffer]");
25034 if (svg && buffer) {
25035 const markup = buffer.textContent || "";
25036 if (svg.innerHTML !== markup) {
25037 svg.innerHTML = markup;
25038 }
25039 }
25040 }
25041 _wireActivateEvent() {
25042 if (this._activateWired) {
25043 return;
25044 }
25045 const root = this.shadowRoot;
25046 if (!root) {
25047 return;
25048 }
25049 const button = root.querySelector("button");
25050 if (!button) {
25051 return;
25052 }
25053 this._activateWired = true;
25054 button.addEventListener("click", () => {
25055 this.dispatchEvent(
25056 new CustomEvent("wpd-button-activate", {
25057 bubbles: true,
25058 composed: true,
25059 cancelable: true
25060 })
25061 );
25062 });
25063 }
25064 };
25065 _WpdWindowButton.props = ["icon", "active", "danger"];
25066 _WpdWindowButton.styles = [styles$1];
25067 _WpdWindowButton.help = {
25068 title: "Window button",
25069 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.",
25070 status: "stable",
25071 since: "0.9.0",
25072 props: [
25073 {
25074 name: "icon",
25075 type: "'minimize' | 'maximize' | 'fullscreen' | 'fullscreen-exit' | 'detach' | 'reload' | 'close' | 'menu'",
25076 description: "Which built-in inline SVG to paint. Omit to supply your own via the slot."
25077 },
25078 {
25079 name: "active",
25080 type: "boolean attribute",
25081 description: "Applies the pressed-down look (used e.g. while a menu it triggers is open)."
25082 },
25083 {
25084 name: "danger",
25085 type: "boolean attribute",
25086 description: "Swaps the hover wash to red — used by the close button."
25087 }
25088 ],
25089 slots: [
25090 { name: "(default)", description: "Optional custom icon markup (inline SVG) when `icon` is omitted." }
25091 ],
25092 cssProps: [
25093 { name: "--wpd-btn-color", description: "Resting foreground." },
25094 { name: "--wpd-btn-color-hover", description: "Hover foreground." },
25095 { name: "--wpd-btn-bg-hover", description: "Hover background wash." },
25096 { name: "--wpd-btn-bg-active", description: "Pressed background." },
25097 { name: "--wpd-btn-danger-hover", description: "Hover background for danger variant." },
25098 { name: "--wpd-btn-outline", description: "Focus outline colour." }
25099 ],
25100 example: html`
25101 <wpd-cluster gap="2">
25102 <wpd-window-button icon="minimize"></wpd-window-button>
25103 <wpd-window-button icon="maximize"></wpd-window-button>
25104 <wpd-window-button icon="menu"></wpd-window-button>
25105 <wpd-window-button icon="close" danger></wpd-window-button>
25106 </wpd-cluster>
25107 `
25108 };
25109 let WpdWindowButton = _WpdWindowButton;
25110 defineComponent("wpd-window-button", WpdWindowButton);
25111 const DEFAULT_STICKY_TITLE = "Sticky Note";
25112 const LEGACY_METADATA_PREFIX = "<!-- wpworkspace-sticky:";
25113 const LEGACY_METADATA_SUFFIX = "-->";
25114 const TITLE_MAX = 64;
25115 const GENERATED_TITLE_MAX = 48;
25116 const EXCERPT_MAX = 180;
25117 function noteFromGuideline(guideline) {
25118 const title = titleField(guideline.title);
25119 const content = removeLegacyMetadataComment(
25120 textFieldValue(guideline.content, { stripHtmlForRendered: true })
25121 );
25122 const modifiedMs = modifiedTimeMs(guideline);
25123 return {
25124 localId: `guideline:${guideline.id}`,
25125 guidelineId: guideline.id,
25126 title,
25127 body: editorBody(title, content),
25128 modified: guideline.modified,
25129 ...modifiedMs > 0 ? { modifiedMs } : {},
25130 link: guideline.link,
25131 termIds: Array.isArray(guideline.wp_guideline_type) ? guideline.wp_guideline_type.filter(isFiniteNumber) : []
25132 };
25133 }
25134 function titleField(field) {
25135 const candidates = [];
25136 if (typeof field === "string") {
25137 candidates.push(field);
25138 } else if (field && typeof field === "object") {
25139 if (typeof field.raw === "string") {
25140 candidates.push(field.raw);
25141 }
25142 if (typeof field.rendered === "string") {
25143 candidates.push(stripHtml(field.rendered));
25144 }
25145 }
25146 for (const candidate of candidates) {
25147 const trimmed = stripHtml(candidate).trim();
25148 if (trimmed) {
25149 return trimmed;
25150 }
25151 }
25152 return DEFAULT_STICKY_TITLE;
25153 }
25154 function textFieldValue(field, options = {}) {
25155 if (typeof field === "string") {
25156 return field;
25157 }
25158 if (!field || typeof field !== "object") {
25159 return "";
25160 }
25161 if (typeof field.raw === "string" && field.raw.length > 0) {
25162 return field.raw;
25163 }
25164 if (typeof field.rendered === "string") {
25165 return options.stripHtmlForRendered ? stripHtml(field.rendered) : field.rendered;
25166 }
25167 return "";
25168 }
25169 function titleForBody(body) {
25170 const line = body.split(/\r?\n/).find((item) => item.trim().length > 0)?.trim();
25171 const title = line && line.length > 0 ? line : DEFAULT_STICKY_TITLE;
25172 return truncate(title, TITLE_MAX);
25173 }
25174 function generatedTitle(body) {
25175 const collapsed = body.replace(/\s+/g, " ").trim();
25176 const title = collapsed || DEFAULT_STICKY_TITLE;
25177 return truncate(title, GENERATED_TITLE_MAX);
25178 }
25179 function editorBody(title, content) {
25180 const trimmedTitle = title.trim();
25181 if (!trimmedTitle) {
25182 return content;
25183 }
25184 const firstLine = content.split(/\r?\n/)[0]?.trim();
25185 if (firstLine === trimmedTitle) {
25186 return content;
25187 }
25188 if (!content) {
25189 return trimmedTitle;
25190 }
25191 return `${trimmedTitle}
25192 ${content}`;
25193 }
25194 function noteComponentsForBody(editorValue, fallbackTitle = DEFAULT_STICKY_TITLE) {
25195 const fallback = fallbackTitle.trim() || DEFAULT_STICKY_TITLE;
25196 const title = titleForBody(editorValue);
25197 const firstNewline = editorValue.search(/\r?\n/);
25198 if (firstNewline === -1) {
25199 const resolvedTitle = title === DEFAULT_STICKY_TITLE ? fallback : title;
25200 return {
25201 title: resolvedTitle,
25202 content: "",
25203 excerpt: excerptFor(resolvedTitle)
25204 };
25205 }
25206 let content = editorValue.slice(firstNewline);
25207 content = content.replace(/^\r?\n/, "");
25208 if (content.startsWith("\n")) {
25209 content = content.slice(1);
25210 }
25211 return {
25212 title,
25213 content,
25214 excerpt: excerptFor(content.trim() ? content : title)
25215 };
25216 }
25217 function excerptFor(body) {
25218 const collapsed = body.replace(/[\n\t]+/g, " ").trim();
25219 return truncate(collapsed, EXCERPT_MAX);
25220 }
25221 function removeLegacyMetadataComment(content) {
25222 if (!content.startsWith(LEGACY_METADATA_PREFIX) || !content.includes(LEGACY_METADATA_SUFFIX)) {
25223 return content;
25224 }
25225 const end = content.indexOf(LEGACY_METADATA_SUFFIX);
25226 let body = content.slice(end + LEGACY_METADATA_SUFFIX.length);
25227 if (body.startsWith("\r\n")) {
25228 body = body.slice(2);
25229 } else if (body.startsWith("\n")) {
25230 body = body.slice(1);
25231 }
25232 return body;
25233 }
25234 function stripHtml(value) {
25235 if (typeof document !== "undefined") {
25236 const template = document.createElement("template");
25237 template.innerHTML = value;
25238 return (template.content.textContent ?? "").trim();
25239 }
25240 return value.replace(/<[^>]*>/g, "").trim();
25241 }
25242 function truncate(value, max) {
25243 return value.length > max ? `${value.slice(0, max)}...` : value;
25244 }
25245 function modifiedTimeMs(guideline) {
25246 if (typeof guideline.desktop_mode_modified_ms === "number" && Number.isFinite(guideline.desktop_mode_modified_ms)) {
25247 return guideline.desktop_mode_modified_ms;
25248 }
25249 if (!guideline.modified) {
25250 return 0;
25251 }
25252 const parsed = Date.parse(guideline.modified);
25253 return Number.isFinite(parsed) ? parsed : 0;
25254 }
25255 function isFiniteNumber(value) {
25256 return typeof value === "number" && Number.isFinite(value);
25257 }
25258 class StickyNotesRestError extends Error {
25259 constructor(message, status) {
25260 super(message);
25261 this.name = "StickyNotesRestError";
25262 this.status = status;
25263 }
25264 }
25265 async function resolveStickyTerms(config) {
25266 const terms = await fetchStickyTermCandidates(config);
25267 const picked = pickStickyTerms(
25268 [...terms.artifactTerms, ...terms.artifactsTerms],
25269 terms.noteTerms,
25270 terms.stickyTerms
25271 );
25272 if (picked) {
25273 return picked;
25274 }
25275 const artifact = await ensureTerm(config, {
25276 slug: "artifact",
25277 name: "Artifact",
25278 parent: 0
25279 });
25280 const note = await ensureTerm(config, {
25281 slug: "note",
25282 name: "Note",
25283 parent: artifact.id
25284 });
25285 const sticky = await ensureTerm(config, {
25286 slug: "sticky",
25287 name: "Sticky",
25288 parent: artifact.id
25289 });
25290 return {
25291 stickyTermId: sticky.id,
25292 termIds: uniqueNumbers([artifact.id, note.id, sticky.id])
25293 };
25294 }
25295 async function fetchStickyTermCandidates(config) {
25296 const [artifactTerms, artifactsTerms, noteTerms, stickyTerms] = await Promise.all([
25297 fetchTermsBySlug(config, "artifact"),
25298 fetchTermsBySlug(config, "artifacts"),
25299 fetchTermsBySlug(config, "note"),
25300 fetchTermsBySlug(config, "sticky")
25301 ]);
25302 return {
25303 artifactTerms,
25304 artifactsTerms,
25305 noteTerms,
25306 stickyTerms
25307 };
25308 }
25309 function pickStickyTerms(artifactTerms, noteTerms, stickyTerms) {
25310 if (stickyTerms.length === 0) {
25311 return null;
25312 }
25313 const artifact = artifactTerms.find(
25314 (term) => ["artifact", "artifacts"].includes(term.slug)
25315 ) ?? artifactTerms[0] ?? null;
25316 const sticky = artifact ? stickyTerms.find((term) => Number(term.parent) === artifact.id) ?? stickyTerms[0] : stickyTerms[0];
25317 if (!sticky) {
25318 return null;
25319 }
25320 const note = artifact ? noteTerms.find((term) => Number(term.parent) === artifact.id) ?? null : null;
25321 return {
25322 stickyTermId: sticky.id,
25323 termIds: uniqueNumbers([
25324 artifact?.id,
25325 note?.id,
25326 sticky.id
25327 ])
25328 };
25329 }
25330 async function fetchStickyNotes(config, stickyTermId) {
25331 const guidelines = await requestJson(
25332 config,
25333 pathWithQuery("wp/v2/guidelines", {
25334 context: "edit",
25335 status: "private",
25336 per_page: "100",
25337 orderby: "modified",
25338 order: "desc",
25339 wp_guideline_type: String(stickyTermId)
25340 }),
25341 void 0,
25342 true
25343 );
25344 return guidelines.filter(
25345 (guideline) => Array.isArray(guideline.wp_guideline_type) ? guideline.wp_guideline_type.includes(stickyTermId) : true
25346 ).map(noteFromGuideline);
25347 }
25348 async function saveStickyNote(config, note, terms) {
25349 const components = noteComponentsForBody(note.body, note.title);
25350 const payload = {
25351 status: "private",
25352 title: components.title,
25353 content: components.content,
25354 excerpt: components.excerpt
25355 };
25356 if (note.guidelineId === null) {
25357 payload.wp_guideline_type = terms.termIds;
25358 }
25359 const path = note.guidelineId === null ? "wp/v2/guidelines" : `wp/v2/guidelines/${note.guidelineId}`;
25360 const guideline = await requestJson(
25361 config,
25362 path,
25363 {
25364 method: "POST",
25365 headers: {
25366 "Content-Type": "application/json"
25367 },
25368 body: JSON.stringify(payload)
25369 },
25370 false
25371 );
25372 return noteFromGuideline(guideline);
25373 }
25374 function buildGuidelineEditUrl(adminUrl, guidelineId) {
25375 const url = new URL("post.php", adminUrl);
25376 url.searchParams.set("post", String(guidelineId));
25377 url.searchParams.set("action", "edit");
25378 return url.toString();
25379 }
25380 async function fetchTermsBySlug(config, slug) {
25381 try {
25382 return await requestJson(
25383 config,
25384 pathWithQuery("wp/v2/wp_guideline_type", {
25385 context: "edit",
25386 slug,
25387 per_page: "100"
25388 }),
25389 void 0,
25390 true
25391 );
25392 } catch (error) {
25393 if (error instanceof StickyNotesRestError && (error.status === 404 || error.status === 400)) {
25394 return [];
25395 }
25396 throw error;
25397 }
25398 }
25399 async function ensureTerm(config, term) {
25400 const existing = await fetchTermsBySlug(config, term.slug);
25401 const byParent = existing.find(
25402 (item) => Number(item.parent ?? 0) === term.parent
25403 );
25404 if (byParent) {
25405 return byParent;
25406 }
25407 if (existing[0]) {
25408 return existing[0];
25409 }
25410 try {
25411 return await requestJson(
25412 config,
25413 "wp/v2/wp_guideline_type",
25414 {
25415 method: "POST",
25416 headers: {
25417 "Content-Type": "application/json"
25418 },
25419 body: JSON.stringify(term)
25420 },
25421 true
25422 );
25423 } catch (error) {
25424 const fallback = await fetchTermsBySlug(config, term.slug);
25425 if (fallback[0]) {
25426 return fallback[0];
25427 }
25428 throw error;
25429 }
25430 }
25431 async function requestJson(config, path, init2, silent = true) {
25432 const response = await trackedFetch$1(
25433 joinRestUrl(restRoot(config), path),
25434 init2,
25435 {
25436 source: "desktop-mode/sticky-notes",
25437 silent
25438 }
25439 );
25440 if (!response.ok) {
25441 throw new StickyNotesRestError(
25442 response.statusText || `${DEFAULT_STICKY_TITLE} request failed`,
25443 response.status
25444 );
25445 }
25446 return await response.json();
25447 }
25448 function restRoot(config) {
25449 if (config.restUrl) {
25450 return config.restUrl;
25451 }
25452 return `${window.location.origin}/wp-json/`;
25453 }
25454 function pathWithQuery(path, query) {
25455 const params = new URLSearchParams();
25456 Object.entries(query).forEach(([key, value]) => {
25457 params.set(key, value);
25458 });
25459 return `${path}?${params.toString()}`;
25460 }
25461 function uniqueNumbers(values) {
25462 const out = [];
25463 values.forEach((value) => {
25464 if (typeof value === "number" && Number.isFinite(value) && !out.includes(value)) {
25465 out.push(value);
25466 }
25467 });
25468 return out;
25469 }
25470 const SUBSCRIBE_FIELD = "desktop_mode_sticky_notes_subscribe";
25471 const RESPONSE_FIELD = "desktop_mode_sticky_notes";
25472 let started$3 = false;
25473 let target = null;
25474 function startStickyNotesHeartbeat(nextTarget) {
25475 target = nextTarget;
25476 if (started$3) {
25477 return;
25478 }
25479 started$3 = true;
25480 heartbeat.contribute(
25481 SUBSCRIBE_FIELD,
25482 () => target?.getHeartbeatSubscription()
25483 );
25484 heartbeat.subscribe(
25485 RESPONSE_FIELD,
25486 (payload) => {
25487 target?.applyHeartbeatPayload(payload);
25488 }
25489 );
25490 }
25491 const GEOMETRY_KEY = "desktop-mode-sticky-notes-geometry";
25492 const DEFAULT_WIDTH = 264;
25493 const DEFAULT_HEIGHT = 176;
25494 const MIN_WIDTH = 180;
25495 const MIN_HEIGHT = 128;
25496 const EDGE_PADDING = 16;
25497 const SAVE_DEBOUNCE_MS = 1e3;
25498 class StickyNotesLayer {
25499 constructor(options) {
25500 this.root = null;
25501 this.terms = null;
25502 this.controllers = /* @__PURE__ */ new Map();
25503 this.contextMenuInstalled = false;
25504 this.desktopHooksInstalled = false;
25505 this.highWaterMs = 0;
25506 this.zIndexCounter = 0;
25507 this.host = options.host;
25508 this.config = options.config;
25509 this.openArtifact = options.openArtifact;
25510 this.getActiveDesktopId = options.getActiveDesktopId ?? (() => "desktop-1");
25511 this.onError = options.onError;
25512 }
25513 async boot() {
25514 try {
25515 this.terms = await resolveStickyTerms(this.config);
25516 if (!this.terms) {
25517 return;
25518 }
25519 this.installContextMenu();
25520 this.installDesktopHooks();
25521 const notes = await fetchStickyNotes(
25522 this.config,
25523 this.terms.stickyTermId
25524 );
25525 this.bumpHighWaterFromNotes(notes);
25526 startStickyNotesHeartbeat(this);
25527 if (notes.length === 0) {
25528 return;
25529 }
25530 this.ensureRoot();
25531 sortNotesByModified(notes).forEach(
25532 (note, index2) => this.upsert(note, index2)
25533 );
25534 } catch (error) {
25535 if (error instanceof Error) {
25536 console.debug("[desktop-mode] Sticky notes unavailable:", error.message);
25537 }
25538 }
25539 }
25540 createNote(body = "") {
25541 if (!this.terms) {
25542 return;
25543 }
25544 const note = {
25545 localId: `local:${Date.now()}:${Math.random().toString(36).slice(2)}`,
25546 guidelineId: null,
25547 title: body.trim() ? generatedTitle(body) : DEFAULT_STICKY_TITLE,
25548 body,
25549 termIds: this.terms.termIds
25550 };
25551 const controller = this.upsert(note, this.controllers.size, {
25552 activate: true
25553 });
25554 controller.focus();
25555 }
25556 upsert(note, index2, options = {}) {
25557 this.ensureRoot();
25558 const key = noteKey(note);
25559 const existing = this.controllers.get(key);
25560 if (existing) {
25561 existing.replace(note);
25562 if (options.activate) {
25563 this.bringToFront(existing);
25564 }
25565 return existing;
25566 }
25567 const controller = new StickyNoteController({
25568 layer: this,
25569 note,
25570 index: index2
25571 });
25572 this.controllers.set(key, controller);
25573 this.root?.appendChild(controller.element);
25574 this.assignZIndex(controller);
25575 this.applyDesktopVisibility(controller);
25576 if (options.activate) {
25577 this.bringToFront(controller);
25578 }
25579 return controller;
25580 }
25581 ensureRoot() {
25582 if (this.root) {
25583 return this.root;
25584 }
25585 const root = document.createElement("section");
25586 root.className = "desktop-mode-sticky-notes";
25587 root.setAttribute("aria-label", __("Sticky notes"));
25588 this.host.appendChild(root);
25589 this.root = root;
25590 return root;
25591 }
25592 installContextMenu() {
25593 if (this.contextMenuInstalled) {
25594 return;
25595 }
25596 this.contextMenuInstalled = true;
25597 addFilter(
25598 "desktop-mode.wallpaper-context-menu",
25599 "desktop-mode/sticky-notes",
25600 (items) => {
25601 if (!Array.isArray(items) || !this.terms) {
25602 return items;
25603 }
25604 if (items.some(
25605 (item) => item.id === "new-sticky-note"
25606 )) {
25607 return items;
25608 }
25609 return [
25610 ...items,
25611 {
25612 id: "new-sticky-note",
25613 label: __("New sticky note"),
25614 icon: "dashicons-edit-page",
25615 sort: 14,
25616 onClick: () => this.createNote()
25617 }
25618 ];
25619 }
25620 );
25621 }
25622 installDesktopHooks() {
25623 if (this.desktopHooksInstalled) {
25624 return;
25625 }
25626 this.desktopHooksInstalled = true;
25627 addAction(
25628 HOOKS.DESKTOP_SWITCHED,
25629 "desktop-mode/sticky-notes",
25630 () => this.refreshDesktopVisibility()
25631 );
25632 addAction(
25633 HOOKS.DESKTOP_CLOSED,
25634 "desktop-mode/sticky-notes",
25635 (detail) => {
25636 this.migrateDesktopAssignments(detail?.desktopId, detail?.migratedTo);
25637 this.refreshDesktopVisibility();
25638 }
25639 );
25640 }
25641 save(note) {
25642 if (!this.terms) {
25643 return Promise.reject(new Error(__("Sticky term is unavailable.")));
25644 }
25645 return saveStickyNote(this.config, note, this.terms);
25646 }
25647 getHeartbeatSubscription() {
25648 if (!this.terms) {
25649 return void 0;
25650 }
25651 return {
25652 stickyTermId: this.terms.stickyTermId,
25653 knownIds: this.knownGuidelineIds(),
25654 version: this.highWaterMs
25655 };
25656 }
25657 applyHeartbeatPayload(payload) {
25658 for (const guideline of payload.notes ?? []) {
25659 const note = noteFromGuideline(guideline);
25660 this.upsertRemote(note);
25661 }
25662 for (const id of payload.removed ?? []) {
25663 this.forgetGuidelineId(id);
25664 }
25665 if (typeof payload.serverTimeMs === "number" && Number.isFinite(payload.serverTimeMs) && payload.serverTimeMs > this.highWaterMs) {
25666 this.highWaterMs = payload.serverTimeMs;
25667 }
25668 if (payload.truncated) {
25669 void this.reloadFromServer();
25670 }
25671 }
25672 openNoteArtifact(note) {
25673 if (note.guidelineId === null) {
25674 return;
25675 }
25676 this.openArtifact(
25677 buildGuidelineEditUrl(this.config.adminUrl, note.guidelineId),
25678 note.title,
25679 note.guidelineId
25680 );
25681 }
25682 notifyError(message) {
25683 this.onError?.(message);
25684 }
25685 hostSize() {
25686 return {
25687 width: Math.max(1, this.host.clientWidth),
25688 height: Math.max(1, this.host.clientHeight)
25689 };
25690 }
25691 defaultGeometry(index2) {
25692 const { width: hostWidth, height: hostHeight } = this.hostSize();
25693 const width = Math.min(
25694 DEFAULT_WIDTH,
25695 Math.max(MIN_WIDTH, hostWidth - EDGE_PADDING * 2)
25696 );
25697 const height = Math.min(
25698 DEFAULT_HEIGHT,
25699 Math.max(MIN_HEIGHT, hostHeight - EDGE_PADDING * 2)
25700 );
25701 const offset = index2 % 8 * 28;
25702 const left = clamp(
25703 hostWidth - width - 32 - offset,
25704 EDGE_PADDING,
25705 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
25706 );
25707 const top = clamp(
25708 32 + offset,
25709 EDGE_PADDING,
25710 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
25711 );
25712 return {
25713 x: left / hostWidth,
25714 y: top / hostHeight,
25715 width,
25716 height
25717 };
25718 }
25719 forget(controller) {
25720 this.controllers.delete(noteKey(controller.note));
25721 controller.dispose();
25722 controller.element.remove();
25723 if (this.controllers.size === 0) {
25724 this.root?.remove();
25725 this.root = null;
25726 }
25727 }
25728 replaceControllerKey(oldKey, controller) {
25729 const newKey = noteKey(controller.note);
25730 this.controllers.delete(oldKey);
25731 this.controllers.set(newKey, controller);
25732 moveStoredGeometry(oldKey, newKey);
25733 this.applyDesktopVisibility(controller);
25734 }
25735 bumpHighWaterFromNote(note) {
25736 const modifiedMs = noteModifiedMs(note);
25737 if (modifiedMs > this.highWaterMs) {
25738 this.highWaterMs = modifiedMs;
25739 }
25740 }
25741 bringToFront(controller) {
25742 controller.setZIndex(this.nextZIndex());
25743 }
25744 geometryForNote(note, index2) {
25745 const key = noteKey(note);
25746 const loaded = loadGeometry(key);
25747 const desktopId = this.normalizeDesktopId(loaded?.desktopId);
25748 const geometry = loaded ? { ...loaded, desktopId } : { ...this.defaultGeometry(index2), desktopId };
25749 if (!loaded || loaded.desktopId !== geometry.desktopId) {
25750 saveGeometry(key, geometry);
25751 }
25752 return geometry;
25753 }
25754 upsertRemote(note) {
25755 const key = noteKey(note);
25756 const existing = this.controllers.get(key);
25757 if (existing) {
25758 if (!existing.shouldReplaceFromRemote(note)) {
25759 this.bumpHighWaterFromNote(note);
25760 return existing;
25761 }
25762 existing.replace(note);
25763 this.bumpHighWaterFromNote(note);
25764 return existing;
25765 }
25766 const controller = this.upsert(note, this.controllers.size);
25767 this.bumpHighWaterFromNote(note);
25768 return controller;
25769 }
25770 forgetGuidelineId(guidelineId) {
25771 for (const controller of this.controllers.values()) {
25772 if (controller.note.guidelineId === guidelineId) {
25773 this.forget(controller);
25774 return;
25775 }
25776 }
25777 }
25778 knownGuidelineIds() {
25779 const ids = [];
25780 for (const controller of this.controllers.values()) {
25781 if (controller.note.guidelineId !== null) {
25782 ids.push(controller.note.guidelineId);
25783 }
25784 }
25785 return ids;
25786 }
25787 bumpHighWaterFromNotes(notes) {
25788 notes.forEach((note) => this.bumpHighWaterFromNote(note));
25789 }
25790 assignZIndex(controller) {
25791 controller.setZIndex(this.nextZIndex());
25792 }
25793 nextZIndex() {
25794 this.zIndexCounter += 1;
25795 return this.zIndexCounter;
25796 }
25797 applyDesktopVisibility(controller) {
25798 controller.setVisible(this.isNoteOnActiveDesktop(controller.note));
25799 }
25800 refreshDesktopVisibility() {
25801 for (const controller of this.controllers.values()) {
25802 this.applyDesktopVisibility(controller);
25803 }
25804 }
25805 isNoteOnActiveDesktop(note) {
25806 const key = noteKey(note);
25807 const geometry = loadGeometry(key);
25808 const desktopId = this.normalizeDesktopId(geometry?.desktopId);
25809 if (geometry && geometry.desktopId !== desktopId) {
25810 saveGeometry(key, { ...geometry, desktopId });
25811 }
25812 return desktopId === this.activeDesktopId();
25813 }
25814 migrateDesktopAssignments(desktopId, migratedTo) {
25815 if (!desktopId || !migratedTo || desktopId === migratedTo) {
25816 return;
25817 }
25818 const map = readGeometryMap();
25819 let changed = false;
25820 Object.entries(map).forEach(([key, geometry]) => {
25821 if (geometry.desktopId === desktopId) {
25822 map[key] = {
25823 ...geometry,
25824 desktopId: this.normalizeDesktopId(migratedTo)
25825 };
25826 changed = true;
25827 }
25828 });
25829 if (changed) {
25830 writeGeometryMap(map);
25831 }
25832 }
25833 activeDesktopId() {
25834 try {
25835 const id = this.getActiveDesktopId();
25836 return typeof id === "string" && id ? id : "desktop-1";
25837 } catch {
25838 return "desktop-1";
25839 }
25840 }
25841 normalizeDesktopId(desktopId) {
25842 if (!desktopId) {
25843 return this.activeDesktopId();
25844 }
25845 return desktopId;
25846 }
25847 async reloadFromServer() {
25848 if (!this.terms) {
25849 return;
25850 }
25851 try {
25852 const notes = await fetchStickyNotes(
25853 this.config,
25854 this.terms.stickyTermId
25855 );
25856 const ids = /* @__PURE__ */ new Set();
25857 sortNotesByModified(notes).forEach((note) => {
25858 if (note.guidelineId !== null) {
25859 ids.add(note.guidelineId);
25860 }
25861 this.upsertRemote(note);
25862 });
25863 this.knownGuidelineIds().forEach((id) => {
25864 if (!ids.has(id)) {
25865 this.forgetGuidelineId(id);
25866 }
25867 });
25868 } catch {
25869 }
25870 }
25871 }
25872 class StickyNoteController {
25873 constructor(options) {
25874 this.saveTimer = null;
25875 this.geometryTimer = null;
25876 this.saving = false;
25877 this.saveAgain = false;
25878 this.resizeObserver = null;
25879 this.disposed = false;
25880 this.layer = options.layer;
25881 this.note = options.note;
25882 this.index = options.index;
25883 this.element = document.createElement("article");
25884 this.element.className = "desktop-mode-sticky-note";
25885 this.element.dataset.stickyNoteId = noteKey(this.note);
25886 this.titleEl = document.createElement("span");
25887 this.editor = document.createElement("wpd-textarea");
25888 this.statusEl = document.createElement("wpd-save-status");
25889 this.openButton = document.createElement("wpd-window-button");
25890 this.paint();
25891 this.applyGeometry(this.layer.geometryForNote(this.note, this.index));
25892 this.element.addEventListener(
25893 "pointerdown",
25894 () => this.layer.bringToFront(this),
25895 { capture: true }
25896 );
25897 this.element.addEventListener("focusin", () => this.layer.bringToFront(this));
25898 this.watchResize();
25899 }
25900 focus() {
25901 window.setTimeout(() => this.editor.focusInput?.(), 0);
25902 }
25903 replace(note) {
25904 this.note = note;
25905 this.element.dataset.stickyNoteId = noteKey(this.note);
25906 this.titleEl.textContent = this.note.title;
25907 this.editor.setAttribute("value", this.note.body);
25908 this.refreshOpenButton();
25909 }
25910 shouldReplaceFromRemote(note) {
25911 if (this.hasLocalChanges()) {
25912 return false;
25913 }
25914 const currentMs = noteModifiedMs(this.note);
25915 const incomingMs = noteModifiedMs(note);
25916 if (currentMs > 0 && incomingMs > 0 && incomingMs <= currentMs && this.note.title === note.title && this.note.body === note.body) {
25917 return false;
25918 }
25919 return true;
25920 }
25921 setZIndex(zIndex) {
25922 this.element.style.zIndex = String(zIndex);
25923 }
25924 setVisible(visible) {
25925 this.element.style.display = visible ? "" : "none";
25926 }
25927 dispose() {
25928 this.disposed = true;
25929 if (this.saveTimer !== null) {
25930 window.clearTimeout(this.saveTimer);
25931 this.saveTimer = null;
25932 }
25933 if (this.geometryTimer !== null) {
25934 window.clearTimeout(this.geometryTimer);
25935 this.geometryTimer = null;
25936 }
25937 this.resizeObserver?.disconnect();
25938 this.resizeObserver = null;
25939 }
25940 paint() {
25941 this.element.innerHTML = "";
25942 this.element.style.minWidth = `${MIN_WIDTH}px`;
25943 this.element.style.minHeight = `${MIN_HEIGHT}px`;
25944 const header = document.createElement("div");
25945 header.className = "desktop-mode-sticky-note__header";
25946 const grip = document.createElement("span");
25947 grip.className = "desktop-mode-sticky-note__grip";
25948 grip.setAttribute("aria-hidden", "true");
25949 this.titleEl.className = "desktop-mode-sticky-note__title";
25950 this.titleEl.textContent = this.note.title;
25951 this.statusEl.setAttribute("mode", "icon");
25952 this.statusEl.setAttribute("phase", "idle");
25953 this.statusEl.className = "desktop-mode-sticky-note__status";
25954 this.openButton.setAttribute("icon", "detach");
25955 this.openButton.setAttribute("title", __("Open artifact"));
25956 this.openButton.className = "desktop-mode-sticky-note__open";
25957 this.openButton.addEventListener("wpd-button-activate", () => {
25958 this.layer.openNoteArtifact(this.note);
25959 });
25960 const close = document.createElement("wpd-window-button");
25961 close.setAttribute("icon", "close");
25962 close.setAttribute("danger", "");
25963 close.setAttribute("title", __("Hide sticky note"));
25964 close.className = "desktop-mode-sticky-note__close";
25965 close.addEventListener("wpd-button-activate", () => this.close());
25966 header.append(grip, this.titleEl, this.statusEl, this.openButton, close);
25967 header.addEventListener("pointerdown", (event) => this.startDrag(event));
25968 this.editor.className = "desktop-mode-sticky-note__editor";
25969 this.editor.setAttribute("aria-label", __("Sticky note text"));
25970 this.editor.setAttribute("rows", "8");
25971 this.editor.setAttribute("value", this.note.body);
25972 this.installEditorKeyboardGuard();
25973 this.editor.addEventListener("wpd-input-change", (event) => {
25974 const detail = event.detail;
25975 this.note.body = detail.value;
25976 this.note.title = titleForBody(detail.value);
25977 this.titleEl.textContent = this.note.title;
25978 this.setPhase("pending");
25979 this.scheduleSave();
25980 });
25981 this.editor.addEventListener("wpd-input-commit", () => this.flushSave());
25982 this.element.append(header, this.editor);
25983 this.refreshOpenButton();
25984 }
25985 installEditorKeyboardGuard() {
25986 ["keydown", "keypress", "keyup"].forEach((eventName) => {
25987 this.editor.addEventListener(eventName, (event) => {
25988 event.stopPropagation();
25989 });
25990 });
25991 }
25992 refreshOpenButton() {
25993 const disabled = this.note.guidelineId === null;
25994 this.openButton.classList.toggle("is-disabled", disabled);
25995 this.openButton.setAttribute("aria-disabled", disabled ? "true" : "false");
25996 }
25997 close() {
25998 if (this.note.guidelineId === null && this.note.body.trim().length === 0) {
25999 this.layer.forget(this);
26000 return;
26001 }
26002 this.flushSave();
26003 this.layer.forget(this);
26004 }
26005 scheduleSave() {
26006 if (this.note.guidelineId === null && this.note.body.trim().length === 0) {
26007 this.setPhase("idle");
26008 return;
26009 }
26010 if (this.saveTimer !== null) {
26011 window.clearTimeout(this.saveTimer);
26012 }
26013 this.saveTimer = window.setTimeout(() => {
26014 this.saveTimer = null;
26015 void this.save();
26016 }, SAVE_DEBOUNCE_MS);
26017 }
26018 flushSave() {
26019 if (this.saveTimer !== null) {
26020 window.clearTimeout(this.saveTimer);
26021 this.saveTimer = null;
26022 }
26023 if (this.note.guidelineId !== null || this.note.body.trim().length > 0) {
26024 void this.save();
26025 }
26026 }
26027 async save() {
26028 if (this.saving) {
26029 this.saveAgain = true;
26030 this.setPhase("pending");
26031 return;
26032 }
26033 this.saving = true;
26034 this.setPhase("saving");
26035 const bodyAtSave = this.note.body;
26036 try {
26037 const saved = await this.layer.save({
26038 ...this.note,
26039 body: bodyAtSave
26040 });
26041 if (this.disposed) {
26042 return;
26043 }
26044 const oldKey = noteKey(this.note);
26045 this.note.guidelineId = saved.guidelineId;
26046 this.note.modified = saved.modified;
26047 this.note.link = saved.link;
26048 this.note.termIds = saved.termIds.length > 0 ? saved.termIds : this.note.termIds;
26049 if (this.note.body === bodyAtSave) {
26050 this.note.title = saved.title;
26051 this.titleEl.textContent = saved.title;
26052 }
26053 if (oldKey !== noteKey(this.note)) {
26054 this.element.dataset.stickyNoteId = noteKey(this.note);
26055 this.layer.replaceControllerKey(oldKey, this);
26056 }
26057 this.layer.bumpHighWaterFromNote(this.note);
26058 this.refreshOpenButton();
26059 this.setPhase("saved");
26060 } catch (error) {
26061 if (this.disposed) {
26062 return;
26063 }
26064 const message = error instanceof Error ? error.message : __("Could not save sticky note.");
26065 this.setPhase("failed", message);
26066 this.layer.notifyError(message);
26067 } finally {
26068 this.saving = false;
26069 if (!this.disposed && this.saveAgain) {
26070 this.saveAgain = false;
26071 this.scheduleSave();
26072 }
26073 }
26074 }
26075 setPhase(phase, error) {
26076 this.statusEl.setAttribute("phase", phase);
26077 if (error) {
26078 this.statusEl.setAttribute("error", error);
26079 this.statusEl.setAttribute("title", error);
26080 } else {
26081 this.statusEl.removeAttribute("error");
26082 this.statusEl.removeAttribute("title");
26083 }
26084 }
26085 hasLocalChanges() {
26086 const phase = this.statusEl.getAttribute("phase");
26087 return this.saveTimer !== null || this.saving || this.saveAgain || phase === "pending" || phase === "failed";
26088 }
26089 startDrag(event) {
26090 if (event.button !== 0) {
26091 return;
26092 }
26093 const target2 = event.target;
26094 if (target2?.closest("wpd-window-button, wpd-save-status")) {
26095 return;
26096 }
26097 event.preventDefault();
26098 const startRect = this.element.getBoundingClientRect();
26099 const hostRect = this.layerHostRect();
26100 const startLeft = startRect.left - hostRect.left;
26101 const startTop = startRect.top - hostRect.top;
26102 const startX = event.clientX;
26103 const startY = event.clientY;
26104 this.element.classList.add("desktop-mode-sticky-note--dragging");
26105 this.element.setPointerCapture?.(event.pointerId);
26106 const move = (moveEvent) => {
26107 const width = this.element.offsetWidth;
26108 const height = this.element.offsetHeight;
26109 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26110 const left = clamp(
26111 startLeft + moveEvent.clientX - startX,
26112 EDGE_PADDING,
26113 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
26114 );
26115 const top = clamp(
26116 startTop + moveEvent.clientY - startY,
26117 EDGE_PADDING,
26118 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
26119 );
26120 this.element.style.left = `${left}px`;
26121 this.element.style.top = `${top}px`;
26122 };
26123 const up = (upEvent) => {
26124 this.element.classList.remove("desktop-mode-sticky-note--dragging");
26125 this.element.releasePointerCapture?.(upEvent.pointerId);
26126 document.removeEventListener("pointermove", move);
26127 document.removeEventListener("pointerup", up);
26128 this.persistGeometry();
26129 };
26130 document.addEventListener("pointermove", move);
26131 document.addEventListener("pointerup", up);
26132 }
26133 applyGeometry(geometry) {
26134 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26135 const width = clamp(geometry.width, MIN_WIDTH, hostWidth - EDGE_PADDING * 2);
26136 const height = clamp(geometry.height, MIN_HEIGHT, hostHeight - EDGE_PADDING * 2);
26137 const left = clamp(
26138 geometry.x * hostWidth,
26139 EDGE_PADDING,
26140 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
26141 );
26142 const top = clamp(
26143 geometry.y * hostHeight,
26144 EDGE_PADDING,
26145 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
26146 );
26147 this.element.style.left = `${left}px`;
26148 this.element.style.top = `${top}px`;
26149 this.element.style.width = `${width}px`;
26150 this.element.style.height = `${height}px`;
26151 }
26152 watchResize() {
26153 if (typeof ResizeObserver === "undefined") {
26154 return;
26155 }
26156 this.resizeObserver = new ResizeObserver(() => {
26157 if (this.geometryTimer !== null) {
26158 window.clearTimeout(this.geometryTimer);
26159 }
26160 this.geometryTimer = window.setTimeout(() => {
26161 this.geometryTimer = null;
26162 this.persistGeometry();
26163 }, 150);
26164 });
26165 this.resizeObserver.observe(this.element);
26166 }
26167 persistGeometry() {
26168 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26169 const left = parseFloat(this.element.style.left) || 0;
26170 const top = parseFloat(this.element.style.top) || 0;
26171 const existing = loadGeometry(noteKey(this.note));
26172 saveGeometry(noteKey(this.note), {
26173 ...existing ?? {},
26174 x: clamp(left / hostWidth, 0, 1),
26175 y: clamp(top / hostHeight, 0, 1),
26176 width: this.element.offsetWidth,
26177 height: this.element.offsetHeight
26178 });
26179 }
26180 layerHostRect() {
26181 const parent = this.element.parentElement?.parentElement;
26182 return (parent ?? document.body).getBoundingClientRect();
26183 }
26184 }
26185 function bootStickyNotes(options) {
26186 const layer = new StickyNotesLayer(options);
26187 void layer.boot();
26188 return layer;
26189 }
26190 function noteKey(note) {
26191 return note.guidelineId === null ? note.localId : `guideline:${note.guidelineId}`;
26192 }
26193 function noteModifiedMs(note) {
26194 if (typeof note.modifiedMs === "number" && Number.isFinite(note.modifiedMs)) {
26195 return note.modifiedMs;
26196 }
26197 if (!note.modified) {
26198 return 0;
26199 }
26200 const parsed = Date.parse(note.modified);
26201 return Number.isFinite(parsed) ? parsed : 0;
26202 }
26203 function sortNotesByModified(notes) {
26204 return [...notes].sort((a, b) => noteModifiedMs(a) - noteModifiedMs(b));
26205 }
26206 function loadGeometry(key) {
26207 const map = readGeometryMap();
26208 const value = map[key];
26209 if (!value || !Number.isFinite(value.x) || !Number.isFinite(value.y) || !Number.isFinite(value.width) || !Number.isFinite(value.height)) {
26210 return null;
26211 }
26212 return value;
26213 }
26214 function saveGeometry(key, geometry) {
26215 const map = readGeometryMap();
26216 map[key] = geometry;
26217 writeGeometryMap(map);
26218 }
26219 function moveStoredGeometry(oldKey, newKey) {
26220 if (oldKey === newKey) {
26221 return;
26222 }
26223 const map = readGeometryMap();
26224 if (map[oldKey]) {
26225 map[newKey] = map[oldKey];
26226 delete map[oldKey];
26227 writeGeometryMap(map);
26228 }
26229 }
26230 function readGeometryMap() {
26231 try {
26232 const raw = window.localStorage.getItem(GEOMETRY_KEY);
26233 return raw ? JSON.parse(raw) : {};
26234 } catch {
26235 return {};
26236 }
26237 }
26238 function writeGeometryMap(map) {
26239 try {
26240 window.localStorage.setItem(GEOMETRY_KEY, JSON.stringify(map));
26241 } catch {
26242 }
26243 }
26244 function clamp(value, min, max) {
26245 if (max < min) {
26246 return min;
26247 }
26248 return Math.min(max, Math.max(min, value));
26249 }
26250 const clock = {
26251 id: "clock",
26252 // Labels/descriptions on built-in defs stay string-literal at
26253 // module-eval time so the extract-pot pass picks them up. The
26254 // values are wrapped in `__()` so they translate at runtime.
26255 get label() {
26256 return __("Clock");
26257 },
26258 get description() {
26259 return __("Local time and date, refreshed every second.");
26260 },
26261 icon: "dashicons-clock",
26262 mount: (container) => {
26263 container.classList.add("desktop-mode-widget-clock");
26264 const time = document.createElement("div");
26265 time.className = "desktop-mode-widget-clock__time";
26266 container.appendChild(time);
26267 const date = document.createElement("div");
26268 date.className = "desktop-mode-widget-clock__date";
26269 container.appendChild(date);
26270 const render2 = () => {
26271 const now = /* @__PURE__ */ new Date();
26272 time.textContent = now.toLocaleTimeString(void 0, {
26273 hour: "2-digit",
26274 minute: "2-digit"
26275 });
26276 date.textContent = now.toLocaleDateString(void 0, {
26277 weekday: "long",
26278 month: "short",
26279 day: "numeric"
26280 });
26281 };
26282 render2();
26283 const msUntilNextSecond = 1e3 - Date.now() % 1e3;
26284 let interval = null;
26285 const kickoff = window.setTimeout(() => {
26286 render2();
26287 interval = window.setInterval(render2, 1e3);
26288 }, msUntilNextSecond);
26289 return () => {
26290 window.clearTimeout(kickoff);
26291 if (interval !== null) {
26292 window.clearInterval(interval);
26293 }
26294 };
26295 }
26296 };
26297 function registerBuiltInWidgets() {
26298 register(clock);
26299 }
26300 function createWidgetRegistrySync(deps2) {
26301 const { layer } = deps2;
26302 const registered = /* @__PURE__ */ new Set();
26303 const loadedScripts = /* @__PURE__ */ new Set();
26304 const ensureScript = async (entry) => {
26305 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
26306 return;
26307 }
26308 try {
26309 await loadVendorScript(entry.scriptUrl, {
26310 translations: entry.scriptTranslations,
26311 l10n: entry.scriptL10n,
26312 before: entry.scriptBefore,
26313 after: entry.scriptAfter
26314 });
26315 } catch (err) {
26316 doAction(HOOKS.SHELL_ERROR, {
26317 scope: "widget-script-load",
26318 id: entry.id,
26319 error: err
26320 });
26321 }
26322 loadedScripts.add(entry.scriptUrl);
26323 };
26324 const buildDefFromEntry = (entry) => {
26325 const globals = window.desktopModeWidgets || {};
26326 const mount = globals[entry.id];
26327 if (!mount) {
26328 doAction(HOOKS.SHELL_ERROR, {
26329 scope: "widget-missing-mount",
26330 id: entry.id,
26331 error: new Error(
26332 `[desktop-mode] No mount callback on window.desktopModeWidgets["${entry.id}"]. Plugin script loaded but didn't register. Check the plugin's enqueue + global assignment.`
26333 )
26334 });
26335 return null;
26336 }
26337 return {
26338 id: entry.id,
26339 label: entry.label,
26340 description: entry.description,
26341 icon: entry.icon,
26342 movable: entry.movable,
26343 resizable: entry.resizable,
26344 minWidth: entry.minWidth || void 0,
26345 minHeight: entry.minHeight || void 0,
26346 maxWidth: entry.maxWidth || void 0,
26347 maxHeight: entry.maxHeight || void 0,
26348 defaultWidth: entry.defaultWidth || void 0,
26349 defaultHeight: entry.defaultHeight || void 0,
26350 mount
26351 };
26352 };
26353 const registerEntry = async (entry) => {
26354 if (registered.has(entry.id)) {
26355 return;
26356 }
26357 await ensureScript(entry);
26358 const def = buildDefFromEntry(entry);
26359 if (!def) {
26360 return;
26361 }
26362 try {
26363 register(def);
26364 } catch (err) {
26365 doAction(HOOKS.SHELL_ERROR, {
26366 scope: "widget-register",
26367 id: entry.id,
26368 error: err
26369 });
26370 return;
26371 }
26372 registered.add(entry.id);
26373 refreshWidgetPicker();
26374 if (layer) {
26375 layer.mountIfEnabled(entry.id);
26376 }
26377 };
26378 const unregisterEntry = (id) => {
26379 if (!registered.has(id)) {
26380 return;
26381 }
26382 layer?.unmount(id);
26383 unregister(id);
26384 registered.delete(id);
26385 refreshWidgetPicker();
26386 };
26387 return async (list2) => {
26388 const incoming = /* @__PURE__ */ new Set();
26389 for (const entry of list2) {
26390 incoming.add(entry.id);
26391 }
26392 for (const id of Array.from(registered)) {
26393 if (!incoming.has(id)) {
26394 unregisterEntry(id);
26395 }
26396 }
26397 for (const entry of list2) {
26398 if (!registered.has(entry.id)) {
26399 await registerEntry(entry);
26400 }
26401 }
26402 };
26403 }
26404 const WPD_COMPONENT_TAGS = [
26405 "wpd-section",
26406 "wpd-button",
26407 "wpd-swatch",
26408 "wpd-swatch-grid",
26409 "wpd-segmented",
26410 "wpd-segment",
26411 "wpd-select",
26412 "wpd-option",
26413 "wpd-multiselect",
26414 "wpd-color-field",
26415 "wpd-range-field",
26416 "wpd-text-field",
26417 "wpd-number-field",
26418 "wpd-checkbox",
26419 "wpd-checkbox-label",
26420 "wpd-toast",
26421 "wpd-toast-container",
26422 "wpd-tabs",
26423 "wpd-tab",
26424 "wpd-tabpanel",
26425 "wpd-window-button",
26426 "wpd-menu",
26427 "wpd-menu-item",
26428 "wpd-context-menu",
26429 "wpd-context-menu-option",
26430 "wpd-confirm-dialog",
26431 "wpd-modal",
26432 "wpd-user-search",
26433 "wpd-role-picker",
26434 "wpd-flyout",
26435 "wpd-tab-chip",
26436 "wpd-stack",
26437 "wpd-cluster",
26438 "wpd-icon",
26439 "wpd-body",
26440 "wpd-panel",
26441 "wpd-row",
26442 "wpd-grid",
26443 "wpd-display",
26444 "wpd-empty-state",
26445 "wpd-key",
26446 "wpd-code",
26447 "wpd-badge",
26448 "wpd-log",
26449 "wpd-steps",
26450 "wpd-step",
26451 "wpd-table",
26452 "wpd-spinner",
26453 "wpd-relative-time",
26454 "wpd-avatar",
26455 "wpd-textarea",
26456 "wpd-chip",
26457 "wpd-tag-input",
26458 "wpd-form",
26459 "wpd-save-status",
26460 "wpd-category-picker",
26461 "wpd-crumb-chain",
26462 "wpd-card",
26463 "wpd-notice"
26464 ];
26465 const KNOWN = new Set(WPD_COMPONENT_TAGS);
26466 const WARN_GRACE_MS = 2e3;
26467 const warnedTags = /* @__PURE__ */ new Set();
26468 const observedRoots = /* @__PURE__ */ new WeakSet();
26469 let started$2 = false;
26470 function distance(a, b) {
26471 const m = a.length;
26472 const n = b.length;
26473 if (m === 0) {
26474 return n;
26475 }
26476 if (n === 0) {
26477 return m;
26478 }
26479 const dp = new Array(n + 1);
26480 for (let j = 0; j <= n; j++) {
26481 dp[j] = j;
26482 }
26483 for (let i = 1; i <= m; i++) {
26484 let prev = dp[0];
26485 dp[0] = i;
26486 for (let j = 1; j <= n; j++) {
26487 const tmp = dp[j];
26488 dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
26489 prev = tmp;
26490 }
26491 }
26492 return dp[n];
26493 }
26494 function suggest(tag) {
26495 let best = null;
26496 let bestD = Infinity;
26497 for (const known of KNOWN) {
26498 const d = distance(tag, known);
26499 if (d < bestD) {
26500 bestD = d;
26501 best = known;
26502 }
26503 }
26504 return bestD > 0 && bestD <= 3 ? best : null;
26505 }
26506 function folderFor(tag) {
26507 return tag.startsWith("wpd-") ? tag.slice(4) : tag;
26508 }
26509 function warnFor(tag, sample) {
26510 if (warnedTags.has(tag)) {
26511 return;
26512 }
26513 warnedTags.add(tag);
26514 const isKnown = KNOWN.has(tag);
26515 if (isKnown) {
26516 const folder = folderFor(tag);
26517 console.error(
26518 `[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.
26519
26520 Fix — side-effect-import the component module from wherever you render it:
26521
26522 import '<rel>/ui/components/${folder}/${folder}';
26523
26524 Or pull every wpd-* component in one go (heavier — only do this from an entry bundle):
26525
26526 import '<rel>/ui/components';
26527
26528 See docs/components-reference.md for the full list.`,
26529 "\nFirst offending element:",
26530 sample
26531 );
26532 return;
26533 }
26534 const guess = suggest(tag);
26535 if (guess) {
26536 console.error(
26537 `[wp.desktop] <${tag}> is not a registered wpd-* component. Did you mean <${guess}>?
26538
26539 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'.`,
26540 "\nFirst offending element:",
26541 sample
26542 );
26543 return;
26544 }
26545 console.error(
26546 `[wp.desktop] <${tag}> looks like a wpd-* tag but no component by that name exists.
26547
26548 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.`,
26549 "\nFirst offending element:",
26550 sample
26551 );
26552 }
26553 function checkElement(el) {
26554 const tag = el.tagName.toLowerCase();
26555 if (!tag.startsWith("wpd-")) {
26556 return;
26557 }
26558 if (warnedTags.has(tag)) {
26559 return;
26560 }
26561 if (customElements.get(tag)) {
26562 return;
26563 }
26564 let settled = false;
26565 customElements.whenDefined(tag).then(() => {
26566 settled = true;
26567 });
26568 setTimeout(() => {
26569 if (settled) {
26570 return;
26571 }
26572 if (customElements.get(tag)) {
26573 return;
26574 }
26575 warnFor(tag, el);
26576 }, WARN_GRACE_MS);
26577 }
26578 function walk(root) {
26579 if (root instanceof Element) {
26580 checkElement(root);
26581 if (root.shadowRoot) {
26582 observeRoot(root.shadowRoot);
26583 }
26584 }
26585 const all2 = root.querySelectorAll("*");
26586 for (let i = 0; i < all2.length; i++) {
26587 const el = all2[i];
26588 checkElement(el);
26589 if (el.shadowRoot) {
26590 observeRoot(el.shadowRoot);
26591 }
26592 }
26593 }
26594 function observeRoot(root) {
26595 if (observedRoots.has(root)) {
26596 return;
26597 }
26598 observedRoots.add(root);
26599 walk(root);
26600 const mo = new MutationObserver((records) => {
26601 for (let i = 0; i < records.length; i++) {
26602 const added = records[i].addedNodes;
26603 for (let j = 0; j < added.length; j++) {
26604 const node = added[j];
26605 if (node.nodeType === 1) {
26606 walk(node);
26607 }
26608 }
26609 }
26610 });
26611 mo.observe(root, { childList: true, subtree: true });
26612 }
26613 function patchAttachShadow() {
26614 const proto = Element.prototype;
26615 const original = proto.attachShadow;
26616 if (original.__wpdPatched) {
26617 return;
26618 }
26619 const patched = function(init2) {
26620 const root = original.call(this, init2);
26621 if (root.mode === "open") {
26622 observeRoot(root);
26623 }
26624 return root;
26625 };
26626 patched.__wpdPatched = true;
26627 proto.attachShadow = patched;
26628 }
26629 function startMissingImportWarner() {
26630 if (started$2) {
26631 return;
26632 }
26633 if (typeof document === "undefined") {
26634 return;
26635 }
26636 started$2 = true;
26637 patchAttachShadow();
26638 observeRoot(document);
26639 }
26640 const TRASHABLE_SHORTCUT_KINDS = /* @__PURE__ */ new Set(["post"]);
26641 function getMyWordpressTrashApi() {
26642 const api = window.wp?.desktop?.myWordpress;
26643 return api && typeof api.trashEntity === "function" ? api : null;
26644 }
26645 const TRASH_DROP_ACTIVE_ATTR = "data-desktop-mode-trash-drop-active";
26646 const RECYCLE_BIN_WINDOW_ID = "desktop-mode-recycle-bin";
26647 const BIN_TILE_SELECTORS = [
26648 `.desktop-mode-file-tile[data-file-ref="${RECYCLE_BIN_WINDOW_ID}"]`,
26649 `[data-icon-id="${RECYCLE_BIN_WINDOW_ID}"]`,
26650 `[data-system-id="${RECYCLE_BIN_WINDOW_ID}"]`
26651 ];
26652 function findBinTile() {
26653 for (const sel of BIN_TILE_SELECTORS) {
26654 const el = document.querySelector(sel);
26655 if (el instanceof HTMLElement) {
26656 return el;
26657 }
26658 }
26659 return null;
26660 }
26661 let _installed = false;
26662 let _dockDeregister = null;
26663 let _windowDeregister = null;
26664 let _binMutationObserver = null;
26665 function isDesktopFilePayload(session) {
26666 return session.payload.type === "desktop-file";
26667 }
26668 function isShortcutPayload(session) {
26669 return session.payload.type === "shortcut";
26670 }
26671 function isTrashableShortcut(data) {
26672 if (!data.kind || !data.ref || !data.entityId) {
26673 return false;
26674 }
26675 if (!TRASHABLE_SHORTCUT_KINDS.has(data.kind)) {
26676 return false;
26677 }
26678 const numericRef = Number.parseInt(data.ref, 10);
26679 if (!Number.isFinite(numericRef) || numericRef <= 0) {
26680 return false;
26681 }
26682 return getMyWordpressTrashApi() !== null;
26683 }
26684 function registerOn(dragManager, id, el) {
26685 return dragManager.registerDropTarget({
26686 id,
26687 element: el,
26688 // Override the ghost-chip label: while the cursor is over
26689 // the bin the user is trashing, not creating a shortcut /
26690 // moving the placement. The DragManager swaps this in for
26691 // the payload-default "Drop here to create shortcut" /
26692 // "Drop here to move" chip text whenever this target is the
26693 // current accept-mode target.
26694 acceptLabel: __("Move to Trash", "desktop-mode"),
26695 // Reject the drop UP FRONT when the viewer can't trash the
26696 // payload's placement (e.g. an item inside a read-only
26697 // shared folder, or someone else's tile in a shared
26698 // namespace). `accept` flipping to `false` means the
26699 // drop-active highlight never lights up + onDrop never
26700 // fires + the drag manager surfaces a `rejected` outcome.
26701 // The user sees the icon snap back instead of attempting a
26702 // REST call that would 403 and only log to the console.
26703 accept: (payload) => {
26704 if (payload.type === "desktop-file") {
26705 const data = payload.data;
26706 const placement = data?.placement;
26707 if (!placement) {
26708 return false;
26709 }
26710 if (placement.file?.ref === RECYCLE_BIN_WINDOW_ID) {
26711 return false;
26712 }
26713 return placement.canTrash !== false;
26714 }
26715 if (payload.type === "shortcut") {
26716 const data = payload.data;
26717 return isTrashableShortcut(data);
26718 }
26719 return false;
26720 },
26721 onEnter: () => {
26722 el.setAttribute(TRASH_DROP_ACTIVE_ATTR, "");
26723 },
26724 onLeave: () => {
26725 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
26726 },
26727 onDrop: (session) => {
26728 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
26729 if (isDesktopFilePayload(session)) {
26730 const placement = session.payload.data.placement;
26731 void trashByFileType(placement);
26732 return;
26733 }
26734 if (isShortcutPayload(session)) {
26735 const data = session.payload.data;
26736 const api = getMyWordpressTrashApi();
26737 if (!api?.trashEntity || !data.entityId) {
26738 return;
26739 }
26740 const numericRef = Number.parseInt(data.ref, 10);
26741 if (!Number.isFinite(numericRef) || numericRef <= 0) {
26742 return;
26743 }
26744 void api.trashEntity(data.entityId, numericRef).catch(
26745 (err) => {
26746 console.error(
26747 "[desktop-mode] recycle-bin: shortcut trash failed:",
26748 err
26749 );
26750 }
26751 );
26752 }
26753 }
26754 });
26755 }
26756 function installRecycleBinDropTargets(dragManager) {
26757 if (_installed) {
26758 return;
26759 }
26760 _installed = true;
26761 const reprobeTile = () => {
26762 const el = findBinTile();
26763 if (!el) {
26764 _dockDeregister?.();
26765 _dockDeregister = null;
26766 return;
26767 }
26768 if (_dockDeregister && getRegisteredElementId(dragManager) === el) {
26769 return;
26770 }
26771 _dockDeregister?.();
26772 _dockDeregister = registerOn(dragManager, "recycle-bin-dock", el);
26773 };
26774 reprobeTile();
26775 document.addEventListener("desktop-mode-files-changed", reprobeTile);
26776 document.addEventListener("desktop-mode-desktop-icons-rendered", reprobeTile);
26777 addAction(
26778 HOOKS.DOCK_AFTER_RENDER,
26779 "desktop-mode/files/recycle-bin-dock-target",
26780 reprobeTile
26781 );
26782 if (typeof MutationObserver !== "undefined") {
26783 _binMutationObserver = new MutationObserver(() => {
26784 reprobeTile();
26785 });
26786 const desktopArea = document.getElementById("desktop-mode-area") ?? document.body;
26787 _binMutationObserver.observe(desktopArea, {
26788 childList: true,
26789 subtree: true
26790 });
26791 }
26792 addAction(
26793 HOOKS.WINDOW_OPENED,
26794 "desktop-mode/files/recycle-bin-window-target",
26795 (detail) => {
26796 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
26797 return;
26798 }
26799 _windowDeregister?.();
26800 _windowDeregister = null;
26801 const el = document.querySelector(
26802 "[data-desktop-mode-recycle-bin-root]"
26803 );
26804 if (el instanceof HTMLElement) {
26805 _windowDeregister = registerOn(
26806 dragManager,
26807 "recycle-bin-window",
26808 el
26809 );
26810 }
26811 }
26812 );
26813 addAction(
26814 HOOKS.WINDOW_CLOSED,
26815 "desktop-mode/files/recycle-bin-window-cleanup",
26816 (detail) => {
26817 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
26818 return;
26819 }
26820 _windowDeregister?.();
26821 _windowDeregister = null;
26822 }
26823 );
26824 }
26825 function getRegisteredElementId(dragManager) {
26826 const t = dragManager.debug().listTargets().find((target2) => target2.id === "recycle-bin-dock");
26827 return t ? t.element : null;
26828 }
26829 let started$1 = false;
26830 let highWaterMs = 0;
26831 function startFilesHeartbeat() {
26832 if (started$1) {
26833 return;
26834 }
26835 started$1 = true;
26836 heartbeat.contribute("desktop_mode_files_subscribe", () => {
26837 const state2 = getFilesState();
26838 const folderVersions = {};
26839 for (const [id, folder] of state2.folders) {
26840 folderVersions[String(id)] = folder.updatedAtMs;
26841 }
26842 return {
26843 folderVersions,
26844 placementsVersion: highWaterMs,
26845 sharesVersion: sharesStore().state.sharesVersion
26846 };
26847 });
26848 heartbeat.subscribe("desktop_mode_files", (payload) => {
26849 applyDelta(payload);
26850 });
26851 }
26852 function applyDelta(payload) {
26853 const folders = payload.folders ?? [];
26854 for (const folder of folders) {
26855 upsertFolder(folder, "remote");
26856 if (folder.updatedAtMs > highWaterMs) {
26857 highWaterMs = folder.updatedAtMs;
26858 }
26859 }
26860 const placements = payload.placements ?? [];
26861 for (const placement of placements) {
26862 upsertPlacement(placement, "remote");
26863 if (placement.updatedAtMs > highWaterMs) {
26864 highWaterMs = placement.updatedAtMs;
26865 }
26866 }
26867 const removed = payload.removed ?? {};
26868 for (const id of removed.folders ?? []) {
26869 removeFolder(id, "remote");
26870 }
26871 for (const id of removed.placements ?? []) {
26872 removePlacement(id, "remote");
26873 }
26874 if (typeof payload.serverTimeMs === "number" && payload.serverTimeMs > highWaterMs) {
26875 highWaterMs = payload.serverTimeMs;
26876 }
26877 const pending2 = payload.shares?.pending;
26878 if (Array.isArray(pending2) && pending2.length > 0) {
26879 ingestPendingInvites(pending2);
26880 }
26881 if (payload.truncated) {
26882 const hydrated = Array.from(getFilesState().hydratedFolders);
26883 for (const folderId of hydrated) {
26884 void listPlacements(folderId).then((res) => {
26885 setFolderPlacements(folderId, res.placements);
26886 }).catch(() => {
26887 });
26888 }
26889 }
26890 }
26891 let started = false;
26892 const unsubscribers = [];
26893 function startFilesRestoreSync() {
26894 if (started) {
26895 return;
26896 }
26897 started = true;
26898 const onChange = (payload) => {
26899 const detail = payload;
26900 if (!detail || detail.action !== "untrashed") {
26901 return;
26902 }
26903 resyncFromServer();
26904 };
26905 unsubscribers.push(
26906 subscribe$2("desktop-mode.placement.changed", onChange),
26907 subscribe$2("desktop-mode.shortcut.changed", onChange),
26908 subscribe$2("desktop-mode.folder.changed", onChange)
26909 );
26910 }
26911 function resyncFromServer() {
26912 void listFolders().then((res) => {
26913 setFolders(res.folders);
26914 }).catch((err) => {
26915 console.error(
26916 "[desktop-mode] files restore-sync: listFolders failed",
26917 err
26918 );
26919 });
26920 const hydrated = Array.from(getFilesState().hydratedFolders);
26921 for (const folderId of hydrated) {
26922 void listPlacements(folderId).then((res) => {
26923 setFolderPlacements(folderId, res.placements);
26924 }).catch((err) => {
26925 console.error(
26926 "[desktop-mode] files restore-sync: listPlacements failed for",
26927 folderId,
26928 err
26929 );
26930 });
26931 }
26932 }
26933 const MENU_CLASS = "desktop-mode-wallpaper-menu";
26934 let activeMenu = null;
26935 function isWallpaperMenuOpen() {
26936 return activeMenu !== null;
26937 }
26938 let openGeneration = 0;
26939 function openWallpaperMenu(host, pos, items, options = {}) {
26940 closeWallpaperMenu();
26941 const myGen = ++openGeneration;
26942 openWithShellOverlays(
26943 () => myGen === openGeneration,
26944 () => openWallpaperMenuImmediate(host, pos, items, options)
26945 );
26946 }
26947 function openWallpaperMenuImmediate(host, pos, items, options = {}) {
26948 if (items.length === 0) {
26949 return;
26950 }
26951 items = items.slice().sort((a, b) => {
26952 const sa = typeof a.sort === "number" ? a.sort : 100;
26953 const sb = typeof b.sort === "number" ? b.sort : 100;
26954 if (sa !== sb) {
26955 return sa - sb;
26956 }
26957 return a.label.localeCompare(b.label);
26958 });
26959 const menu = document.createElement("wpd-context-menu");
26960 menu.setAttribute("open", "");
26961 menu.classList.add(MENU_CLASS);
26962 menu.style.left = `${pos.x}px`;
26963 menu.style.top = `${pos.y}px`;
26964 const itemById = /* @__PURE__ */ new Map();
26965 let activeFlyout2 = null;
26966 let activeFlyoutParent = null;
26967 const closeActiveFlyout = () => {
26968 if (activeFlyout2) {
26969 activeFlyout2.remove();
26970 activeFlyout2 = null;
26971 activeFlyoutParent = null;
26972 }
26973 };
26974 for (const item of items) {
26975 itemById.set(item.id, item);
26976 const opt = document.createElement("wpd-context-menu-option");
26977 opt.dataset.menuItemId = item.id;
26978 opt.setAttribute("value", item.id);
26979 if (item.heading) {
26980 opt.setAttribute("heading", "");
26981 }
26982 if (item.disabled) {
26983 opt.setAttribute("disabled", "");
26984 }
26985 if (item.icon) {
26986 opt.setAttribute("icon", sanitizeClass(item.icon));
26987 }
26988 const hasChildren2 = Array.isArray(item.children) && item.children.length > 0;
26989 if (hasChildren2) {
26990 opt.setAttribute("has-children", "");
26991 }
26992 opt.textContent = item.label;
26993 opt.addEventListener("mouseenter", () => {
26994 if (hasChildren2) {
26995 openFlyout2(item, opt);
26996 return;
26997 }
26998 closeActiveFlyout();
26999 });
27000 menu.appendChild(opt);
27001 }
27002 menu.addEventListener("wpd-context-menu-pick", (e) => {
27003 const detail = e.detail;
27004 const item = itemById.get(detail.id) ?? null;
27005 if (!item) {
27006 return;
27007 }
27008 if (Array.isArray(item.children) && item.children.length > 0) {
27009 e.stopPropagation();
27010 if (activeFlyoutParent && activeFlyoutParent.id === item.id) {
27011 closeActiveFlyout();
27012 return;
27013 }
27014 const anchor = menu.querySelector(
27015 `[data-menu-item-id="${item.id}"]`
27016 );
27017 if (anchor) {
27018 openFlyout2(item, anchor);
27019 }
27020 return;
27021 }
27022 closeWallpaperMenu();
27023 void item.onClick(new MouseEvent("click"));
27024 });
27025 function openFlyout2(parent, anchor) {
27026 closeActiveFlyout();
27027 const fly = document.createElement("wpd-context-menu");
27028 fly.setAttribute("open", "");
27029 fly.classList.add(MENU_CLASS, `${MENU_CLASS}--flyout`);
27030 fly.dataset.parentId = parent.id;
27031 const sortedKids = (parent.children ?? []).slice().sort((a, b) => {
27032 const sa = typeof a.sort === "number" ? a.sort : 100;
27033 const sb = typeof b.sort === "number" ? b.sort : 100;
27034 if (sa !== sb) {
27035 return sa - sb;
27036 }
27037 return a.label.localeCompare(b.label);
27038 });
27039 for (const child of sortedKids) {
27040 const kopt = document.createElement("wpd-context-menu-option");
27041 kopt.dataset.menuItemId = child.id;
27042 kopt.setAttribute("value", child.id);
27043 if (child.icon) {
27044 kopt.setAttribute("icon", sanitizeClass(child.icon));
27045 }
27046 if (child.disabled) {
27047 kopt.setAttribute("disabled", "");
27048 }
27049 if (child.checked) {
27050 kopt.setAttribute("checked", "");
27051 }
27052 kopt.textContent = child.label;
27053 kopt.addEventListener("wpd-context-menu-pick", (e) => {
27054 e.stopPropagation();
27055 closeWallpaperMenu();
27056 void child.onClick(new MouseEvent("click"));
27057 });
27058 fly.appendChild(kopt);
27059 }
27060 document.body.appendChild(fly);
27061 activeFlyout2 = fly;
27062 activeFlyoutParent = parent;
27063 positionFlyout2(fly, anchor);
27064 }
27065 function positionFlyout2(fly, anchor) {
27066 const ar = anchor.getBoundingClientRect();
27067 fly.style.position = "fixed";
27068 fly.style.left = `${ar.right}px`;
27069 fly.style.top = `${ar.top}px`;
27070 const fr = fly.getBoundingClientRect();
27071 if (fr.right > window.innerWidth) {
27072 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
27073 }
27074 if (fr.bottom > window.innerHeight) {
27075 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
27076 }
27077 }
27078 host.appendChild(menu);
27079 activeMenu = menu;
27080 const rect = menu.getBoundingClientRect();
27081 if (rect.right > window.innerWidth) {
27082 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
27083 }
27084 if (rect.bottom > window.innerHeight) {
27085 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
27086 }
27087 const detach = attachDismissable(menu, {
27088 close: () => closeWallpaperMenu(),
27089 siblingSelectors: [`.${MENU_CLASS}--flyout`],
27090 excludeOutsideTarget: options.excludeOutsideTarget
27091 });
27092 menu.addEventListener("wallpaper-menu-closed", detach);
27093 doAction("desktop-mode.wallpaper-menu.opened", { items: items.map((i) => i.id) });
27094 }
27095 function closeWallpaperMenu() {
27096 if (!activeMenu) {
27097 return;
27098 }
27099 document.querySelectorAll(`.${MENU_CLASS}--flyout`).forEach((el) => el.remove());
27100 activeMenu.dispatchEvent(new CustomEvent("wallpaper-menu-closed"));
27101 activeMenu.remove();
27102 activeMenu = null;
27103 doAction("desktop-mode.wallpaper-menu.closed", {});
27104 }
27105 function buildMenuItems(deps2) {
27106 const builtIn = [
27107 {
27108 id: "create-folder",
27109 label: deps2.labels.createFolder,
27110 icon: "dashicons-portfolio",
27111 sort: 10,
27112 onClick: () => deps2.createFolder()
27113 },
27114 {
27115 id: "new-url",
27116 label: deps2.labels.newUrl,
27117 icon: "dashicons-admin-links",
27118 sort: 12,
27119 onClick: () => deps2.createUrl()
27120 },
27121 {
27122 id: "sort-by",
27123 label: deps2.labels.sortHeading,
27124 icon: "dashicons-sort",
27125 sort: 16,
27126 onClick: () => void 0,
27127 children: [
27128 {
27129 id: "sort-name-asc",
27130 label: deps2.labels.sortNameAsc,
27131 sort: 10,
27132 checked: deps2.currentSortMode === "name-asc",
27133 onClick: () => deps2.sortIcons("name-asc")
27134 },
27135 {
27136 id: "sort-name-desc",
27137 label: deps2.labels.sortNameDesc,
27138 sort: 20,
27139 checked: deps2.currentSortMode === "name-desc",
27140 onClick: () => deps2.sortIcons("name-desc")
27141 },
27142 {
27143 id: "sort-date-desc",
27144 label: deps2.labels.sortDateDesc,
27145 sort: 30,
27146 checked: deps2.currentSortMode === "date-desc",
27147 onClick: () => deps2.sortIcons("date-desc")
27148 },
27149 {
27150 id: "sort-date-asc",
27151 label: deps2.labels.sortDateAsc,
27152 sort: 40,
27153 checked: deps2.currentSortMode === "date-asc",
27154 onClick: () => deps2.sortIcons("date-asc")
27155 }
27156 ]
27157 },
27158 ...deps2.includeShowDesktop === false ? [] : [
27159 {
27160 id: "show-desktop",
27161 label: deps2.labels.showDesktop,
27162 icon: "dashicons-desktop",
27163 sort: 20,
27164 onClick: () => deps2.toggleShowDesktop()
27165 }
27166 ],
27167 {
27168 id: "os-settings",
27169 label: deps2.labels.osSettings,
27170 icon: "dashicons-admin-generic",
27171 sort: 30,
27172 onClick: () => deps2.openOsSettings()
27173 }
27174 ];
27175 const serverItems = (deps2.serverItems ?? []).map(
27176 (s) => serverItemToMenuItem(s, deps2)
27177 );
27178 const merged = [...builtIn, ...serverItems];
27179 const filtered = applyFilters(
27180 "desktop-mode.wallpaper-context-menu",
27181 merged
27182 );
27183 return Array.isArray(filtered) ? filtered : merged;
27184 }
27185 function serverItemToMenuItem(server, deps2) {
27186 return {
27187 id: server.id,
27188 label: server.label,
27189 icon: server.icon,
27190 sort: server.sort,
27191 disabled: server.disabled,
27192 onClick: () => {
27193 if (server.callbackId) {
27194 const cb = deps2.serverCallbacks?.[server.callbackId];
27195 if (typeof cb === "function") {
27196 return cb();
27197 }
27198 }
27199 doAction("desktop-mode.wallpaper-context-menu.activated", {
27200 id: server.id,
27201 callbackId: server.callbackId ?? ""
27202 });
27203 }
27204 };
27205 }
27206 function sanitizeClass(raw) {
27207 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
27208 }
27209 const ROOT_CLASS = "desktop-mode-url-dialog";
27210 let active = null;
27211 function closeUrlDialog() {
27212 if (!active) {
27213 return;
27214 }
27215 active.dispatchEvent(new CustomEvent("url-dialog-closed"));
27216 active.remove();
27217 active = null;
27218 doAction("desktop-mode.files.url-dialog.closed", {});
27219 }
27220 function openUrlDialog(options) {
27221 closeUrlDialog();
27222 const decision = applyFilters(
27223 "desktop-mode.files.url-dialog",
27224 null,
27225 options
27226 );
27227 if (decision === false) {
27228 return;
27229 }
27230 const overlay = document.createElement("div");
27231 overlay.className = `${ROOT_CLASS}__overlay desktop-mode-create-folder-dialog__overlay`;
27232 overlay.setAttribute("role", "presentation");
27233 const dialog2 = document.createElement("div");
27234 dialog2.className = `${ROOT_CLASS} desktop-mode-create-folder-dialog`;
27235 dialog2.setAttribute("role", "dialog");
27236 dialog2.setAttribute("aria-modal", "true");
27237 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS}-title`);
27238 const title = document.createElement("h2");
27239 title.id = `${ROOT_CLASS}-title`;
27240 title.className = "desktop-mode-create-folder-dialog__title";
27241 title.textContent = options.title;
27242 dialog2.appendChild(title);
27243 if (options.description) {
27244 const desc = document.createElement("p");
27245 desc.className = `${ROOT_CLASS}__description`;
27246 desc.textContent = options.description;
27247 dialog2.appendChild(desc);
27248 }
27249 const nameField = document.createElement("wpd-text-field");
27250 nameField.setAttribute("label", options.nameLabel ?? "Name");
27251 nameField.setAttribute("value", options.initialName ?? "");
27252 nameField.setAttribute("placeholder", "My web app");
27253 nameField.setAttribute("autocomplete", "off");
27254 dialog2.appendChild(nameField);
27255 const urlField = document.createElement("wpd-text-field");
27256 urlField.setAttribute("label", options.urlLabel ?? "URL");
27257 urlField.setAttribute("value", options.initialUrl ?? "https://");
27258 urlField.setAttribute("placeholder", "https://example.com");
27259 urlField.setAttribute("type", "url");
27260 urlField.setAttribute("autocomplete", "off");
27261 dialog2.appendChild(urlField);
27262 const error = document.createElement("p");
27263 error.className = "desktop-mode-create-folder-dialog__error";
27264 error.hidden = true;
27265 error.setAttribute("role", "alert");
27266 dialog2.appendChild(error);
27267 const actions = document.createElement("div");
27268 actions.className = "desktop-mode-create-folder-dialog__actions";
27269 const cancel = document.createElement("button");
27270 cancel.type = "button";
27271 cancel.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--secondary";
27272 cancel.textContent = "Cancel";
27273 const submit = document.createElement("button");
27274 submit.type = "button";
27275 submit.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--primary";
27276 submit.textContent = options.submitLabel ?? "Create";
27277 actions.appendChild(cancel);
27278 actions.appendChild(submit);
27279 dialog2.appendChild(actions);
27280 overlay.appendChild(dialog2);
27281 document.body.appendChild(overlay);
27282 active = overlay;
27283 queueMicrotask(() => {
27284 const input = nameField.shadowRoot?.querySelector("input");
27285 input?.focus();
27286 input?.select();
27287 });
27288 doAction("desktop-mode.files.url-dialog.opened", {});
27289 const readValue = (field) => {
27290 const v = field.value;
27291 if (typeof v === "string") {
27292 return v;
27293 }
27294 return field.shadowRoot?.querySelector("input")?.value ?? "";
27295 };
27296 const setBusy = (busy) => {
27297 nameField.disabled = busy;
27298 urlField.disabled = busy;
27299 cancel.disabled = busy;
27300 submit.disabled = busy;
27301 dialog2.classList.toggle("desktop-mode-create-folder-dialog--busy", busy);
27302 };
27303 const showError = (msg) => {
27304 error.textContent = msg;
27305 error.hidden = false;
27306 };
27307 const doCancel = () => {
27308 closeUrlDialog();
27309 options.onCancel?.();
27310 };
27311 const doSubmit = async () => {
27312 const url = readValue(urlField).trim();
27313 if (!url) {
27314 showError("Please enter a URL.");
27315 return;
27316 }
27317 const finalUrl = /^[a-z][a-z0-9+\-.]*:/i.test(url) ? url : `https://${url}`;
27318 try {
27319 new URL(finalUrl);
27320 } catch {
27321 showError("That doesn't look like a valid URL.");
27322 return;
27323 }
27324 const name = readValue(nameField).trim();
27325 error.hidden = true;
27326 setBusy(true);
27327 try {
27328 await options.onSubmit({ name, url: finalUrl });
27329 closeUrlDialog();
27330 } catch (err) {
27331 setBusy(false);
27332 showError(err instanceof Error ? err.message : "Could not save.");
27333 }
27334 };
27335 cancel.addEventListener("click", () => doCancel());
27336 submit.addEventListener("click", () => void doSubmit());
27337 overlay.addEventListener("click", (e) => {
27338 if (e.target === overlay) {
27339 doCancel();
27340 }
27341 });
27342 const onKey = (e) => {
27343 if (e.key === "Escape") {
27344 e.preventDefault();
27345 doCancel();
27346 } else if (e.key === "Enter" && !e.isComposing) {
27347 e.preventDefault();
27348 void doSubmit();
27349 }
27350 };
27351 dialog2.addEventListener("keydown", onKey);
27352 overlay.addEventListener("url-dialog-closed", () => {
27353 dialog2.removeEventListener("keydown", onKey);
27354 });
27355 }
27356 const _earlyReadyQueue = [];
27357 let _earlyReady = false;
27358 (function installEarlyDesktopShim() {
27359 const w = window;
27360 if (!w.wp) {
27361 w.wp = {};
27362 }
27363 if (w.wp.desktop) {
27364 return;
27365 }
27366 const shim = {
27367 whenReady(cb) {
27368 if (typeof cb !== "function") {
27369 return;
27370 }
27371 if (_earlyReady) {
27372 Promise.resolve().then(cb);
27373 return;
27374 }
27375 _earlyReadyQueue.push(cb);
27376 },
27377 ready(cb) {
27378 shim.whenReady(cb);
27379 },
27380 isReady() {
27381 return _earlyReady;
27382 }
27383 };
27384 w.wp.desktop = shim;
27385 })();
27386 const OS_SETTINGS_WINDOW_ID = "desktop-mode-os-settings";
27387 let _idleBootQueue = [];
27388 let _idleBootTimeout = Number.POSITIVE_INFINITY;
27389 let _idleBootScheduled = false;
27390 function scheduleIdleBoot(cb, timeout = 1500) {
27391 _idleBootQueue.push(cb);
27392 if (timeout < _idleBootTimeout) {
27393 _idleBootTimeout = timeout;
27394 }
27395 if (_idleBootScheduled) {
27396 return;
27397 }
27398 _idleBootScheduled = true;
27399 const drain = () => {
27400 const callbacks = _idleBootQueue;
27401 _idleBootQueue = [];
27402 _idleBootTimeout = Number.POSITIVE_INFINITY;
27403 _idleBootScheduled = false;
27404 for (const fn of callbacks) {
27405 try {
27406 fn();
27407 } catch (err) {
27408 if (typeof console !== "undefined") {
27409 console.error(
27410 "[desktop-mode] scheduleIdleBoot callback threw:",
27411 err
27412 );
27413 }
27414 }
27415 }
27416 };
27417 if (typeof window.requestIdleCallback === "function") {
27418 window.requestIdleCallback(drain, { timeout: _idleBootTimeout });
27419 } else {
27420 window.setTimeout(drain, 0);
27421 }
27422 }
27423 function init() {
27424 const config = window.desktopModeConfig;
27425 if (!config) {
27426 return;
27427 }
27428 const desktopArea = document.getElementById("desktop-mode-area");
27429 if (!desktopArea) {
27430 return;
27431 }
27432 const manager = new WindowManager(desktopArea);
27433 const wallpaperEl = document.getElementById("desktop-mode-wallpaper");
27434 const pluginUrl = config.pluginUrl || "";
27435 let wallpaperLayer = null;
27436 if (wallpaperEl) {
27437 wallpaperLayer = new WallpaperLayer(wallpaperEl, pluginUrl);
27438 }
27439 const widgetsEl = document.getElementById("desktop-mode-widgets");
27440 let widgetLayer = null;
27441 registerBuiltInWidgets();
27442 installDefaultDockRailRenderer();
27443 if (widgetsEl) {
27444 widgetLayer = new WidgetLayer(widgetsEl, pluginUrl);
27445 }
27446 registerModule({
27447 id: "pixijs",
27448 url: `${pluginUrl}/assets/vendor/pixi.min.js`,
27449 isReady: () => typeof window.PIXI !== "undefined"
27450 });
27451 const osSettings = new OsSettings(
27452 {
27453 mediaUrl: config.mediaUrl,
27454 restNonce: config.restNonce,
27455 canUpload: !!config.canUpload,
27456 isAdmin: !!config.currentUserIsAdmin,
27457 aiPlatformSettings: config.aiPlatformSettings ?? null,
27458 aiPlatformSettingsUrl: config.aiPlatformSettingsUrl ?? "",
27459 extendedOptions: config.extendedOptions ?? null,
27460 extendedOptionsUrl: config.extendedOptionsUrl ?? "",
27461 osSettingsPanelBundleUrl: config.osSettingsPanelBundleUrl ?? ""
27462 },
27463 wallpaperLayer ?? new WallpaperLayer(document.createElement("div"), pluginUrl)
27464 );
27465 osSettings.apply();
27466 const aiAssistant = new AiAssistantStub(
27467 {
27468 aiSearchUrl: config.aiSearchUrl ?? "",
27469 aiSearchStreamUrl: config.aiSearchStreamUrl ?? "",
27470 restNonce: config.restNonce,
27471 // Transport picker lives in OS Settings → AI Settings. Read
27472 // live (not captured at construction) so a change applies on
27473 // the next search without a page reload.
27474 getTransport: () => osSettings.getOsSettingsSnapshot().ai.transport
27475 },
27476 config.aiAssistantBundleUrl ?? ""
27477 );
27478 aiAssistant.attachAsk(
27479 createAsk({
27480 config: () => config,
27481 fallbackContext: () => ({
27482 close: () => aiAssistant.close(),
27483 openInWindow: (url, title, icon) => {
27484 manager.open({
27485 url,
27486 title,
27487 icon: icon ?? "dashicons-admin-generic"
27488 });
27489 },
27490 confirm: (msg) => wpdConfirm({ message: msg })
27491 })
27492 })
27493 );
27494 const dragBridge = new DragBridge();
27495 const dragManager = new DragManager();
27496 document.addEventListener(DRAG_EVENTS.START, (e) => {
27497 const detail = e.detail;
27498 const payload = detail?.payload;
27499 if (!payload) {
27500 return;
27501 }
27502 if (payload.type !== "shortcut" && payload.type !== "desktop-file") {
27503 return;
27504 }
27505 const bridgePayload = payload.data?.bridgePayload;
27506 if (bridgePayload) {
27507 dragBridge.start(bridgePayload);
27508 }
27509 });
27510 document.addEventListener(DRAG_EVENTS.END, () => {
27511 dragBridge.end();
27512 });
27513 scheduleIdleBoot(() => installIframeDropTargets(dragManager));
27514 window.addEventListener("message", (e) => {
27515 if (e.origin !== window.location.origin) {
27516 return;
27517 }
27518 const data = e.data;
27519 if (!data || data.type !== "desktop-mode-drop-failed") {
27520 return;
27521 }
27522 showToast({
27523 message: "Could not insert into the editor."
27524 });
27525 });
27526 registerPalette({
27527 id: "desktop-mode-ai-assistant",
27528 label: "AI Assistant",
27529 open: () => aiAssistant.open(),
27530 close: () => aiAssistant.close(),
27531 isOpen: () => aiAssistant.isOpen
27532 });
27533 installPaletteShortcut();
27534 installWindowSwitcherShortcut(manager);
27535 installDesktopArrowShortcuts(manager);
27536 scheduleIdleBoot(() => {
27537 new IframeCommandBridge({
27538 manager,
27539 adminUrl: config.adminUrl
27540 }).install();
27541 new ShellCommandHarvester({
27542 manager,
27543 adminUrl: config.adminUrl
27544 }).install();
27545 });
27546 document.addEventListener("desktop-mode-open-ai", () => {
27547 openPaletteOnly("desktop-mode-ai-assistant");
27548 });
27549 const bottomDockEl = document.getElementById("desktop-mode-dock");
27550 const shellEl = document.getElementById("desktop-mode-shell");
27551 const shellBody = shellEl?.querySelector(
27552 ".desktop-mode-shell__body"
27553 );
27554 let layoutDispatcher = null;
27555 const nativeWindows = createNativeWindowSync({
27556 manager,
27557 appendSystemTile: (item) => layoutDispatcher?.appendSystemTile(item),
27558 removeSystemTile: (id) => layoutDispatcher?.removeSystemTile(id)
27559 });
27560 const syncNativeWindows = nativeWindows.sync;
27561 bindNativeUrlRemap({
27562 getSnapshot: () => osSettings.getOsSettingsSnapshot(),
27563 openById: (id) => nativeWindows.openById(id),
27564 adminUrl: config.adminUrl
27565 });
27566 const findDockEntryForUrl2 = (url) => {
27567 const targetSlug = deriveWindowId(url, config.adminUrl);
27568 const items = layoutDispatcher ? layoutDispatcher.getMenuItems() : config.dockItems ?? [];
27569 for (const item of items) {
27570 if (deriveWindowId(item.url, config.adminUrl) === targetSlug) {
27571 return {
27572 title: item.title,
27573 icon: item.icon,
27574 url: item.url,
27575 submenu: item.submenu,
27576 multi: item.multi
27577 };
27578 }
27579 for (const sub of item.submenu ?? []) {
27580 if (deriveWindowId(sub.url, config.adminUrl) === targetSlug) {
27581 return {
27582 title: sub.title,
27583 // Sub-menu entries inherit the parent tile's
27584 // icon — that's the dock's own convention and
27585 // avoids painting a generic glyph on a window
27586 // the user knows by its parent's identity.
27587 icon: item.icon,
27588 // `url` holds the PARENT tile's landing page, so
27589 // the new window's synthetic "back to parent"
27590 // tab links to the dock URL (themes.php) rather
27591 // than to the sub-page itself.
27592 url: item.url,
27593 multi: item.multi
27594 };
27595 }
27596 }
27597 }
27598 return null;
27599 };
27600 bindAdminLinkDispatch({
27601 adminUrl: config.adminUrl,
27602 deriveSlug: (url) => deriveWindowId(url, config.adminUrl),
27603 openWindow: (windowConfig) => {
27604 void manager.open(windowConfig);
27605 },
27606 findDockEntry: findDockEntryForUrl2
27607 });
27608 registerNativeUrlRemap({
27609 id: "desktop-mode-posts",
27610 nativeWindowId: "desktop-mode-posts",
27611 matches: (_url, parsed) => {
27612 if (!parsed.pathname.endsWith("/edit.php")) {
27613 return false;
27614 }
27615 const postType = parsed.searchParams.get("post_type");
27616 return !postType || postType === "post";
27617 },
27618 enabled: (snapshot) => snapshot.nativePostsEnabled === true
27619 });
27620 registerNativeUrlRemap({
27621 id: "desktop-mode-pages",
27622 nativeWindowId: "desktop-mode-pages",
27623 matches: (_url, parsed) => {
27624 if (!parsed.pathname.endsWith("/edit.php")) {
27625 return false;
27626 }
27627 return parsed.searchParams.get("post_type") === "page";
27628 },
27629 enabled: (snapshot) => snapshot.nativePagesEnabled === true
27630 });
27631 registerNativeUrlRemap({
27632 id: "desktop-mode-users",
27633 nativeWindowId: "desktop-mode-users",
27634 matches: (_url, parsed) => parsed.pathname.endsWith("/users.php"),
27635 enabled: (snapshot) => snapshot.nativeUsersEnabled === true
27636 });
27637 registerNativeUrlRemap({
27638 id: "desktop-mode-user-edit",
27639 nativeWindowId: "desktop-mode-user-edit",
27640 matches: (_url, parsed) => {
27641 const path = parsed.pathname;
27642 if (path.endsWith("/profile.php")) {
27643 return true;
27644 }
27645 if (path.endsWith("/user-edit.php")) {
27646 return parsed.searchParams.has("user_id");
27647 }
27648 return false;
27649 },
27650 enabled: (snapshot) => snapshot.nativeUsersEnabled === true,
27651 onMatch: (_url, parsed) => {
27652 const userId = parseInt(
27653 parsed.searchParams.get("user_id") ?? "0",
27654 10
27655 );
27656 if (userId > 0) {
27657 setUserEditTarget(userId);
27658 }
27659 }
27660 });
27661 registerNativeUrlRemap({
27662 id: "desktop-mode-comments",
27663 nativeWindowId: "desktop-mode-comments",
27664 matches: (_url, parsed) => parsed.pathname.endsWith("/edit-comments.php"),
27665 enabled: (snapshot) => snapshot.nativeCommentsEnabled === true
27666 });
27667 registerNativeUrlRemap({
27668 id: "desktop-mode-plugins",
27669 nativeWindowId: "desktop-mode-plugins",
27670 matches: (_url, parsed) => {
27671 const path = parsed.pathname;
27672 return path.endsWith("/plugins.php") || path.endsWith("/plugin-install.php");
27673 },
27674 enabled: (snapshot) => snapshot.nativePluginsEnabled === true,
27675 onMatch: (_url, parsed) => {
27676 const tab = parsed.pathname.endsWith("/plugin-install.php") ? "browse" : "installed";
27677 void Promise.resolve().then(() => tabTarget).then((m) => {
27678 m.setPluginsWindowTab(tab);
27679 });
27680 }
27681 });
27682 if (bottomDockEl && shellEl && shellBody && config.dockItems) {
27683 desktopArea.classList.add("desktop-mode-area--with-dock");
27684 const initialLayout = osSettings.getOsSettingsSnapshot().desktopLayout;
27685 const renderIcons2 = (icons) => {
27686 renderDesktopIcons(desktopArea, icons, {
27687 openWindow: nativeWindows.openById,
27688 manager,
27689 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
27690 });
27691 };
27692 layoutDispatcher = createLayoutDispatcher(
27693 {
27694 shellRoot: shellEl,
27695 shellBody,
27696 bottomDockEl,
27697 desktopArea,
27698 windowManager: manager,
27699 adminUrl: config.adminUrl,
27700 renderIcons: renderIcons2,
27701 getSettings: () => {
27702 const snap = osSettings.getOsSettingsSnapshot();
27703 return {
27704 itemVisibility: snap.itemVisibility,
27705 dockOrder: snap.dockOrder
27706 };
27707 }
27708 },
27709 initialLayout,
27710 config.dockItems,
27711 config.desktopIcons
27712 );
27713 layoutDispatcher.appendSystemTile(
27714 {
27715 id: OS_SETTINGS_WINDOW_ID,
27716 title: "OS Settings",
27717 icon: "dashicons-desktop",
27718 // "Open" for the dock dot means "open on the currently
27719 // active desktop." OS Settings on another desktop
27720 // shouldn't paint the dot on the active view.
27721 isOpen: () => {
27722 const win = manager.getById(OS_SETTINGS_WINDOW_ID);
27723 if (!win) {
27724 return false;
27725 }
27726 return (win.config.desktopId || manager.getActiveDesktopId()) === manager.getActiveDesktopId();
27727 },
27728 onOpen: openOsSettings
27729 },
27730 "core"
27731 );
27732 if (!isStandaloneDisplay()) {
27733 layoutDispatcher.appendSystemTile(
27734 getInstallTileDef(
27735 config.pwa?.appName || "WordPress",
27736 showToast
27737 ),
27738 "core"
27739 );
27740 }
27741 window.matchMedia("(display-mode: standalone)").addEventListener("change", (e) => {
27742 if (e.matches) {
27743 layoutDispatcher?.removeSystemTile(
27744 "desktop-mode-pwa-install"
27745 );
27746 }
27747 });
27748 void isLikelyInstalled().then((installed2) => {
27749 if (installed2) {
27750 layoutDispatcher?.removeSystemTile(
27751 "desktop-mode-pwa-install"
27752 );
27753 }
27754 });
27755 }
27756 function openOsSettings() {
27757 void manager.open({
27758 id: OS_SETTINGS_WINDOW_ID,
27759 baseId: OS_SETTINGS_WINDOW_ID,
27760 url: "#os-settings",
27761 title: "OS Settings",
27762 icon: "dashicons-desktop",
27763 native: true,
27764 render: (body) => osSettings.renderPanel(body),
27765 width: 820,
27766 height: 720,
27767 minWidth: 560,
27768 minHeight: 480
27769 });
27770 }
27771 function openBugReport() {
27772 void manager.open({
27773 id: BUG_REPORT_WINDOW_ID,
27774 baseId: BUG_REPORT_WINDOW_ID,
27775 url: `#${BUG_REPORT_WINDOW_ID}`,
27776 title: "Report a bug",
27777 icon: "dashicons-buddicons-replies",
27778 native: true,
27779 render: (body) => renderBugReport(body),
27780 width: 560,
27781 height: 620,
27782 minWidth: 420,
27783 minHeight: 480
27784 });
27785 }
27786 document.addEventListener("desktop-mode-open-bug-report", () => {
27787 openBugReport();
27788 });
27789 if (layoutDispatcher) {
27790 layoutDispatcher.appendSystemTile(
27791 {
27792 id: BUG_REPORT_WINDOW_ID,
27793 title: "Report a bug",
27794 icon: "dashicons-buddicons-replies",
27795 isOpen: () => {
27796 const win = manager.getById(BUG_REPORT_WINDOW_ID);
27797 if (!win) {
27798 return false;
27799 }
27800 return (win.config.desktopId || manager.getActiveDesktopId()) === manager.getActiveDesktopId();
27801 },
27802 onOpen: openBugReport
27803 },
27804 "core"
27805 );
27806 layoutDispatcher.appendSystemTile(
27807 getExitDesktopModeTileDef(),
27808 "core"
27809 );
27810 }
27811 const dock = layoutDispatcher?.getPrimary() ?? null;
27812 void syncNativeWindows(
27813 Array.isArray(config.nativeWindows) ? config.nativeWindows : []
27814 );
27815 const hasSession = hasRestorableSession(config.session);
27816 const sessionRestore = hasSession ? restoreSession(manager, config, desktopArea).catch((err) => {
27817 if (typeof console !== "undefined") {
27818 console.error("[desktop-mode] session restore failed:", err);
27819 }
27820 }) : Promise.resolve();
27821 const defaultEnabled = config.defaultWindow?.enabled !== false;
27822 const defaultUrlEarly = config.defaultWindow?.url ?? "";
27823 const isNativeDefault = typeof defaultUrlEarly === "string" && defaultUrlEarly.startsWith("native:");
27824 if (shouldAutoOpenCurrentPage({
27825 fromPortal: config.fromPortal,
27826 fromPortalIntent: config.fromPortalIntent,
27827 hasSession,
27828 defaultEnabled,
27829 isNativeDefault
27830 })) {
27831 void sessionRestore.then(
27832 () => openCurrentPage(manager, config).catch((err) => {
27833 if (typeof console !== "undefined") {
27834 console.error("[desktop-mode] openCurrentPage failed:", err);
27835 }
27836 })
27837 );
27838 }
27839 const saveSession = createSessionSaver(manager, config);
27840 wireSessionEvents(saveSession);
27841 const setDefaultWindow = async (url) => {
27842 try {
27843 const response = await trackedFetch(
27844 manager,
27845 config.defaultWindowUrl,
27846 {
27847 method: "POST",
27848 credentials: "same-origin",
27849 headers: {
27850 "Content-Type": "application/json",
27851 "X-WP-Nonce": config.restNonce
27852 },
27853 body: JSON.stringify({ url })
27854 },
27855 { source: "desktop-mode/default-window" }
27856 );
27857 if (!response.ok) {
27858 throw new Error(`HTTP ${response.status}`);
27859 }
27860 const data = await response.json();
27861 config.defaultWindow = data;
27862 document.dispatchEvent(
27863 new CustomEvent("desktop-mode-default-window-changed", {
27864 detail: data
27865 })
27866 );
27867 } catch (err) {
27868 doAction(HOOKS.SHELL_ERROR, { scope: "default-window-save", error: err });
27869 if (typeof console !== "undefined") {
27870 console.error(
27871 "[desktop-mode] Failed to save default window:",
27872 err
27873 );
27874 }
27875 }
27876 };
27877 manager.onToggleStartupRequested = (win) => {
27878 const currentPref = config.defaultWindow;
27879 const isNative = !!win.config.native;
27880 const winValue = isNative ? `native:${win.id}` : win.getCurrentUrl();
27881 const matchesCurrent = isNative ? currentPref?.url === winValue : urlMatchKey(currentPref?.url ?? "") === urlMatchKey(winValue);
27882 const alreadyDefault = !!currentPref?.enabled && matchesCurrent;
27883 void setDefaultWindow(alreadyDefault ? null : winValue);
27884 };
27885 if (config.defaultWindow?.enabled && config.fromPortal && !config.fromPortalIntent && !hasSession && isNativeDefault) {
27886 const nativeId = defaultUrlEarly.slice("native:".length);
27887 queueMicrotask(() => {
27888 if (nativeId === OS_SETTINGS_WINDOW_ID) {
27889 openOsSettings();
27890 return;
27891 }
27892 void nativeWindows.openById(nativeId);
27893 });
27894 }
27895 const placeSystemTile = (item) => {
27896 layoutDispatcher?.appendSystemTile(item);
27897 };
27898 const syncServerWidgets = createWidgetRegistrySync({
27899 layer: widgetLayer
27900 });
27901 void syncServerWidgets(
27902 Array.isArray(config.serverWidgets) ? config.serverWidgets : []
27903 );
27904 const syncServerWallpapers = createWallpaperRegistrySync({
27905 osSettings
27906 });
27907 void syncServerWallpapers(
27908 Array.isArray(config.serverWallpapers) ? config.serverWallpapers : []
27909 );
27910 const syncServerCommands = createCommandRegistrySync();
27911 void syncServerCommands(
27912 Array.isArray(config.serverCommandScripts) ? config.serverCommandScripts : [],
27913 Array.isArray(config.serverCommands) ? config.serverCommands : []
27914 );
27915 const syncServerSettingsTabs = createSettingsTabRegistrySync();
27916 void syncServerSettingsTabs(
27917 Array.isArray(config.serverSettingsTabScripts) ? config.serverSettingsTabScripts : [],
27918 Array.isArray(config.serverSettingsTabs) ? config.serverSettingsTabs : []
27919 );
27920 const syncServerTitleBarButtons = createTitleBarButtonRegistrySync();
27921 void syncServerTitleBarButtons(
27922 Array.isArray(config.serverTitleBarButtonScripts) ? config.serverTitleBarButtonScripts : []
27923 );
27924 const syncServerDockRailRenderers = createDockRailRendererSync();
27925 void syncServerDockRailRenderers(
27926 Array.isArray(config.serverDockRailRendererScripts) ? config.serverDockRailRendererScripts : []
27927 );
27928 const syncServerWindowThemes = createWindowThemeRegistrySync();
27929 void syncServerWindowThemes(
27930 Array.isArray(config.serverWindowThemeScripts) ? config.serverWindowThemeScripts : [],
27931 Array.isArray(config.serverWindowThemes) ? config.serverWindowThemes : []
27932 );
27933 registerBuiltInControls();
27934 const syncServerWindowControls = createWindowControlRegistrySync();
27935 void syncServerWindowControls(
27936 Array.isArray(config.serverWindowControlScripts) ? config.serverWindowControlScripts : [],
27937 Array.isArray(config.serverWindowControls) ? config.serverWindowControls : []
27938 );
27939 const syncServerWindowSlots = createWindowSlotRegistrySync();
27940 void syncServerWindowSlots(
27941 Array.isArray(config.serverWindowSlotScripts) ? config.serverWindowSlotScripts : [],
27942 Array.isArray(config.serverWindowSlots) ? config.serverWindowSlots : []
27943 );
27944 applyServerWindowNotices(
27945 Array.isArray(config.serverWindowNotices) ? config.serverWindowNotices : []
27946 );
27947 const syncServerWindowChromes = createWindowChromeRegistrySync();
27948 void syncServerWindowChromes(
27949 Array.isArray(config.serverWindowChromeScripts) ? config.serverWindowChromeScripts : [],
27950 Array.isArray(config.serverWindowChromes) ? config.serverWindowChromes : []
27951 );
27952 const connectionBridge = createConnectionBridge(manager);
27953 attachBroadcastBus(manager);
27954 scheduleIdleBoot(() => installBroadcastReceiver());
27955 installWindowLoadingTransitions();
27956 addAction(
27957 "desktop-mode.shell.toast",
27958 "desktop-mode/shell-toast",
27959 (payload) => {
27960 if (!payload || typeof payload.message !== "string") {
27961 return;
27962 }
27963 showToast({
27964 message: payload.message,
27965 action: payload.action,
27966 duration: payload.duration
27967 });
27968 }
27969 );
27970 const cfgWithBin = config;
27971 const cfgCountRaw = cfgWithBin.recycleBinCount;
27972 startRecycleBinBadge(
27973 Number(cfgCountRaw) || 0,
27974 typeof cfgWithBin.recycleBinCountUrl === "string" ? cfgWithBin.recycleBinCountUrl : ""
27975 );
27976 registerBuiltInPeekRenderers({
27977 getRecycleBinCount: _currentRecycleBinBadge
27978 });
27979 window.__desktopModeConnectionBridge = connectionBridge;
27980 addAction(HOOKS.WINDOW_CLOSED, "desktop-mode/connection-cleanup", (e) => {
27981 if (e?.windowId) {
27982 connectionBridge.onWindowClosed(e.windowId);
27983 }
27984 });
27985 addAction(HOOKS.IFRAME_READY, "desktop-mode/connection-rearm", (e) => {
27986 if (e?.windowId) {
27987 connectionBridge.onIframeReady(e.windowId);
27988 }
27989 });
27990 const registerWindow = createRegisterWindow(manager);
27991 const renderIcons = (icons) => {
27992 if (layoutDispatcher) {
27993 layoutDispatcher.applyDesktopIcons(icons);
27994 return;
27995 }
27996 renderDesktopIcons(desktopArea, icons, {
27997 openWindow: nativeWindows.openById,
27998 manager,
27999 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
28000 });
28001 };
28002 const refreshMenu = bindMenuRefresh({
28003 layoutDispatcher,
28004 config,
28005 syncNativeWindows,
28006 syncServerWidgets,
28007 syncServerWallpapers,
28008 syncServerCommands,
28009 syncServerSettingsTabs,
28010 syncServerTitleBarButtons,
28011 syncServerDockRailRenderers,
28012 renderIcons
28013 });
28014 osSettings.subscribeOsSettings((snapshot) => {
28015 if (!layoutDispatcher) {
28016 return;
28017 }
28018 const prevLayout = layoutDispatcher.getLayout();
28019 layoutDispatcher.setLayout(snapshot.desktopLayout);
28020 desktopApi.dock = layoutDispatcher.getPrimary();
28021 desktopApi.sideDock = layoutDispatcher.getSide();
28022 desktopApi.desktopLayout = snapshot.desktopLayout;
28023 if (prevLayout === snapshot.desktopLayout) {
28024 layoutDispatcher.refresh();
28025 }
28026 syncShortcutsWithVisibility(
28027 snapshot.itemVisibility,
28028 snapshot.dockPromotedPositions
28029 );
28030 setCurrentLayout(snapshot.desktopLayout);
28031 });
28032 installShortcutsSync(
28033 () => osSettings.getOsSettingsSnapshot().itemVisibility,
28034 () => osSettings.getOsSettingsSnapshot().dockPromotedPositions
28035 );
28036 setCurrentLayout(osSettings.getOsSettingsSnapshot().desktopLayout);
28037 const desktopApi = buildPublicApi({
28038 manager,
28039 dock,
28040 layoutDispatcher,
28041 osSettings,
28042 iconsApi,
28043 filesApi,
28044 saveSession,
28045 widgetLayer,
28046 registerWindow,
28047 openWindowById: nativeWindows.openById,
28048 openNewWindowById: nativeWindows.openNewById,
28049 placeSystemTile,
28050 setDefaultWindow,
28051 refreshMenu,
28052 openOsSettings,
28053 aiAssistant,
28054 dragBridge,
28055 dragManager,
28056 connect: connectionBridge.connect,
28057 getConnection: connectionBridge.getConnection,
28058 config
28059 });
28060 installPublicApi(desktopApi);
28061 scheduleIdleBoot(() => installRecycleBinDropTargets(dragManager));
28062 bootHeartbeatBus();
28063 scheduleIdleBoot(() => bootNonceRefresh());
28064 bootStickyNotes({
28065 host: desktopArea,
28066 config,
28067 getActiveDesktopId: () => manager.getActiveDesktopId(),
28068 openArtifact: (url, title) => {
28069 const id = deriveWindowId(url, config.adminUrl);
28070 void manager.open({
28071 id,
28072 baseId: id,
28073 url,
28074 title,
28075 icon: "dashicons-edit-page"
28076 });
28077 },
28078 onError: (message) => {
28079 showToast({ message });
28080 }
28081 });
28082 installOpenDeps({
28083 openUrl: ({ id, url, title, icon }) => {
28084 if (tryNativeUrlRemap(url)) {
28085 return true;
28086 }
28087 void manager.open({ id, baseId: id, url, title, icon });
28088 return true;
28089 },
28090 openNativeWindow: (id) => nativeWindows.openById(id),
28091 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
28092 });
28093 setUserAssociations(
28094 config.userFileAssociations ?? {}
28095 );
28096 if (typeof config.filesUrl === "string" && config.filesUrl) {
28097 installRestDeps({
28098 baseUrl: config.filesUrl,
28099 nonce: config.restNonce
28100 });
28101 const rootHost = document.getElementById("desktop-mode-area");
28102 if (rootHost) {
28103 const layerHandle = mountFilesLayer(rootHost, 0);
28104 const reveal = () => {
28105 if (!desktopArea.classList.contains("desktop-mode-area--booting")) {
28106 return;
28107 }
28108 requestAnimationFrame(() => {
28109 desktopArea.classList.remove("desktop-mode-area--booting");
28110 });
28111 };
28112 const safetyTimer = setTimeout(reveal, 2e3);
28113 void layerHandle.hydrated.then(() => {
28114 clearTimeout(safetyTimer);
28115 reveal();
28116 });
28117 }
28118 }
28119 scheduleIdleBoot(() => startFilesHeartbeat());
28120 scheduleIdleBoot(() => startFilesRestoreSync());
28121 scheduleIdleBoot(() => bootPresenceProbe());
28122 doAction(HOOKS.COMPONENTS_REGISTERED, { tags: [...WPD_COMPONENT_TAGS] });
28123 registerBuiltInCommands();
28124 bootstrapPwa(config, showToast);
28125 const overlayPreload = () => {
28126 preloadShellOverlays(config.shellOverlaysBundleUrl ?? "");
28127 preloadWindowSystem(config.windowSystemBundleUrl ?? "");
28128 };
28129 if (typeof window.requestIdleCallback === "function") {
28130 window.requestIdleCallback(overlayPreload, { timeout: 1500 });
28131 } else {
28132 window.setTimeout(overlayPreload, 0);
28133 }
28134 doAction(HOOKS.INIT, { config });
28135 _earlyReady = true;
28136 const queued = _earlyReadyQueue.splice(0);
28137 for (const cb of queued) {
28138 try {
28139 cb();
28140 } catch (err) {
28141 doAction(HOOKS.SHELL_ERROR, {
28142 scope: "when-ready-cb",
28143 error: err
28144 });
28145 if (typeof console !== "undefined") {
28146 console.error("[desktop-mode] whenReady cb threw:", err);
28147 }
28148 }
28149 }
28150 osSettings.apply();
28151 widgetLayer?.hydrate();
28152 window.addEventListener("pagehide", () => {
28153 wallpaperLayer?.teardownActive();
28154 widgetLayer?.disposeAll();
28155 });
28156 bindShellLifecycle();
28157 bindTopWindowLinkInterceptor(manager, config);
28158 const relayoutRoot = (transform, persist2 = true) => {
28159 const root = filesApi.store.getState().placementsByFolder.get(0) ?? [];
28160 const ordered = transform(root);
28161 const rowsPerCol = Math.max(
28162 1,
28163 Math.floor((desktopArea.clientHeight - 16) / 110)
28164 );
28165 const occupied = /* @__PURE__ */ new Set();
28166 let i = 0;
28167 for (const p of ordered) {
28168 const cell = snapToEmptyCell(
28169 16 + Math.floor(i / rowsPerCol) * 96,
28170 16 + i % rowsPerCol * 110,
28171 occupied,
28172 desktopArea
28173 );
28174 occupied.add(`${cell.col},${cell.row}`);
28175 i++;
28176 if (p.x === cell.x && p.y === cell.y) {
28177 continue;
28178 }
28179 filesApi.store.upsertPlacement({
28180 ...p,
28181 x: cell.x,
28182 y: cell.y,
28183 sortOrder: i
28184 });
28185 if (!persist2) {
28186 continue;
28187 }
28188 void updatePlacement(p.id, {
28189 x: cell.x,
28190 y: cell.y,
28191 sortOrder: i
28192 }).catch((err) => {
28193 console.error("[desktop-mode] relayout persist failed", err);
28194 });
28195 }
28196 };
28197 const rootSortTransform = (mode) => (arr) => {
28198 const sorted = arr.slice();
28199 switch (mode) {
28200 case "name-asc":
28201 sorted.sort(
28202 (a, b) => a.file.title.localeCompare(b.file.title)
28203 );
28204 break;
28205 case "name-desc":
28206 sorted.sort(
28207 (a, b) => b.file.title.localeCompare(a.file.title)
28208 );
28209 break;
28210 case "date-asc":
28211 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
28212 break;
28213 case "date-desc":
28214 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
28215 break;
28216 }
28217 return sorted;
28218 };
28219 const ROOT_SORT_MODE_KEY = "desktop-mode:root-sort-mode";
28220 const isRootSortMode = (v) => v === "name-asc" || v === "name-desc" || v === "date-asc" || v === "date-desc";
28221 let rootSortMode = (() => {
28222 try {
28223 const raw = window.localStorage.getItem(ROOT_SORT_MODE_KEY);
28224 return isRootSortMode(raw) ? raw : null;
28225 } catch {
28226 return null;
28227 }
28228 })();
28229 const setRootSortMode = (mode) => {
28230 rootSortMode = mode;
28231 try {
28232 if (mode) {
28233 window.localStorage.setItem(ROOT_SORT_MODE_KEY, mode);
28234 } else {
28235 window.localStorage.removeItem(ROOT_SORT_MODE_KEY);
28236 }
28237 } catch {
28238 }
28239 };
28240 addAction(
28241 "desktop-mode.files.tile-manually-placed",
28242 "desktop-mode/root-sort-clear",
28243 (payload) => {
28244 const folderId = payload?.folderId;
28245 if (folderId === 0) {
28246 setRootSortMode(null);
28247 }
28248 }
28249 );
28250 if (typeof ResizeObserver !== "undefined") {
28251 let lastW = desktopArea.clientWidth;
28252 let lastH = desktopArea.clientHeight;
28253 const ro = new ResizeObserver(() => {
28254 if (!rootSortMode) {
28255 return;
28256 }
28257 const w = desktopArea.clientWidth;
28258 const h = desktopArea.clientHeight;
28259 if (w === lastW && h === lastH) {
28260 return;
28261 }
28262 lastW = w;
28263 lastH = h;
28264 relayoutRoot(rootSortTransform(rootSortMode), false);
28265 });
28266 ro.observe(desktopArea);
28267 }
28268 let pointerdownOnWallpaper = false;
28269 desktopArea.addEventListener("pointerdown", (e) => {
28270 if (!e.isPrimary) {
28271 return;
28272 }
28273 pointerdownOnWallpaper = e.target === desktopArea;
28274 });
28275 desktopArea.addEventListener("click", (e) => {
28276 if (!osSettings.state.showDesktopOnWallpaperClick) {
28277 return;
28278 }
28279 if (e.target !== desktopArea) {
28280 return;
28281 }
28282 if (!pointerdownOnWallpaper) {
28283 return;
28284 }
28285 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
28286 return;
28287 }
28288 if (isWallpaperMenuOpen()) {
28289 return;
28290 }
28291 if (dragManager.recentlyEndedDrag()) {
28292 return;
28293 }
28294 manager.toggleShowDesktop();
28295 });
28296 desktopArea.addEventListener("contextmenu", (e) => {
28297 if (e.target !== desktopArea) {
28298 return;
28299 }
28300 e.preventDefault();
28301 const clientX = e.clientX;
28302 const clientY = e.clientY;
28303 (() => {
28304 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
28305 return;
28306 }
28307 if (isWallpaperMenuOpen()) {
28308 closeWallpaperMenu();
28309 return;
28310 }
28311 const dropClient = { x: clientX, y: clientY };
28312 const cellAtClick = () => {
28313 const rect = desktopArea.getBoundingClientRect();
28314 const rawX = Math.max(0, dropClient.x - rect.left);
28315 const rawY = Math.max(0, dropClient.y - rect.top);
28316 const occupied = buildOccupiedSet(
28317 filesApi.store.getState().placementsByFolder.get(0) ?? []
28318 );
28319 return snapToEmptyCell(rawX, rawY, occupied, desktopArea);
28320 };
28321 const createUrlPlacement = (dialogTitle, description) => {
28322 openUrlDialog({
28323 title: dialogTitle,
28324 description,
28325 nameLabel: "Name",
28326 urlLabel: "URL",
28327 submitLabel: "Create",
28328 onSubmit: async ({ name, url }) => {
28329 const cell = cellAtClick();
28330 const placement = await createPlacement({
28331 type: "link",
28332 ref: url,
28333 parentId: 0,
28334 x: cell.x,
28335 y: cell.y,
28336 meta: name ? { name } : void 0
28337 });
28338 filesApi.store.upsertPlacement(placement);
28339 }
28340 });
28341 };
28342 const items = buildMenuItems({
28343 createFolder: () => {
28344 openCreateFolderDialog({
28345 onSubmit: async (name) => {
28346 const folder = await createFolder({ name });
28347 const cell = cellAtClick();
28348 const placement = await createPlacement({
28349 type: "folder",
28350 ref: String(folder.id),
28351 parentId: 0,
28352 x: cell.x,
28353 y: cell.y
28354 });
28355 filesApi.store.upsertFolder(folder);
28356 filesApi.store.upsertPlacement(placement);
28357 }
28358 });
28359 },
28360 createUrl: () => createUrlPlacement(
28361 "New URL",
28362 "Opens the URL in a new browser tab."
28363 ),
28364 toggleShowDesktop: () => manager.toggleShowDesktop(),
28365 openOsSettings: () => openOsSettings(),
28366 sortIcons: (mode) => {
28367 setRootSortMode(mode);
28368 relayoutRoot(rootSortTransform(mode));
28369 },
28370 currentSortMode: rootSortMode,
28371 includeShowDesktop: !osSettings.state.showDesktopOnWallpaperClick,
28372 labels: {
28373 createFolder: "New folder",
28374 showDesktop: "Show desktop",
28375 osSettings: "OS Settings",
28376 sortHeading: "Sort by",
28377 sortNameAsc: "Name (A → Z)",
28378 sortNameDesc: "Name (Z → A)",
28379 sortDateAsc: "Date (oldest first)",
28380 sortDateDesc: "Date (newest first)",
28381 newUrl: "New URL"
28382 },
28383 serverItems: config.serverWallpaperMenuItems ?? []
28384 });
28385 openWallpaperMenu(
28386 document.body,
28387 { x: clientX, y: clientY },
28388 items
28389 );
28390 })();
28391 });
28392 void Promise.resolve().then(() => index).then((mod) => {
28393 mod.bootOsFileDrop({
28394 config: config.dropConfig,
28395 mediaUrl: config.mediaUrl,
28396 restNonce: config.restNonce
28397 });
28398 });
28399 document.dispatchEvent(
28400 new CustomEvent("desktop-mode-init", {
28401 detail: { config, restored: hasSession }
28402 })
28403 );
28404 }
28405 startMissingImportWarner();
28406 if (document.readyState === "loading") {
28407 document.addEventListener("DOMContentLoaded", init);
28408 } else {
28409 init();
28410 }
28411 const _initial = {
28412 tab: null,
28413 requestedAt: 0
28414 };
28415 let _store = null;
28416 function getStore() {
28417 if (_store) {
28418 return _store;
28419 }
28420 const w = window;
28421 const factory = w.wp?.desktop?.createSharedStore;
28422 if (typeof factory !== "function") {
28423 return null;
28424 }
28425 _store = factory(
28426 "desktop-mode/plugins-window/tab-target",
28427 () => ({ ..._initial })
28428 );
28429 return _store;
28430 }
28431 function setPluginsWindowTab(tab) {
28432 const store2 = getStore();
28433 if (store2) {
28434 store2.state.tab = tab;
28435 store2.state.requestedAt = Date.now();
28436 store2.notify();
28437 return;
28438 }
28439 const w = window;
28440 w._wpdPluginsWindowTab = { tab, requestedAt: Date.now() };
28441 }
28442 function consumePluginsWindowTab() {
28443 const store2 = getStore();
28444 if (store2) {
28445 const tab = store2.state.tab;
28446 if (tab !== null) {
28447 store2.state.tab = null;
28448 store2.state.requestedAt = 0;
28449 store2.notify();
28450 }
28451 return tab;
28452 }
28453 const w = window;
28454 const prev = w._wpdPluginsWindowTab;
28455 if (prev) {
28456 w._wpdPluginsWindowTab = { tab: null, requestedAt: 0 };
28457 return prev.tab;
28458 }
28459 return null;
28460 }
28461 function subscribePluginsWindowTab(cb) {
28462 const store2 = getStore();
28463 if (!store2) {
28464 return () => {
28465 };
28466 }
28467 return store2.subscribe((state2) => cb({ ...state2 }));
28468 }
28469 const tabTarget = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
28470 __proto__: null,
28471 consumePluginsWindowTab,
28472 setPluginsWindowTab,
28473 subscribePluginsWindowTab
28474 }, Symbol.toStringTag, { value: "Module" }));
28475 const FILE_DROP_HOOKS = {
28476 /**
28477 * Filter — fires once per drop, after the manager has parsed
28478 * the OS `DataTransfer` into `File[]` and BEFORE the mime /
28479 * size filter runs.
28480 *
28481 * Signature: `(files: File[], ctx: DropContext) => File[]`.
28482 * Return an empty array to abort the drop silently.
28483 */
28484 FILES_DETECTED: "desktop-mode.drop.files-detected",
28485 /**
28486 * Action — fires after the mime / size filter has rejected
28487 * one or more files. Payload: `{ rejections: DropRejection[],
28488 * context: DropContext }`. The shell toasts a default message;
28489 * subscribers can surface a custom UX (a side panel with the
28490 * list, an analytics call).
28491 */
28492 FILES_REJECTED: "desktop-mode.drop.files-rejected",
28493 /**
28494 * Filter — fires per file before the upload dialog renders.
28495 * Receives `DropFileEntry` (the underlying file + the
28496 * manager's default `fields`). Mutate `fields` (or return a
28497 * new object) to change what the user sees in the form.
28498 *
28499 * Signature: `(entry: DropFileEntry, ctx: DropContext)
28500 * => DropFileEntry`.
28501 */
28502 DIALOG_FIELDS: "desktop-mode.drop.dialog-fields",
28503 /**
28504 * Filter — last call before the manager `POST`s to
28505 * `wp/v2/media`. Receives `{ file: File, fields:
28506 * DropDialogFields, mime: string }`. Return `null` to cancel
28507 * the upload entirely (e.g. a plugin handled it via a
28508 * different endpoint).
28509 *
28510 * Signature: `(payload, ctx: DropContext) => payload | null`.
28511 */
28512 BEFORE_UPLOAD: "desktop-mode.drop.before-upload",
28513 /**
28514 * Action — fires once `BEFORE_UPLOAD` has cleared and the XHR
28515 * is `open()`ed, immediately before `send()`. Payload:
28516 * `{ file: File, fields: DropDialogFields, context: DropContext,
28517 * abort: () => void }`. The `abort` handle aborts the in-flight
28518 * request; the manager rejects with `UploadAbortedError` and
28519 * fires `UPLOAD_FAILED` with that error.
28520 *
28521 * Pair with `UPLOAD_PROGRESS` to drive a progress UI; pair with
28522 * `AFTER_UPLOAD` / `UPLOAD_FAILED` to know when the upload ends.
28523 *
28524 * @since 0.31.0
28525 */
28526 UPLOAD_STARTED: "desktop-mode.drop.upload-started",
28527 /**
28528 * Action — fires for every `XMLHttpRequestUpload.progress` event.
28529 * Payload: `{ file: File, fields: DropDialogFields, context:
28530 * DropContext, loaded: number, total: number, indeterminate:
28531 * boolean }`. `total` is `0` and `indeterminate` is `true` when
28532 * the request body length isn't known (rare for multipart, but
28533 * possible on transcoding proxies); subscribers should treat
28534 * that as an indeterminate state.
28535 *
28536 * A synthetic 100%-loaded event is dispatched once the `upload`
28537 * stream emits `load` so a HUD can show a definite "wrapping up"
28538 * state while the server finishes the response.
28539 *
28540 * @since 0.31.0
28541 */
28542 UPLOAD_PROGRESS: "desktop-mode.drop.upload-progress",
28543 /**
28544 * Action — fires after a successful upload. Payload:
28545 * `{ file: File, result: DropUploadResult, fields:
28546 * DropDialogFields, context: DropContext }`.
28547 *
28548 * The `file` field carries the same `File` reference that
28549 * `UPLOAD_STARTED` / `UPLOAD_PROGRESS` exposed (i.e. the
28550 * payload returned by the `BEFORE_UPLOAD` filter, in case a
28551 * plugin swapped the file). Subscribers tracking per-file
28552 * state — progress HUDs, sequence counters — should match on
28553 * this identity rather than the filename: two drops of
28554 * `photo.jpg` from different folders would otherwise route
28555 * each other's success event to the wrong row.
28556 *
28557 * @since 0.31.0 the `file` field was added; pre-0.31.0 code
28558 * that destructured `{ result, fields, context }` keeps working.
28559 */
28560 AFTER_UPLOAD: "desktop-mode.drop.after-upload",
28561 /**
28562 * Action — fires after an upload fails. Payload:
28563 * `{ file: File, error: Error, context: DropContext }`.
28564 * `error` is an `UploadAbortedError` when the failure came
28565 * from the caller invoking the `abort()` handle on
28566 * `UPLOAD_STARTED`.
28567 *
28568 * `file` carries the same identity as `UPLOAD_STARTED` /
28569 * `UPLOAD_PROGRESS` / `AFTER_UPLOAD` — the post-`BEFORE_UPLOAD`
28570 * `File`, in case a plugin swapped it. Match by reference, not
28571 * filename: a HUD that keys its row map on the started-File
28572 * needs the same key here, otherwise the row stays stuck in
28573 * "running" after a failure when a `BEFORE_UPLOAD` filter
28574 * replaced the file.
28575 */
28576 UPLOAD_FAILED: "desktop-mode.drop.upload-failed"
28577 };
28578 const IFRAME_PASSTHROUGH_SELECTORS = [
28579 ".components-drop-zone",
28580 "[data-drop-zone]",
28581 ".uploader-window",
28582 ".media-frame-content"
28583 ];
28584 function dragHasFiles(ev) {
28585 const types = ev.dataTransfer?.types;
28586 if (!types) {
28587 return false;
28588 }
28589 const list2 = types;
28590 if (typeof list2.includes === "function") {
28591 return list2.includes("Files");
28592 }
28593 if (typeof list2.contains === "function") {
28594 return list2.contains("Files");
28595 }
28596 for (let i = 0; i < list2.length; i++) {
28597 if (list2[i] === "Files") {
28598 return true;
28599 }
28600 }
28601 return false;
28602 }
28603 function resolveWindowIdFromSource(source) {
28604 if (!source) {
28605 return void 0;
28606 }
28607 const iframes = document.querySelectorAll("iframe");
28608 for (const f of Array.from(iframes)) {
28609 if (f.contentWindow === source) {
28610 const host = f.closest("[data-window-id]");
28611 return host?.getAttribute("data-window-id") || void 0;
28612 }
28613 }
28614 return void 0;
28615 }
28616 function mountOsFileDropManager(opts) {
28617 const host = window;
28618 if (host.__desktopModeOsFileDropMounted) {
28619 return host.__desktopModeOsFileDropMounted;
28620 }
28621 if (!opts.config.enabled) {
28622 return mountNoOp();
28623 }
28624 const overlayEl = ensureDropOverlay();
28625 let dragDepth = 0;
28626 let dragWatchdog = null;
28627 const resetOverlay = () => {
28628 dragDepth = 0;
28629 overlayEl.classList.remove("is-active");
28630 if (dragWatchdog !== null) {
28631 clearTimeout(dragWatchdog);
28632 dragWatchdog = null;
28633 }
28634 };
28635 const bumpWatchdog = () => {
28636 if (dragWatchdog !== null) {
28637 clearTimeout(dragWatchdog);
28638 }
28639 dragWatchdog = setTimeout(resetOverlay, 250);
28640 };
28641 const onDragEnter = (ev) => {
28642 if (!dragHasFiles(ev)) {
28643 return;
28644 }
28645 ev.preventDefault();
28646 dragDepth++;
28647 overlayEl.classList.add("is-active");
28648 bumpWatchdog();
28649 };
28650 const onDragOver = (ev) => {
28651 if (!dragHasFiles(ev)) {
28652 return;
28653 }
28654 if (ev.defaultPrevented) {
28655 resetOverlay();
28656 return;
28657 }
28658 ev.preventDefault();
28659 if (ev.dataTransfer) {
28660 ev.dataTransfer.dropEffect = "copy";
28661 }
28662 bumpWatchdog();
28663 };
28664 const onDragLeave = () => {
28665 dragDepth = Math.max(0, dragDepth - 1);
28666 if (dragDepth === 0) {
28667 overlayEl.classList.remove("is-active");
28668 }
28669 };
28670 const onDrop = (ev) => {
28671 if (!dragHasFiles(ev)) {
28672 return;
28673 }
28674 if (ev.defaultPrevented) {
28675 resetOverlay();
28676 return;
28677 }
28678 ev.preventDefault();
28679 resetOverlay();
28680 const files = ev.dataTransfer?.files ? Array.from(ev.dataTransfer.files) : [];
28681 if (files.length === 0) {
28682 return;
28683 }
28684 const ctx = classifyDropTarget(ev);
28685 void handleFiles(files, ctx, opts);
28686 };
28687 const onDragEnd2 = () => resetOverlay();
28688 const onVisibilityChange = () => {
28689 if (document.visibilityState === "hidden") {
28690 resetOverlay();
28691 }
28692 };
28693 const onIframeMessage = (ev) => {
28694 if (ev.origin !== window.location.origin) {
28695 return;
28696 }
28697 const data = ev.data;
28698 if (!data || data.type !== "desktop-mode-os-file-drop") {
28699 return;
28700 }
28701 if (!Array.isArray(data.files) || data.files.length === 0) {
28702 return;
28703 }
28704 const files = data.files.filter((f) => f instanceof File);
28705 if (files.length === 0) {
28706 return;
28707 }
28708 const windowId = resolveWindowIdFromSource(ev.source);
28709 if (!windowId) {
28710 return;
28711 }
28712 const ctx = {
28713 surface: "iframe",
28714 windowId,
28715 x: typeof data.x === "number" ? data.x : 0,
28716 y: typeof data.y === "number" ? data.y : 0
28717 };
28718 dragDepth = 0;
28719 overlayEl.classList.remove("is-active");
28720 void handleFiles(files, ctx, opts);
28721 };
28722 window.addEventListener("dragenter", onDragEnter);
28723 window.addEventListener("dragover", onDragOver);
28724 window.addEventListener("dragleave", onDragLeave);
28725 window.addEventListener("drop", onDrop);
28726 window.addEventListener("dragend", onDragEnd2);
28727 document.addEventListener("visibilitychange", onVisibilityChange);
28728 window.addEventListener("blur", onDragEnd2);
28729 window.addEventListener("message", onIframeMessage);
28730 const manager = {
28731 dispose: () => {
28732 window.removeEventListener("dragenter", onDragEnter);
28733 window.removeEventListener("dragover", onDragOver);
28734 window.removeEventListener("dragleave", onDragLeave);
28735 window.removeEventListener("drop", onDrop);
28736 window.removeEventListener("dragend", onDragEnd2);
28737 document.removeEventListener(
28738 "visibilitychange",
28739 onVisibilityChange
28740 );
28741 window.removeEventListener("blur", onDragEnd2);
28742 window.removeEventListener("message", onIframeMessage);
28743 overlayEl.remove();
28744 delete window.__desktopModeOsFileDropMounted;
28745 }
28746 };
28747 host.__desktopModeOsFileDropMounted = manager;
28748 return manager;
28749 }
28750 function ensureDropOverlay() {
28751 const existing = document.querySelector(".desktop-mode-os-drop-overlay");
28752 if (existing) {
28753 return existing;
28754 }
28755 const el = document.createElement("div");
28756 el.className = "desktop-mode-os-drop-overlay";
28757 el.setAttribute("aria-hidden", "true");
28758 el.style.cssText = [
28759 "position:fixed",
28760 "inset:0",
28761 "pointer-events:none",
28762 "z-index:200",
28763 "opacity:0",
28764 "transition:opacity 120ms ease",
28765 "background:radial-gradient(circle at center, rgba(34,113,177,0.18) 0%, rgba(34,113,177,0.06) 60%, transparent 100%)",
28766 "box-shadow:inset 0 0 0 3px rgba(34,113,177,0.55)"
28767 ].join(";");
28768 const label = document.createElement("div");
28769 label.style.cssText = [
28770 "position:absolute",
28771 "top:50%",
28772 "left:50%",
28773 "transform:translate(-50%,-50%)",
28774 "padding:14px 22px",
28775 "border-radius:12px",
28776 "background:rgba(20,20,24,0.78)",
28777 "color:#fff",
28778 "font:600 14px/1.2 -apple-system,BlinkMacSystemFont,sans-serif",
28779 "letter-spacing:0.02em"
28780 ].join(";");
28781 label.textContent = "Drop to upload";
28782 el.appendChild(label);
28783 document.body.appendChild(el);
28784 const style = document.createElement("style");
28785 style.textContent = ".desktop-mode-os-drop-overlay.is-active{opacity:1!important;}";
28786 document.head.appendChild(style);
28787 return el;
28788 }
28789 function mountNoOp() {
28790 const cancel = (ev) => {
28791 if (!dragHasFiles(ev)) {
28792 return;
28793 }
28794 const target2 = ev.target;
28795 if (target2?.closest && IFRAME_PASSTHROUGH_SELECTORS.some((s) => target2.closest(s))) {
28796 return;
28797 }
28798 ev.preventDefault();
28799 };
28800 window.addEventListener("dragover", cancel);
28801 window.addEventListener("drop", cancel);
28802 const host = window;
28803 const manager = {
28804 dispose: () => {
28805 window.removeEventListener("dragover", cancel);
28806 window.removeEventListener("drop", cancel);
28807 delete host.__desktopModeOsFileDropMounted;
28808 }
28809 };
28810 host.__desktopModeOsFileDropMounted = manager;
28811 return manager;
28812 }
28813 function classifyDropTarget(ev) {
28814 const x = ev.clientX;
28815 const y = ev.clientY;
28816 let node = ev.target;
28817 while (node && node !== document.body) {
28818 if (node.tagName === "IFRAME") {
28819 const id = node.closest(
28820 "[data-window-id]"
28821 );
28822 return {
28823 surface: "iframe",
28824 windowId: id?.getAttribute("data-window-id") || void 0,
28825 x,
28826 y
28827 };
28828 }
28829 if (node.hasAttribute("data-window-id")) {
28830 return {
28831 surface: "window",
28832 windowId: node.getAttribute("data-window-id") || void 0,
28833 x,
28834 y
28835 };
28836 }
28837 if (node.classList.contains("desktop-mode-folder-grid")) {
28838 return { surface: "folder", x, y };
28839 }
28840 if (node.id === "desktop-mode-wallpaper" || node.classList.contains("desktop-mode-wallpaper") || node.classList.contains("desktop-mode-desktop")) {
28841 return { surface: "wallpaper", x, y };
28842 }
28843 node = node.parentElement;
28844 }
28845 return { surface: "unknown", x, y };
28846 }
28847 async function handleFiles(rawFiles, ctx, opts) {
28848 const detected = applyFilters(
28849 FILE_DROP_HOOKS.FILES_DETECTED,
28850 rawFiles,
28851 ctx
28852 );
28853 if (!Array.isArray(detected) || detected.length === 0) {
28854 return;
28855 }
28856 const { accepted, rejected } = partitionByPolicy(
28857 detected,
28858 opts.config
28859 );
28860 if (rejected.length > 0) {
28861 doAction(FILE_DROP_HOOKS.FILES_REJECTED, {
28862 rejections: rejected,
28863 context: ctx
28864 });
28865 showToast({
28866 message: rejected.length === 1 ? rejected[0].message : `${rejected.length} files couldn't be uploaded.`
28867 });
28868 }
28869 if (accepted.length === 0) {
28870 return;
28871 }
28872 const entries = accepted.map(({ file, mime }) => {
28873 const base = {
28874 file,
28875 mime,
28876 fields: defaultFields(file, mime)
28877 };
28878 const filtered = applyFilters(
28879 FILE_DROP_HOOKS.DIALOG_FIELDS,
28880 base,
28881 ctx
28882 );
28883 if (!filtered || typeof filtered !== "object" || !("fields" in filtered) || typeof filtered.fields !== "object") {
28884 return base;
28885 }
28886 return filtered;
28887 });
28888 await opts.openDialog(entries, ctx);
28889 }
28890 function partitionByPolicy(files, config) {
28891 const accepted = [];
28892 const rejected = [];
28893 for (const file of files) {
28894 if (file.size === 0) {
28895 rejected.push({
28896 file,
28897 reason: "empty",
28898 message: `“${file.name}” is empty.`
28899 });
28900 continue;
28901 }
28902 if (config.maxSize > 0 && file.size > config.maxSize) {
28903 rejected.push({
28904 file,
28905 reason: "size",
28906 message: `“${file.name}” exceeds the ${formatBytes$1(
28907 config.maxSize
28908 )} upload limit.`
28909 });
28910 continue;
28911 }
28912 const mime = resolveAllowedMime(
28913 file,
28914 config.allowedMimes,
28915 config.extToMime
28916 );
28917 if (!mime) {
28918 rejected.push({
28919 file,
28920 reason: "mime",
28921 message: `“${file.name}” is not an allowed file type.`
28922 });
28923 continue;
28924 }
28925 accepted.push({ file, mime });
28926 }
28927 return { accepted, rejected };
28928 }
28929 function resolveAllowedMime(file, allowedMimes, extToMime) {
28930 if (allowedMimes.length === 0) {
28931 return null;
28932 }
28933 const lower = file.type.toLowerCase();
28934 if (lower && allowedMimes.includes(lower)) {
28935 return lower;
28936 }
28937 const ext = extensionOf(file.name);
28938 if (!ext) {
28939 return null;
28940 }
28941 if (extToMime) {
28942 for (const [key, mime] of Object.entries(extToMime)) {
28943 if (key.split("|").includes(ext) && allowedMimes.includes(mime)) {
28944 return mime;
28945 }
28946 }
28947 return null;
28948 }
28949 const guess = EXTENSION_GUESSES[ext];
28950 if (guess && allowedMimes.includes(guess)) {
28951 return guess;
28952 }
28953 return null;
28954 }
28955 const EXTENSION_GUESSES = {
28956 jpg: "image/jpeg",
28957 jpeg: "image/jpeg",
28958 png: "image/png",
28959 gif: "image/gif",
28960 webp: "image/webp",
28961 avif: "image/avif",
28962 heic: "image/heic",
28963 heif: "image/heif",
28964 svg: "image/svg+xml",
28965 mp4: "video/mp4",
28966 mov: "video/quicktime",
28967 webm: "video/webm",
28968 mp3: "audio/mpeg",
28969 wav: "audio/wav",
28970 pdf: "application/pdf"
28971 };
28972 function extensionOf(name) {
28973 const dot = name.lastIndexOf(".");
28974 if (dot < 0) {
28975 return "";
28976 }
28977 return name.slice(dot + 1).toLowerCase();
28978 }
28979 function defaultFields(file, mime) {
28980 const safeName = sanitizeFilename(file.name);
28981 const ext = extensionOf(safeName);
28982 const stem = ext ? safeName.slice(0, safeName.length - ext.length - 1) : safeName;
28983 const title = humanize(stem);
28984 return {
28985 title,
28986 altText: mime.startsWith("image/") ? title : "",
28987 caption: "",
28988 description: "",
28989 filename: safeName
28990 };
28991 }
28992 function sanitizeFilename(name) {
28993 const cleaned = name.replace(/[\\/]/g, "-").replace(/[\x00-\x1f\x7f]/g, "").replace(/\s+/g, " ").replace(/ *- */g, "-").replace(/-+/g, "-").trim().replace(/^[-.]+|[-.]+$/g, "");
28994 return cleaned || "upload";
28995 }
28996 function humanize(stem) {
28997 const spaced = stem.replace(/[-_]+/g, " ").trim();
28998 if (!spaced) {
28999 return "Upload";
29000 }
29001 return spaced.charAt(0).toUpperCase() + spaced.slice(1);
29002 }
29003 function formatBytes$1(bytes) {
29004 if (bytes >= 1024 * 1024) {
29005 return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
29006 }
29007 if (bytes >= 1024) {
29008 return `${(bytes / 1024).toFixed(0)} KB`;
29009 }
29010 return `${bytes} B`;
29011 }
29012 function formatBytes(bytes) {
29013 if (!Number.isFinite(bytes) || bytes <= 0) {
29014 return "0 B";
29015 }
29016 const units = ["B", "KB", "MB", "GB", "TB"];
29017 let v = bytes;
29018 let i = 0;
29019 while (v >= 1024 && i < units.length - 1) {
29020 v /= 1024;
29021 i++;
29022 }
29023 const decimals = v >= 100 || i === 0 ? 0 : 1;
29024 return `${v.toFixed(decimals)} ${units[i]}`;
29025 }
29026 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}}`;
29027 const _WpdProgressBar = class _WpdProgressBar extends Component {
29028 constructor() {
29029 super(...arguments);
29030 this._ownedAriaLabel = null;
29031 }
29032 render() {
29033 return html`<div class="root" part="root">
29034 <div class="header" part="header" hidden>
29035 <span class="label" part="label"></span>
29036 <span class="percent" part="percent"></span>
29037 </div>
29038 <div class="track" part="track">
29039 <div class="fill" part="fill"></div>
29040 </div>
29041 </div>`;
29042 }
29043 requestUpdate() {
29044 super.requestUpdate();
29045 queueMicrotask(() => this._paint());
29046 }
29047 connectedCallback() {
29048 super.connectedCallback();
29049 queueMicrotask(() => this._paint());
29050 }
29051 _paint() {
29052 const root = this.shadowRoot;
29053 if (!root) {
29054 return;
29055 }
29056 const max = this._readMax();
29057 const indeterminate = this.hasAttribute("indeterminate") || max <= 0;
29058 const value = indeterminate ? 0 : this._readValue(max);
29059 const ratio = indeterminate ? 0 : value / max;
29060 const percent = Math.round(ratio * 100);
29061 const label = this.getAttribute("label") ?? "";
29062 const showPercent = this.hasAttribute("show-percent");
29063 const fill = root.querySelector(".fill");
29064 if (fill && !indeterminate) {
29065 fill.style.width = `${(ratio * 100).toFixed(2)}%`;
29066 } else if (fill && indeterminate) {
29067 fill.style.removeProperty("width");
29068 }
29069 const header = root.querySelector(".header");
29070 const labelEl = root.querySelector(".label");
29071 const percentEl = root.querySelector(".percent");
29072 if (header && labelEl && percentEl) {
29073 const visible = label || showPercent && !indeterminate;
29074 header.hidden = !visible;
29075 labelEl.textContent = label;
29076 percentEl.hidden = !(showPercent && !indeterminate);
29077 percentEl.textContent = `${percent}%`;
29078 }
29079 this._syncAria(max, value, indeterminate, label);
29080 const track = root.querySelector(".track");
29081 if (track) {
29082 track.setAttribute("role", "progressbar");
29083 track.setAttribute("aria-valuemin", "0");
29084 if (indeterminate) {
29085 track.removeAttribute("aria-valuenow");
29086 track.removeAttribute("aria-valuemax");
29087 } else {
29088 track.setAttribute("aria-valuemax", String(max));
29089 track.setAttribute("aria-valuenow", String(value));
29090 }
29091 if (label) {
29092 track.setAttribute("aria-label", label);
29093 } else {
29094 track.removeAttribute("aria-label");
29095 }
29096 }
29097 }
29098 _syncAria(max, value, indeterminate, label) {
29099 this.setAttribute("role", "progressbar");
29100 this.setAttribute("aria-valuemin", "0");
29101 if (indeterminate) {
29102 this.removeAttribute("aria-valuenow");
29103 this.removeAttribute("aria-valuemax");
29104 } else {
29105 this.setAttribute("aria-valuemax", String(max));
29106 this.setAttribute("aria-valuenow", String(value));
29107 }
29108 const existing = this.getAttribute("aria-label");
29109 if (label) {
29110 if (existing === null || existing === this._ownedAriaLabel) {
29111 this.setAttribute("aria-label", label);
29112 this._ownedAriaLabel = label;
29113 }
29114 } else if (existing !== null && existing === this._ownedAriaLabel) {
29115 this.removeAttribute("aria-label");
29116 this._ownedAriaLabel = null;
29117 }
29118 }
29119 _readMax() {
29120 const attr = this.getAttribute("max");
29121 if (attr === null) {
29122 return 100;
29123 }
29124 const raw = parseFloat(attr);
29125 return Number.isFinite(raw) ? raw : 100;
29126 }
29127 _readValue(max) {
29128 const raw = parseFloat(this.getAttribute("value") ?? "0");
29129 if (!Number.isFinite(raw)) {
29130 return 0;
29131 }
29132 if (raw < 0) {
29133 return 0;
29134 }
29135 if (raw > max) {
29136 return max;
29137 }
29138 return raw;
29139 }
29140 };
29141 _WpdProgressBar.props = [
29142 "value",
29143 "max",
29144 "indeterminate",
29145 "tone",
29146 "label",
29147 "showPercent"
29148 ];
29149 _WpdProgressBar.styles = [styles];
29150 _WpdProgressBar.help = {
29151 title: "Progress bar",
29152 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.",
29153 status: "experimental",
29154 since: "0.31.0",
29155 props: [
29156 {
29157 name: "value",
29158 type: "number",
29159 default: "0",
29160 description: "Current progress. Clamped to `[0, max]`."
29161 },
29162 {
29163 name: "max",
29164 type: "number",
29165 default: "100",
29166 description: "Maximum value. Setting `max <= 0` forces indeterminate."
29167 },
29168 {
29169 name: "indeterminate",
29170 type: "boolean",
29171 description: "Show the sweeping indeterminate animation instead of a value-driven fill. The `value` attribute is ignored while this is set."
29172 },
29173 {
29174 name: "tone",
29175 type: '"default" | "success" | "warning" | "danger"',
29176 default: "default",
29177 description: "Tints the fill from the shared status palette."
29178 },
29179 {
29180 name: "label",
29181 type: "string",
29182 description: "Optional inline label rendered above the track. Also wired into `aria-label` when set."
29183 },
29184 {
29185 name: "show-percent",
29186 type: "boolean",
29187 description: "Render a right-aligned percent readout next to the label. Only meaningful in determinate mode."
29188 }
29189 ],
29190 cssProps: [
29191 {
29192 name: "--wpd-progress-track-bg",
29193 default: "var(--desktop-mode-control-bg, rgba(0,0,0,0.08))"
29194 },
29195 {
29196 name: "--wpd-progress-fill",
29197 default: "var(--wp-admin-theme-color, #2271b1)"
29198 },
29199 { name: "--wpd-progress-height", default: "6px" },
29200 { name: "--wpd-progress-radius", default: "999px" },
29201 { name: "--wpd-progress-label-color", default: "inherit" },
29202 { name: "--wpd-progress-label-size", default: "12px" },
29203 { name: "--wpd-progress-label-gap", default: "4px" }
29204 ],
29205 example: html`<wpd-progress-bar
29206 value="42"
29207 label="Uploading hero.jpg"
29208 show-percent
29209 ></wpd-progress-bar>`
29210 };
29211 let WpdProgressBar = _WpdProgressBar;
29212 defineComponent("wpd-progress-bar", WpdProgressBar);
29213 const ROWS = /* @__PURE__ */ new Map();
29214 let panel = null;
29215 function mountUploadProgressHud() {
29216 if (document.body.hasAttribute("data-desktop-mode-suppress-upload-hud")) {
29217 return;
29218 }
29219 if (window.__wpdUploadHud) {
29220 return;
29221 }
29222 window.__wpdUploadHud = true;
29223 const ns = "desktop-mode/os-file-drop-hud";
29224 addAction(
29225 FILE_DROP_HOOKS.UPLOAD_STARTED,
29226 ns,
29227 (payload) => onStarted(payload.file, payload.fields, payload.abort)
29228 );
29229 addAction(
29230 FILE_DROP_HOOKS.UPLOAD_PROGRESS,
29231 ns,
29232 (payload) => onProgress(
29233 payload.file,
29234 payload.loaded,
29235 payload.total,
29236 payload.indeterminate
29237 )
29238 );
29239 addAction(
29240 FILE_DROP_HOOKS.AFTER_UPLOAD,
29241 ns,
29242 (payload) => onComplete(payload.file, payload.fields, payload.result)
29243 );
29244 addAction(
29245 FILE_DROP_HOOKS.UPLOAD_FAILED,
29246 ns,
29247 (payload) => onFailed(payload.file, payload.error)
29248 );
29249 }
29250 function onStarted(file, fields, abort) {
29251 const p = ensurePanel();
29252 const row = document.createElement("div");
29253 row.className = "desktop-mode-upload-hud__row";
29254 const meta = document.createElement("div");
29255 meta.className = "desktop-mode-upload-hud__meta";
29256 const name = document.createElement("div");
29257 name.className = "desktop-mode-upload-hud__name";
29258 name.textContent = fields.filename || file.name;
29259 name.title = fields.filename || file.name;
29260 const statusEl = document.createElement("div");
29261 statusEl.className = "desktop-mode-upload-hud__status";
29262 statusEl.textContent = "Uploading…";
29263 meta.append(name, statusEl);
29264 const bar = document.createElement("wpd-progress-bar");
29265 bar.setAttribute("indeterminate", "");
29266 bar.setAttribute("show-percent", "");
29267 const actions = document.createElement("div");
29268 actions.className = "desktop-mode-upload-hud__actions";
29269 const cancelBtn = document.createElement("wpd-button");
29270 cancelBtn.setAttribute("variant", "tertiary");
29271 cancelBtn.setAttribute("size", "small");
29272 cancelBtn.textContent = "Cancel";
29273 cancelBtn.addEventListener("click", () => {
29274 const r = ROWS.get(file);
29275 if (!r) {
29276 return;
29277 }
29278 if (r.state === "running") {
29279 r.statusEl.textContent = "Cancelling…";
29280 r.cancelBtn.disabled = true;
29281 r.abort();
29282 } else {
29283 dismissRow(r);
29284 }
29285 });
29286 actions.appendChild(cancelBtn);
29287 row.append(meta, bar, actions);
29288 p.querySelector(".desktop-mode-upload-hud__list").appendChild(row);
29289 ROWS.set(file, {
29290 file,
29291 abort,
29292 root: row,
29293 bar,
29294 statusEl,
29295 cancelBtn,
29296 state: "running",
29297 lingerTimer: null
29298 });
29299 updateHeader();
29300 }
29301 function onProgress(file, loaded, total, indeterminate) {
29302 const r = ROWS.get(file);
29303 if (!r || r.state !== "running") {
29304 return;
29305 }
29306 if (indeterminate || total <= 0) {
29307 r.bar.setAttribute("indeterminate", "");
29308 r.statusEl.textContent = `${formatBytes(loaded)} sent`;
29309 } else {
29310 r.bar.removeAttribute("indeterminate");
29311 r.bar.setAttribute("max", String(total));
29312 r.bar.setAttribute("value", String(loaded));
29313 r.statusEl.textContent = `${formatBytes(loaded)} / ${formatBytes(total)}`;
29314 }
29315 }
29316 function onComplete(file, fields, result) {
29317 const r = ROWS.get(file);
29318 if (!r) {
29319 return;
29320 }
29321 r.state = "success";
29322 r.bar.removeAttribute("indeterminate");
29323 r.bar.setAttribute("value", "100");
29324 r.bar.setAttribute("max", "100");
29325 r.bar.setAttribute("tone", "success");
29326 r.statusEl.textContent = "Uploaded";
29327 r.cancelBtn.textContent = "Dismiss";
29328 r.lingerTimer = setTimeout(() => dismissRow(r), 2500);
29329 updateHeader();
29330 activity.publish("desktop-mode/upload-hud-complete", {
29331 filename: fields.filename || result.filename,
29332 attachmentId: result.id
29333 });
29334 }
29335 function onFailed(file, error) {
29336 const r = ROWS.get(file);
29337 if (!r) {
29338 return;
29339 }
29340 r.bar.removeAttribute("indeterminate");
29341 r.bar.setAttribute("tone", "danger");
29342 r.cancelBtn.textContent = "Dismiss";
29343 r.cancelBtn.disabled = false;
29344 if (error.name === "UploadAbortedError") {
29345 r.state = "aborted";
29346 r.statusEl.textContent = "Cancelled";
29347 } else {
29348 r.state = "failed";
29349 r.statusEl.textContent = error.message || "Upload failed";
29350 }
29351 updateHeader();
29352 }
29353 function dismissRow(r) {
29354 if (r.lingerTimer) {
29355 clearTimeout(r.lingerTimer);
29356 }
29357 ROWS.delete(r.file);
29358 r.root.remove();
29359 updateHeader();
29360 if (ROWS.size === 0 && panel) {
29361 panel.hidden = true;
29362 }
29363 }
29364 function ensurePanel() {
29365 if (panel && panel.isConnected) {
29366 panel.hidden = false;
29367 return panel;
29368 }
29369 const p = document.createElement("div");
29370 p.className = "desktop-mode-upload-hud";
29371 p.setAttribute("role", "region");
29372 p.setAttribute("aria-label", "Uploads");
29373 const header = document.createElement("div");
29374 header.className = "desktop-mode-upload-hud__header";
29375 const title = document.createElement("div");
29376 title.className = "desktop-mode-upload-hud__title";
29377 title.textContent = "Uploads";
29378 const closeBtn = document.createElement("button");
29379 closeBtn.type = "button";
29380 closeBtn.className = "desktop-mode-upload-hud__close";
29381 closeBtn.setAttribute("aria-label", "Hide upload panel");
29382 closeBtn.textContent = "×";
29383 closeBtn.addEventListener("click", () => {
29384 for (const r of [...ROWS.values()]) {
29385 if (r.state !== "running") {
29386 dismissRow(r);
29387 }
29388 }
29389 if (ROWS.size === 0) {
29390 p.hidden = true;
29391 }
29392 });
29393 header.append(title, closeBtn);
29394 const list2 = document.createElement("div");
29395 list2.className = "desktop-mode-upload-hud__list";
29396 p.append(header, list2);
29397 document.body.appendChild(p);
29398 panel = p;
29399 return p;
29400 }
29401 function updateHeader() {
29402 if (!panel) {
29403 return;
29404 }
29405 const title = panel.querySelector(
29406 ".desktop-mode-upload-hud__title"
29407 );
29408 if (!title) {
29409 return;
29410 }
29411 const total = ROWS.size;
29412 const running = [...ROWS.values()].filter((r) => r.state === "running").length;
29413 if (running > 0) {
29414 title.textContent = running === total ? `Uploading ${running} file${running === 1 ? "" : "s"}…` : `${running} of ${total} uploading…`;
29415 } else if (total > 0) {
29416 title.textContent = `Uploads (${total})`;
29417 } else {
29418 title.textContent = "Uploads";
29419 }
29420 }
29421 function mountMediaLibraryRefresher() {
29422 if (document.body.hasAttribute(
29423 "data-desktop-mode-suppress-media-library-refresh"
29424 )) {
29425 return;
29426 }
29427 const sentinel = window;
29428 if (sentinel.__wpdMediaLibraryRefresher) {
29429 return;
29430 }
29431 sentinel.__wpdMediaLibraryRefresher = true;
29432 addAction(
29433 FILE_DROP_HOOKS.AFTER_UPLOAD,
29434 "desktop-mode/os-file-drop-library-refresh",
29435 () => refreshOpenLibraries()
29436 );
29437 }
29438 function refreshOpenLibraries() {
29439 const iframes = document.querySelectorAll("iframe");
29440 for (const frame of Array.from(iframes)) {
29441 if (!isMediaLibraryUrl(resolveIframeUrl(frame))) {
29442 continue;
29443 }
29444 try {
29445 frame.contentWindow?.location.reload();
29446 } catch {
29447 const reloadHref = resolveIframeUrl(frame);
29448 if (reloadHref) {
29449 frame.setAttribute("src", reloadHref);
29450 }
29451 }
29452 }
29453 }
29454 function resolveIframeUrl(frame) {
29455 try {
29456 return frame.contentWindow?.location.href ?? frame.src ?? "";
29457 } catch {
29458 return frame.src ?? "";
29459 }
29460 }
29461 function isMediaLibraryUrl(url) {
29462 if (!url) {
29463 return false;
29464 }
29465 return /\/wp-admin\/upload\.php(?:[?#]|$)/.test(url);
29466 }
29467 function bootOsFileDrop(args) {
29468 const config = args.config || {
29469 enabled: false,
29470 allowedMimes: [],
29471 maxSize: 0
29472 };
29473 mountUploadProgressHud();
29474 mountMediaLibraryRefresher();
29475 mountOsFileDropManager({
29476 config,
29477 mediaUrl: args.mediaUrl,
29478 restNonce: args.restNonce,
29479 openDialog: async (entries, ctx) => {
29480 const { openUploadDialog: openUploadDialog2 } = await Promise.resolve().then(() => dialog);
29481 await openUploadDialog2({
29482 entries,
29483 context: ctx,
29484 mediaUrl: args.mediaUrl,
29485 restNonce: args.restNonce
29486 });
29487 }
29488 });
29489 }
29490 const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
29491 __proto__: null,
29492 FILE_DROP_HOOKS,
29493 bootOsFileDrop
29494 }, Symbol.toStringTag, { value: "Module" }));
29495 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}`;
29496 const _WpdTextField = class _WpdTextField extends Component {
29497 constructor() {
29498 super(...arguments);
29499 this._revealed = false;
29500 }
29501 connectedCallback() {
29502 super.connectedCallback();
29503 ensureAutoId(this);
29504 }
29505 render() {
29506 const label = this.label || "";
29507 const value = this.value ?? "";
29508 const placeholder = this.placeholder || "";
29509 const disabled = this.disabled !== null;
29510 const readonly = this.readonly !== null;
29511 const declaredAutocomplete = this.autocomplete;
29512 const declaredType = this.type || "text";
29513 const isPassword = declaredType === "password";
29514 let autocomplete = declaredAutocomplete || "off";
29515 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
29516 autocomplete = "new-password";
29517 }
29518 const maxLength = this.maxlength;
29519 const minLength = this.minlength;
29520 const pattern = this.pattern || "";
29521 const name = this.name || "";
29522 const suffix = this.suffix || "";
29523 const invalid = this.invalid !== null;
29524 const reveal = this.reveal !== null;
29525 const isPasswordIntent = declaredType === "password";
29526 const isMasked = isPasswordIntent && !(reveal && this._revealed);
29527 let effectiveType;
29528 if (isPasswordIntent) {
29529 effectiveType = "text";
29530 } else if (reveal && this._revealed) {
29531 effectiveType = "text";
29532 } else {
29533 effectiveType = declaredType;
29534 }
29535 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
29536 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
29537 const hostId = this.id || "wpd-unnamed";
29538 const inputId = `${hostId}__input`;
29539 return html`
29540 ${label ? html`<label
29541 class="wpd-text-field__label"
29542 for=${inputId}
29543 >${label}</label>` : html``}
29544 <span class=${rowClass}>
29545 <input
29546 id=${inputId}
29547 class=${inputClass}
29548 type=${effectiveType}
29549 .value=${value}
29550 placeholder=${placeholder}
29551 ?disabled=${disabled}
29552 ?readonly=${readonly}
29553 autocomplete=${autocomplete}
29554 maxlength=${maxLength ?? ""}
29555 minlength=${minLength ?? ""}
29556 pattern=${pattern}
29557 name=${name}
29558 aria-invalid=${invalid ? "true" : "false"}
29559 aria-label=${label || ""}
29560 @input=${(e) => this._onInput(e)}
29561 @change=${(e) => this._onChange(e)}
29562 @keydown=${(e) => this._onKeyDown(e)}
29563 />
29564 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
29565 ${reveal ? this._renderRevealButton(disabled) : html``}
29566 </span>
29567 `;
29568 }
29569 _renderRevealButton(disabled) {
29570 const label = this._revealed ? "Hide" : "Show";
29571 return html`
29572 <button
29573 type="button"
29574 class="wpd-text-field__reveal"
29575 aria-label=${label}
29576 aria-pressed=${this._revealed ? "true" : "false"}
29577 ?disabled=${disabled}
29578 tabindex="0"
29579 @click=${() => this._onToggleReveal()}
29580 >
29581 ${this._revealed ? _iconEyeOff() : _iconEye()}
29582 </button>
29583 `;
29584 }
29585 _onToggleReveal() {
29586 this._revealed = !this._revealed;
29587 this.requestUpdate();
29588 }
29589 _onInput(e) {
29590 const input = e.target;
29591 this.value = input.value;
29592 this.emit("wpd-input-change", { value: input.value });
29593 }
29594 _onChange(e) {
29595 const input = e.target;
29596 this.emit("wpd-input-commit", { value: input.value });
29597 }
29598 _onKeyDown(e) {
29599 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
29600 const input = e.target;
29601 this.emit("wpd-submit", { value: input.value });
29602 }
29603 }
29604 };
29605 _WpdTextField.props = [
29606 "label",
29607 "value",
29608 "placeholder",
29609 "disabled",
29610 "readonly",
29611 "autocomplete",
29612 "type",
29613 "maxlength",
29614 "minlength",
29615 "pattern",
29616 "name",
29617 "suffix",
29618 "invalid",
29619 "reveal"
29620 ];
29621 _WpdTextField.styles = [textFieldStyles];
29622 _WpdTextField.help = {
29623 title: "Text field",
29624 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.",
29625 status: "stable",
29626 since: "0.11.0",
29627 props: [
29628 { name: "label", type: "string", description: "Visible label above the input." },
29629 { name: "value", type: "string", description: "Current input value; reflected two-way." },
29630 { name: "placeholder", type: "string", description: "Native placeholder string." },
29631 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
29632 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
29633 {
29634 name: "autocomplete",
29635 type: "string",
29636 default: "off",
29637 description: "Forwarded to the native input autocomplete attribute."
29638 },
29639 {
29640 name: "type",
29641 type: "string",
29642 default: "text",
29643 description: "Native input type (text, password, email, search, tel, url)."
29644 },
29645 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
29646 { name: "minlength", type: "integer (string)", description: "Native minlength." },
29647 { name: "pattern", type: "regex string", description: "Native validation pattern." },
29648 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
29649 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
29650 {
29651 name: "invalid",
29652 type: "boolean attribute",
29653 description: "Marks the field aria-invalid and applies the error style."
29654 },
29655 {
29656 name: "reveal",
29657 type: "boolean attribute",
29658 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
29659 }
29660 ],
29661 events: [
29662 {
29663 name: "wpd-input-change",
29664 description: "Fires on every input keystroke.",
29665 detail: "{ value: string }"
29666 },
29667 {
29668 name: "wpd-input-commit",
29669 description: "Fires on the native change event (blur / Enter).",
29670 detail: "{ value: string }"
29671 },
29672 {
29673 name: "wpd-submit",
29674 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
29675 detail: "{ value: string }"
29676 }
29677 ],
29678 cssProps: [
29679 { name: "--desktop-mode-text", description: "Text colour." },
29680 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
29681 { name: "--desktop-mode-border", description: "Input outline." },
29682 { name: "--desktop-mode-window-bg", description: "Input background." }
29683 ],
29684 example: html`
29685 <wpd-stack gap="8">
29686 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
29687 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
29688 </wpd-stack>
29689 `
29690 };
29691 let WpdTextField = _WpdTextField;
29692 defineComponent("wpd-text-field", WpdTextField);
29693 function _iconEye() {
29694 return html`
29695 <svg
29696 viewBox="0 0 16 16"
29697 width="14"
29698 height="14"
29699 fill="none"
29700 stroke="currentColor"
29701 stroke-width="1.5"
29702 stroke-linecap="round"
29703 stroke-linejoin="round"
29704 aria-hidden="true"
29705 focusable="false"
29706 >
29707 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
29708 <circle cx="8" cy="8" r="2" />
29709 </svg>
29710 `;
29711 }
29712 function _iconEyeOff() {
29713 return html`
29714 <svg
29715 viewBox="0 0 16 16"
29716 width="14"
29717 height="14"
29718 fill="none"
29719 stroke="currentColor"
29720 stroke-width="1.5"
29721 stroke-linecap="round"
29722 stroke-linejoin="round"
29723 aria-hidden="true"
29724 focusable="false"
29725 >
29726 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
29727 <circle cx="8" cy="8" r="2" />
29728 <line x1="2" y1="2" x2="14" y2="14" />
29729 </svg>
29730 `;
29731 }
29732 async function uploadFile(args) {
29733 const initial = {
29734 file: args.file,
29735 mime: args.mime,
29736 fields: args.fields
29737 };
29738 const filtered = applyFilters(
29739 FILE_DROP_HOOKS.BEFORE_UPLOAD,
29740 initial,
29741 args.context
29742 );
29743 if (!filtered) {
29744 throw new UploadCancelledError();
29745 }
29746 const body = new FormData();
29747 const renamed = filtered.fields.filename !== filtered.file.name ? new File([filtered.file], filtered.fields.filename, {
29748 type: filtered.mime || filtered.file.type
29749 }) : filtered.file;
29750 body.append("file", renamed);
29751 body.append("title", filtered.fields.title);
29752 body.append("alt_text", filtered.fields.altText);
29753 body.append("caption", filtered.fields.caption);
29754 body.append("description", filtered.fields.description);
29755 return new Promise((resolve2, reject) => {
29756 const xhr = new XMLHttpRequest();
29757 xhr.open("POST", args.mediaUrl, true);
29758 xhr.withCredentials = true;
29759 xhr.setRequestHeader("X-WP-Nonce", args.restNonce);
29760 xhr.responseType = "text";
29761 let aborted = false;
29762 let bodyFullySent = false;
29763 let cancelRequested = false;
29764 const abort = () => {
29765 cancelRequested = true;
29766 if (bodyFullySent) {
29767 return;
29768 }
29769 aborted = true;
29770 try {
29771 xhr.abort();
29772 } catch {
29773 }
29774 };
29775 doAction(FILE_DROP_HOOKS.UPLOAD_STARTED, {
29776 file: filtered.file,
29777 fields: filtered.fields,
29778 context: args.context,
29779 abort
29780 });
29781 xhr.upload.addEventListener("progress", (e) => {
29782 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
29783 file: filtered.file,
29784 fields: filtered.fields,
29785 context: args.context,
29786 loaded: e.loaded,
29787 total: e.lengthComputable ? e.total : 0,
29788 indeterminate: !e.lengthComputable
29789 });
29790 });
29791 xhr.upload.addEventListener("load", () => {
29792 bodyFullySent = true;
29793 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
29794 file: filtered.file,
29795 fields: filtered.fields,
29796 context: args.context,
29797 loaded: filtered.file.size,
29798 total: filtered.file.size,
29799 indeterminate: false
29800 });
29801 });
29802 xhr.addEventListener("error", () => {
29803 if (aborted) {
29804 return;
29805 }
29806 const error = new Error("Network error during upload.");
29807 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29808 // `filtered.file` — same identity as UPLOAD_STARTED /
29809 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
29810 // that swapped the File would otherwise route this
29811 // failure to a row keyed by the original (pre-swap)
29812 // File, leaving the HUD row stuck in "running".
29813 file: filtered.file,
29814 error,
29815 context: args.context
29816 });
29817 reject(error);
29818 });
29819 xhr.addEventListener("abort", () => {
29820 const error = new UploadAbortedError();
29821 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29822 // `filtered.file` — same identity as UPLOAD_STARTED /
29823 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
29824 // that swapped the File would otherwise route this
29825 // failure to a row keyed by the original (pre-swap)
29826 // File, leaving the HUD row stuck in "running".
29827 file: filtered.file,
29828 error,
29829 context: args.context
29830 });
29831 reject(error);
29832 });
29833 xhr.addEventListener("load", () => {
29834 if (aborted) {
29835 return;
29836 }
29837 if (xhr.status < 200 || xhr.status >= 300) {
29838 const message = extractXhrMessage(xhr);
29839 const error = new Error(message);
29840 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29841 file: filtered.file,
29842 error,
29843 context: args.context
29844 });
29845 reject(error);
29846 return;
29847 }
29848 let data;
29849 try {
29850 data = JSON.parse(xhr.responseText);
29851 } catch (err) {
29852 const error = err instanceof Error ? err : new Error("Could not parse server response.");
29853 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29854 file: filtered.file,
29855 error,
29856 context: args.context
29857 });
29858 reject(error);
29859 return;
29860 }
29861 if (cancelRequested && data.id) {
29862 void deleteAttachment(
29863 args.mediaUrl,
29864 args.restNonce,
29865 data.id
29866 );
29867 const error = new UploadAbortedError();
29868 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29869 file: filtered.file,
29870 error,
29871 context: args.context
29872 });
29873 reject(error);
29874 return;
29875 }
29876 const result = {
29877 id: data.id,
29878 url: data.source_url,
29879 mime: data.mime_type || filtered.mime,
29880 title: data.title?.rendered || filtered.fields.title,
29881 filename: data.media_details?.file || filtered.fields.filename
29882 };
29883 doAction(FILE_DROP_HOOKS.AFTER_UPLOAD, {
29884 file: filtered.file,
29885 result,
29886 fields: filtered.fields,
29887 context: args.context
29888 });
29889 resolve2(result);
29890 });
29891 xhr.send(body);
29892 });
29893 }
29894 class UploadCancelledError extends Error {
29895 constructor() {
29896 super("Upload cancelled by desktop-mode.drop.before-upload filter.");
29897 this.name = "UploadCancelledError";
29898 }
29899 }
29900 class UploadAbortedError extends Error {
29901 constructor() {
29902 super("Upload aborted by the caller.");
29903 this.name = "UploadAbortedError";
29904 }
29905 }
29906 function deleteAttachment(mediaUrl, restNonce, id) {
29907 const url = `${mediaUrl.replace(/\/$/, "")}/${id}?force=true`;
29908 const cleanup = new XMLHttpRequest();
29909 cleanup.open("DELETE", url, true);
29910 cleanup.withCredentials = true;
29911 cleanup.setRequestHeader("X-WP-Nonce", restNonce);
29912 return new Promise((resolve2) => {
29913 cleanup.addEventListener("loadend", () => {
29914 if (cleanup.status < 200 || cleanup.status >= 300) {
29915 console.warn(
29916 `[os-file-drop] late-cancel cleanup failed for attachment ${id} (HTTP ${cleanup.status}). The attachment remains in the Media Library; delete it manually.`
29917 );
29918 }
29919 resolve2();
29920 });
29921 cleanup.addEventListener("error", () => {
29922 console.warn(
29923 `[os-file-drop] late-cancel cleanup network error for attachment ${id}. The attachment remains in the Media Library; delete it manually.`
29924 );
29925 resolve2();
29926 });
29927 try {
29928 cleanup.send();
29929 } catch (err) {
29930 console.warn(
29931 `[os-file-drop] late-cancel cleanup could not be dispatched for attachment ${id}:`,
29932 err
29933 );
29934 resolve2();
29935 }
29936 });
29937 }
29938 function extractXhrMessage(xhr) {
29939 const fallback = `Upload failed (HTTP ${xhr.status}).`;
29940 const text = xhr.responseText;
29941 if (!text) {
29942 return fallback;
29943 }
29944 try {
29945 const data = JSON.parse(text);
29946 if (data && typeof data.message === "string") {
29947 return data.message;
29948 }
29949 } catch {
29950 }
29951 return fallback;
29952 }
29953 async function openUploadDialog(args) {
29954 if (args.entries.length === 0) {
29955 return;
29956 }
29957 const modal = document.createElement("wpd-modal");
29958 modal.setAttribute("open", "");
29959 modal.setAttribute("size", "md");
29960 modal.setAttribute(
29961 "title",
29962 args.entries.length === 1 ? "Upload to Media Library" : `Upload ${args.entries.length} files to Media Library`
29963 );
29964 document.body.appendChild(modal);
29965 const draft = args.entries.map((entry) => ({
29966 ...entry.fields
29967 }));
29968 const renderBody = () => {
29969 modal.innerHTML = "";
29970 const list2 = document.createElement("div");
29971 list2.style.cssText = "display:flex;flex-direction:column;gap:18px;max-height:60vh;overflow:auto;padding-right:6px;";
29972 args.entries.forEach((entry, i) => {
29973 list2.appendChild(renderEntry(entry, draft[i], i + 1));
29974 });
29975 modal.appendChild(list2);
29976 const footer = document.createElement("div");
29977 footer.setAttribute("slot", "footer");
29978 footer.style.cssText = "display:flex;gap:8px;justify-content:flex-end;";
29979 const cancel = document.createElement("wpd-button");
29980 cancel.setAttribute("variant", "secondary");
29981 cancel.textContent = "Cancel";
29982 cancel.addEventListener("click", () => {
29983 modal.remove();
29984 });
29985 const upload = document.createElement("wpd-button");
29986 upload.setAttribute("variant", "primary");
29987 upload.textContent = args.entries.length === 1 ? "Upload" : `Upload ${args.entries.length} files`;
29988 upload.addEventListener("click", () => {
29989 void runUploads(upload, cancel);
29990 });
29991 footer.appendChild(cancel);
29992 footer.appendChild(upload);
29993 modal.appendChild(footer);
29994 };
29995 const renderEntry = (entry, fields, index2) => {
29996 const wrap = document.createElement("div");
29997 wrap.style.cssText = "display:flex;flex-direction:column;gap:8px;border:1px solid rgba(255,255,255,0.08);border-radius:10px;padding:14px;";
29998 const heading = document.createElement("div");
29999 heading.style.cssText = "display:flex;gap:10px;align-items:center;font-weight:600;";
30000 const tag = document.createElement("span");
30001 tag.textContent = args.entries.length === 1 ? "" : `#${index2} · `;
30002 tag.style.opacity = "0.6";
30003 const fname = document.createElement("span");
30004 fname.textContent = entry.file.name;
30005 fname.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
30006 const size = document.createElement("span");
30007 size.textContent = `${entry.mime || "unknown"} · ${formatBytes(
30008 entry.file.size
30009 )}`;
30010 size.style.cssText = "opacity:0.6;font-size:12px;";
30011 heading.appendChild(tag);
30012 heading.appendChild(fname);
30013 heading.appendChild(size);
30014 wrap.appendChild(heading);
30015 wrap.appendChild(textField("Title", fields.title, (v) => fields.title = v));
30016 wrap.appendChild(textField("Filename", fields.filename, (v) => fields.filename = v));
30017 if (entry.mime.startsWith("image/")) {
30018 wrap.appendChild(
30019 textField("Alt text", fields.altText, (v) => fields.altText = v)
30020 );
30021 }
30022 wrap.appendChild(textField("Caption", fields.caption, (v) => fields.caption = v));
30023 wrap.appendChild(
30024 textareaField("Description", fields.description, (v) => fields.description = v)
30025 );
30026 return wrap;
30027 };
30028 const runUploads = async (uploadBtn, cancelBtn) => {
30029 uploadBtn.disabled = true;
30030 cancelBtn.disabled = true;
30031 uploadBtn.textContent = "Uploading…";
30032 const total = args.entries.length;
30033 let successes = 0;
30034 let failures = 0;
30035 let cancelled = 0;
30036 const failureDetails = [];
30037 for (let i = 0; i < total; i++) {
30038 const entry = args.entries[i];
30039 try {
30040 await uploadFile({
30041 file: entry.file,
30042 mime: entry.mime,
30043 fields: draft[i],
30044 context: args.context,
30045 mediaUrl: args.mediaUrl,
30046 restNonce: args.restNonce
30047 });
30048 successes++;
30049 } catch (err) {
30050 if (err instanceof UploadCancelledError) {
30051 cancelled++;
30052 continue;
30053 }
30054 if (err instanceof UploadAbortedError) {
30055 cancelled++;
30056 continue;
30057 }
30058 failures++;
30059 const message = err instanceof Error ? err.message : "Upload failed.";
30060 failureDetails.push(`“${entry.file.name}” — ${message}`);
30061 }
30062 }
30063 modal.remove();
30064 showBatchSummaryToast({
30065 total,
30066 successes,
30067 failures,
30068 cancelled,
30069 failureDetails
30070 });
30071 };
30072 renderBody();
30073 await new Promise((resolve2) => {
30074 modal.addEventListener("wpd-modal-cancel", () => {
30075 modal.remove();
30076 resolve2();
30077 });
30078 const observer = new MutationObserver(() => {
30079 if (!modal.isConnected) {
30080 observer.disconnect();
30081 resolve2();
30082 }
30083 });
30084 observer.observe(document.body, { childList: true, subtree: true });
30085 });
30086 }
30087 function textField(label, value, onChange) {
30088 const el = document.createElement("wpd-text-field");
30089 el.setAttribute("label", label);
30090 el.setAttribute("value", value);
30091 el.addEventListener("input", () => {
30092 const v = el.value;
30093 if (typeof v === "string") {
30094 onChange(v);
30095 }
30096 });
30097 return el;
30098 }
30099 function textareaField(label, value, onChange) {
30100 const el = document.createElement("wpd-textarea");
30101 el.setAttribute("label", label);
30102 el.setAttribute("value", value);
30103 el.setAttribute("rows", "3");
30104 el.addEventListener("input", () => {
30105 const v = el.value;
30106 if (typeof v === "string") {
30107 onChange(v);
30108 }
30109 });
30110 return el;
30111 }
30112 function showBatchSummaryToast(args) {
30113 const { total, successes, failures, cancelled, failureDetails } = args;
30114 if (total === 0) {
30115 return;
30116 }
30117 if (total === 1) {
30118 if (successes === 1) {
30119 showToast({ message: "Uploaded to Media Library." });
30120 } else if (failures === 1 && failureDetails[0]) {
30121 showToast({ message: failureDetails[0] });
30122 } else if (cancelled === 1) {
30123 showToast({ message: "Upload cancelled." });
30124 }
30125 return;
30126 }
30127 if (successes === total) {
30128 showToast({
30129 message: `Uploaded ${successes} files to Media Library.`
30130 });
30131 return;
30132 }
30133 if (cancelled === total) {
30134 showToast({ message: "All uploads cancelled." });
30135 return;
30136 }
30137 if (failures === total) {
30138 showToast({
30139 message: failures === 1 && failureDetails[0] ? failureDetails[0] : `${failures} uploads failed.`
30140 });
30141 return;
30142 }
30143 const parts = [];
30144 if (successes > 0) {
30145 parts.push(
30146 `Uploaded ${successes} file${successes === 1 ? "" : "s"}.`
30147 );
30148 }
30149 if (cancelled > 0) {
30150 parts.push(`Cancelled ${cancelled}.`);
30151 }
30152 if (failures > 0) {
30153 parts.push(`Failed ${failures}.`);
30154 }
30155 showToast({ message: parts.join(" ") });
30156 }
30157 const dialog = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
30158 __proto__: null,
30159 openUploadDialog
30160 }, Symbol.toStringTag, { value: "Module" }));
30161 exports.clampGeometryToViewport = clampGeometryToViewport;
30162 Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
30163 return exports;
30164 }({});
30165