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

30,192 lines 986.2 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 /**
8402 * Switch the active settings tab. Records the choice on
8403 * {@link activeTabId} (so the next render mounts on it) and, when
8404 * the panel is currently mounted, flips the live `<wpd-tabs>` value
8405 * in place so an already-open OS Settings window jumps to the tab
8406 * without a full re-render. Deep-linking entry points
8407 * (`openOsSettings({ tabId })`) call this after opening the window.
8408 *
8409 * @param tabId Settings tab id, e.g. `'ai'`, `'apps-icons'`.
8410 */
8411 focusTab(tabId) {
8412 this.activeTabId = tabId;
8413 const body = this._lastRenderedBody;
8414 if (!body?.isConnected) {
8415 return;
8416 }
8417 const tabs = body.querySelector("wpd-tabs");
8418 if (tabs) {
8419 tabs.value = tabId;
8420 }
8421 }
8422 renderPanel(body) {
8423 this._lastRenderedBody = body;
8424 const fn = window.desktopModeRenderOsSettingsPanel;
8425 if (fn) {
8426 fn(this, body);
8427 return;
8428 }
8429 void loadOsSettingsPanelBundle(
8430 this.config.osSettingsPanelBundleUrl ?? ""
8431 ).then((render2) => {
8432 if (!body.isConnected) {
8433 return;
8434 }
8435 render2(this, body);
8436 }).catch((err) => {
8437 if (typeof console !== "undefined") {
8438 console.error(
8439 "[desktop-mode] OS Settings panel failed to load:",
8440 err
8441 );
8442 }
8443 });
8444 }
8445 }
8446 const EXIT_DESKTOP_MODE_TILE_ID = "desktop-mode-exit";
8447 function getExitDesktopModeTileDef() {
8448 return {
8449 id: EXIT_DESKTOP_MODE_TILE_ID,
8450 title: __("Exit Desktop Mode"),
8451 // `dashicons-exit` (door with arrow) is the clearest "leave"
8452 // glyph in the WordPress set, distinct from `dashicons-desktop`
8453 // used by OS Settings.
8454 icon: "dashicons-exit",
8455 onOpen: () => {
8456 void exitDesktopMode();
8457 }
8458 };
8459 }
8460 async function exitDesktopMode() {
8461 const cfg = window.desktopModeAdminBar;
8462 const fallback = cfg?.classicUrl || "/wp-admin/";
8463 if (!cfg?.ajaxUrl || !cfg?.nonce) {
8464 navigateTop(fallback);
8465 return;
8466 }
8467 const body = new URLSearchParams();
8468 body.set("action", "save-desktop-mode");
8469 body.set("nonce", cfg.nonce);
8470 body.set("enabled", "");
8471 let target2 = fallback;
8472 try {
8473 const res = await fetch(cfg.ajaxUrl, {
8474 method: "POST",
8475 headers: {
8476 "Content-Type": "application/x-www-form-urlencoded"
8477 },
8478 body: body.toString(),
8479 credentials: "same-origin"
8480 });
8481 if (res.ok) {
8482 const json = await res.json();
8483 if (json?.success && json.data?.redirect) {
8484 target2 = json.data.redirect;
8485 }
8486 }
8487 } catch {
8488 }
8489 navigateTop(target2);
8490 }
8491 function navigateTop(url) {
8492 try {
8493 window.top.location.href = url;
8494 } catch {
8495 window.location.href = url;
8496 }
8497 }
8498 const _initial$1 = {
8499 userId: null,
8500 requestedAt: 0,
8501 tabRequested: false
8502 };
8503 let _store$2 = null;
8504 function getStore$1() {
8505 if (_store$2) {
8506 return _store$2;
8507 }
8508 const w = window;
8509 const factory = w.wp?.desktop?.createSharedStore;
8510 if (typeof factory !== "function") {
8511 return null;
8512 }
8513 _store$2 = factory(
8514 "desktop-mode/user-edit/target",
8515 () => ({ ..._initial$1 })
8516 );
8517 return _store$2;
8518 }
8519 function setUserEditTarget(userId) {
8520 const store2 = getStore$1();
8521 if (store2) {
8522 store2.state.userId = userId;
8523 store2.state.requestedAt = Date.now();
8524 store2.state.tabRequested = true;
8525 store2.notify();
8526 return;
8527 }
8528 const w = window;
8529 w._wpdUserEditTarget = {
8530 userId,
8531 requestedAt: Date.now(),
8532 tabRequested: true
8533 };
8534 }
8535 const pending = /* @__PURE__ */ new Map();
8536 function loadVendorScript(url, extras) {
8537 const existing = pending.get(url);
8538 if (existing) {
8539 return existing;
8540 }
8541 const promise = new Promise((resolve2, reject) => {
8542 const selector = `script[data-desktop-mode-vendor="${cssEscape(url)}"]`;
8543 const preexisting = document.querySelector(selector);
8544 if (preexisting) {
8545 if (preexisting.dataset.loaded === "1") {
8546 resolve2();
8547 return;
8548 }
8549 preexisting.addEventListener("load", () => resolve2(), { once: true });
8550 preexisting.addEventListener(
8551 "error",
8552 () => reject(new Error(`Failed to load ${url}`)),
8553 { once: true }
8554 );
8555 return;
8556 }
8557 if (extras?.translations) {
8558 injectInline(extras.translations);
8559 }
8560 for (const code of extras?.l10n ?? []) {
8561 injectInline(code);
8562 }
8563 for (const code of extras?.before ?? []) {
8564 injectInline(code);
8565 }
8566 const script = document.createElement("script");
8567 script.src = url;
8568 script.async = true;
8569 script.dataset.desktopModeVendor = url;
8570 script.addEventListener(
8571 "load",
8572 () => {
8573 script.dataset.loaded = "1";
8574 for (const code of extras?.after ?? []) {
8575 injectInline(code);
8576 }
8577 resolve2();
8578 },
8579 { once: true }
8580 );
8581 script.addEventListener(
8582 "error",
8583 () => {
8584 pending.delete(url);
8585 script.remove();
8586 reject(new Error(`Failed to load ${url}`));
8587 },
8588 { once: true }
8589 );
8590 document.head.appendChild(script);
8591 });
8592 pending.set(url, promise);
8593 return promise;
8594 }
8595 function injectInline(code) {
8596 if (!code) {
8597 return;
8598 }
8599 const tag = document.createElement("script");
8600 tag.textContent = code;
8601 tag.dataset.desktopModeVendorInline = "1";
8602 document.head.appendChild(tag);
8603 }
8604 function cssEscape(value) {
8605 if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
8606 return CSS.escape(value);
8607 }
8608 return value.replace(/["\\]/g, "\\$&");
8609 }
8610 const registry$7 = /* @__PURE__ */ new Map();
8611 function registerModule(def) {
8612 if (!def || typeof def.id !== "string" || def.id === "") {
8613 if (typeof console !== "undefined") {
8614 console.warn("[desktop-mode] Ignored invalid module registration:", def);
8615 }
8616 return;
8617 }
8618 if (typeof def.url !== "string" || def.url === "") {
8619 if (typeof console !== "undefined") {
8620 console.warn(
8621 `[desktop-mode] Module "${def.id}" has no url; ignored.`
8622 );
8623 }
8624 return;
8625 }
8626 registry$7.set(def.id, def);
8627 }
8628 function moduleIds() {
8629 return Array.from(registry$7.keys());
8630 }
8631 async function loadModules(ids) {
8632 if (!ids || ids.length === 0) {
8633 return;
8634 }
8635 const unknown = ids.filter((id) => !registry$7.has(id));
8636 if (unknown.length > 0) {
8637 throw new Error(
8638 `[desktop-mode] Unknown module(s) in needs: ${unknown.map((id) => `"${id}"`).join(", ")}. Known modules: ${moduleIds().join(", ") || "(none)"}.`
8639 );
8640 }
8641 await Promise.all(
8642 ids.map((id) => {
8643 const def = registry$7.get(id);
8644 if (!def) {
8645 return Promise.resolve();
8646 }
8647 if (def.isReady && def.isReady()) {
8648 return Promise.resolve();
8649 }
8650 return loadVendorScript(def.url);
8651 })
8652 );
8653 }
8654 function createContext(id, pluginUrl) {
8655 return {
8656 id,
8657 pluginUrl,
8658 prefersReducedMotion: prefersReducedMotion(),
8659 visible: !document.hidden
8660 };
8661 }
8662 function prefersReducedMotion() {
8663 if (typeof window.matchMedia !== "function") {
8664 return false;
8665 }
8666 return window.matchMedia("( prefers-reduced-motion: reduce )").matches;
8667 }
8668 class WallpaperLayer {
8669 constructor(element, pluginUrl) {
8670 this.generation = 0;
8671 this.active = null;
8672 this.boundVisibilityChange = () => {
8673 if (!this.active) {
8674 return;
8675 }
8676 doAction(HOOKS.WALLPAPER_VISIBILITY, {
8677 id: this.active.id,
8678 state: document.hidden ? "hidden" : "visible"
8679 });
8680 };
8681 this.element = element;
8682 this.pluginUrl = pluginUrl;
8683 document.addEventListener("visibilitychange", this.boundVisibilityChange);
8684 }
8685 /**
8686 * Apply a wallpaper definition. Safe to call from any event
8687 * handler — handles type dispatch, teardown of the prior active
8688 * canvas, and race-safe async mounts.
8689 */
8690 apply(def) {
8691 const gen = ++this.generation;
8692 this.teardownActive();
8693 if (def.type === "css") {
8694 this.applyCss(def);
8695 return;
8696 }
8697 this.applyCanvas(def, gen);
8698 }
8699 /**
8700 * Imperative teardown entry point — called from desktop.ts on
8701 * `pagehide` so a canvas wallpaper's ticker doesn't compete with
8702 * the session-beacon flush at unload.
8703 */
8704 teardownActive() {
8705 if (!this.active) {
8706 return;
8707 }
8708 const { id, teardown } = this.active;
8709 this.active = null;
8710 doAction(HOOKS.WALLPAPER_UNMOUNTING, { id });
8711 try {
8712 teardown();
8713 } catch (err) {
8714 doAction(HOOKS.SHELL_ERROR, { scope: "wallpaper-teardown", id, error: err });
8715 if (typeof console !== "undefined") {
8716 console.error(
8717 `[desktop-mode] Wallpaper "${id}" teardown threw:`,
8718 err
8719 );
8720 }
8721 }
8722 this.element.innerHTML = "";
8723 }
8724 /** Remove listeners. Not called in normal flow — reserved for tests. */
8725 dispose() {
8726 this.teardownActive();
8727 document.removeEventListener("visibilitychange", this.boundVisibilityChange);
8728 }
8729 applyCss(def) {
8730 const value = def.resolveValue ? def.resolveValue(createContext(def.id, this.pluginUrl)) : def.value;
8731 if (typeof value === "string") {
8732 this.element.style.setProperty("--desktop-mode-bg", value);
8733 const shell = document.getElementById("desktop-mode-shell");
8734 shell?.style.setProperty("--desktop-mode-bg", value);
8735 }
8736 }
8737 applyCanvas(def, gen) {
8738 const ctx = createContext(def.id, this.pluginUrl);
8739 doAction(HOOKS.WALLPAPER_MOUNTING, { id: def.id, container: this.element, ctx });
8740 const depsReady = def.needs && def.needs.length > 0 ? loadModules(def.needs) : Promise.resolve();
8741 const onResolve = (teardown) => {
8742 if (gen !== this.generation) {
8743 try {
8744 teardown();
8745 } catch {
8746 }
8747 return;
8748 }
8749 this.active = { id: def.id, teardown };
8750 doAction(HOOKS.WALLPAPER_MOUNTED, { id: def.id, container: this.element, ctx });
8751 };
8752 depsReady.then(
8753 () => {
8754 if (gen !== this.generation) {
8755 return;
8756 }
8757 let result;
8758 try {
8759 result = def.mount(this.element, ctx);
8760 } catch (err) {
8761 this.handleMountFailure(def.id, err);
8762 return;
8763 }
8764 if (isThenable$1(result)) {
8765 result.then(onResolve, (err) => {
8766 if (gen !== this.generation) {
8767 return;
8768 }
8769 this.handleMountFailure(def.id, err);
8770 });
8771 return;
8772 }
8773 onResolve(result);
8774 },
8775 (err) => {
8776 if (gen !== this.generation) {
8777 return;
8778 }
8779 this.handleMountFailure(def.id, err);
8780 }
8781 );
8782 }
8783 handleMountFailure(id, err) {
8784 this.element.innerHTML = "";
8785 doAction(HOOKS.WALLPAPER_MOUNT_FAILED, { id, error: err });
8786 doAction(HOOKS.SHELL_ERROR, { scope: "wallpaper-mount", id, error: err });
8787 if (typeof console !== "undefined") {
8788 console.error(
8789 `[desktop-mode] Wallpaper "${id}" failed to mount:`,
8790 err
8791 );
8792 }
8793 }
8794 }
8795 function isThenable$1(value) {
8796 return !!value && typeof value === "object" && typeof value.then === "function";
8797 }
8798 function createWallpaperRegistrySync(deps2) {
8799 const { osSettings } = deps2;
8800 const registered = /* @__PURE__ */ new Set();
8801 const loadedScripts = /* @__PURE__ */ new Set();
8802 const ensureScript = async (entry) => {
8803 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
8804 return;
8805 }
8806 try {
8807 await loadVendorScript(entry.scriptUrl, {
8808 translations: entry.scriptTranslations,
8809 l10n: entry.scriptL10n,
8810 before: entry.scriptBefore,
8811 after: entry.scriptAfter
8812 });
8813 } catch (err) {
8814 doAction(HOOKS.SHELL_ERROR, {
8815 scope: "wallpaper-script-load",
8816 id: entry.id,
8817 error: err
8818 });
8819 }
8820 loadedScripts.add(entry.scriptUrl);
8821 };
8822 const readDef = (id) => {
8823 const globals = window.desktopModeWallpapers || {};
8824 return globals[id] ?? null;
8825 };
8826 const defFromCssEntry = (entry) => {
8827 if (entry.type !== "css" || entry.value === "") {
8828 return null;
8829 }
8830 return {
8831 id: entry.id,
8832 label: entry.label,
8833 type: "css",
8834 value: entry.value,
8835 preview: entry.preview !== "" ? entry.preview : entry.value
8836 };
8837 };
8838 const registerEntry = async (entry) => {
8839 if (registered.has(entry.id)) {
8840 return;
8841 }
8842 const cssDef = defFromCssEntry(entry);
8843 if (cssDef) {
8844 register$2(cssDef);
8845 registered.add(entry.id);
8846 osSettings.apply();
8847 return;
8848 }
8849 await ensureScript(entry);
8850 const def = readDef(entry.id);
8851 if (!def) {
8852 doAction(HOOKS.SHELL_ERROR, {
8853 scope: "wallpaper-missing-def",
8854 id: entry.id,
8855 error: new Error(
8856 `[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.`
8857 )
8858 });
8859 return;
8860 }
8861 try {
8862 register$2(def);
8863 } catch (err) {
8864 doAction(HOOKS.SHELL_ERROR, {
8865 scope: "wallpaper-register",
8866 id: entry.id,
8867 error: err
8868 });
8869 return;
8870 }
8871 registered.add(entry.id);
8872 osSettings.apply();
8873 };
8874 const unregisterEntry = (id) => {
8875 if (!registered.has(id)) {
8876 return;
8877 }
8878 unregister$2(id);
8879 registered.delete(id);
8880 osSettings.apply();
8881 };
8882 return async (list2) => {
8883 const incoming = /* @__PURE__ */ new Set();
8884 for (const entry of list2) {
8885 incoming.add(entry.id);
8886 }
8887 for (const id of Array.from(registered)) {
8888 if (!incoming.has(id)) {
8889 unregisterEntry(id);
8890 }
8891 }
8892 for (const entry of list2) {
8893 if (!registered.has(entry.id)) {
8894 await registerEntry(entry);
8895 }
8896 }
8897 };
8898 }
8899 const COMMAND_SLUG = /^[a-z0-9_/-]+$/;
8900 const commandRegistryStore = createSharedStore(
8901 "desktop-mode/commands-registry",
8902 () => ({
8903 registry: /* @__PURE__ */ new Map(),
8904 listeners: /* @__PURE__ */ new Set()
8905 })
8906 );
8907 const registry$6 = commandRegistryStore.state.registry;
8908 const listeners$9 = commandRegistryStore.state.listeners;
8909 function registerCommand(cmd) {
8910 const errors = [];
8911 const slug = typeof cmd?.slug === "string" ? cmd.slug.trim().toLowerCase() : "";
8912 if (!cmd || typeof cmd !== "object") {
8913 errors.push("def (not an object)");
8914 } else {
8915 if (typeof cmd.slug !== "string" || cmd.slug.trim() === "") {
8916 errors.push("slug (missing)");
8917 } else if (!COMMAND_SLUG.test(slug)) {
8918 errors.push(
8919 `slug (must match ${COMMAND_SLUG} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
8920 );
8921 }
8922 if (typeof cmd.label !== "string" || cmd.label.trim() === "") {
8923 errors.push("label (missing)");
8924 }
8925 if (typeof cmd.run !== "function") {
8926 errors.push("run (must be a function)");
8927 }
8928 }
8929 throwOnRegistrationErrors("Command", errors, cmd);
8930 registry$6.set(slug, { ...cmd, slug });
8931 notify$b();
8932 }
8933 function unregisterCommand(slug) {
8934 if (registry$6.delete(slug.toLowerCase())) {
8935 notify$b();
8936 }
8937 }
8938 function unregisterByOwner(owner) {
8939 if (!owner) {
8940 return 0;
8941 }
8942 let removed = 0;
8943 for (const [slug, cmd] of Array.from(registry$6.entries())) {
8944 if (cmd.owner === owner) {
8945 registry$6.delete(slug);
8946 removed++;
8947 }
8948 }
8949 if (removed > 0) {
8950 notify$b();
8951 }
8952 return removed;
8953 }
8954 function listCommands() {
8955 return Array.from(registry$6.values());
8956 }
8957 function listAiCallableCommands() {
8958 const out = [];
8959 for (const cmd of registry$6.values()) {
8960 if (cmd.aiCallable !== true) {
8961 continue;
8962 }
8963 out.push({
8964 slug: cmd.slug,
8965 label: cmd.label,
8966 description: cmd.description ?? "",
8967 hint: cmd.hint ?? ""
8968 });
8969 }
8970 return out;
8971 }
8972 function findCommand(slug) {
8973 return registry$6.get(slug.toLowerCase()) ?? null;
8974 }
8975 function notify$b() {
8976 const snapshot = Array.from(listeners$9);
8977 for (const cb of snapshot) {
8978 try {
8979 cb();
8980 } catch (err) {
8981 if (typeof console !== "undefined") {
8982 console.error("[desktop-mode] command-registry listener threw:", err);
8983 }
8984 }
8985 }
8986 }
8987 function createCommandRegistrySync() {
8988 const loadedHandles = /* @__PURE__ */ new Set();
8989 const loadedUrls = /* @__PURE__ */ new Set();
8990 let prevSlugsByHandle = /* @__PURE__ */ new Map();
8991 const ensureScript = async (entry) => {
8992 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
8993 loadedHandles.add(entry.handle);
8994 return;
8995 }
8996 try {
8997 await loadVendorScript(entry.scriptUrl, {
8998 translations: entry.scriptTranslations,
8999 l10n: entry.scriptL10n,
9000 before: entry.scriptBefore,
9001 after: entry.scriptAfter
9002 });
9003 } catch (err) {
9004 doAction(HOOKS.SHELL_ERROR, {
9005 scope: "command-script-load",
9006 handle: entry.handle,
9007 url: entry.scriptUrl,
9008 error: err
9009 });
9010 return;
9011 }
9012 loadedUrls.add(entry.scriptUrl);
9013 loadedHandles.add(entry.handle);
9014 };
9015 const slugsByHandleFrom = (commands) => {
9016 const map = /* @__PURE__ */ new Map();
9017 if (!commands) {
9018 return map;
9019 }
9020 for (const entry of commands) {
9021 if (!entry.scriptHandle || !entry.slug) {
9022 continue;
9023 }
9024 let set = map.get(entry.scriptHandle);
9025 if (!set) {
9026 set = /* @__PURE__ */ new Set();
9027 map.set(entry.scriptHandle, set);
9028 }
9029 set.add(entry.slug);
9030 }
9031 return map;
9032 };
9033 const collectSlugsToRemove = (handle) => {
9034 const slugs = /* @__PURE__ */ new Set();
9035 for (const cmd of listCommands()) {
9036 if (cmd.owner === handle) {
9037 slugs.add(cmd.slug);
9038 }
9039 }
9040 const declared = prevSlugsByHandle.get(handle);
9041 if (declared) {
9042 for (const slug of declared) {
9043 slugs.add(slug);
9044 }
9045 }
9046 return slugs;
9047 };
9048 return async (scripts, commands) => {
9049 const incomingHandles = /* @__PURE__ */ new Set();
9050 for (const entry of scripts) {
9051 if (entry.handle) {
9052 incomingHandles.add(entry.handle);
9053 }
9054 }
9055 for (const handle of Array.from(loadedHandles)) {
9056 if (incomingHandles.has(handle)) {
9057 continue;
9058 }
9059 for (const slug of collectSlugsToRemove(handle)) {
9060 unregisterCommand(slug);
9061 }
9062 loadedHandles.delete(handle);
9063 }
9064 for (const entry of scripts) {
9065 if (!entry.handle || loadedHandles.has(entry.handle)) {
9066 continue;
9067 }
9068 await ensureScript(entry);
9069 }
9070 prevSlugsByHandle = slugsByHandleFrom(commands);
9071 };
9072 }
9073 const store$a = createSharedStore(
9074 "desktop-mode/settings-tab-registry",
9075 () => ({
9076 registry: /* @__PURE__ */ new Map(),
9077 listeners: /* @__PURE__ */ new Set()
9078 })
9079 );
9080 const registry$5 = store$a.state.registry;
9081 const listeners$8 = store$a.state.listeners;
9082 function registerSettingsTab(tab) {
9083 if (!tab || typeof tab.id !== "string" || tab.id.trim() === "") {
9084 return;
9085 }
9086 if (typeof tab.label !== "string" || tab.label.trim() === "") {
9087 return;
9088 }
9089 if (typeof tab.render !== "function") {
9090 return;
9091 }
9092 const id = tab.id.trim().toLowerCase();
9093 if (!/^[a-z0-9_\-]+$/.test(id)) {
9094 if (typeof console !== "undefined") {
9095 console.warn(
9096 "[desktop-mode] registerSettingsTab: id must be [a-z0-9_-]+, got",
9097 tab.id
9098 );
9099 }
9100 return;
9101 }
9102 registry$5.set(id, { ...tab, id });
9103 notify$a();
9104 }
9105 function unregisterSettingsTab(id) {
9106 if (registry$5.delete(id.toLowerCase())) {
9107 notify$a();
9108 }
9109 }
9110 function unregisterSettingsTabsByOwner(owner) {
9111 if (!owner) {
9112 return 0;
9113 }
9114 let removed = 0;
9115 for (const [id, tab] of Array.from(registry$5.entries())) {
9116 if (tab.owner === owner) {
9117 registry$5.delete(id);
9118 removed++;
9119 }
9120 }
9121 if (removed > 0) {
9122 notify$a();
9123 }
9124 return removed;
9125 }
9126 function listSettingsTabs() {
9127 return Array.from(registry$5.values()).sort(
9128 (a, b) => (a.order ?? 100) - (b.order ?? 100)
9129 );
9130 }
9131 function notify$a() {
9132 const snapshot = Array.from(listeners$8);
9133 for (const cb of snapshot) {
9134 try {
9135 cb();
9136 } catch (err) {
9137 if (typeof console !== "undefined") {
9138 console.error(
9139 "[desktop-mode] settings-tab-registry listener threw:",
9140 err
9141 );
9142 }
9143 }
9144 }
9145 }
9146 function createSettingsTabRegistrySync() {
9147 const loadedHandles = /* @__PURE__ */ new Set();
9148 const loadedUrls = /* @__PURE__ */ new Set();
9149 let prevIdsByHandle = /* @__PURE__ */ new Map();
9150 const ensureScript = async (entry) => {
9151 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9152 loadedHandles.add(entry.handle);
9153 return;
9154 }
9155 try {
9156 await loadVendorScript(entry.scriptUrl, {
9157 translations: entry.scriptTranslations,
9158 l10n: entry.scriptL10n,
9159 before: entry.scriptBefore,
9160 after: entry.scriptAfter
9161 });
9162 } catch (err) {
9163 doAction(HOOKS.SHELL_ERROR, {
9164 scope: "settings-tab-script-load",
9165 handle: entry.handle,
9166 url: entry.scriptUrl,
9167 error: err
9168 });
9169 return;
9170 }
9171 loadedUrls.add(entry.scriptUrl);
9172 loadedHandles.add(entry.handle);
9173 };
9174 const idsByHandleFrom = (tabs) => {
9175 const map = /* @__PURE__ */ new Map();
9176 if (!tabs) {
9177 return map;
9178 }
9179 for (const entry of tabs) {
9180 if (!entry.scriptHandle || !entry.id) {
9181 continue;
9182 }
9183 let set = map.get(entry.scriptHandle);
9184 if (!set) {
9185 set = /* @__PURE__ */ new Set();
9186 map.set(entry.scriptHandle, set);
9187 }
9188 set.add(entry.id);
9189 }
9190 return map;
9191 };
9192 const removeByHandle = (handle) => {
9193 unregisterSettingsTabsByOwner(handle);
9194 const declared = prevIdsByHandle.get(handle);
9195 if (declared) {
9196 const present = new Set(
9197 listSettingsTabs().map((t) => t.id)
9198 );
9199 for (const id of declared) {
9200 if (present.has(id)) {
9201 unregisterSettingsTab(id);
9202 }
9203 }
9204 }
9205 };
9206 return async (scripts, tabs) => {
9207 const incomingHandles = /* @__PURE__ */ new Set();
9208 for (const entry of scripts) {
9209 if (entry.handle) {
9210 incomingHandles.add(entry.handle);
9211 }
9212 }
9213 for (const handle of Array.from(loadedHandles)) {
9214 if (incomingHandles.has(handle)) {
9215 continue;
9216 }
9217 removeByHandle(handle);
9218 loadedHandles.delete(handle);
9219 }
9220 for (const entry of scripts) {
9221 if (!entry.handle || loadedHandles.has(entry.handle)) {
9222 continue;
9223 }
9224 await ensureScript(entry);
9225 }
9226 prevIdsByHandle = idsByHandleFrom(tabs);
9227 };
9228 }
9229 const store$9 = createSharedStore(
9230 "desktop-mode/title-bar-buttons-registry",
9231 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9232 );
9233 const registry$4 = store$9.state.registry;
9234 const listeners$7 = store$9.state.listeners;
9235 const TITLE_BAR_BUTTON_ID = /^[a-z0-9_/-]+$/;
9236 function registerTitleBarButton(def) {
9237 const errors = [];
9238 if (!def || typeof def !== "object") {
9239 errors.push("def (not an object)");
9240 } else {
9241 if (typeof def.id !== "string" || def.id.trim() === "") {
9242 errors.push("id (missing)");
9243 } else if (!TITLE_BAR_BUTTON_ID.test(def.id.trim().toLowerCase())) {
9244 errors.push(
9245 `id (must match ${TITLE_BAR_BUTTON_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9246 );
9247 }
9248 if (typeof def.label !== "string" || def.label.trim() === "") {
9249 errors.push("label (missing)");
9250 }
9251 if (typeof def.icon !== "string" || def.icon.trim() === "") {
9252 errors.push("icon (missing)");
9253 }
9254 if (typeof def.match !== "function") {
9255 errors.push("match (must be a function)");
9256 }
9257 if (typeof def.onClick !== "function" && typeof def.render !== "function") {
9258 errors.push("onClick|render (at least one must be a function)");
9259 }
9260 }
9261 throwOnRegistrationErrors("TitleBarButton", errors, def);
9262 const id = def.id.trim().toLowerCase();
9263 registry$4.set(id, { ...def, id });
9264 notify$9();
9265 }
9266 function unregisterTitleBarButton(id) {
9267 if (registry$4.delete(id.toLowerCase())) {
9268 notify$9();
9269 }
9270 }
9271 function unregisterTitleBarButtonsByOwner(owner) {
9272 if (!owner) {
9273 return 0;
9274 }
9275 let removed = 0;
9276 for (const [id, def] of Array.from(registry$4.entries())) {
9277 if (def.owner === owner) {
9278 registry$4.delete(id);
9279 removed++;
9280 }
9281 }
9282 if (removed > 0) {
9283 notify$9();
9284 }
9285 return removed;
9286 }
9287 function listTitleBarButtons() {
9288 return Array.from(registry$4.values()).sort(
9289 (a, b) => (a.order ?? 100) - (b.order ?? 100)
9290 );
9291 }
9292 function notify$9() {
9293 const snapshot = Array.from(listeners$7);
9294 for (const cb of snapshot) {
9295 try {
9296 cb();
9297 } catch (err) {
9298 if (typeof console !== "undefined") {
9299 console.error(
9300 "[desktop-mode] title-bar-button registry listener threw:",
9301 err
9302 );
9303 }
9304 }
9305 }
9306 }
9307 function createTitleBarButtonRegistrySync() {
9308 const loadedHandles = /* @__PURE__ */ new Set();
9309 const loadedUrls = /* @__PURE__ */ new Set();
9310 const ensureScript = async (entry) => {
9311 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9312 loadedHandles.add(entry.handle);
9313 return;
9314 }
9315 try {
9316 await loadVendorScript(entry.scriptUrl, {
9317 translations: entry.scriptTranslations,
9318 l10n: entry.scriptL10n,
9319 before: entry.scriptBefore,
9320 after: entry.scriptAfter
9321 });
9322 } catch (err) {
9323 doAction(HOOKS.SHELL_ERROR, {
9324 scope: "titlebar-button-script-load",
9325 handle: entry.handle,
9326 url: entry.scriptUrl,
9327 error: err
9328 });
9329 return;
9330 }
9331 loadedUrls.add(entry.scriptUrl);
9332 loadedHandles.add(entry.handle);
9333 };
9334 return async (scripts) => {
9335 const incomingHandles = /* @__PURE__ */ new Set();
9336 for (const entry of scripts) {
9337 if (entry.handle) {
9338 incomingHandles.add(entry.handle);
9339 }
9340 }
9341 for (const handle of Array.from(loadedHandles)) {
9342 if (incomingHandles.has(handle)) {
9343 continue;
9344 }
9345 unregisterTitleBarButtonsByOwner(handle);
9346 loadedHandles.delete(handle);
9347 }
9348 for (const entry of scripts) {
9349 if (!entry.handle || loadedHandles.has(entry.handle)) {
9350 continue;
9351 }
9352 await ensureScript(entry);
9353 }
9354 };
9355 }
9356 function createDockRailRendererSync() {
9357 const loadedHandles = /* @__PURE__ */ new Set();
9358 const loadedUrls = /* @__PURE__ */ new Set();
9359 const ensureScript = async (entry) => {
9360 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9361 loadedHandles.add(entry.handle);
9362 return;
9363 }
9364 try {
9365 await loadVendorScript(entry.scriptUrl, {
9366 translations: entry.scriptTranslations,
9367 l10n: entry.scriptL10n,
9368 before: entry.scriptBefore,
9369 after: entry.scriptAfter
9370 });
9371 } catch (err) {
9372 doAction(HOOKS.SHELL_ERROR, {
9373 scope: "dock-rail-renderer-script-load",
9374 handle: entry.handle,
9375 url: entry.scriptUrl,
9376 error: err
9377 });
9378 return;
9379 }
9380 loadedUrls.add(entry.scriptUrl);
9381 loadedHandles.add(entry.handle);
9382 };
9383 return async (scripts) => {
9384 const incomingHandles = /* @__PURE__ */ new Set();
9385 for (const entry of scripts) {
9386 if (entry.handle) {
9387 incomingHandles.add(entry.handle);
9388 }
9389 }
9390 for (const handle of Array.from(loadedHandles)) {
9391 if (incomingHandles.has(handle)) {
9392 continue;
9393 }
9394 unregisterByOwner$1(handle);
9395 loadedHandles.delete(handle);
9396 }
9397 for (const entry of scripts) {
9398 if (!entry.handle || loadedHandles.has(entry.handle)) {
9399 continue;
9400 }
9401 await ensureScript(entry);
9402 }
9403 };
9404 }
9405 const store$8 = createSharedStore(
9406 "desktop-mode/window-themes-registry",
9407 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9408 );
9409 const registry$3 = store$8.state.registry;
9410 const listeners$6 = store$8.state.listeners;
9411 const WINDOW_THEME_ID = /^[a-z0-9_/-]+$/;
9412 function registerWindowTheme(def) {
9413 const errors = [];
9414 if (!def || typeof def !== "object") {
9415 errors.push("def (not an object)");
9416 } else {
9417 if (typeof def.id !== "string" || def.id.trim() === "") {
9418 errors.push("id (missing)");
9419 } else if (!WINDOW_THEME_ID.test(def.id.trim().toLowerCase())) {
9420 errors.push(
9421 `id (must match ${WINDOW_THEME_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9422 );
9423 }
9424 if (!def.tokens || typeof def.tokens !== "object") {
9425 errors.push("tokens (must be an object of CSS custom-property → value)");
9426 } else {
9427 for (const key of Object.keys(def.tokens)) {
9428 if (!key.startsWith("--")) {
9429 errors.push(
9430 `tokens.${key} (CSS custom-property keys must start with "--")`
9431 );
9432 break;
9433 }
9434 }
9435 }
9436 if (typeof def.match !== "function") {
9437 errors.push("match (must be a function)");
9438 }
9439 }
9440 throwOnRegistrationErrors("WindowTheme", errors, def);
9441 const id = def.id.trim().toLowerCase();
9442 registry$3.set(id, { ...def, id });
9443 notify$8();
9444 }
9445 function unregisterWindowTheme(id) {
9446 if (registry$3.delete(id.toLowerCase())) {
9447 notify$8();
9448 }
9449 }
9450 function unregisterWindowThemesByOwner(owner) {
9451 if (!owner) {
9452 return 0;
9453 }
9454 let removed = 0;
9455 for (const [id, def] of Array.from(registry$3.entries())) {
9456 if (def.owner === owner) {
9457 registry$3.delete(id);
9458 removed++;
9459 }
9460 }
9461 if (removed > 0) {
9462 notify$8();
9463 }
9464 return removed;
9465 }
9466 function listWindowThemes() {
9467 return Array.from(registry$3.values()).sort(
9468 (a, b) => (a.priority ?? 100) - (b.priority ?? 100)
9469 );
9470 }
9471 function notify$8() {
9472 const snapshot = Array.from(listeners$6);
9473 for (const cb of snapshot) {
9474 try {
9475 cb();
9476 } catch (err) {
9477 if (typeof console !== "undefined") {
9478 console.error(
9479 "[desktop-mode] window-theme registry listener threw:",
9480 err
9481 );
9482 }
9483 }
9484 }
9485 }
9486 function createWindowThemeRegistrySync() {
9487 const loadedHandles = /* @__PURE__ */ new Set();
9488 const loadedUrls = /* @__PURE__ */ new Set();
9489 let prevIdsByHandle = /* @__PURE__ */ new Map();
9490 const shellRegistered = /* @__PURE__ */ new Set();
9491 const ensureScript = async (entry) => {
9492 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9493 loadedHandles.add(entry.handle);
9494 return;
9495 }
9496 try {
9497 await loadVendorScript(entry.scriptUrl, {
9498 translations: entry.scriptTranslations,
9499 l10n: entry.scriptL10n,
9500 before: entry.scriptBefore,
9501 after: entry.scriptAfter
9502 });
9503 } catch (err) {
9504 doAction(HOOKS.SHELL_ERROR, {
9505 scope: "window-theme-script-load",
9506 handle: entry.handle,
9507 url: entry.scriptUrl,
9508 error: err
9509 });
9510 return;
9511 }
9512 loadedUrls.add(entry.scriptUrl);
9513 loadedHandles.add(entry.handle);
9514 };
9515 const idsByHandleFrom = (themes) => {
9516 const map = /* @__PURE__ */ new Map();
9517 if (!themes) {
9518 return map;
9519 }
9520 for (const entry of themes) {
9521 if (!entry.scriptHandle || !entry.id) {
9522 continue;
9523 }
9524 let set = map.get(entry.scriptHandle);
9525 if (!set) {
9526 set = /* @__PURE__ */ new Set();
9527 map.set(entry.scriptHandle, set);
9528 }
9529 set.add(entry.id);
9530 }
9531 return map;
9532 };
9533 const collectIdsToRemove = (handle) => {
9534 const ids = /* @__PURE__ */ new Set();
9535 for (const def of listWindowThemes()) {
9536 if (def.owner === handle) {
9537 ids.add(def.id);
9538 }
9539 }
9540 const declared = prevIdsByHandle.get(handle);
9541 if (declared) {
9542 for (const id of declared) {
9543 ids.add(id);
9544 }
9545 }
9546 return ids;
9547 };
9548 const applyMetadata = (themes) => {
9549 if (!themes) {
9550 return;
9551 }
9552 for (const entry of themes) {
9553 if (!entry.id || !entry.tokens) {
9554 continue;
9555 }
9556 try {
9557 registerWindowTheme({
9558 id: entry.id,
9559 label: entry.label,
9560 tokens: entry.tokens,
9561 priority: entry.priority,
9562 match: () => true,
9563 owner: entry.scriptHandle || void 0
9564 });
9565 shellRegistered.add(entry.id);
9566 } catch (err) {
9567 doAction(HOOKS.SHELL_ERROR, {
9568 scope: "window-theme-shell-register",
9569 id: entry.id,
9570 error: err
9571 });
9572 }
9573 }
9574 };
9575 return async (scripts, themes) => {
9576 const incomingHandles = /* @__PURE__ */ new Set();
9577 for (const entry of scripts) {
9578 if (entry.handle) {
9579 incomingHandles.add(entry.handle);
9580 }
9581 }
9582 for (const handle of Array.from(loadedHandles)) {
9583 if (incomingHandles.has(handle)) {
9584 continue;
9585 }
9586 const ids = collectIdsToRemove(handle);
9587 for (const id of ids) {
9588 unregisterWindowTheme(id);
9589 shellRegistered.delete(id);
9590 }
9591 unregisterWindowThemesByOwner(handle);
9592 loadedHandles.delete(handle);
9593 }
9594 applyMetadata(themes);
9595 for (const entry of scripts) {
9596 if (!entry.handle || loadedHandles.has(entry.handle)) {
9597 continue;
9598 }
9599 await ensureScript(entry);
9600 }
9601 prevIdsByHandle = idsByHandleFrom(themes);
9602 };
9603 }
9604 const store$7 = createSharedStore(
9605 "desktop-mode/window-controls-registry",
9606 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9607 );
9608 const registry$2 = store$7.state.registry;
9609 const listeners$5 = store$7.state.listeners;
9610 const WINDOW_CONTROL_ID = /^[a-z0-9_/-]+$/;
9611 function registerWindowControl(def) {
9612 const errors = [];
9613 if (!def || typeof def !== "object") {
9614 errors.push("def (not an object)");
9615 } else {
9616 if (typeof def.id !== "string" || def.id.trim() === "") {
9617 errors.push("id (missing)");
9618 } else if (!WINDOW_CONTROL_ID.test(def.id.trim().toLowerCase())) {
9619 errors.push(
9620 `id (must match ${WINDOW_CONTROL_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9621 );
9622 }
9623 if (typeof def.label !== "string" || def.label.trim() === "") {
9624 errors.push("label (missing)");
9625 }
9626 if (typeof def.onClick !== "function" && typeof def.render !== "function") {
9627 errors.push("onClick|render (at least one must be a function)");
9628 }
9629 if (typeof def.render !== "function") {
9630 if (typeof def.icon !== "string" || def.icon.trim() === "") {
9631 errors.push("icon (required when render is omitted)");
9632 }
9633 }
9634 if (typeof def.match !== "function") {
9635 errors.push("match (must be a function)");
9636 }
9637 if (def.placement !== void 0 && def.placement !== "left" && def.placement !== "right" && def.placement !== "controls") {
9638 errors.push('placement (must be "left", "right", or "controls")');
9639 }
9640 }
9641 throwOnRegistrationErrors("WindowControl", errors, def);
9642 const id = def.id.trim().toLowerCase();
9643 registry$2.set(id, { ...def, id });
9644 notify$7();
9645 }
9646 function unregisterWindowControl(id) {
9647 if (registry$2.delete(id.toLowerCase())) {
9648 notify$7();
9649 }
9650 }
9651 function unregisterWindowControlsByOwner(owner) {
9652 if (!owner) {
9653 return 0;
9654 }
9655 let removed = 0;
9656 for (const [id, def] of Array.from(registry$2.entries())) {
9657 if (def.owner === owner) {
9658 registry$2.delete(id);
9659 removed++;
9660 }
9661 }
9662 if (removed > 0) {
9663 notify$7();
9664 }
9665 return removed;
9666 }
9667 function listWindowControls() {
9668 return Array.from(registry$2.values()).sort((a, b) => {
9669 const oa = a.order ?? 100;
9670 const ob = b.order ?? 100;
9671 if (oa !== ob) {
9672 return oa - ob;
9673 }
9674 return a.id.localeCompare(b.id);
9675 });
9676 }
9677 function notify$7() {
9678 const snapshot = Array.from(listeners$5);
9679 for (const cb of snapshot) {
9680 try {
9681 cb();
9682 } catch (err) {
9683 if (typeof console !== "undefined") {
9684 console.error(
9685 "[desktop-mode] window-control registry listener threw:",
9686 err
9687 );
9688 }
9689 }
9690 }
9691 }
9692 function registerBuiltInControls() {
9693 registerWindowControl({
9694 id: "core/minimize",
9695 label: __("Minimize"),
9696 icon: "minimize",
9697 placement: "controls",
9698 order: 10,
9699 core: true,
9700 match: () => true,
9701 onClick: (win) => {
9702 win.minimize();
9703 }
9704 });
9705 registerWindowControl({
9706 id: "core/maximize",
9707 label: __("Maximize"),
9708 icon: "maximize",
9709 placement: "controls",
9710 order: 20,
9711 core: true,
9712 match: () => true,
9713 onClick: (win) => {
9714 win.toggleMaximize();
9715 }
9716 });
9717 registerWindowControl({
9718 id: "core/focus-tab",
9719 label: __("Enter fullscreen"),
9720 icon: "fullscreen",
9721 placement: "controls",
9722 order: 30,
9723 core: true,
9724 match: () => true,
9725 onClick: (win) => {
9726 win.toggleFullscreen();
9727 }
9728 });
9729 registerWindowControl({
9730 id: "core/close",
9731 label: __("Close"),
9732 icon: "close",
9733 placement: "controls",
9734 order: 50,
9735 core: true,
9736 match: () => true,
9737 onClick: (win) => {
9738 win.close();
9739 }
9740 });
9741 }
9742 function createWindowControlRegistrySync() {
9743 const loadedHandles = /* @__PURE__ */ new Set();
9744 const loadedUrls = /* @__PURE__ */ new Set();
9745 let prevIdsByHandle = /* @__PURE__ */ new Map();
9746 const ensureScript = async (entry) => {
9747 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9748 loadedHandles.add(entry.handle);
9749 return;
9750 }
9751 try {
9752 await loadVendorScript(entry.scriptUrl, {
9753 translations: entry.scriptTranslations,
9754 l10n: entry.scriptL10n,
9755 before: entry.scriptBefore,
9756 after: entry.scriptAfter
9757 });
9758 } catch (err) {
9759 doAction(HOOKS.SHELL_ERROR, {
9760 scope: "window-control-script-load",
9761 handle: entry.handle,
9762 url: entry.scriptUrl,
9763 error: err
9764 });
9765 return;
9766 }
9767 loadedUrls.add(entry.scriptUrl);
9768 loadedHandles.add(entry.handle);
9769 };
9770 const idsByHandleFrom = (controls) => {
9771 const map = /* @__PURE__ */ new Map();
9772 if (!controls) {
9773 return map;
9774 }
9775 for (const entry of controls) {
9776 if (!entry.scriptHandle || !entry.id) {
9777 continue;
9778 }
9779 let set = map.get(entry.scriptHandle);
9780 if (!set) {
9781 set = /* @__PURE__ */ new Set();
9782 map.set(entry.scriptHandle, set);
9783 }
9784 set.add(entry.id);
9785 }
9786 return map;
9787 };
9788 const collectIdsToRemove = (handle) => {
9789 const ids = /* @__PURE__ */ new Set();
9790 for (const def of listWindowControls()) {
9791 if (def.owner === handle) {
9792 ids.add(def.id);
9793 }
9794 }
9795 const declared = prevIdsByHandle.get(handle);
9796 if (declared) {
9797 for (const id of declared) {
9798 ids.add(id);
9799 }
9800 }
9801 return ids;
9802 };
9803 return async (scripts, controls) => {
9804 const incomingHandles = /* @__PURE__ */ new Set();
9805 for (const entry of scripts) {
9806 if (entry.handle) {
9807 incomingHandles.add(entry.handle);
9808 }
9809 }
9810 for (const handle of Array.from(loadedHandles)) {
9811 if (incomingHandles.has(handle)) {
9812 continue;
9813 }
9814 for (const id of collectIdsToRemove(handle)) {
9815 unregisterWindowControl(id);
9816 }
9817 unregisterWindowControlsByOwner(handle);
9818 loadedHandles.delete(handle);
9819 }
9820 for (const entry of scripts) {
9821 if (!entry.handle || loadedHandles.has(entry.handle)) {
9822 continue;
9823 }
9824 await ensureScript(entry);
9825 }
9826 prevIdsByHandle = idsByHandleFrom(controls);
9827 };
9828 }
9829 const store$6 = createSharedStore(
9830 "desktop-mode/window-slots-registry",
9831 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9832 );
9833 const registry$1 = store$6.state.registry;
9834 const listeners$4 = store$6.state.listeners;
9835 const WINDOW_SLOT_ID = /^[a-z0-9_/-]+$/;
9836 const KNOWN_SLOTS = /* @__PURE__ */ new Set([
9837 "before-titlebar",
9838 "before-icon",
9839 "icon",
9840 "title",
9841 "after-title",
9842 "before-controls",
9843 "controls",
9844 "after-controls",
9845 "after-titlebar"
9846 ]);
9847 function registerWindowSlot(def) {
9848 const errors = [];
9849 if (!def || typeof def !== "object") {
9850 errors.push("def (not an object)");
9851 } else {
9852 if (typeof def.id !== "string" || def.id.trim() === "") {
9853 errors.push("id (missing)");
9854 } else if (!WINDOW_SLOT_ID.test(def.id.trim().toLowerCase())) {
9855 errors.push(
9856 `id (must match ${WINDOW_SLOT_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9857 );
9858 }
9859 if (typeof def.slot !== "string" || def.slot.trim() === "") {
9860 errors.push("slot (missing)");
9861 } else if (!KNOWN_SLOTS.has(def.slot)) {
9862 errors.push(
9863 `slot (must be one of ${Array.from(KNOWN_SLOTS).join(", ")})`
9864 );
9865 }
9866 if (typeof def.match !== "function") {
9867 errors.push("match (must be a function)");
9868 }
9869 if (typeof def.render !== "function") {
9870 errors.push("render (must be a function)");
9871 }
9872 }
9873 throwOnRegistrationErrors("WindowSlot", errors, def);
9874 const id = def.id.trim().toLowerCase();
9875 registry$1.set(id, { ...def, id });
9876 notify$6();
9877 }
9878 function unregisterWindowSlot(id) {
9879 if (registry$1.delete(id.toLowerCase())) {
9880 notify$6();
9881 }
9882 }
9883 function unregisterWindowSlotsByOwner(owner) {
9884 if (!owner) {
9885 return 0;
9886 }
9887 let removed = 0;
9888 for (const [id, def] of Array.from(registry$1.entries())) {
9889 if (def.owner === owner) {
9890 registry$1.delete(id);
9891 removed++;
9892 }
9893 }
9894 if (removed > 0) {
9895 notify$6();
9896 }
9897 return removed;
9898 }
9899 function listWindowSlots() {
9900 return Array.from(registry$1.values()).sort((a, b) => {
9901 const oa = a.order ?? 100;
9902 const ob = b.order ?? 100;
9903 if (oa !== ob) {
9904 return oa - ob;
9905 }
9906 return a.id.localeCompare(b.id);
9907 });
9908 }
9909 function notify$6() {
9910 const snapshot = Array.from(listeners$4);
9911 for (const cb of snapshot) {
9912 try {
9913 cb();
9914 } catch (err) {
9915 if (typeof console !== "undefined") {
9916 console.error(
9917 "[desktop-mode] window-slot registry listener threw:",
9918 err
9919 );
9920 }
9921 }
9922 }
9923 }
9924 function createWindowSlotRegistrySync() {
9925 const loadedHandles = /* @__PURE__ */ new Set();
9926 const loadedUrls = /* @__PURE__ */ new Set();
9927 let prevIdsByHandle = /* @__PURE__ */ new Map();
9928 const ensureScript = async (entry) => {
9929 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9930 loadedHandles.add(entry.handle);
9931 return;
9932 }
9933 try {
9934 await loadVendorScript(entry.scriptUrl, {
9935 translations: entry.scriptTranslations,
9936 l10n: entry.scriptL10n,
9937 before: entry.scriptBefore,
9938 after: entry.scriptAfter
9939 });
9940 } catch (err) {
9941 doAction(HOOKS.SHELL_ERROR, {
9942 scope: "window-slot-script-load",
9943 handle: entry.handle,
9944 url: entry.scriptUrl,
9945 error: err
9946 });
9947 return;
9948 }
9949 loadedUrls.add(entry.scriptUrl);
9950 loadedHandles.add(entry.handle);
9951 };
9952 const idsByHandleFrom = (slots) => {
9953 const map = /* @__PURE__ */ new Map();
9954 if (!slots) {
9955 return map;
9956 }
9957 for (const entry of slots) {
9958 if (!entry.scriptHandle || !entry.id) {
9959 continue;
9960 }
9961 let set = map.get(entry.scriptHandle);
9962 if (!set) {
9963 set = /* @__PURE__ */ new Set();
9964 map.set(entry.scriptHandle, set);
9965 }
9966 set.add(entry.id);
9967 }
9968 return map;
9969 };
9970 const collectIdsToRemove = (handle) => {
9971 const ids = /* @__PURE__ */ new Set();
9972 for (const def of listWindowSlots()) {
9973 if (def.owner === handle) {
9974 ids.add(def.id);
9975 }
9976 }
9977 const declared = prevIdsByHandle.get(handle);
9978 if (declared) {
9979 for (const id of declared) {
9980 ids.add(id);
9981 }
9982 }
9983 return ids;
9984 };
9985 return async (scripts, slots) => {
9986 const incomingHandles = /* @__PURE__ */ new Set();
9987 for (const entry of scripts) {
9988 if (entry.handle) {
9989 incomingHandles.add(entry.handle);
9990 }
9991 }
9992 for (const handle of Array.from(loadedHandles)) {
9993 if (incomingHandles.has(handle)) {
9994 continue;
9995 }
9996 for (const id of collectIdsToRemove(handle)) {
9997 unregisterWindowSlot(id);
9998 }
9999 unregisterWindowSlotsByOwner(handle);
10000 loadedHandles.delete(handle);
10001 }
10002 for (const entry of scripts) {
10003 if (!entry.handle || loadedHandles.has(entry.handle)) {
10004 continue;
10005 }
10006 await ensureScript(entry);
10007 }
10008 prevIdsByHandle = idsByHandleFrom(slots);
10009 };
10010 }
10011 const KEY_PREFIX = "desktop-mode-notice-dismissed";
10012 function currentUserSuffix() {
10013 const w = window.wp;
10014 const uid = w?.desktop?.config?.currentUserId;
10015 if (typeof uid === "number" && uid > 0) {
10016 return String(uid);
10017 }
10018 return "anon";
10019 }
10020 function storageKey() {
10021 return `${KEY_PREFIX}:${currentUserSuffix()}`;
10022 }
10023 function readMap() {
10024 try {
10025 const raw = window.localStorage.getItem(storageKey());
10026 if (!raw) {
10027 return {};
10028 }
10029 const parsed = JSON.parse(raw);
10030 if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
10031 return parsed;
10032 }
10033 } catch {
10034 }
10035 return {};
10036 }
10037 function writeMap(map) {
10038 try {
10039 window.localStorage.setItem(storageKey(), JSON.stringify(map));
10040 } catch {
10041 }
10042 }
10043 function isNoticeDismissed(id) {
10044 if (!id) {
10045 return false;
10046 }
10047 return readMap()[id] === true;
10048 }
10049 function markNoticeDismissed(id) {
10050 if (!id) {
10051 return;
10052 }
10053 const map = readMap();
10054 map[id] = true;
10055 writeMap(map);
10056 }
10057 function clearNoticeDismissed(id) {
10058 if (!id) {
10059 return;
10060 }
10061 const map = readMap();
10062 if (map[id]) {
10063 delete map[id];
10064 writeMap(map);
10065 }
10066 }
10067 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 ) )}`;
10068 const _WpdNotice = class _WpdNotice extends Component {
10069 connectedCallback() {
10070 super.connectedCallback();
10071 if (!this.hasAttribute("role")) {
10072 this.setAttribute("role", "status");
10073 }
10074 if (!this.hasAttribute("tone")) {
10075 this.setAttribute("tone", "info");
10076 }
10077 const id = this.getAttribute("notice-id");
10078 if (id && isNoticeDismissed(id)) {
10079 this.hidden = true;
10080 }
10081 }
10082 /**
10083 * Imperatively dismiss the notice — hides the host and records
10084 * the dismissal in localStorage when `notice-id` is set.
10085 */
10086 dismiss() {
10087 this.hidden = true;
10088 const id = this.getAttribute("notice-id");
10089 if (id) {
10090 markNoticeDismissed(id);
10091 }
10092 this.emit("wpd-notice-dismiss", { noticeId: id ?? void 0 });
10093 }
10094 /**
10095 * Clear a previously recorded dismissal and re-show the notice.
10096 * Useful in tests and for "Show again" affordances.
10097 */
10098 undismiss() {
10099 const id = this.getAttribute("notice-id");
10100 if (id) {
10101 clearNoticeDismissed(id);
10102 }
10103 this.hidden = false;
10104 }
10105 render() {
10106 const icon = this.getAttribute("icon");
10107 const dismissible = !this.hasAttribute("not-dismissible");
10108 return html`
10109 <span
10110 class="wpd-notice__icon dashicons ${icon ?? ""}"
10111 ?hidden=${!icon}
10112 aria-hidden="true"
10113 ></span>
10114 <span class="wpd-notice__label"><slot></slot></span>
10115 <button
10116 type="button"
10117 class="wpd-notice__close"
10118 ?hidden=${!dismissible}
10119 aria-label=${__("Dismiss notice")}
10120 @click=${(e) => this._onDismiss(e)}
10121 >
10122 <svg viewBox="0 0 14 14" aria-hidden="true">
10123 <path
10124 d="M3 3 L11 11 M11 3 L3 11"
10125 stroke="currentColor"
10126 stroke-width="1.6"
10127 stroke-linecap="round"
10128 fill="none"
10129 ></path>
10130 </svg>
10131 </button>
10132 `;
10133 }
10134 _onDismiss(e) {
10135 e.preventDefault();
10136 e.stopPropagation();
10137 this.dismiss();
10138 }
10139 };
10140 _WpdNotice.props = ["tone", "notDismissible", "icon", "noticeId"];
10141 _WpdNotice.styles = [styles$6];
10142 _WpdNotice.help = {
10143 title: "Notice",
10144 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.",
10145 status: "experimental",
10146 since: "0.22.0",
10147 props: [
10148 {
10149 name: "tone",
10150 type: '"info" | "success" | "warning" | "error" | "danger" | "neutral"',
10151 description: "Color palette. Defaults to `info`. `error` and `danger` are aliases."
10152 },
10153 {
10154 name: "not-dismissible",
10155 type: "boolean",
10156 description: "Suppress the trailing close button. Defaults to dismissible."
10157 },
10158 {
10159 name: "icon",
10160 type: "string",
10161 description: "Optional Dashicons class for a leading glyph (e.g. `dashicons-info`)."
10162 },
10163 {
10164 name: "notice-id",
10165 type: "string",
10166 description: "Persistence key. When set, the notice records its dismissed state in localStorage so it stays closed across reloads for the same user."
10167 }
10168 ],
10169 slots: [
10170 {
10171 name: "(default)",
10172 description: "Message HTML. Links, `<strong>`, `<em>`, and other inline formatting are allowed."
10173 }
10174 ],
10175 events: [
10176 {
10177 name: "wpd-notice-dismiss",
10178 description: "Fires after the user clicks the close button.",
10179 detail: "{ noticeId?: string }"
10180 }
10181 ],
10182 cssProps: [
10183 { name: "--wpd-notice-bg", description: "Background color." },
10184 { name: "--wpd-notice-accent", description: "Left-edge stripe + icon color." },
10185 { name: "--wpd-notice-color", description: "Text color." },
10186 { name: "--wpd-notice-border", description: "Bottom border color." },
10187 { name: "--wpd-notice-link", description: "Color for slotted <a> elements." }
10188 ],
10189 example: html`
10190 <wpd-notice tone="warning" notice-id="docs/example">
10191 Heads up — this is a demo notice.
10192 <a href="#">Learn more</a>.
10193 </wpd-notice>
10194 `
10195 };
10196 let WpdNotice = _WpdNotice;
10197 defineComponent("wpd-notice", WpdNotice);
10198 const store$5 = createSharedStore(
10199 "desktop-mode/window-notices",
10200 () => ({ entries: /* @__PURE__ */ new Map() })
10201 );
10202 const ID_PATTERN = /^[a-z0-9_/-]+$/;
10203 function slotIdFor(id) {
10204 return `desktop-mode-notice/${id.toLowerCase()}`;
10205 }
10206 function buildNoticeElement(entry) {
10207 const el = document.createElement("wpd-notice");
10208 el.setAttribute("tone", entry.tone ?? "info");
10209 el.setAttribute("notice-id", entry.id);
10210 if (entry.dismissible === false) {
10211 el.setAttribute("not-dismissible", "");
10212 }
10213 if (entry.icon) {
10214 el.setAttribute("icon", entry.icon);
10215 }
10216 el.innerHTML = entry.message;
10217 return el;
10218 }
10219 function registerWindowNotice(entry) {
10220 if (!entry || typeof entry !== "object") {
10221 return () => {
10222 };
10223 }
10224 const id = String(entry.id ?? "").trim().toLowerCase();
10225 if (!id || !ID_PATTERN.test(id)) {
10226 return () => {
10227 };
10228 }
10229 if (typeof entry.message !== "string" || entry.message === "") {
10230 return () => {
10231 };
10232 }
10233 const normalised = { ...entry, id };
10234 store$5.state.entries.set(id, normalised);
10235 const slotId = slotIdFor(id);
10236 registerWindowSlot({
10237 id: slotId,
10238 slot: "after-titlebar",
10239 order: normalised.order ?? 100,
10240 // Append rather than clear — every notice slot entry appends
10241 // its own `<wpd-notice>` so multiple notices stack.
10242 replace: false,
10243 owner: normalised.owner,
10244 match: (win) => {
10245 const def = store$5.state.entries.get(id);
10246 if (!def) {
10247 return false;
10248 }
10249 if (typeof def.match !== "function") {
10250 return true;
10251 }
10252 try {
10253 return def.match(win) === true;
10254 } catch {
10255 return false;
10256 }
10257 },
10258 render: (host) => {
10259 const def = store$5.state.entries.get(id);
10260 if (!def) {
10261 return;
10262 }
10263 host.appendChild(buildNoticeElement(def));
10264 }
10265 });
10266 return () => unregisterWindowNotice(id);
10267 }
10268 function unregisterWindowNotice(id) {
10269 const key = String(id ?? "").trim().toLowerCase();
10270 if (!key) {
10271 return;
10272 }
10273 if (store$5.state.entries.delete(key)) {
10274 unregisterWindowSlot(slotIdFor(key));
10275 }
10276 }
10277 function listWindowNotices() {
10278 return Array.from(store$5.state.entries.values()).sort((a, b) => {
10279 const oa = a.order ?? 100;
10280 const ob = b.order ?? 100;
10281 if (oa !== ob) {
10282 return oa - ob;
10283 }
10284 return a.id.localeCompare(b.id);
10285 });
10286 }
10287 function dismissWindowNotice(id) {
10288 const key = String(id ?? "").trim().toLowerCase();
10289 if (!key) {
10290 return;
10291 }
10292 markNoticeDismissed(key);
10293 }
10294 function undismissWindowNotice(id) {
10295 const key = String(id ?? "").trim().toLowerCase();
10296 if (!key) {
10297 return;
10298 }
10299 clearNoticeDismissed(key);
10300 }
10301 function buildMatcher(match) {
10302 if (!match) {
10303 return void 0;
10304 }
10305 const ids = /* @__PURE__ */ new Set();
10306 if (typeof match.window === "string" && match.window !== "") {
10307 ids.add(match.window);
10308 }
10309 if (Array.isArray(match.windows)) {
10310 for (const id of match.windows) {
10311 if (typeof id === "string" && id !== "") {
10312 ids.add(id);
10313 }
10314 }
10315 }
10316 const needle = typeof match.urlContains === "string" && match.urlContains !== "" ? match.urlContains.toLowerCase() : null;
10317 if (ids.size === 0 && needle === null) {
10318 return void 0;
10319 }
10320 return (w) => {
10321 if (ids.size > 0 && !ids.has(w.id)) {
10322 return false;
10323 }
10324 if (needle !== null) {
10325 const url = typeof w.config.url === "string" ? w.config.url.toLowerCase() : "";
10326 if (!url.includes(needle)) {
10327 return false;
10328 }
10329 }
10330 return true;
10331 };
10332 }
10333 function applyServerWindowNotices(entries) {
10334 const wanted = /* @__PURE__ */ new Set();
10335 for (const entry of entries) {
10336 if (!entry || typeof entry.id !== "string" || !entry.id) {
10337 continue;
10338 }
10339 wanted.add(entry.id.toLowerCase());
10340 registerWindowNotice({
10341 id: entry.id,
10342 message: entry.message,
10343 tone: entry.tone,
10344 dismissible: entry.dismissible !== false,
10345 icon: entry.icon,
10346 match: buildMatcher(entry.match),
10347 order: typeof entry.order === "number" ? entry.order : void 0,
10348 // `owner` tag marks every server-shipped notice so a
10349 // targeted cleanup is trivial if/when we surface a sweep
10350 // helper later. Matches the convention used by the
10351 // command / settings-tab sync modules.
10352 owner: "__server__"
10353 });
10354 }
10355 for (const existing of listWindowNotices()) {
10356 if (existing.owner !== "__server__") {
10357 continue;
10358 }
10359 if (!wanted.has(existing.id)) {
10360 unregisterWindowNotice(existing.id);
10361 }
10362 }
10363 }
10364 const store$4 = createSharedStore(
10365 "desktop-mode/window-chrome-registry",
10366 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
10367 );
10368 const registry = store$4.state.registry;
10369 const listeners$3 = store$4.state.listeners;
10370 const WINDOW_CHROME_ID = /^[a-z0-9_/-]+$/;
10371 function registerWindowChrome(def) {
10372 const errors = [];
10373 if (!def || typeof def !== "object") {
10374 errors.push("def (not an object)");
10375 } else {
10376 if (typeof def.id !== "string" || def.id.trim() === "") {
10377 errors.push("id (missing)");
10378 } else if (!WINDOW_CHROME_ID.test(def.id.trim().toLowerCase())) {
10379 errors.push(
10380 `id (must match ${WINDOW_CHROME_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
10381 );
10382 }
10383 if (typeof def.match !== "function") {
10384 errors.push("match (must be a function)");
10385 }
10386 if (typeof def.render !== "function") {
10387 errors.push("render (must be a function)");
10388 }
10389 }
10390 throwOnRegistrationErrors("WindowChrome", errors, def);
10391 const id = def.id.trim().toLowerCase();
10392 registry.set(id, { ...def, id });
10393 notify$5();
10394 }
10395 function unregisterWindowChrome(id) {
10396 if (registry.delete(id.toLowerCase())) {
10397 notify$5();
10398 }
10399 }
10400 function unregisterWindowChromesByOwner(owner) {
10401 if (!owner) {
10402 return 0;
10403 }
10404 let removed = 0;
10405 for (const [id, def] of Array.from(registry.entries())) {
10406 if (def.owner === owner) {
10407 registry.delete(id);
10408 removed++;
10409 }
10410 }
10411 if (removed > 0) {
10412 notify$5();
10413 }
10414 return removed;
10415 }
10416 function listWindowChromes() {
10417 return Array.from(registry.values()).sort(
10418 (a, b) => a.id.localeCompare(b.id)
10419 );
10420 }
10421 function notify$5() {
10422 const snapshot = Array.from(listeners$3);
10423 for (const cb of snapshot) {
10424 try {
10425 cb();
10426 } catch (err) {
10427 if (typeof console !== "undefined") {
10428 console.error(
10429 "[desktop-mode] window-chrome registry listener threw:",
10430 err
10431 );
10432 }
10433 }
10434 }
10435 }
10436 function createWindowChromeRegistrySync() {
10437 const loadedHandles = /* @__PURE__ */ new Set();
10438 const loadedUrls = /* @__PURE__ */ new Set();
10439 let prevIdsByHandle = /* @__PURE__ */ new Map();
10440 const ensureScript = async (entry) => {
10441 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
10442 loadedHandles.add(entry.handle);
10443 return;
10444 }
10445 try {
10446 await loadVendorScript(entry.scriptUrl, {
10447 translations: entry.scriptTranslations,
10448 l10n: entry.scriptL10n,
10449 before: entry.scriptBefore,
10450 after: entry.scriptAfter
10451 });
10452 } catch (err) {
10453 doAction(HOOKS.SHELL_ERROR, {
10454 scope: "window-chrome-script-load",
10455 handle: entry.handle,
10456 url: entry.scriptUrl,
10457 error: err
10458 });
10459 return;
10460 }
10461 loadedUrls.add(entry.scriptUrl);
10462 loadedHandles.add(entry.handle);
10463 };
10464 const idsByHandleFrom = (chromes) => {
10465 const map = /* @__PURE__ */ new Map();
10466 if (!chromes) {
10467 return map;
10468 }
10469 for (const entry of chromes) {
10470 if (!entry.scriptHandle || !entry.id) {
10471 continue;
10472 }
10473 let set = map.get(entry.scriptHandle);
10474 if (!set) {
10475 set = /* @__PURE__ */ new Set();
10476 map.set(entry.scriptHandle, set);
10477 }
10478 set.add(entry.id);
10479 }
10480 return map;
10481 };
10482 const collectIdsToRemove = (handle) => {
10483 const ids = /* @__PURE__ */ new Set();
10484 for (const def of listWindowChromes()) {
10485 if (def.owner === handle) {
10486 ids.add(def.id);
10487 }
10488 }
10489 const declared = prevIdsByHandle.get(handle);
10490 if (declared) {
10491 for (const id of declared) {
10492 ids.add(id);
10493 }
10494 }
10495 return ids;
10496 };
10497 return async (scripts, chromes) => {
10498 const incomingHandles = /* @__PURE__ */ new Set();
10499 for (const entry of scripts) {
10500 if (entry.handle) {
10501 incomingHandles.add(entry.handle);
10502 }
10503 }
10504 for (const handle of Array.from(loadedHandles)) {
10505 if (incomingHandles.has(handle)) {
10506 continue;
10507 }
10508 for (const id of collectIdsToRemove(handle)) {
10509 unregisterWindowChrome(id);
10510 }
10511 unregisterWindowChromesByOwner(handle);
10512 loadedHandles.delete(handle);
10513 }
10514 for (const entry of scripts) {
10515 if (!entry.handle || loadedHandles.has(entry.handle)) {
10516 continue;
10517 }
10518 await ensureScript(entry);
10519 }
10520 prevIdsByHandle = idsByHandleFrom(chromes);
10521 };
10522 }
10523 const INITIAL_ORIGIN$2 = window.location.origin;
10524 let _connSeq = 0;
10525 const _connections = /* @__PURE__ */ new Map();
10526 const _connectionsByTarget = /* @__PURE__ */ new Map();
10527 const _syntheticIframes = /* @__PURE__ */ new Map();
10528 function registerSyntheticIframe(windowId, iframe) {
10529 _syntheticIframes.set(windowId, iframe);
10530 return () => {
10531 if (_syntheticIframes.get(windowId) === iframe) {
10532 _syntheticIframes.delete(windowId);
10533 }
10534 };
10535 }
10536 function nextId() {
10537 return `desktop-mode-conn-${++_connSeq}`;
10538 }
10539 function createConnectionBridge(manager) {
10540 const sendToIframe = (win, message) => {
10541 try {
10542 win.contentWindow?.postMessage(message, INITIAL_ORIGIN$2);
10543 } catch (err) {
10544 if (typeof console !== "undefined") {
10545 console.error(
10546 "[desktop-mode] connection: postMessage failed",
10547 err
10548 );
10549 }
10550 }
10551 };
10552 const connect = (targetWindowId, opts = {}) => {
10553 const id = nextId();
10554 const topics = Array.isArray(opts.topics) ? [...opts.topics] : [];
10555 const subs = /* @__PURE__ */ new Map();
10556 const queue = [];
10557 let isOpen = false;
10558 let destroyed = false;
10559 const targetIframe = () => {
10560 const synth = _syntheticIframes.get(targetWindowId);
10561 if (synth) {
10562 return synth;
10563 }
10564 const w = manager.getById(targetWindowId);
10565 return w?.iframe ?? null;
10566 };
10567 const isNativeTarget = () => {
10568 if (targetIframe()) {
10569 return false;
10570 }
10571 const w = manager.getById(targetWindowId);
10572 return !!w && w.config?.native === true;
10573 };
10574 const nativeSubUnsubs = [];
10575 const flushQueue = () => {
10576 const iframe2 = targetIframe();
10577 if (!iframe2) {
10578 return;
10579 }
10580 while (queue.length) {
10581 const msg = queue.shift();
10582 sendToIframe(iframe2, {
10583 type: "desktop-mode-bridge-publish",
10584 connectionId: id,
10585 topic: msg.topic,
10586 payload: msg.payload
10587 });
10588 }
10589 };
10590 const conn = {
10591 id,
10592 target: targetWindowId,
10593 isOpen: () => isOpen,
10594 subscribe(topic, cb) {
10595 const wrapped = cb;
10596 if (isNativeTarget()) {
10597 const off = addParentSubscriber(
10598 targetWindowId,
10599 topic,
10600 (payload, meta) => {
10601 doAction(HOOKS.CONNECTION_MESSAGE, {
10602 connectionId: id,
10603 topic: meta.channel,
10604 direction: "in"
10605 });
10606 try {
10607 wrapped(payload, { topic: meta.channel });
10608 } catch (err) {
10609 if (typeof console !== "undefined") {
10610 console.error(
10611 "[desktop-mode] connection subscriber threw:",
10612 err
10613 );
10614 }
10615 }
10616 }
10617 );
10618 nativeSubUnsubs.push(off);
10619 return off;
10620 }
10621 let bucket22 = subs.get(topic);
10622 if (!bucket22) {
10623 bucket22 = /* @__PURE__ */ new Set();
10624 subs.set(topic, bucket22);
10625 }
10626 bucket22.add(wrapped);
10627 return () => {
10628 bucket22?.delete(wrapped);
10629 };
10630 },
10631 send(topic, payload) {
10632 if (destroyed) {
10633 return;
10634 }
10635 doAction(HOOKS.CONNECTION_MESSAGE, {
10636 connectionId: id,
10637 topic,
10638 direction: "out"
10639 });
10640 if (isNativeTarget()) {
10641 dispatchToNative(targetWindowId, topic, payload);
10642 return;
10643 }
10644 if (!isOpen) {
10645 queue.push({ topic, payload });
10646 return;
10647 }
10648 const iframe2 = targetIframe();
10649 if (!iframe2) {
10650 return;
10651 }
10652 sendToIframe(iframe2, {
10653 type: "desktop-mode-bridge-publish",
10654 connectionId: id,
10655 topic,
10656 payload
10657 });
10658 },
10659 disconnect() {
10660 conn._destroy("disconnect");
10661 },
10662 _targetWindow: targetIframe,
10663 _handleIframeMessage(data) {
10664 if (!data || typeof data !== "object") {
10665 return;
10666 }
10667 const msg = data;
10668 if (msg.type === "desktop-mode-bridge-handshake-ack") {
10669 if (isOpen) {
10670 return;
10671 }
10672 isOpen = true;
10673 doAction(HOOKS.CONNECTION_OPENED, {
10674 connectionId: id,
10675 targetWindowId,
10676 topics,
10677 // Ship the live Connection alongside the id so
10678 // iframe-initiated connections can be subscribed
10679 // to directly from the hook handler — without
10680 // `wp.desktop.getConnection(id)` plumbing the
10681 // payload would carry the id but no way to call
10682 // `.subscribe()` against it.
10683 connection: conn
10684 });
10685 try {
10686 opts.onOpen?.();
10687 } catch (err) {
10688 if (typeof console !== "undefined") {
10689 console.error(
10690 "[desktop-mode] connection.onOpen threw:",
10691 err
10692 );
10693 }
10694 }
10695 flushQueue();
10696 return;
10697 }
10698 if (msg.type === "desktop-mode-bridge-publish") {
10699 const m = data;
10700 const topic = typeof m.topic === "string" ? m.topic : "";
10701 if (!topic) {
10702 return;
10703 }
10704 doAction(HOOKS.CONNECTION_MESSAGE, {
10705 connectionId: id,
10706 topic,
10707 direction: "in"
10708 });
10709 const exact = subs.get(topic);
10710 if (exact) {
10711 for (const cb of Array.from(exact)) {
10712 try {
10713 cb(m.payload, { topic });
10714 } catch (err) {
10715 if (typeof console !== "undefined") {
10716 console.error(
10717 "[desktop-mode] connection subscriber threw:",
10718 err
10719 );
10720 }
10721 }
10722 }
10723 }
10724 const wildcard = subs.get("*");
10725 if (wildcard) {
10726 for (const cb of Array.from(wildcard)) {
10727 try {
10728 cb(m.payload, { topic });
10729 } catch (err) {
10730 if (typeof console !== "undefined") {
10731 console.error(
10732 "[desktop-mode] connection wildcard subscriber threw:",
10733 err
10734 );
10735 }
10736 }
10737 }
10738 }
10739 return;
10740 }
10741 if (msg.type === "desktop-mode-bridge-disconnect") {
10742 conn._destroy("disconnect");
10743 }
10744 },
10745 _destroy(reason) {
10746 if (destroyed) {
10747 return;
10748 }
10749 destroyed = true;
10750 const wasOpen = isOpen;
10751 isOpen = false;
10752 _connections.delete(id);
10753 const targetSet = _connectionsByTarget.get(targetWindowId);
10754 if (targetSet) {
10755 targetSet.delete(id);
10756 if (targetSet.size === 0) {
10757 _connectionsByTarget.delete(targetWindowId);
10758 }
10759 }
10760 for (const off of nativeSubUnsubs.splice(0)) {
10761 try {
10762 off();
10763 } catch {
10764 }
10765 }
10766 if (wasOpen) {
10767 const iframe2 = targetIframe();
10768 if (iframe2) {
10769 sendToIframe(iframe2, {
10770 type: "desktop-mode-bridge-disconnect",
10771 connectionId: id
10772 });
10773 }
10774 }
10775 doAction(HOOKS.CONNECTION_CLOSED, {
10776 connectionId: id,
10777 reason
10778 });
10779 try {
10780 opts.onClose?.(reason);
10781 } catch (err) {
10782 if (typeof console !== "undefined") {
10783 console.error(
10784 "[desktop-mode] connection.onClose threw:",
10785 err
10786 );
10787 }
10788 }
10789 }
10790 };
10791 _connections.set(id, conn);
10792 let bucket2 = _connectionsByTarget.get(targetWindowId);
10793 if (!bucket2) {
10794 bucket2 = /* @__PURE__ */ new Set();
10795 _connectionsByTarget.set(targetWindowId, bucket2);
10796 }
10797 bucket2.add(id);
10798 if (isNativeTarget()) {
10799 Promise.resolve().then(() => {
10800 if (destroyed || isOpen) {
10801 return;
10802 }
10803 isOpen = true;
10804 doAction(HOOKS.CONNECTION_OPENED, {
10805 connectionId: id,
10806 targetWindowId,
10807 topics
10808 });
10809 try {
10810 opts.onOpen?.();
10811 } catch (err) {
10812 if (typeof console !== "undefined") {
10813 console.error(
10814 "[desktop-mode] connection.onOpen threw:",
10815 err
10816 );
10817 }
10818 }
10819 });
10820 return conn;
10821 }
10822 const iframe = targetIframe();
10823 if (iframe) {
10824 sendToIframe(iframe, {
10825 type: "desktop-mode-bridge-handshake",
10826 connectionId: id,
10827 targetWindowId,
10828 topics
10829 });
10830 }
10831 return conn;
10832 };
10833 const routeIncomingFromIframe = (data, windowId) => {
10834 if (!data || typeof data !== "object") {
10835 return;
10836 }
10837 const msg = data;
10838 if (typeof msg.type !== "string" || !msg.type.startsWith("desktop-mode-bridge-")) {
10839 return;
10840 }
10841 if (msg.type === "desktop-mode-bridge-connection-request" && typeof msg.requestId === "string" && typeof windowId === "string" && windowId !== "") {
10842 handleConnectionRequest(windowId, msg.requestId, Array.isArray(msg.topics) ? msg.topics : []);
10843 return;
10844 }
10845 if (typeof msg.connectionId !== "string") {
10846 return;
10847 }
10848 const conn = _connections.get(msg.connectionId);
10849 conn?._handleIframeMessage(data);
10850 };
10851 const handleConnectionRequest = (windowId, requestId, topics) => {
10852 const synth = _syntheticIframes.get(windowId);
10853 const iframe = synth ?? manager.getById(windowId)?.iframe ?? null;
10854 if (!iframe) {
10855 return;
10856 }
10857 const decision = applyFilters(
10858 HOOKS.IFRAME_CONNECTION_REQUEST,
10859 true,
10860 { windowId, requestId, topics: topics.slice() }
10861 );
10862 if (decision === false) {
10863 try {
10864 iframe.contentWindow?.postMessage({
10865 type: "desktop-mode-bridge-connection-ack",
10866 requestId,
10867 accepted: false,
10868 reason: "rejected"
10869 }, INITIAL_ORIGIN$2);
10870 } catch {
10871 }
10872 return;
10873 }
10874 const finalTopics = decision && typeof decision === "object" && Array.isArray(decision.topics) ? decision.topics : topics;
10875 const conn = connect(windowId, { topics: finalTopics });
10876 try {
10877 iframe.contentWindow?.postMessage({
10878 type: "desktop-mode-bridge-connection-ack",
10879 requestId,
10880 accepted: true,
10881 connectionId: conn.id
10882 }, INITIAL_ORIGIN$2);
10883 } catch {
10884 }
10885 };
10886 const onIframeReady = (windowId) => {
10887 const bucket2 = _connectionsByTarget.get(windowId);
10888 if (!bucket2) {
10889 return;
10890 }
10891 for (const connId of Array.from(bucket2)) {
10892 const conn = _connections.get(connId);
10893 if (!conn || conn.isOpen()) {
10894 continue;
10895 }
10896 const iframe = conn._targetWindow();
10897 if (!iframe) {
10898 continue;
10899 }
10900 sendToIframe(iframe, {
10901 type: "desktop-mode-bridge-handshake",
10902 connectionId: conn.id,
10903 targetWindowId: conn.target,
10904 topics: []
10905 // already negotiated client-side; iframe re-uses
10906 });
10907 }
10908 };
10909 const onWindowClosed = (windowId) => {
10910 const bucket2 = _connectionsByTarget.get(windowId);
10911 if (!bucket2) {
10912 return;
10913 }
10914 for (const connId of Array.from(bucket2)) {
10915 const conn = _connections.get(connId);
10916 conn?._destroy("window-closed");
10917 }
10918 };
10919 const getConnection = (connectionId) => {
10920 const conn = _connections.get(connectionId);
10921 return conn ?? null;
10922 };
10923 return {
10924 connect,
10925 getConnection,
10926 routeIncomingFromIframe,
10927 onIframeReady,
10928 onWindowClosed
10929 };
10930 }
10931 const __vite_import_meta_env__ = {};
10932 function devLog(...args) {
10933 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;
10934 if (mode !== "production") {
10935 console.log(...args);
10936 }
10937 }
10938 const OWNER_PREFIX = "iframe:";
10939 function ownerFor(windowId) {
10940 return OWNER_PREFIX + windowId;
10941 }
10942 function iconFor(harvested) {
10943 if (harvested.icon && typeof harvested.icon === "string" && harvested.icon.startsWith("dashicons-")) {
10944 return harvested.icon;
10945 }
10946 return harvested.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
10947 }
10948 function slugFor(windowId, name) {
10949 const safeName = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
10950 const safeWin = windowId.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
10951 return `win-${safeWin}-${safeName}`;
10952 }
10953 class IframeCommandBridge {
10954 constructor(opts) {
10955 this.subscribedWindowId = null;
10956 this.manager = opts.manager;
10957 this.adminUrl = opts.adminUrl;
10958 }
10959 /** Wire up the focus / close / message listeners. Idempotent. */
10960 install() {
10961 document.addEventListener("desktop-mode-window-focused", (e) => {
10962 const detail = e.detail;
10963 if (detail && typeof detail.windowId === "string") {
10964 this.onFocused(detail.windowId);
10965 }
10966 });
10967 document.addEventListener("desktop-mode-window-closed", (e) => {
10968 const detail = e.detail;
10969 if (detail && typeof detail.windowId === "string") {
10970 unregisterByOwner(ownerFor(detail.windowId));
10971 if (this.subscribedWindowId === detail.windowId) {
10972 this.subscribedWindowId = null;
10973 }
10974 }
10975 });
10976 document.addEventListener("desktop-mode-window-changed", (e) => {
10977 const detail = e.detail;
10978 if (!detail || typeof detail.windowId !== "string") {
10979 return;
10980 }
10981 if (detail.reason !== "state") {
10982 return;
10983 }
10984 if (detail.state !== "minimized") {
10985 return;
10986 }
10987 if (this.subscribedWindowId === detail.windowId) {
10988 this.subscribedWindowId = null;
10989 }
10990 });
10991 window.addEventListener("message", (e) => {
10992 if (e.origin !== window.location.origin) {
10993 return;
10994 }
10995 const data = e.data;
10996 if (!data || typeof data.type !== "string") {
10997 return;
10998 }
10999 if (data.type === "desktop-mode-bridge-ready") {
11000 const win2 = this.manager.findByIframeSource(e.source);
11001 if (win2 && win2.id === this.subscribedWindowId) {
11002 this.sendSubscribe(win2.id);
11003 }
11004 return;
11005 }
11006 if (data.type !== "desktop-mode-commands-list") {
11007 return;
11008 }
11009 if (!Array.isArray(data.commands)) {
11010 return;
11011 }
11012 const win = this.manager.findByIframeSource(e.source);
11013 if (!win) {
11014 return;
11015 }
11016 if (win.id !== this.subscribedWindowId) {
11017 return;
11018 }
11019 this.applyList(win.id, data.commands);
11020 });
11021 const focused = this.manager.getFocused();
11022 if (focused) {
11023 this.onFocused(focused.id);
11024 }
11025 }
11026 onFocused(windowId) {
11027 if (this.subscribedWindowId === windowId) {
11028 return;
11029 }
11030 if (this.subscribedWindowId) {
11031 const prev = this.manager.getById(this.subscribedWindowId);
11032 if (prev && prev.iframe && prev.iframe.contentWindow) {
11033 try {
11034 prev.iframe.contentWindow.postMessage(
11035 { type: "desktop-mode-commands-unsubscribe" },
11036 window.location.origin
11037 );
11038 } catch {
11039 }
11040 }
11041 unregisterByOwner(ownerFor(this.subscribedWindowId));
11042 }
11043 this.subscribedWindowId = windowId;
11044 this.sendSubscribe(windowId);
11045 }
11046 sendSubscribe(windowId) {
11047 const win = this.manager.getById(windowId);
11048 if (!win) {
11049 return;
11050 }
11051 if (!win.iframe) {
11052 return;
11053 }
11054 if (!win.iframe.contentWindow) {
11055 return;
11056 }
11057 try {
11058 win.iframe.contentWindow.postMessage(
11059 { type: "desktop-mode-commands-subscribe" },
11060 window.location.origin
11061 );
11062 } catch (err) {
11063 devLog("[wpd-cmd:parent] sendSubscribe: postMessage threw", err);
11064 }
11065 }
11066 applyList(windowId, commands) {
11067 const owner = ownerFor(windowId);
11068 unregisterByOwner(owner);
11069 for (const cmd of commands) {
11070 if (!cmd || !cmd.name || !cmd.label) {
11071 continue;
11072 }
11073 const slug = slugFor(windowId, cmd.name);
11074 const safeSvg = typeof cmd.iconSvg === "string" && cmd.iconSvg !== "" ? sanitizeIconSvg(cmd.iconSvg) : "";
11075 const def = {
11076 slug,
11077 label: cmd.label,
11078 icon: iconFor(cmd),
11079 iconSvg: safeSvg !== "" ? safeSvg : void 0,
11080 owner,
11081 // Harvested commands are contextual by construction —
11082 // they come from whichever window has focus. Surface
11083 // them eagerly so the user sees "Duplicate block" /
11084 // "Toggle distraction free" without having to type `/`
11085 // first.
11086 eager: true,
11087 run: cmd.kind === "navigate" && cmd.url ? this.runNavigate(cmd.url, cmd.label, iconFor(cmd)) : this.runProxy(windowId, cmd.name)
11088 };
11089 try {
11090 registerCommand(def);
11091 } catch (err) {
11092 console.error(
11093 "[desktop-mode] iframe-bridge: dropping bad command",
11094 def,
11095 err
11096 );
11097 }
11098 }
11099 }
11100 runNavigate(url, title, icon) {
11101 return (_args, ctx) => {
11102 ctx.close();
11103 if (tryNativeUrlRemap(url)) {
11104 return;
11105 }
11106 const id = deriveWindowId(url, this.adminUrl);
11107 this.manager.open({ id, baseId: id, url, title, icon });
11108 };
11109 }
11110 runProxy(windowId, name) {
11111 return (_args, ctx) => {
11112 ctx.close();
11113 const win = this.manager.getById(windowId);
11114 if (!win || !win.iframe || !win.iframe.contentWindow) {
11115 return;
11116 }
11117 try {
11118 win.iframe.contentWindow.postMessage(
11119 { type: "desktop-mode-commands-invoke", name },
11120 window.location.origin
11121 );
11122 } catch {
11123 }
11124 this.manager.focus(win);
11125 };
11126 }
11127 }
11128 const OWNER = "global";
11129 const NAV_HREF_LITERAL_RE = /(?:document\.location\.href|window\.location\.href|location\.href)\s*=\s*['"]([^'"$]+?)['"]/;
11130 const NAV_ASSIGN_LITERAL_RE = /(?:document\.location|window\.location|location)\s*=\s*['"]([^'"$]+?)['"]/;
11131 const NAV_CALL_LITERAL_RE = /location\.(?:assign|replace)\s*\(\s*['"]([^'"$]+?)['"]\s*\)/;
11132 const NAV_INTENT_RE = /(?:document\.location|window\.location|location)\s*(?:\.href\s*)?=|location\.(?:assign|replace)\s*\(/;
11133 const SITE_EDITOR_INTENT_RE = /getSiteEditorPage\s*\(|site-editor\.php/;
11134 const SITE_EDITOR_NAME_RE = /^(wp_template_part|wp_template|wp_navigation|wp_block)-(.+)$/;
11135 function lookupMenuCommand(name) {
11136 const list2 = window.__desktopModeMenuCommands;
11137 if (!Array.isArray(list2)) {
11138 return null;
11139 }
11140 for (const entry of list2) {
11141 if (entry && typeof entry === "object" && entry.name === name && typeof entry.url === "string" && entry.url !== "") {
11142 return {
11143 label: typeof entry.label === "string" ? entry.label : "",
11144 url: entry.url
11145 };
11146 }
11147 }
11148 return null;
11149 }
11150 class ShellCommandHarvester {
11151 constructor(opts) {
11152 this.mounted = false;
11153 this.host = null;
11154 this.root = null;
11155 this.kindCache = /* @__PURE__ */ Object.create(null);
11156 this.callbackCache = /* @__PURE__ */ Object.create(null);
11157 this.lastFingerprint = "";
11158 this.manager = opts.manager;
11159 this.adminUrl = opts.adminUrl;
11160 }
11161 /** Mount the harvester. Idempotent. Safe to call before `wp.data` loads. */
11162 install() {
11163 this.tryMount(0);
11164 }
11165 tryMount(attempt) {
11166 if (this.mounted) {
11167 return;
11168 }
11169 const wp = window.wp;
11170 if (!wp || !wp.data || !wp.element || typeof wp.data.subscribe !== "function") {
11171 if (attempt < 40) {
11172 window.setTimeout(() => this.tryMount(attempt + 1), 150);
11173 }
11174 return;
11175 }
11176 this.mount();
11177 }
11178 mount() {
11179 const wp = window.wp;
11180 const el = wp.element;
11181 const data = wp.data;
11182 const createEl = el.createElement;
11183 const useEffect = el.useEffect;
11184 const useRef = el.useRef;
11185 const useMemo = el.useMemo;
11186 const useSelect = data.useSelect;
11187 if (typeof createEl !== "function" || typeof useEffect !== "function" || typeof useRef !== "function" || typeof useMemo !== "function" || typeof useSelect !== "function" || typeof el.createRoot !== "function") {
11188 return;
11189 }
11190 this.mounted = true;
11191 const host = document.createElement("div");
11192 host.setAttribute("aria-hidden", "true");
11193 host.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;left:-9999px;top:-9999px;";
11194 (document.body || document.documentElement).appendChild(host);
11195 this.host = host;
11196 const bucket2 = {
11197 perLoader: {},
11198 statics: [],
11199 loadersList: []
11200 };
11201 const fingerprint2 = (cmds) => {
11202 if (!Array.isArray(cmds) || cmds.length === 0) {
11203 return "";
11204 }
11205 const keys = new Array(cmds.length);
11206 for (let i = 0; i < cmds.length; i++) {
11207 const c = cmds[i];
11208 keys[i] = c && c.name ? c.name : "";
11209 }
11210 return keys.join("|");
11211 };
11212 const mergeAndPublish = () => {
11213 let merged = [];
11214 for (const name of bucket2.loadersList) {
11215 const slice = bucket2.perLoader[name];
11216 if (Array.isArray(slice)) {
11217 merged = merged.concat(slice);
11218 }
11219 }
11220 if (Array.isArray(bucket2.statics)) {
11221 merged = merged.concat(bucket2.statics);
11222 }
11223 this.callbackCache = /* @__PURE__ */ Object.create(null);
11224 for (const cc of merged) {
11225 if (cc && cc.name && typeof cc.callback === "function") {
11226 this.callbackCache[cc.name] = cc.callback;
11227 }
11228 }
11229 this.publish(merged);
11230 };
11231 const LoaderSlot = (props) => {
11232 const loader = props.loader;
11233 let result = null;
11234 try {
11235 result = loader.hook({ search: "" });
11236 } catch {
11237 }
11238 const cmds = result && Array.isArray(result.commands) ? result.commands : [];
11239 const key = useMemo(() => fingerprint2(cmds), [cmds]);
11240 useEffect(() => {
11241 bucket2.perLoader[loader.name] = cmds;
11242 mergeAndPublish();
11243 }, [key]);
11244 useEffect(() => {
11245 return () => {
11246 delete bucket2.perLoader[loader.name];
11247 mergeAndPublish();
11248 };
11249 }, []);
11250 return null;
11251 };
11252 const Harvester = () => {
11253 const loaders = useSelect((s) => {
11254 const ss = s("core/commands");
11255 if (!ss || typeof ss.getCommandLoaders !== "function") {
11256 return [];
11257 }
11258 return [
11259 ...ss.getCommandLoaders(false) || [],
11260 ...ss.getCommandLoaders(true) || []
11261 ];
11262 }, []);
11263 const staticCmds = useSelect((s) => {
11264 const ss = s("core/commands");
11265 if (!ss || typeof ss.getCommands !== "function") {
11266 return [];
11267 }
11268 return [
11269 ...ss.getCommands(false) || [],
11270 ...ss.getCommands(true) || []
11271 ];
11272 }, []);
11273 const loadersNames = useMemo(() => {
11274 return Array.isArray(loaders) ? loaders.map((l) => l ? l.name || "" : "") : [];
11275 }, [loaders]);
11276 const loadersKey = loadersNames.join("|");
11277 useEffect(() => {
11278 bucket2.loadersList = loadersNames;
11279 mergeAndPublish();
11280 }, [loadersKey]);
11281 const staticKey = useMemo(
11282 () => fingerprint2(Array.isArray(staticCmds) ? staticCmds : []),
11283 [staticCmds]
11284 );
11285 useEffect(() => {
11286 bucket2.statics = Array.isArray(staticCmds) ? staticCmds : [];
11287 mergeAndPublish();
11288 }, [staticKey]);
11289 if (!Array.isArray(loaders) || loaders.length === 0) {
11290 return null;
11291 }
11292 const children = [];
11293 for (const loader of loaders) {
11294 if (!loader || typeof loader.hook !== "function") {
11295 continue;
11296 }
11297 children.push(
11298 createEl(LoaderSlot, { key: loader.name, loader })
11299 );
11300 }
11301 return createEl(el.Fragment || "div", null, children);
11302 };
11303 try {
11304 this.root = el.createRoot(host);
11305 this.root.render(createEl(Harvester));
11306 } catch {
11307 this.mounted = false;
11308 this.root = null;
11309 if (this.host && this.host.parentNode) {
11310 this.host.parentNode.removeChild(this.host);
11311 }
11312 this.host = null;
11313 }
11314 }
11315 publish(raw) {
11316 const seen = /* @__PURE__ */ Object.create(null);
11317 const classified = [];
11318 for (const cmd of raw) {
11319 if (!cmd || !cmd.name || !cmd.label) {
11320 continue;
11321 }
11322 if (cmd.disabled) {
11323 continue;
11324 }
11325 if (seen[cmd.name]) {
11326 continue;
11327 }
11328 seen[cmd.name] = true;
11329 classified.push(this.classify(cmd));
11330 }
11331 let key = "";
11332 for (const c of classified) {
11333 key += `${c.name}|${c.kind}|${c.url || ""}
11334 `;
11335 }
11336 if (key === this.lastFingerprint) {
11337 return;
11338 }
11339 this.lastFingerprint = key;
11340 unregisterByOwner(OWNER);
11341 for (const c of classified) {
11342 if (c.kind === "skip") {
11343 continue;
11344 }
11345 const slug = `global-${c.name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-")}`;
11346 const icon = this.iconFor(c);
11347 const def = {
11348 slug,
11349 label: c.label,
11350 icon,
11351 iconSvg: c.iconSvg && c.iconSvg !== "" ? sanitizeIconSvg(c.iconSvg) : void 0,
11352 owner: OWNER,
11353 // NOT eager. The palette splits the registry into two
11354 // disjoint surfaces: `eager` commands show on empty
11355 // input (and are excluded from slash search at
11356 // `src/ai-assistant/impl.ts:494`); non-eager commands
11357 // show when the user types `/<query>`. The WP baseline
11358 // is large (~150 entries) and meant to be searched —
11359 // surfacing it eagerly would drown the iframe-harvested
11360 // contextual shortcuts on every open. Slash-search is
11361 // the right surface for it, matching the native WP
11362 // palette UX (open, type, find).
11363 run: c.kind === "navigate" && c.url ? this.runNavigate(c.url, c.windowTitle || c.label, icon) : this.runInvoke(c.name, c.label, icon)
11364 };
11365 try {
11366 registerCommand(def);
11367 } catch (err) {
11368 console.error(
11369 "[desktop-mode] shell-harvester: dropping bad command",
11370 def,
11371 err
11372 );
11373 }
11374 }
11375 }
11376 classify(cmd) {
11377 const out = {
11378 name: String(cmd.name),
11379 label: String(cmd.label),
11380 icon: typeof cmd.icon === "string" ? cmd.icon : void 0,
11381 iconSvg: void 0,
11382 kind: "action",
11383 url: void 0,
11384 callback: typeof cmd.callback === "function" ? cmd.callback : void 0
11385 };
11386 const cached = this.kindCache[out.name];
11387 if (cached) {
11388 out.kind = cached.kind;
11389 out.url = cached.url;
11390 out.iconSvg = cached.iconSvg;
11391 return out;
11392 }
11393 if (cmd.icon && typeof cmd.icon !== "string") {
11394 out.iconSvg = this.renderIcon(cmd.icon);
11395 }
11396 const menuEntry = lookupMenuCommand(out.name);
11397 if (menuEntry) {
11398 try {
11399 out.url = new URL(menuEntry.url, this.adminUrl).toString();
11400 out.kind = "navigate";
11401 if (menuEntry.label !== "") {
11402 out.windowTitle = menuEntry.label;
11403 }
11404 } catch {
11405 out.kind = "skip";
11406 }
11407 this.kindCache[out.name] = {
11408 kind: out.kind,
11409 url: out.url,
11410 iconSvg: out.iconSvg
11411 };
11412 return out;
11413 }
11414 if (typeof cmd.callback === "function") {
11415 let src = "";
11416 try {
11417 src = Function.prototype.toString.call(cmd.callback);
11418 } catch {
11419 src = "";
11420 }
11421 const literal = src.match(NAV_HREF_LITERAL_RE) || src.match(NAV_ASSIGN_LITERAL_RE) || src.match(NAV_CALL_LITERAL_RE);
11422 if (literal && literal[1]) {
11423 try {
11424 out.url = new URL(literal[1], window.location.href).toString();
11425 out.kind = "navigate";
11426 } catch {
11427 out.kind = "action";
11428 }
11429 } else if (NAV_INTENT_RE.test(src)) {
11430 const isSiteEditorIntent = SITE_EDITOR_INTENT_RE.test(src);
11431 const nameMatch = isSiteEditorIntent ? out.name.match(SITE_EDITOR_NAME_RE) : null;
11432 if (nameMatch) {
11433 const entityType = nameMatch[1];
11434 const entityId = nameMatch[2];
11435 const p = `/${entityType}/${entityId}`;
11436 try {
11437 const siteEditor = new URL("site-editor.php", this.adminUrl);
11438 siteEditor.searchParams.set("p", p);
11439 siteEditor.searchParams.set("canvas", "edit");
11440 out.url = siteEditor.toString();
11441 out.kind = "navigate";
11442 } catch {
11443 out.kind = "skip";
11444 }
11445 } else {
11446 out.kind = "skip";
11447 }
11448 }
11449 }
11450 this.kindCache[out.name] = {
11451 kind: out.kind,
11452 url: out.url,
11453 iconSvg: out.iconSvg
11454 };
11455 return out;
11456 }
11457 renderIcon(icon) {
11458 const wp = window.wp;
11459 if (!wp || !wp.element || typeof wp.element.renderToString !== "function") {
11460 return "";
11461 }
11462 try {
11463 const rendered = wp.element.renderToString(icon);
11464 if (typeof rendered === "string" && rendered.toLowerCase().startsWith("<svg")) {
11465 return rendered;
11466 }
11467 } catch {
11468 }
11469 return "";
11470 }
11471 iconFor(c) {
11472 if (c.icon && c.icon.startsWith("dashicons-")) {
11473 return c.icon;
11474 }
11475 return c.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
11476 }
11477 runNavigate(url, title, icon) {
11478 return (_args, ctx) => {
11479 ctx.close();
11480 if (tryNativeUrlRemap(url)) {
11481 return;
11482 }
11483 const id = deriveWindowId(url, this.adminUrl);
11484 this.manager.open({ id, baseId: id, url, title, icon });
11485 };
11486 }
11487 runInvoke(name, title, icon) {
11488 return (_args, ctx) => {
11489 ctx.close();
11490 const cb = this.callbackCache[name];
11491 if (typeof cb !== "function") {
11492 return;
11493 }
11494 const captured = this.runWithNavCapture(cb);
11495 if (captured) {
11496 const id = deriveWindowId(captured, this.adminUrl);
11497 this.manager.open({ id, baseId: id, url: captured, title, icon });
11498 }
11499 };
11500 }
11501 /**
11502 * Invoke `cb` with navigation sinks (`document.location`,
11503 * `window.location`, `location.assign`, `location.replace`)
11504 * shadowed so any assignment is captured instead of navigating
11505 * the shell. Returns the captured URL or `null` if the callback
11506 * was a pure JS action.
11507 *
11508 * The shadow uses `Object.defineProperty` on the document /
11509 * window instance to override the prototype's accessor for the
11510 * duration of the call. `delete` afterwards unshadows so the
11511 * native setter is restored.
11512 */
11513 runWithNavCapture(cb) {
11514 let captured = null;
11515 const setCaptured = (v) => {
11516 if (captured === null && typeof v === "string" && v !== "") {
11517 captured = v;
11518 }
11519 };
11520 const realLocation = window.location;
11521 const locationProxy = new Proxy(realLocation, {
11522 get(target2, prop) {
11523 const value = target2[prop];
11524 if (prop === "assign" || prop === "replace") {
11525 return (url) => setCaptured(url);
11526 }
11527 if (typeof value === "function") {
11528 return value.bind(target2);
11529 }
11530 return value;
11531 },
11532 set(_target, prop, value) {
11533 if (prop === "href") {
11534 setCaptured(value);
11535 return true;
11536 }
11537 return true;
11538 }
11539 });
11540 const shadowed = [];
11541 const installShadow = (obj) => {
11542 try {
11543 Object.defineProperty(obj, "location", {
11544 configurable: true,
11545 get: () => locationProxy,
11546 set: (v) => setCaptured(v)
11547 });
11548 shadowed.push({ obj, key: "location" });
11549 } catch {
11550 }
11551 };
11552 installShadow(document);
11553 installShadow(window);
11554 try {
11555 cb({ close: () => {
11556 } });
11557 } catch {
11558 } finally {
11559 for (const s of shadowed) {
11560 try {
11561 delete s.obj[s.key];
11562 } catch {
11563 }
11564 }
11565 }
11566 return captured;
11567 }
11568 }
11569 const seed$2 = [];
11570 function register(def) {
11571 throwOnRegistrationErrors(
11572 "Widget",
11573 collectRegistrationErrors(def, WIDGET_CHECKS),
11574 def
11575 );
11576 const idx = seed$2.findIndex((w) => w.id === def.id);
11577 if (idx >= 0) {
11578 seed$2[idx] = def;
11579 } else {
11580 seed$2.push(def);
11581 }
11582 }
11583 function unregister(id) {
11584 const idx = seed$2.findIndex((w) => w.id === id);
11585 if (idx >= 0) {
11586 seed$2.splice(idx, 1);
11587 }
11588 }
11589 function all() {
11590 const copy = seed$2.slice();
11591 const filtered = applyFilters(HOOKS.WIDGETS, copy);
11592 if (!Array.isArray(filtered)) {
11593 if (typeof console !== "undefined") {
11594 console.warn(
11595 "[desktop-mode] `desktop-mode.widgets` filter returned a non-array; falling back to seed list."
11596 );
11597 }
11598 return copy;
11599 }
11600 return filtered.filter(isValidDef);
11601 }
11602 function get(id) {
11603 return all().find((w) => w.id === id);
11604 }
11605 const WIDGET_CHECKS = [
11606 {
11607 field: "id",
11608 message: "missing or not a non-empty string",
11609 valid: (d) => typeof d.id === "string" && d.id !== ""
11610 },
11611 {
11612 field: "label",
11613 message: "missing or not a non-empty string",
11614 valid: (d) => typeof d.label === "string" && d.label !== ""
11615 },
11616 {
11617 field: "description",
11618 message: "not a string",
11619 valid: (d) => typeof d.description === "string"
11620 },
11621 {
11622 field: "icon",
11623 message: "missing or not a non-empty string",
11624 valid: (d) => typeof d.icon === "string" && d.icon !== ""
11625 },
11626 {
11627 field: "mount",
11628 message: "not a function",
11629 valid: (d) => typeof d.mount === "function"
11630 }
11631 ];
11632 function isValidDef(def) {
11633 return collectRegistrationErrors(def, WIDGET_CHECKS).length === 0;
11634 }
11635 let active$2 = null;
11636 function openWidgetPicker(options) {
11637 if (active$2) {
11638 return;
11639 }
11640 const panel2 = document.createElement("div");
11641 panel2.className = "desktop-mode-widget-picker";
11642 panel2.setAttribute("role", "menu");
11643 panel2.setAttribute("aria-label", __("Add widget"));
11644 const title = document.createElement("div");
11645 title.className = "desktop-mode-widget-picker__title";
11646 title.textContent = __("Add widget");
11647 panel2.appendChild(title);
11648 const list2 = document.createElement("div");
11649 list2.className = "desktop-mode-widget-picker__list";
11650 panel2.appendChild(list2);
11651 paintList(list2, options);
11652 document.body.appendChild(panel2);
11653 positionPanel(panel2, options.anchor);
11654 const onOutsidePointerDown = (e) => {
11655 const target2 = e.target;
11656 if (!target2) {
11657 return;
11658 }
11659 if (panel2.contains(target2) || options.anchor.contains(target2)) {
11660 return;
11661 }
11662 closeWidgetPicker();
11663 };
11664 window.setTimeout(() => {
11665 document.addEventListener("pointerdown", onOutsidePointerDown, true);
11666 }, 0);
11667 const onKeyDown = (e) => {
11668 if (e.key === "Escape") {
11669 closeWidgetPicker();
11670 }
11671 };
11672 document.addEventListener("keydown", onKeyDown);
11673 active$2 = { panel: panel2, options, onOutsidePointerDown, onKeyDown };
11674 const first = list2.querySelector(
11675 "button:not([disabled])"
11676 );
11677 first?.focus();
11678 }
11679 function refreshWidgetPicker() {
11680 if (!active$2) {
11681 return;
11682 }
11683 const list2 = active$2.panel.querySelector(
11684 ".desktop-mode-widget-picker__list"
11685 );
11686 if (list2) {
11687 paintList(list2, active$2.options);
11688 }
11689 }
11690 function closeWidgetPicker() {
11691 if (!active$2) {
11692 return;
11693 }
11694 document.removeEventListener(
11695 "pointerdown",
11696 active$2.onOutsidePointerDown,
11697 true
11698 );
11699 document.removeEventListener("keydown", active$2.onKeyDown);
11700 active$2.panel.remove();
11701 active$2 = null;
11702 }
11703 function paintList(list2, options) {
11704 list2.innerHTML = "";
11705 const enabled = new Set(options.enabledIds());
11706 const defs = options.registry();
11707 if (defs.length === 0) {
11708 const empty = document.createElement("div");
11709 empty.className = "desktop-mode-widget-picker__empty";
11710 empty.textContent = __(
11711 "No widgets available. Activate a plugin that registers one, or see the docs for the registerWidget API."
11712 );
11713 list2.appendChild(empty);
11714 return;
11715 }
11716 for (const def of defs) {
11717 const entry = document.createElement("button");
11718 entry.type = "button";
11719 entry.className = "desktop-mode-widget-picker__entry";
11720 const isAdded = enabled.has(def.id);
11721 if (isAdded) {
11722 entry.classList.add(
11723 "desktop-mode-widget-picker__entry--added"
11724 );
11725 entry.disabled = true;
11726 entry.setAttribute("aria-disabled", "true");
11727 }
11728 entry.setAttribute("role", "menuitem");
11729 let ariaLabel;
11730 if (isAdded) {
11731 ariaLabel = sprintf(__("%s (already added)"), def.label);
11732 } else {
11733 ariaLabel = sprintf(__("Add %s"), def.label);
11734 }
11735 entry.setAttribute("aria-label", ariaLabel);
11736 const icon = document.createElement("span");
11737 icon.className = `desktop-mode-widget-picker__entry-icon dashicons ${def.icon}`;
11738 icon.setAttribute("aria-hidden", "true");
11739 entry.appendChild(icon);
11740 const textWrap = document.createElement("span");
11741 textWrap.className = "desktop-mode-widget-picker__entry-text";
11742 const label = document.createElement("span");
11743 label.className = "desktop-mode-widget-picker__entry-label";
11744 label.textContent = def.label;
11745 textWrap.appendChild(label);
11746 if (def.description) {
11747 const desc = document.createElement("span");
11748 desc.className = "desktop-mode-widget-picker__entry-description";
11749 desc.textContent = def.description;
11750 textWrap.appendChild(desc);
11751 }
11752 entry.appendChild(textWrap);
11753 if (isAdded) {
11754 const status = document.createElement("span");
11755 status.className = "desktop-mode-widget-picker__entry-status";
11756 status.textContent = __("Added");
11757 entry.appendChild(status);
11758 }
11759 if (!isAdded) {
11760 entry.addEventListener("click", (e) => {
11761 e.preventDefault();
11762 e.stopPropagation();
11763 options.onAdd(def.id);
11764 });
11765 }
11766 list2.appendChild(entry);
11767 }
11768 }
11769 function positionPanel(panel2, anchor) {
11770 const rect = anchor.getBoundingClientRect();
11771 panel2.style.position = "fixed";
11772 panel2.style.left = "0px";
11773 panel2.style.top = "0px";
11774 panel2.style.visibility = "hidden";
11775 const panelRect = panel2.getBoundingClientRect();
11776 const width = panelRect.width || 320;
11777 const height = panelRect.height || 200;
11778 const gap = 6;
11779 let left = rect.right - width;
11780 let top = rect.top - height - gap;
11781 if (left < 8) {
11782 left = 8;
11783 }
11784 if (top < 8) {
11785 top = rect.bottom + gap;
11786 }
11787 panel2.style.left = `${Math.round(left)}px`;
11788 panel2.style.top = `${Math.round(top)}px`;
11789 panel2.style.visibility = "";
11790 }
11791 const FLOATING_CLASS = "desktop-mode-widgets__card--floating";
11792 const MOVABLE_CLASS = "desktop-mode-widgets__card--movable";
11793 const RESIZABLE_CLASS = "desktop-mode-widgets__card--resizable";
11794 const DRAGGING_CLASS = "desktop-mode-widgets__card--dragging";
11795 const RESIZING_CLASS = "desktop-mode-widgets__card--resizing";
11796 const DEFAULT_MIN_WIDTH = 160;
11797 const DEFAULT_MIN_HEIGHT = 80;
11798 const DEFAULT_WIDTH$1 = 280;
11799 const DEFAULT_HEIGHT$1 = 180;
11800 const VIEWPORT_MARGIN = 20;
11801 const DRAG_THRESHOLD_PX$1 = 5;
11802 const DRAG_THRESHOLD_SQUARED = DRAG_THRESHOLD_PX$1 * DRAG_THRESHOLD_PX$1;
11803 const DRAG_EXCLUDED_SELECTORS = 'input, textarea, select, button, a, [contenteditable="true"]';
11804 function buildFrame(def, ctx, handlers) {
11805 const card = document.createElement("div");
11806 card.className = "desktop-mode-widgets__card";
11807 card.dataset.widgetId = def.id;
11808 const movable = def.movable === true;
11809 const resizable = def.resizable === true;
11810 if (movable) {
11811 card.classList.add(MOVABLE_CLASS);
11812 }
11813 if (resizable) {
11814 card.classList.add(RESIZABLE_CLASS);
11815 }
11816 if (movable) {
11817 card.appendChild(buildChrome(def, handlers.onRemove, handlers.onRedock));
11818 } else {
11819 card.appendChild(buildCornerClose(def, handlers.onRemove));
11820 }
11821 const body = document.createElement("div");
11822 body.className = "desktop-mode-widgets__card-body";
11823 card.appendChild(body);
11824 let isFloating = false;
11825 if (ctx.geometry) {
11826 applyGeometry(card, ctx.geometry);
11827 card.classList.add(FLOATING_CLASS);
11828 isFloating = true;
11829 }
11830 const resizeCleanups = [];
11831 if (resizable) {
11832 for (const dir of allHandleDirs()) {
11833 const handle = document.createElement("div");
11834 handle.className = `desktop-mode-widgets__resize desktop-mode-widgets__resize--${dir}`;
11835 handle.setAttribute("aria-hidden", "true");
11836 handle.dataset.dir = dir;
11837 card.appendChild(handle);
11838 resizeCleanups.push(
11839 attachResize(card, handle, dir, def, ctx, handlers, () => isFloating)
11840 );
11841 }
11842 }
11843 let dragCleanup = null;
11844 if (movable) {
11845 const chrome = card.querySelector(
11846 ".desktop-mode-widgets__chrome"
11847 );
11848 if (chrome) {
11849 dragCleanup = attachDrag(card, chrome, def, ctx, handlers, (next) => {
11850 isFloating = next;
11851 });
11852 }
11853 }
11854 return {
11855 card,
11856 body,
11857 dispose: () => {
11858 for (const fn of resizeCleanups) {
11859 try {
11860 fn();
11861 } catch {
11862 }
11863 }
11864 if (dragCleanup) {
11865 try {
11866 dragCleanup();
11867 } catch {
11868 }
11869 }
11870 card.remove();
11871 }
11872 };
11873 }
11874 function buildChrome(def, onRemove, onRedock) {
11875 const chrome = document.createElement("header");
11876 chrome.className = "desktop-mode-widgets__chrome";
11877 const grip = document.createElement("span");
11878 grip.className = "desktop-mode-widgets__grip";
11879 grip.setAttribute("aria-hidden", "true");
11880 chrome.appendChild(grip);
11881 const title = document.createElement("span");
11882 title.className = "desktop-mode-widgets__title";
11883 title.textContent = def.label;
11884 chrome.appendChild(title);
11885 chrome.appendChild(buildRedockButton(def, onRedock));
11886 const close = buildCloseButton(def, onRemove);
11887 chrome.appendChild(close);
11888 return chrome;
11889 }
11890 function buildRedockButton(def, onRedock) {
11891 const btn = document.createElement("button");
11892 btn.type = "button";
11893 btn.className = "desktop-mode-widgets__card-redock";
11894 btn.setAttribute(
11895 "aria-label",
11896 // translators: %s is the widget label (e.g., "Clock")
11897 sprintf(__("Dock %s back to widget column"), def.label)
11898 );
11899 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>';
11900 btn.addEventListener("click", (e) => {
11901 e.preventDefault();
11902 e.stopPropagation();
11903 onRedock();
11904 });
11905 btn.dataset.noDrag = "true";
11906 return btn;
11907 }
11908 function buildCornerClose(def, onRemove) {
11909 const close = buildCloseButton(def, onRemove);
11910 close.classList.add("desktop-mode-widgets__card-close--corner");
11911 return close;
11912 }
11913 function buildCloseButton(def, onRemove) {
11914 const close = document.createElement("button");
11915 close.type = "button";
11916 close.className = "desktop-mode-widgets__card-close";
11917 close.setAttribute("aria-label", sprintf(__("Remove %s"), def.label));
11918 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>';
11919 close.addEventListener("click", (e) => {
11920 e.preventDefault();
11921 e.stopPropagation();
11922 onRemove();
11923 });
11924 return close;
11925 }
11926 function attachDrag(card, chrome, def, ctx, handlers, setFloating) {
11927 let pointerId = null;
11928 let startX = 0;
11929 let startY = 0;
11930 let initialLeft = 0;
11931 let initialTop = 0;
11932 let committed = false;
11933 const onDown = (e) => {
11934 if (e.button !== 0) {
11935 return;
11936 }
11937 const target2 = e.target;
11938 if (target2 && target2.closest(DRAG_EXCLUDED_SELECTORS)) {
11939 return;
11940 }
11941 e.preventDefault();
11942 pointerId = e.pointerId;
11943 startX = e.clientX;
11944 startY = e.clientY;
11945 committed = false;
11946 initialLeft = parseFloat(card.style.left) || 0;
11947 initialTop = parseFloat(card.style.top) || 0;
11948 chrome.setPointerCapture(pointerId);
11949 };
11950 const commitDrag = () => {
11951 if (!card.classList.contains(FLOATING_CLASS)) {
11952 const parentRect = ctx.floatingParent.getBoundingClientRect();
11953 const rect = card.getBoundingClientRect();
11954 const initial = {
11955 x: rect.left - parentRect.left,
11956 y: rect.top - parentRect.top,
11957 width: rect.width || def.defaultWidth || DEFAULT_WIDTH$1,
11958 height: rect.height || def.defaultHeight || DEFAULT_HEIGHT$1
11959 };
11960 applyGeometry(card, initial);
11961 card.classList.add(FLOATING_CLASS);
11962 setFloating(true);
11963 handlers.onLiberate(initial);
11964 initialLeft = parseFloat(card.style.left) || 0;
11965 initialTop = parseFloat(card.style.top) || 0;
11966 }
11967 card.classList.add(DRAGGING_CLASS);
11968 };
11969 const onMove = (e) => {
11970 if (pointerId === null || e.pointerId !== pointerId) {
11971 return;
11972 }
11973 const dx = e.clientX - startX;
11974 const dy = e.clientY - startY;
11975 if (!committed) {
11976 if (dx * dx + dy * dy < DRAG_THRESHOLD_SQUARED) {
11977 return;
11978 }
11979 committed = true;
11980 commitDrag();
11981 }
11982 const clamped = clampToParent(
11983 initialLeft + dx,
11984 initialTop + dy,
11985 card.offsetWidth,
11986 card.offsetHeight,
11987 ctx.floatingParent
11988 );
11989 card.style.left = `${clamped.x}px`;
11990 card.style.top = `${clamped.y}px`;
11991 };
11992 const onUp = (e) => {
11993 if (pointerId === null || e.pointerId !== pointerId) {
11994 return;
11995 }
11996 try {
11997 chrome.releasePointerCapture(pointerId);
11998 } catch {
11999 }
12000 pointerId = null;
12001 if (!committed) {
12002 return;
12003 }
12004 committed = false;
12005 card.classList.remove(DRAGGING_CLASS);
12006 handlers.onGeometryChanged(currentGeometry(card));
12007 };
12008 chrome.addEventListener("pointerdown", onDown);
12009 chrome.addEventListener("pointermove", onMove);
12010 chrome.addEventListener("pointerup", onUp);
12011 chrome.addEventListener("pointercancel", onUp);
12012 return () => {
12013 chrome.removeEventListener("pointerdown", onDown);
12014 chrome.removeEventListener("pointermove", onMove);
12015 chrome.removeEventListener("pointerup", onUp);
12016 chrome.removeEventListener("pointercancel", onUp);
12017 };
12018 }
12019 function attachResize(card, handle, dir, def, ctx, handlers, isFloating) {
12020 let pointerId = null;
12021 let startX = 0;
12022 let startY = 0;
12023 let startLeft = 0;
12024 let startTop = 0;
12025 let startW = 0;
12026 let startH = 0;
12027 const onDown = (e) => {
12028 if (e.button !== 0) {
12029 return;
12030 }
12031 if (!isFloating() && !isHeightOnlyDir(dir)) {
12032 return;
12033 }
12034 e.preventDefault();
12035 e.stopPropagation();
12036 pointerId = e.pointerId;
12037 startX = e.clientX;
12038 startY = e.clientY;
12039 const rect = card.getBoundingClientRect();
12040 const parentRect = ctx.floatingParent.getBoundingClientRect();
12041 startLeft = rect.left - parentRect.left;
12042 startTop = rect.top - parentRect.top;
12043 startW = rect.width;
12044 startH = rect.height;
12045 handle.setPointerCapture(pointerId);
12046 card.classList.add(RESIZING_CLASS);
12047 };
12048 const onMove = (e) => {
12049 if (pointerId === null || e.pointerId !== pointerId) {
12050 return;
12051 }
12052 const dx = e.clientX - startX;
12053 const dy = e.clientY - startY;
12054 const next = computeResize(
12055 dir,
12056 dx,
12057 dy,
12058 startLeft,
12059 startTop,
12060 startW,
12061 startH,
12062 def,
12063 ctx.floatingParent,
12064 isFloating()
12065 );
12066 if (isFloating()) {
12067 card.style.left = `${next.x}px`;
12068 card.style.top = `${next.y}px`;
12069 card.style.width = `${next.width}px`;
12070 }
12071 card.style.height = `${next.height}px`;
12072 };
12073 const onUp = (e) => {
12074 if (pointerId === null || e.pointerId !== pointerId) {
12075 return;
12076 }
12077 try {
12078 handle.releasePointerCapture(pointerId);
12079 } catch {
12080 }
12081 pointerId = null;
12082 card.classList.remove(RESIZING_CLASS);
12083 handlers.onGeometryChanged(currentGeometry(card));
12084 };
12085 handle.addEventListener("pointerdown", onDown);
12086 handle.addEventListener("pointermove", onMove);
12087 handle.addEventListener("pointerup", onUp);
12088 handle.addEventListener("pointercancel", onUp);
12089 return () => {
12090 handle.removeEventListener("pointerdown", onDown);
12091 handle.removeEventListener("pointermove", onMove);
12092 handle.removeEventListener("pointerup", onUp);
12093 handle.removeEventListener("pointercancel", onUp);
12094 };
12095 }
12096 function allHandleDirs() {
12097 return ["n", "e", "s", "w", "ne", "nw", "se", "sw"];
12098 }
12099 function isHeightOnlyDir(dir) {
12100 return dir === "s";
12101 }
12102 function applyGeometry(card, geometry) {
12103 card.style.left = `${geometry.x}px`;
12104 card.style.top = `${geometry.y}px`;
12105 card.style.width = `${geometry.width}px`;
12106 card.style.height = `${geometry.height}px`;
12107 }
12108 function currentGeometry(card) {
12109 return {
12110 x: parseFloat(card.style.left) || 0,
12111 y: parseFloat(card.style.top) || 0,
12112 width: card.offsetWidth,
12113 height: card.offsetHeight
12114 };
12115 }
12116 function clampToParent(x, y, width, height, parent) {
12117 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
12118 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
12119 const maxX = Math.max(0, parentWidth - width - VIEWPORT_MARGIN);
12120 const maxY = Math.max(0, parentHeight - height - VIEWPORT_MARGIN);
12121 return {
12122 x: Math.min(Math.max(VIEWPORT_MARGIN, x), maxX),
12123 y: Math.min(Math.max(VIEWPORT_MARGIN, y), maxY)
12124 };
12125 }
12126 function computeResize(dir, dx, dy, startLeft, startTop, startW, startH, def, parent, floating) {
12127 const minW = def.minWidth ?? DEFAULT_MIN_WIDTH;
12128 const minH = def.minHeight ?? DEFAULT_MIN_HEIGHT;
12129 const maxW = def.maxWidth ?? Infinity;
12130 const maxH = def.maxHeight ?? Infinity;
12131 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
12132 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
12133 let x = startLeft;
12134 let y = startTop;
12135 let width = startW;
12136 let height = startH;
12137 if (dir === "e" || dir === "ne" || dir === "se") {
12138 width = clamp$1(startW + dx, minW, Math.min(maxW, parentWidth - startLeft));
12139 }
12140 if (dir === "w" || dir === "nw" || dir === "sw") {
12141 const nextWidth = clamp$1(startW - dx, minW, Math.min(maxW, startLeft + startW));
12142 x = startLeft + (startW - nextWidth);
12143 width = nextWidth;
12144 }
12145 if (dir === "s" || dir === "se" || dir === "sw") {
12146 height = clamp$1(
12147 startH + dy,
12148 minH,
12149 Math.min(maxH, parentHeight - startTop)
12150 );
12151 }
12152 if (dir === "n" || dir === "ne" || dir === "nw") {
12153 const nextHeight = clamp$1(startH - dy, minH, Math.min(maxH, startTop + startH));
12154 y = startTop + (startH - nextHeight);
12155 height = nextHeight;
12156 }
12157 if (!floating) {
12158 width = startW;
12159 x = startLeft;
12160 }
12161 return { x, y, width, height };
12162 }
12163 function clamp$1(value, min, max) {
12164 if (max < min) {
12165 return min;
12166 }
12167 return Math.min(Math.max(value, min), max);
12168 }
12169 const IDS_KEY = "desktop-mode-widgets";
12170 const GEOMETRY_KEY$1 = "desktop-mode-widgets-geometry";
12171 function readRawEnabled() {
12172 try {
12173 return window.localStorage.getItem(IDS_KEY);
12174 } catch {
12175 return null;
12176 }
12177 }
12178 function loadEnabledIds() {
12179 const raw = readRawEnabled();
12180 if (raw === null) {
12181 return [];
12182 }
12183 try {
12184 const parsed = JSON.parse(raw);
12185 if (!Array.isArray(parsed)) {
12186 return [];
12187 }
12188 return parsed.filter((x) => typeof x === "string");
12189 } catch {
12190 return [];
12191 }
12192 }
12193 function saveEnabledIds(ids) {
12194 try {
12195 window.localStorage.setItem(IDS_KEY, JSON.stringify(ids));
12196 } catch {
12197 }
12198 }
12199 function loadGeometry$1() {
12200 try {
12201 const raw = window.localStorage.getItem(GEOMETRY_KEY$1);
12202 if (!raw) {
12203 return {};
12204 }
12205 const parsed = JSON.parse(raw);
12206 if (!parsed || typeof parsed !== "object") {
12207 return {};
12208 }
12209 const out = {};
12210 for (const [id, rawEntry] of Object.entries(parsed)) {
12211 const entry = sanitizeGeometry(rawEntry);
12212 if (entry) {
12213 out[id] = entry;
12214 }
12215 }
12216 return out;
12217 } catch {
12218 return {};
12219 }
12220 }
12221 function saveGeometry$1(geometry) {
12222 try {
12223 window.localStorage.setItem(GEOMETRY_KEY$1, JSON.stringify(geometry));
12224 } catch {
12225 }
12226 }
12227 function sanitizeGeometry(raw) {
12228 if (!raw || typeof raw !== "object") {
12229 return null;
12230 }
12231 const { x, y, width, height } = raw;
12232 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) {
12233 return null;
12234 }
12235 return { x, y, width, height };
12236 }
12237 function createWidgetStorage(widgetId) {
12238 const prefix = `desktop-mode.widget.${widgetId}.`;
12239 const safeGet = (key) => {
12240 try {
12241 return localStorage.getItem(prefix + key);
12242 } catch {
12243 return null;
12244 }
12245 };
12246 return {
12247 get(key) {
12248 const raw = safeGet(key);
12249 if (raw === null) {
12250 return null;
12251 }
12252 try {
12253 return JSON.parse(raw);
12254 } catch {
12255 return null;
12256 }
12257 },
12258 set(key, value) {
12259 try {
12260 localStorage.setItem(prefix + key, JSON.stringify(value));
12261 } catch {
12262 }
12263 },
12264 remove(key) {
12265 try {
12266 localStorage.removeItem(prefix + key);
12267 } catch {
12268 }
12269 },
12270 clear() {
12271 try {
12272 for (let i = localStorage.length - 1; i >= 0; i--) {
12273 const key = localStorage.key(i);
12274 if (key && key.startsWith(prefix)) {
12275 localStorage.removeItem(key);
12276 }
12277 }
12278 } catch {
12279 }
12280 }
12281 };
12282 }
12283 const DEFAULT_ENABLED_IDS = ["clock"];
12284 class WidgetLayer {
12285 /**
12286 * @param root The column element (`#desktop-mode-widgets`).
12287 * @param pluginUrl Absolute plugin URL — passed to widget ctx.
12288 * @param floatingHost Parent for liberated (floating) widgets.
12289 * Defaults to the column's parent (the desktop
12290 * area) so floats are bounded by the visible
12291 * desktop, not the 320 px-wide column.
12292 */
12293 constructor(root, pluginUrl, floatingHost) {
12294 this.mounted = /* @__PURE__ */ new Map();
12295 this.generation = 0;
12296 this.root = root;
12297 this.pluginUrl = pluginUrl;
12298 this.enabledIds = loadEnabledIds();
12299 this.geometry = loadGeometry$1();
12300 this.floatingHost = floatingHost ?? root.parentElement ?? root;
12301 this.listEl = document.createElement("div");
12302 this.listEl.className = "desktop-mode-widgets__list";
12303 this.root.appendChild(this.listEl);
12304 this.addTile = this.buildAddTile();
12305 this.root.appendChild(this.addTile);
12306 this.paintEmptyState();
12307 }
12308 /**
12309 * Mount every widget the user has enabled (per localStorage).
12310 * Called once during shell boot, AFTER the registry seed has run
12311 * so built-ins are available. Safe to call multiple times — the
12312 * `mounted` map dedupes.
12313 */
12314 hydrate() {
12315 if (readRawEnabled() === null) {
12316 this.enabledIds = DEFAULT_ENABLED_IDS.filter(
12317 (id) => !!get(id)
12318 );
12319 saveEnabledIds(this.enabledIds);
12320 }
12321 for (const id of this.enabledIds) {
12322 if (this.mounted.has(id)) {
12323 continue;
12324 }
12325 this.mountById(id);
12326 }
12327 this.paintEmptyState();
12328 }
12329 /**
12330 * Add a widget by id — called by the picker after the user
12331 * selects an available entry. Idempotent.
12332 */
12333 add(id) {
12334 if (this.enabledIds.includes(id)) {
12335 return;
12336 }
12337 if (!get(id)) {
12338 return;
12339 }
12340 this.enabledIds.push(id);
12341 saveEnabledIds(this.enabledIds);
12342 this.mountById(id);
12343 this.paintEmptyState();
12344 doAction(HOOKS.WIDGET_ADDED, { id });
12345 refreshWidgetPicker();
12346 }
12347 /**
12348 * Remove a widget by id — called from the card's × button and
12349 * from the picker. Idempotent.
12350 */
12351 remove(id) {
12352 const before = this.enabledIds.length;
12353 this.enabledIds = this.enabledIds.filter((e) => e !== id);
12354 if (this.enabledIds.length === before) {
12355 return;
12356 }
12357 saveEnabledIds(this.enabledIds);
12358 if (this.geometry[id]) {
12359 delete this.geometry[id];
12360 saveGeometry$1(this.geometry);
12361 }
12362 this.unmountById(id);
12363 this.paintEmptyState();
12364 doAction(HOOKS.WIDGET_REMOVED, { id });
12365 refreshWidgetPicker();
12366 }
12367 /** Public read for the picker / external callers. */
12368 getEnabledIds() {
12369 return [...this.enabledIds];
12370 }
12371 /**
12372 * Mount a widget ONLY if it's already in the user's enabled
12373 * list AND not currently mounted. No-op when the widget isn't
12374 * enabled (user never opted in) and no-op when it's already on
12375 * screen. Used by the server-driven sync: when a plugin
12376 * activates mid-session, its widget def registers via the
12377 * sync's path; if the user had previously enabled that widget
12378 * (in a prior session or before the plugin was deactivated),
12379 * we want to bring it back on screen without toggling the
12380 * "enabled" state or firing a `WIDGET_ADDED` action.
12381 *
12382 * The net behaviour is "rehydrate this one widget now that
12383 * its def is finally registered," which is subtly different
12384 * from `ensureMounted` (which OPT-INs the user into enabling
12385 * the widget for the first time).
12386 */
12387 mountIfEnabled(id) {
12388 if (!get(id)) {
12389 return;
12390 }
12391 if (!this.enabledIds.includes(id)) {
12392 return;
12393 }
12394 if (this.mounted.has(id)) {
12395 return;
12396 }
12397 this.mountById(id);
12398 this.paintEmptyState();
12399 }
12400 /**
12401 * Unmount a widget without touching the persisted enablement.
12402 * Used by the server-driven widget-registry sync: when a plugin
12403 * deactivates mid-session, its widget defs disappear from the
12404 * registry and we need to pull any mounted instance off the
12405 * screen — but we deliberately KEEP the id in the user's
12406 * enabled list so re-activating the plugin re-mounts it
12407 * automatically through `hydrate()`.
12408 *
12409 * Idempotent; a no-op when the widget isn't currently mounted.
12410 */
12411 unmount(id) {
12412 if (!this.mounted.has(id)) {
12413 return;
12414 }
12415 this.unmountById(id);
12416 this.paintEmptyState();
12417 }
12418 /**
12419 * Guarantee the widget identified by `id` is currently mounted,
12420 * adding it to the enabled list if it isn't. No-op when the
12421 * widget is already on screen. Intended for companion plugins
12422 * that want to pin their widget programmatically — a monitor
12423 * plugin that auto-pins itself on the first error burst, a
12424 * first-run onboarding flow that ensures the quick-start widget
12425 * is present, etc.
12426 *
12427 * Returns `true` when the widget is mounted (either newly added
12428 * or already present), `false` when the id isn't registered —
12429 * callers can branch on the failure without having to maintain
12430 * their own registry snapshot.
12431 */
12432 ensureMounted(id) {
12433 if (!get(id)) {
12434 return false;
12435 }
12436 if (this.enabledIds.includes(id)) {
12437 return true;
12438 }
12439 this.add(id);
12440 return true;
12441 }
12442 /**
12443 * Tear down every widget. Called on shell unload via `pagehide`
12444 * so intervals / RAF loops stop before the beacon flush.
12445 */
12446 disposeAll() {
12447 for (const id of Array.from(this.mounted.keys())) {
12448 this.unmountById(id);
12449 }
12450 }
12451 // --- Internal ---------------------------------------------------
12452 mountById(id) {
12453 const def = get(id);
12454 if (!def) {
12455 return;
12456 }
12457 const gen = ++this.generation;
12458 const initialGeometry = def.movable === true ? this.geometry[id] : void 0;
12459 const frame = buildFrame(
12460 def,
12461 { floatingParent: this.floatingHost, geometry: initialGeometry },
12462 {
12463 onRemove: () => this.remove(id),
12464 onGeometryChanged: (geom) => this.persistGeometry(id, geom),
12465 onLiberate: (geom) => this.liberate(id, geom),
12466 onRedock: () => this.redock(id)
12467 }
12468 );
12469 const floating = !!initialGeometry;
12470 const record = {
12471 id,
12472 frame,
12473 generation: gen,
12474 teardown: null,
12475 floating
12476 };
12477 this.mounted.set(id, record);
12478 this.placeCard(frame.card, floating);
12479 const ctx = {
12480 id,
12481 pluginUrl: this.pluginUrl,
12482 storage: createWidgetStorage(id)
12483 };
12484 doAction(HOOKS.WIDGET_MOUNTING, { id, container: frame.body, ctx });
12485 const onResolve = (teardown) => {
12486 const current = this.mounted.get(id);
12487 if (!current || current.generation !== gen) {
12488 try {
12489 teardown();
12490 } catch {
12491 }
12492 return;
12493 }
12494 current.teardown = teardown;
12495 doAction(HOOKS.WIDGET_MOUNTED, { id, container: frame.body, ctx });
12496 };
12497 let result;
12498 try {
12499 result = def.mount(frame.body, ctx);
12500 } catch (err) {
12501 this.handleMountFailure(id, err);
12502 return;
12503 }
12504 if (isThenable(result)) {
12505 result.then(onResolve, (err) => {
12506 if (this.mounted.get(id)?.generation === gen) {
12507 this.handleMountFailure(id, err);
12508 }
12509 });
12510 return;
12511 }
12512 onResolve(result);
12513 }
12514 unmountById(id) {
12515 const record = this.mounted.get(id);
12516 if (!record) {
12517 return;
12518 }
12519 doAction(HOOKS.WIDGET_UNMOUNTING, { id });
12520 try {
12521 record.teardown?.();
12522 } catch (err) {
12523 doAction(HOOKS.SHELL_ERROR, { scope: "widget-teardown", id, error: err });
12524 if (typeof console !== "undefined") {
12525 console.error(
12526 `[desktop-mode] Widget "${id}" teardown threw:`,
12527 err
12528 );
12529 }
12530 }
12531 this.generation++;
12532 record.frame.dispose();
12533 this.mounted.delete(id);
12534 }
12535 handleMountFailure(id, err) {
12536 const record = this.mounted.get(id);
12537 if (record) {
12538 record.frame.dispose();
12539 this.mounted.delete(id);
12540 }
12541 doAction(HOOKS.WIDGET_MOUNT_FAILED, { id, error: err });
12542 doAction(HOOKS.SHELL_ERROR, { scope: "widget-mount", id, error: err });
12543 if (typeof console !== "undefined") {
12544 console.error(
12545 `[desktop-mode] Widget "${id}" failed to mount:`,
12546 err
12547 );
12548 }
12549 }
12550 buildAddTile() {
12551 const tile2 = document.createElement("button");
12552 tile2.type = "button";
12553 tile2.className = "desktop-mode-widgets__add";
12554 tile2.setAttribute("aria-label", __("Add widget"));
12555 const plus = document.createElement("span");
12556 plus.className = "desktop-mode-widgets__add-plus";
12557 plus.setAttribute("aria-hidden", "true");
12558 plus.textContent = "+";
12559 const label = document.createElement("span");
12560 label.className = "desktop-mode-widgets__add-label";
12561 label.textContent = __("Add widget");
12562 tile2.appendChild(plus);
12563 tile2.appendChild(label);
12564 tile2.addEventListener("click", (e) => {
12565 e.preventDefault();
12566 e.stopPropagation();
12567 openWidgetPicker({
12568 anchor: tile2,
12569 registry: () => all(),
12570 enabledIds: () => [...this.enabledIds],
12571 onAdd: (id) => this.add(id)
12572 });
12573 });
12574 return tile2;
12575 }
12576 /**
12577 * Drop a card into the right parent based on its floating state.
12578 * Docked cards append to the column list above the `+` tile;
12579 * floating cards append to the desktop-area-level host so they
12580 * sit above the wallpaper and can range across the viewport.
12581 */
12582 placeCard(card, floating) {
12583 if (floating) {
12584 this.floatingHost.appendChild(card);
12585 } else {
12586 this.listEl.appendChild(card);
12587 }
12588 }
12589 /**
12590 * Move a widget from the column into the floating host. Called by
12591 * the frame on the user's first drag of a movable widget.
12592 */
12593 liberate(id, geometry) {
12594 const record = this.mounted.get(id);
12595 if (!record || record.floating) {
12596 return;
12597 }
12598 record.floating = true;
12599 this.floatingHost.appendChild(record.frame.card);
12600 applyGeometry(record.frame.card, geometry);
12601 this.persistGeometry(id, geometry);
12602 this.paintEmptyState();
12603 }
12604 /**
12605 * Inverse of {@link liberate}: move a floating card back into
12606 * the column and drop its persisted geometry so a subsequent
12607 * shell boot brings it up docked. Called when the user clicks
12608 * the re-dock button in the card's chrome header, or
12609 * programmatically by companion plugins via
12610 * `wp.desktop.widgets.redock( id )` /
12611 * `wp.desktop.widgetLayer.redock( id )`.
12612 *
12613 * Idempotent — a docked widget silently no-ops, an unknown id
12614 * silently no-ops. The `--floating` class on the card is
12615 * removed as part of the same write so CSS rules that depend
12616 * on it (re-dock button visibility, absolute positioning) flip
12617 * back in one paint.
12618 *
12619 * @since 0.7.0 (private)
12620 * @since 0.25.0 (public)
12621 */
12622 redock(id) {
12623 const record = this.mounted.get(id);
12624 if (!record || !record.floating) {
12625 return;
12626 }
12627 record.floating = false;
12628 if (this.geometry[id]) {
12629 delete this.geometry[id];
12630 saveGeometry$1(this.geometry);
12631 }
12632 const card = record.frame.card;
12633 card.classList.remove("desktop-mode-widgets__card--floating");
12634 card.style.left = "";
12635 card.style.top = "";
12636 card.style.width = "";
12637 card.style.height = "";
12638 this.listEl.appendChild(card);
12639 this.paintEmptyState();
12640 }
12641 persistGeometry(id, geometry) {
12642 this.geometry[id] = geometry;
12643 saveGeometry$1(this.geometry);
12644 }
12645 /**
12646 * Toggle a `--has-widgets` modifier so CSS can hide the column's
12647 * decorative backdrop when nothing's mounted (keeps the empty
12648 * state clean — just the `+` tile floating in the corner).
12649 *
12650 * Floating widgets don't count toward "has widgets" in the column
12651 * sense — if every enabled widget is floating, the column itself
12652 * shows only the empty state + add tile.
12653 */
12654 paintEmptyState() {
12655 let docked = 0;
12656 for (const record of this.mounted.values()) {
12657 if (!record.floating) {
12658 docked++;
12659 }
12660 }
12661 this.root.classList.toggle(
12662 "desktop-mode-widgets--has-widgets",
12663 docked > 0
12664 );
12665 }
12666 }
12667 function isThenable(x) {
12668 return !!x && (typeof x === "object" || typeof x === "function") && typeof x.then === "function";
12669 }
12670 const DEFAULT_NATIVE_MIN_WIDTH = 280;
12671 const DEFAULT_NATIVE_MIN_HEIGHT = 220;
12672 const DEFAULT_NATIVE_WIDTH = 520;
12673 const DEFAULT_NATIVE_HEIGHT = 400;
12674 function buildIframeContentRender(cfg, cleanups, windowId) {
12675 return (body) => {
12676 const iframe = document.createElement("iframe");
12677 iframe.style.width = "100%";
12678 iframe.style.height = "100%";
12679 iframe.style.border = "0";
12680 iframe.setAttribute("src", cfg.url);
12681 if (typeof cfg.sandbox === "string" && cfg.sandbox !== "") {
12682 iframe.setAttribute("sandbox", cfg.sandbox);
12683 }
12684 body.style.padding = "0";
12685 body.appendChild(iframe);
12686 const unregisterSynth = registerSyntheticIframe(windowId, iframe);
12687 cleanups.push(unregisterSynth);
12688 let targetOrigin;
12689 try {
12690 targetOrigin = new URL(cfg.url, window.location.origin).origin;
12691 } catch {
12692 targetOrigin = window.location.origin;
12693 }
12694 let resolveReady = null;
12695 const readyPromise = new Promise((resolve2) => {
12696 resolveReady = resolve2;
12697 });
12698 const onLoad = () => {
12699 if (cfg.bridge) {
12700 try {
12701 const doc = iframe.contentDocument;
12702 if (doc && !doc.querySelector("script[data-desktop-mode-iframe-bridge]")) {
12703 const bridgeUrl = window.desktopModeConfig?.iframeBridgeUrl;
12704 if (bridgeUrl) {
12705 const s = doc.createElement("script");
12706 s.src = bridgeUrl;
12707 s.setAttribute("data-desktop-mode-iframe-bridge", "1");
12708 doc.head?.appendChild(s);
12709 }
12710 }
12711 } catch {
12712 }
12713 }
12714 markWindowContentReady(windowId);
12715 resolveReady?.();
12716 };
12717 iframe.addEventListener("load", onLoad);
12718 const onMessage = (e) => {
12719 if (!iframe.contentWindow || e.source !== iframe.contentWindow) {
12720 return;
12721 }
12722 if (e.origin !== targetOrigin && e.origin !== window.location.origin) {
12723 return;
12724 }
12725 const data = e.data;
12726 if (data && typeof data === "object" && typeof data.type === "string" && data.type.startsWith("desktop-mode-bridge-")) {
12727 const bridgeRouter = window.__desktopModeConnectionBridge;
12728 bridgeRouter?.routeIncomingFromIframe(data, windowId);
12729 }
12730 if (data && typeof data === "object" && data.type === "desktop-mode-window-publish" && typeof data.channel === "string" && data.channel !== "") {
12731 dispatchFromWindow(
12732 windowId,
12733 data.channel,
12734 data.payload
12735 );
12736 }
12737 try {
12738 cfg.onMessage?.(e.data);
12739 } catch (err) {
12740 if (typeof console !== "undefined") {
12741 console.error(
12742 "[desktop-mode] iframeContent.onMessage threw:",
12743 err
12744 );
12745 }
12746 }
12747 };
12748 window.addEventListener("message", onMessage);
12749 cleanups.push(() => {
12750 window.removeEventListener("message", onMessage);
12751 iframe.removeEventListener("load", onLoad);
12752 });
12753 return readyPromise;
12754 };
12755 }
12756 function createRegisterWindow(manager) {
12757 return async (def) => {
12758 const userRender = def.render;
12759 let render2 = userRender;
12760 const cleanups = [];
12761 if (def.iframeContent) {
12762 if (userRender && typeof console !== "undefined") {
12763 console.warn(
12764 "[desktop-mode] registerWindow: both `render` and `iframeContent` provided — ignoring `render` and using the iframe shorthand. Drop one."
12765 );
12766 }
12767 render2 = buildIframeContentRender(
12768 def.iframeContent,
12769 cleanups,
12770 def.id
12771 );
12772 }
12773 const userOnClose = def.onClose;
12774 const onClose = cleanups.length ? () => {
12775 for (const fn of cleanups) {
12776 try {
12777 fn();
12778 } catch {
12779 }
12780 }
12781 userOnClose?.();
12782 } : userOnClose;
12783 const win = await manager.open({
12784 id: def.id,
12785 baseId: def.baseId || def.id,
12786 native: true,
12787 url: def.url || `#${def.id}`,
12788 title: def.title,
12789 icon: def.icon,
12790 x: def.x ?? 0,
12791 y: def.y ?? 0,
12792 width: def.width ?? DEFAULT_NATIVE_WIDTH,
12793 height: def.height ?? DEFAULT_NATIVE_HEIGHT,
12794 minWidth: def.minWidth ?? DEFAULT_NATIVE_MIN_WIDTH,
12795 minHeight: def.minHeight ?? DEFAULT_NATIVE_MIN_HEIGHT,
12796 render: render2,
12797 onClose,
12798 onResize: def.onResize,
12799 autofocus: def.autofocus,
12800 initialState: def.initialState,
12801 ownerHandle: def.ownerHandle,
12802 multi: def.multi,
12803 desktopId: def.desktopId
12804 });
12805 return win;
12806 };
12807 }
12808 let onWindowInstanceCounter = 0;
12809 function onWindow(id, handlers, options = {}) {
12810 const namespace = `desktop-mode/on-window/${id}/${++onWindowInstanceCounter}`;
12811 const persistent = options.persistent === true;
12812 const bindings = [
12813 ["opened", HOOKS.WINDOW_OPENED],
12814 ["reopened", HOOKS.WINDOW_REOPENED],
12815 ["focused", HOOKS.WINDOW_FOCUSED],
12816 ["blurred", HOOKS.WINDOW_BLURRED],
12817 ["closing", HOOKS.WINDOW_CLOSING],
12818 ["closed", HOOKS.WINDOW_CLOSED],
12819 ["minimized", HOOKS.WINDOW_MINIMIZED],
12820 ["restored", HOOKS.WINDOW_RESTORED],
12821 ["maximized", HOOKS.WINDOW_MAXIMIZED],
12822 ["unmaximized", HOOKS.WINDOW_UNMAXIMIZED],
12823 ["fullscreenEntered", HOOKS.WINDOW_FULLSCREEN_ENTERED],
12824 ["fullscreenExited", HOOKS.WINDOW_FULLSCREEN_EXITED],
12825 ["resized", HOOKS.WINDOW_RESIZED],
12826 ["bodyResized", HOOKS.WINDOW_BODY_RESIZED],
12827 ["boundsChanged", HOOKS.WINDOW_BOUNDS_CHANGED]
12828 ];
12829 const registered = [];
12830 let disposed = false;
12831 const unsubscribe = () => {
12832 if (disposed) {
12833 return;
12834 }
12835 disposed = true;
12836 for (const hookName2 of registered) {
12837 removeAction(hookName2, namespace);
12838 }
12839 };
12840 for (const [key, hookName2] of bindings) {
12841 const handler = handlers[key];
12842 if (!handler) {
12843 continue;
12844 }
12845 registered.push(hookName2);
12846 addAction(hookName2, namespace, (payload) => {
12847 const p = payload;
12848 if (p.windowId !== id) {
12849 return;
12850 }
12851 const { windowId: _w, ...rest } = p;
12852 handler(rest);
12853 if (key === "closed" && !persistent) {
12854 unsubscribe();
12855 }
12856 });
12857 }
12858 return unsubscribe;
12859 }
12860 function createNativeWindowSync(deps2) {
12861 const { manager, appendSystemTile, removeSystemTile } = deps2;
12862 const registered = /* @__PURE__ */ new Set();
12863 const injectedTemplates = /* @__PURE__ */ new Set();
12864 const loadedScripts = /* @__PURE__ */ new Set();
12865 const loadedStyles = /* @__PURE__ */ new Set();
12866 const entriesById = /* @__PURE__ */ new Map();
12867 const resolveSizeForEntry = (entry) => {
12868 const saved = loadNativeWindowGeometry(entry.id);
12869 if (!saved) {
12870 return { width: entry.width, height: entry.height };
12871 }
12872 return {
12873 width: Math.max(saved.width, entry.minWidth),
12874 height: Math.max(saved.height, entry.minHeight)
12875 };
12876 };
12877 const ensureTemplate = (entry) => {
12878 if (injectedTemplates.has(entry.templateId)) {
12879 return;
12880 }
12881 if (document.getElementById(entry.templateId)) {
12882 injectedTemplates.add(entry.templateId);
12883 return;
12884 }
12885 if (!entry.templateHtml) {
12886 return;
12887 }
12888 const tpl = document.createElement("template");
12889 tpl.id = entry.templateId;
12890 tpl.innerHTML = entry.templateHtml;
12891 document.body.appendChild(tpl);
12892 injectedTemplates.add(entry.templateId);
12893 };
12894 const ensureStyle = (entry) => {
12895 const url = entry.styleUrl;
12896 if (!url || loadedStyles.has(url)) {
12897 return;
12898 }
12899 const safeUrl = url.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
12900 const existing = document.head.querySelector(
12901 `link[rel="stylesheet"][href="${safeUrl}"]`
12902 );
12903 if (!existing) {
12904 const link = document.createElement("link");
12905 link.rel = "stylesheet";
12906 link.href = url;
12907 if (entry.styleHandle) {
12908 link.dataset.desktopModeStyleHandle = entry.styleHandle;
12909 }
12910 document.head.appendChild(link);
12911 }
12912 if (Array.isArray(entry.styleInline)) {
12913 for (const css2 of entry.styleInline) {
12914 if (typeof css2 !== "string" || css2 === "") {
12915 continue;
12916 }
12917 const style = document.createElement("style");
12918 if (entry.styleHandle) {
12919 style.dataset.desktopModeStyleHandle = entry.styleHandle;
12920 }
12921 style.textContent = css2;
12922 document.head.appendChild(style);
12923 }
12924 }
12925 loadedStyles.add(url);
12926 };
12927 const ensureScript = async (entry) => {
12928 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
12929 return;
12930 }
12931 try {
12932 await loadVendorScript(entry.scriptUrl, {
12933 translations: entry.scriptTranslations,
12934 l10n: entry.scriptL10n,
12935 before: entry.scriptBefore,
12936 after: entry.scriptAfter
12937 });
12938 } catch (err) {
12939 doAction(HOOKS.SHELL_ERROR, {
12940 scope: "native-window-script-load",
12941 id: entry.id,
12942 error: err
12943 });
12944 }
12945 loadedScripts.add(entry.scriptUrl);
12946 };
12947 const openFromEntry = (entry) => {
12948 const globalRegistry = window.desktopModeNativeWindows || {};
12949 const render2 = globalRegistry[entry.id];
12950 const finalRender = (body, ctx) => {
12951 body.appendChild(cloneTemplate(entry.templateId));
12952 return render2?.(body, ctx);
12953 };
12954 const size = resolveSizeForEntry(entry);
12955 void manager.open({
12956 id: entry.id,
12957 baseId: entry.id,
12958 native: true,
12959 url: `#${entry.id}`,
12960 title: entry.title,
12961 icon: entry.icon,
12962 width: size.width,
12963 height: size.height,
12964 minWidth: entry.minWidth,
12965 minHeight: entry.minHeight,
12966 render: finalRender,
12967 autofocus: entry.autofocus,
12968 ownerHandle: entry.ownerHandle || entry.scriptHandle
12969 });
12970 };
12971 const openNewFromEntry = (entry) => {
12972 const globalRegistry = window.desktopModeNativeWindows || {};
12973 const render2 = globalRegistry[entry.id];
12974 const finalRender = (body, ctx) => {
12975 body.appendChild(cloneTemplate(entry.templateId));
12976 return render2?.(body, ctx);
12977 };
12978 const size = resolveSizeForEntry(entry);
12979 void manager.openNew({
12980 id: entry.id,
12981 baseId: entry.id,
12982 native: true,
12983 url: `#${entry.id}`,
12984 title: entry.title,
12985 icon: entry.icon,
12986 width: size.width,
12987 height: size.height,
12988 minWidth: entry.minWidth,
12989 minHeight: entry.minHeight,
12990 initialState: "normal",
12991 render: finalRender,
12992 autofocus: entry.autofocus,
12993 ownerHandle: entry.ownerHandle || entry.scriptHandle
12994 });
12995 };
12996 const registerTile = async (entry) => {
12997 if (registered.has(entry.id)) {
12998 return;
12999 }
13000 if ("none" === entry.placement) {
13001 ensureTemplate(entry);
13002 ensureStyle(entry);
13003 await ensureScript(entry);
13004 registered.add(entry.id);
13005 return;
13006 }
13007 ensureTemplate(entry);
13008 ensureStyle(entry);
13009 await ensureScript(entry);
13010 appendSystemTile({
13011 id: entry.id,
13012 title: entry.title,
13013 icon: entry.icon,
13014 isOpen: () => !!manager.getById(entry.id),
13015 onOpen: () => openFromEntry(entry)
13016 });
13017 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: entry.id });
13018 registered.add(entry.id);
13019 };
13020 const unregisterTile = (id) => {
13021 if (!registered.has(id)) {
13022 return;
13023 }
13024 removeSystemTile(id);
13025 registered.delete(id);
13026 entriesById.delete(id);
13027 };
13028 const sync = async (list2) => {
13029 const incoming = /* @__PURE__ */ new Set();
13030 for (const entry of list2) {
13031 incoming.add(entry.id);
13032 entriesById.set(entry.id, entry);
13033 }
13034 for (const id of Array.from(registered)) {
13035 if (!incoming.has(id)) {
13036 unregisterTile(id);
13037 }
13038 }
13039 for (const entry of list2) {
13040 if (!registered.has(entry.id)) {
13041 await registerTile(entry);
13042 }
13043 }
13044 };
13045 const openById = (id, opts = {}) => {
13046 const entry = entriesById.get(id);
13047 if (!entry) {
13048 return false;
13049 }
13050 activity.publish("desktop-mode/open-requested", {
13051 windowId: id,
13052 source: opts.source ?? "api"
13053 });
13054 openFromEntry(entry);
13055 return true;
13056 };
13057 const openNewById = (id, opts = {}) => {
13058 const entry = entriesById.get(id);
13059 if (!entry) {
13060 return false;
13061 }
13062 activity.publish("desktop-mode/open-requested", {
13063 windowId: id,
13064 source: opts.source ?? "api"
13065 });
13066 openNewFromEntry(entry);
13067 return true;
13068 };
13069 addAction(
13070 HOOKS.WINDOW_RESIZE_END,
13071 "desktop-mode-native-window-geometry",
13072 (payload) => {
13073 const p = payload;
13074 const windowId = p?.windowId;
13075 const width = p?.width;
13076 const height = p?.height;
13077 if (!windowId || typeof width !== "number" || typeof height !== "number") {
13078 return;
13079 }
13080 const win = manager.getById(windowId);
13081 if (!win) {
13082 return;
13083 }
13084 if (win.state !== "normal") {
13085 return;
13086 }
13087 const baseId = win.config.baseId || win.id;
13088 saveNativeWindowGeometry(baseId, { width, height });
13089 if (win.element) {
13090 saveNativeWindowPosition(baseId, {
13091 x: win.element.offsetLeft,
13092 y: win.element.offsetTop
13093 });
13094 }
13095 }
13096 );
13097 addAction(
13098 HOOKS.WINDOW_DRAG_END,
13099 "desktop-mode-native-window-geometry",
13100 (payload) => {
13101 const windowId = payload?.windowId;
13102 if (!windowId) {
13103 return;
13104 }
13105 const win = manager.getById(windowId);
13106 if (!win) {
13107 return;
13108 }
13109 if (win.state !== "normal") {
13110 return;
13111 }
13112 if (!win.element) {
13113 return;
13114 }
13115 const baseId = win.config.baseId || win.id;
13116 saveNativeWindowGeometry(baseId, {
13117 width: win.element.offsetWidth,
13118 height: win.element.offsetHeight
13119 });
13120 saveNativeWindowPosition(baseId, {
13121 x: win.element.offsetLeft,
13122 y: win.element.offsetTop
13123 });
13124 }
13125 );
13126 addAction(
13127 HOOKS.WINDOW_MAXIMIZED,
13128 "desktop-mode-native-window-geometry",
13129 (payload) => {
13130 const windowId = payload?.windowId;
13131 if (!windowId) {
13132 return;
13133 }
13134 const win = manager.getById(windowId);
13135 if (!win) {
13136 return;
13137 }
13138 const baseId = win.config.baseId || win.id;
13139 const entry = entriesById.get(baseId);
13140 const defaults = entry ? { width: entry.width, height: entry.height } : { width: win.config.width, height: win.config.height };
13141 setNativeWindowSavedState(baseId, "maximized", defaults);
13142 }
13143 );
13144 addAction(
13145 HOOKS.WINDOW_UNMAXIMIZED,
13146 "desktop-mode-native-window-geometry",
13147 (payload) => {
13148 const windowId = payload?.windowId;
13149 if (!windowId) {
13150 return;
13151 }
13152 const win = manager.getById(windowId);
13153 if (!win) {
13154 return;
13155 }
13156 const baseId = win.config.baseId || win.id;
13157 setNativeWindowSavedState(baseId, null);
13158 }
13159 );
13160 return { sync, openById, openNewById };
13161 }
13162 function cloneTemplate(template) {
13163 let tpl = null;
13164 if (typeof template === "string") {
13165 const found = document.getElementById(template);
13166 if (found instanceof HTMLTemplateElement) {
13167 tpl = found;
13168 }
13169 } else {
13170 tpl = template;
13171 }
13172 if (!tpl) {
13173 throw new Error(
13174 `[desktop-mode] cloneTemplate: no <template> found for ${typeof template === "string" ? `#${template}` : "<reference>"}`
13175 );
13176 }
13177 return tpl.content.cloneNode(true);
13178 }
13179 function renderIcon(icon, opts) {
13180 const className = opts.className ?? "";
13181 const title = opts.title ?? "";
13182 if (typeof icon === "string" && icon.startsWith("dashicons-")) {
13183 const el = document.createElement("span");
13184 el.className = `dashicons ${icon} ${className}`.trim();
13185 el.setAttribute("aria-hidden", "true");
13186 return el;
13187 }
13188 if (typeof icon === "string" && icon.startsWith("data:image/svg+xml;base64,")) {
13189 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
13190 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
13191 const el = document.createElement("span");
13192 el.className = className;
13193 el.setAttribute("aria-hidden", "true");
13194 el.style.backgroundImage = `url("${icon}")`;
13195 el.style.backgroundRepeat = "no-repeat";
13196 el.style.backgroundPosition = "center";
13197 el.style.backgroundSize = "contain";
13198 el.style.display = "inline-block";
13199 return el;
13200 }
13201 }
13202 if (typeof icon === "string" && /^data:image\/(png|jpeg|jpg|gif|webp|x-icon|vnd\.microsoft\.icon);base64,/i.test(icon)) {
13203 const commaIdx = icon.indexOf(",");
13204 const payload = commaIdx >= 0 ? icon.slice(commaIdx + 1) : "";
13205 if (/^[A-Za-z0-9+/=]+$/.test(payload)) {
13206 return makeImgIcon(icon, className);
13207 }
13208 }
13209 if (typeof icon === "string" && (icon.startsWith("http://") || icon.startsWith("https://"))) {
13210 return makeImgIcon(icon, className);
13211 }
13212 const span = document.createElement("span");
13213 span.className = `${className} desktop-mode-icon-letter`.trim();
13214 span.setAttribute("aria-hidden", "true");
13215 const letters = letterFromTitle(title);
13216 span.textContent = letters;
13217 const hue = hashTitleToHue(title);
13218 span.style.backgroundColor = `hsl( ${hue}, 60%, 45% )`;
13219 span.style.color = "#fff";
13220 span.style.display = "inline-flex";
13221 span.style.alignItems = "center";
13222 span.style.justifyContent = "center";
13223 span.style.fontWeight = "600";
13224 span.style.borderRadius = "4px";
13225 return span;
13226 }
13227 function makeImgIcon(src, className) {
13228 const img = document.createElement("img");
13229 img.className = className;
13230 img.src = src;
13231 img.alt = "";
13232 img.setAttribute("aria-hidden", "true");
13233 img.draggable = false;
13234 return img;
13235 }
13236 function letterFromTitle(title) {
13237 const trimmed = (title ?? "").trim();
13238 if (trimmed === "") {
13239 return "?";
13240 }
13241 const words = trimmed.split(/\s+/);
13242 if (words.length >= 2) {
13243 return (words[0][0] + words[1][0]).toUpperCase();
13244 }
13245 const first = words[0];
13246 if (first.length >= 2) {
13247 return first.slice(0, 2).toUpperCase();
13248 }
13249 return first.toUpperCase();
13250 }
13251 const BADGE_CLASS = "desktop-mode-icon__badge";
13252 const _badges = /* @__PURE__ */ new Map();
13253 function _safeBadge(count) {
13254 return Math.max(0, Math.floor(Number(count) || 0));
13255 }
13256 function setIconBadge(iconId, count) {
13257 if (!iconId) {
13258 return;
13259 }
13260 const tile2 = _findIconTile(iconId);
13261 if (!tile2) {
13262 return;
13263 }
13264 const safe = _safeBadge(count);
13265 const previous = _badges.get(iconId) ?? 0;
13266 if (safe === previous) {
13267 return;
13268 }
13269 if (safe === 0) {
13270 _badges.delete(iconId);
13271 } else {
13272 _badges.set(iconId, safe);
13273 }
13274 _paintBadgeNode(tile2, safe);
13275 activity.publish("desktop-mode/badge-changed", {
13276 itemId: iconId,
13277 count: safe,
13278 rail: "icon"
13279 });
13280 doAction(HOOKS.ICON_BADGE_CHANGED, {
13281 iconId,
13282 count: safe,
13283 previousCount: previous
13284 });
13285 }
13286 function clearIconBadge(iconId) {
13287 setIconBadge(iconId, 0);
13288 }
13289 function getIconBadge(iconId) {
13290 return _badges.get(iconId) ?? 0;
13291 }
13292 const iconsApi = {
13293 setBadge: setIconBadge,
13294 clearBadge: clearIconBadge,
13295 getBadge: getIconBadge
13296 };
13297 function fingerprintIcons(icons) {
13298 if (!icons || icons.length === 0) {
13299 return "";
13300 }
13301 return icons.map(
13302 (i) => `${i.id}|${i.title}|${i.icon}|${i.window ?? ""}|${i.url ?? ""}|${i.position ?? 0}|${i.pinned ? 1 : 0}`
13303 ).join(";");
13304 }
13305 let _lastFingerprint = "";
13306 function renderDesktopIcons(host, icons, deps2) {
13307 const fp = fingerprintIcons(icons);
13308 if (fp === _lastFingerprint && host.querySelector(":scope > .desktop-mode-icons")) {
13309 return;
13310 }
13311 _lastFingerprint = fp;
13312 const existing = host.querySelector(":scope > .desktop-mode-icons");
13313 if (existing) {
13314 existing.remove();
13315 }
13316 if (!icons || icons.length === 0) {
13317 return;
13318 }
13319 const container = document.createElement("div");
13320 container.className = "desktop-mode-icons";
13321 container.setAttribute("role", "list");
13322 container.setAttribute("aria-label", __("Desktop icons"));
13323 const ordered = [...icons].sort((a, b) => {
13324 const ap = a.pinned ? 0 : 1;
13325 const bp = b.pinned ? 0 : 1;
13326 return ap - bp;
13327 });
13328 const tiles = /* @__PURE__ */ new Map();
13329 for (const entry of ordered) {
13330 const tile2 = buildIcon(entry, deps2);
13331 const stored = _badges.get(entry.id) ?? 0;
13332 if (stored > 0) {
13333 _paintBadgeNode(tile2, stored);
13334 }
13335 container.appendChild(tile2);
13336 tiles.set(entry.id, tile2);
13337 }
13338 host.appendChild(container);
13339 doAction(HOOKS.DESKTOP_ICONS_RENDERED, {
13340 ids: (icons ?? []).map((i) => i.id),
13341 container,
13342 tiles
13343 });
13344 }
13345 function _findIconTile(iconId) {
13346 if (!iconId) {
13347 return null;
13348 }
13349 const container = document.querySelector(
13350 ".desktop-mode-icons"
13351 );
13352 if (!container) {
13353 return null;
13354 }
13355 return container.querySelector(
13356 `[data-icon-id="${_cssEscape(iconId)}"]`
13357 );
13358 }
13359 function _paintBadgeNode(host, count) {
13360 const existing = host.querySelector(
13361 `:scope > .${BADGE_CLASS}`
13362 );
13363 if (count <= 0) {
13364 existing?.remove();
13365 return;
13366 }
13367 const display = count > 99 ? "99+" : String(count);
13368 const ariaLabel = sprintf(
13369 // translators: %d is the number of pending items in a desktop-icon badge.
13370 _n("%d notification", "%d notifications", count),
13371 count
13372 );
13373 if (existing) {
13374 if (existing.textContent !== display) {
13375 existing.textContent = display;
13376 }
13377 existing.setAttribute("aria-label", ariaLabel);
13378 return;
13379 }
13380 const badge = document.createElement("span");
13381 badge.className = BADGE_CLASS;
13382 badge.textContent = display;
13383 badge.setAttribute("aria-label", ariaLabel);
13384 host.appendChild(badge);
13385 }
13386 function _cssEscape(value) {
13387 const c = window.CSS;
13388 return c?.escape ? c.escape(value) : value;
13389 }
13390 function buildIcon(entry, deps2) {
13391 const tile2 = document.createElement("button");
13392 tile2.type = "button";
13393 tile2.className = entry.pinned ? "desktop-mode-icon desktop-mode-icon--pinned" : "desktop-mode-icon";
13394 tile2.dataset.iconId = entry.id;
13395 if (entry.pinned) {
13396 tile2.dataset.pinned = "1";
13397 }
13398 tile2.setAttribute("role", "listitem");
13399 tile2.setAttribute("aria-label", entry.title);
13400 const icon = renderIcon(entry.icon, {
13401 title: entry.title,
13402 className: "desktop-mode-icon__image"
13403 });
13404 tile2.appendChild(icon);
13405 const label = document.createElement("span");
13406 label.className = "desktop-mode-icon__label";
13407 label.textContent = entry.title;
13408 tile2.appendChild(label);
13409 tile2.addEventListener("click", (e) => {
13410 e.stopPropagation();
13411 doAction(HOOKS.DESKTOP_ICON_CLICKED, {
13412 id: entry.id,
13413 target: entry.window ? "window" : "url"
13414 });
13415 openTarget(entry, deps2);
13416 });
13417 tile2.addEventListener("contextmenu", (e) => {
13418 if (entry.pinned) {
13419 return;
13420 }
13421 e.preventDefault();
13422 e.stopPropagation();
13423 openItemVisibilityMenu({
13424 x: e.clientX,
13425 y: e.clientY,
13426 id: entry.id,
13427 title: entry.title,
13428 surface: "desktop"
13429 });
13430 });
13431 return tile2;
13432 }
13433 function openTarget(entry, deps2) {
13434 if (entry.window) {
13435 const opened = deps2.openWindow(entry.window);
13436 if (!opened) {
13437 return;
13438 }
13439 return;
13440 }
13441 if (entry.url) {
13442 if (tryOpenExternalUrl(entry.url)) {
13443 return;
13444 }
13445 try {
13446 const parsed = new URL(entry.url, window.location.origin);
13447 const windowId = deps2.deriveWindowId(parsed.toString());
13448 void deps2.manager.open({
13449 id: windowId,
13450 baseId: windowId,
13451 url: parsed.toString(),
13452 title: entry.title,
13453 icon: entry.icon
13454 });
13455 } catch {
13456 }
13457 }
13458 }
13459 const SIDE_DOCK_ID = "desktop-mode-side-dock";
13460 function coreItemToIconEntry(item, index2) {
13461 return {
13462 id: `dock-core:${item.id}`,
13463 title: item.title,
13464 icon: item.icon,
13465 window: "",
13466 url: item.url,
13467 // Synthesized icons render after server-registered ones; the
13468 // large offset leaves headroom for plugin authors who set
13469 // explicit `position` values.
13470 position: 1e3 + index2
13471 };
13472 }
13473 function createLayoutDispatcher(deps2, initialLayout, initialDockItems, initialServerIcons) {
13474 let layout = initialLayout;
13475 let items = initialDockItems;
13476 let serverIcons = initialServerIcons ?? [];
13477 let primary = null;
13478 let side = null;
13479 let primaryDock = null;
13480 let sideDock = null;
13481 let sideDockEl = null;
13482 const systemTiles = /* @__PURE__ */ new Map();
13483 const railFor = (affinity) => {
13484 if (affinity === "core" && side) {
13485 return side;
13486 }
13487 return primary;
13488 };
13489 const ensureSideDockEl = () => {
13490 const existing = document.getElementById(
13491 SIDE_DOCK_ID
13492 );
13493 if (existing) {
13494 return existing;
13495 }
13496 const el = document.createElement("nav");
13497 el.id = SIDE_DOCK_ID;
13498 el.className = "desktop-mode-dock";
13499 el.setAttribute("role", "toolbar");
13500 el.setAttribute("aria-label", "Core admin navigation");
13501 deps2.shellBody.insertBefore(el, deps2.shellBody.firstChild);
13502 return el;
13503 };
13504 const removeSideDockEl = () => {
13505 if (sideDockEl && sideDockEl.parentNode) {
13506 sideDockEl.parentNode.removeChild(sideDockEl);
13507 }
13508 sideDockEl = null;
13509 };
13510 const readSettings = () => deps2.getSettings?.() ?? { itemVisibility: {}, dockOrder: [] };
13511 const effectiveDockItems = () => {
13512 const dockedNativeWindows = /* @__PURE__ */ new Set();
13513 for (const entry of systemTiles.values()) {
13514 dockedNativeWindows.add(entry.item.id);
13515 }
13516 return applyDockPlacement(
13517 items,
13518 serverIcons,
13519 readSettings(),
13520 dockedNativeWindows
13521 );
13522 };
13523 const partition = () => {
13524 const effective = effectiveDockItems();
13525 const core = [];
13526 const plugin = [];
13527 for (const item of effective) {
13528 if (item.isCore) {
13529 core.push(item);
13530 } else {
13531 plugin.push(item);
13532 }
13533 }
13534 return { core, plugin };
13535 };
13536 const repaintIcons = () => {
13537 const settings = readSettings();
13538 if (layout !== "spatial") {
13539 deps2.renderIcons(
13540 applyDesktopPlacement(serverIcons, items, settings.itemVisibility)
13541 );
13542 return;
13543 }
13544 const { core } = partition();
13545 const synthesized = core.map(coreItemToIconEntry);
13546 const explicitlyPromoted = [];
13547 let synthIndex = 0;
13548 for (const item of items) {
13549 const placement = settings.itemVisibility[item.id];
13550 if (placement === "desktop" || placement === "both") {
13551 explicitlyPromoted.push({
13552 id: `dock:${item.id}`,
13553 title: item.title,
13554 icon: item.icon,
13555 window: "",
13556 url: item.url || "",
13557 position: 2e3 + synthIndex++
13558 });
13559 }
13560 }
13561 deps2.renderIcons([...synthesized, ...explicitlyPromoted]);
13562 };
13563 const tearDownDocks = () => {
13564 if (primary) {
13565 try {
13566 primary.destroy();
13567 } catch (err) {
13568 doAction(HOOKS.SHELL_ERROR, {
13569 scope: "dock-rail-renderer/destroy",
13570 error: err
13571 });
13572 }
13573 primary = null;
13574 primaryDock = null;
13575 }
13576 if (side) {
13577 try {
13578 side.destroy();
13579 } catch (err) {
13580 doAction(HOOKS.SHELL_ERROR, {
13581 scope: "dock-rail-renderer/destroy",
13582 error: err
13583 });
13584 }
13585 side = null;
13586 sideDock = null;
13587 }
13588 };
13589 const mountRail = (mountDeps) => {
13590 const renderer = resolveActive();
13591 if (!renderer) {
13592 doAction(HOOKS.SHELL_ERROR, {
13593 scope: "dock-rail-renderer",
13594 message: "No dock rail renderer is registered."
13595 });
13596 return null;
13597 }
13598 try {
13599 return renderer.mount(mountDeps);
13600 } catch (err) {
13601 doAction(HOOKS.SHELL_ERROR, {
13602 scope: "dock-rail-renderer/mount",
13603 rendererId: renderer.id,
13604 error: err
13605 });
13606 if (renderer === defaultDockRailRenderer) {
13607 return null;
13608 }
13609 try {
13610 return defaultDockRailRenderer.mount(mountDeps);
13611 } catch {
13612 return null;
13613 }
13614 }
13615 };
13616 const buildMountDeps = (container, railItems, orientation) => ({
13617 container,
13618 items: railItems,
13619 // `fullMenu` is the complete admin-menu list. Renderers that
13620 // want to ignore the layout's partitioning (e.g., paint
13621 // every menu item in one ring regardless of `isCore`) read
13622 // this instead of `items`. Snapshot per-mount so a renderer
13623 // holding the array sees a stable list; live updates flow
13624 // through `replaceItems`.
13625 fullMenu: items.slice(),
13626 // Same idea for system tiles — OS Settings, plugin-owned
13627 // native-window launchers, etc. Lets a renderer apply
13628 // uniform treatment across menu + system cohorts in one
13629 // pass. Live updates flow through `appendSystemItem` /
13630 // `removeSystemItem`.
13631 fullSystemTiles: Array.from(systemTiles.values()).map(
13632 (entry) => entry.item
13633 ),
13634 orientation,
13635 windowManager: deps2.windowManager,
13636 adminUrl: deps2.adminUrl,
13637 // `openItem` / `openSubmenuPick` / `openSystemItem` /
13638 // `requestSubmenu` are routing callbacks for custom
13639 // renderers. They mirror exactly what the default renderer
13640 // (`Dock.openPage` / `Dock.openSubmenuPick`) does internally
13641 // — same `deriveWindowId(url, adminUrl)` call, same window-
13642 // config shape — so a custom renderer addresses the same
13643 // window with the same id at runtime. Switching renderer
13644 // mid-session doesn't lose the user's open windows.
13645 openItem: (item) => {
13646 const baseId = deriveWindowId(item.url, deps2.adminUrl);
13647 deps2.windowManager.open({
13648 id: baseId,
13649 baseId,
13650 url: item.url,
13651 parentUrl: item.url,
13652 title: item.title,
13653 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
13654 submenu: item.submenu,
13655 multi: !!item.multi
13656 });
13657 },
13658 openSubmenuPick: (item, sub) => {
13659 deps2.windowManager.open({
13660 id: deriveWindowId(sub.url, deps2.adminUrl),
13661 baseId: deriveWindowId(item.url, deps2.adminUrl),
13662 url: sub.url,
13663 // Pin the synthetic parent tab to the dock landing
13664 // page, not to the sub-page the user picked. Without
13665 // this, a submenu-pick (e.g. clicking "Editor" inside
13666 // Appearance's submenu popover) would open at
13667 // site-editor.php with no way back to themes.php.
13668 parentUrl: item.url,
13669 title: item.title,
13670 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
13671 submenu: item.submenu,
13672 multi: !!item.multi
13673 });
13674 },
13675 openSystemItem: (item) => item.onOpen()
13676 });
13677 const buildDocksForCurrentLayout = () => {
13678 tearDownDocks();
13679 const { core, plugin } = partition();
13680 if (layout === "classic") {
13681 sideDockEl = ensureSideDockEl();
13682 side = mountRail(
13683 buildMountDeps(sideDockEl, core, "left")
13684 );
13685 sideDock = unwrapDefaultDock(side);
13686 primary = mountRail(
13687 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
13688 );
13689 primaryDock = unwrapDefaultDock(primary);
13690 } else if (layout === "unified") {
13691 removeSideDockEl();
13692 primary = mountRail(
13693 buildMountDeps(deps2.bottomDockEl, effectiveDockItems(), "bottom")
13694 );
13695 primaryDock = unwrapDefaultDock(primary);
13696 } else {
13697 removeSideDockEl();
13698 primary = mountRail(
13699 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
13700 );
13701 primaryDock = unwrapDefaultDock(primary);
13702 }
13703 for (const entry of systemTiles.values()) {
13704 railFor(entry.affinity)?.appendSystemItem(entry.item);
13705 }
13706 };
13707 const dispatcher = {
13708 getLayout: () => layout,
13709 getPrimary: () => primaryDock,
13710 getSide: () => sideDock,
13711 setLayout: (next) => {
13712 if (next === layout) {
13713 return;
13714 }
13715 layout = next;
13716 deps2.shellRoot.setAttribute("data-desktop-mode-layout", next);
13717 buildDocksForCurrentLayout();
13718 repaintIcons();
13719 document.dispatchEvent(
13720 new CustomEvent("desktop-mode-layout-changed", {
13721 detail: {
13722 layout: next,
13723 primary: primaryDock,
13724 side: sideDock
13725 }
13726 })
13727 );
13728 },
13729 applyDockItems: (nextItems) => {
13730 items = nextItems;
13731 const { core, plugin } = partition();
13732 if (layout === "classic") {
13733 side?.replaceItems(core);
13734 primary?.replaceItems(plugin);
13735 } else if (layout === "unified") {
13736 primary?.replaceItems(effectiveDockItems());
13737 } else {
13738 primary?.replaceItems(plugin);
13739 }
13740 repaintIcons();
13741 },
13742 applyDesktopIcons: (next) => {
13743 serverIcons = next ?? [];
13744 repaintIcons();
13745 },
13746 appendSystemTile: (item, affinity = "plugin") => {
13747 systemTiles.set(item.id, { item, affinity });
13748 railFor(affinity)?.appendSystemItem(item);
13749 },
13750 removeSystemTile: (id) => {
13751 const entry = systemTiles.get(id);
13752 if (!entry) {
13753 return;
13754 }
13755 systemTiles.delete(id);
13756 railFor(entry.affinity)?.removeSystemItem(id);
13757 },
13758 listSystemTiles: () => Array.from(systemTiles.values()).map((entry) => ({
13759 id: entry.item.id,
13760 title: entry.item.title,
13761 icon: entry.item.icon,
13762 affinity: entry.affinity
13763 })),
13764 getSystemTile: (id) => systemTiles.get(id)?.item ?? null,
13765 getMenuItems: () => items.slice(),
13766 refresh: () => {
13767 const { core, plugin } = partition();
13768 if (layout === "classic") {
13769 side?.replaceItems(core);
13770 primary?.replaceItems(plugin);
13771 } else if (layout === "unified") {
13772 primary?.replaceItems(effectiveDockItems());
13773 } else {
13774 primary?.replaceItems(plugin);
13775 }
13776 repaintIcons();
13777 },
13778 destroy: () => {
13779 tearDownDocks();
13780 removeSideDockEl();
13781 }
13782 };
13783 deps2.shellRoot.setAttribute("data-desktop-mode-layout", layout);
13784 buildDocksForCurrentLayout();
13785 repaintIcons();
13786 let lastResolvedId = resolveActive()?.id ?? null;
13787 subscribe$3(() => {
13788 const nextId2 = resolveActive()?.id ?? null;
13789 if (nextId2 === lastResolvedId) {
13790 return;
13791 }
13792 lastResolvedId = nextId2;
13793 buildDocksForCurrentLayout();
13794 repaintIcons();
13795 document.dispatchEvent(
13796 new CustomEvent("desktop-mode-layout-changed", {
13797 detail: {
13798 layout,
13799 primary: primaryDock,
13800 side: sideDock
13801 }
13802 })
13803 );
13804 });
13805 return dispatcher;
13806 }
13807 function loadImpl(scriptUrl) {
13808 if (window.desktopModeCreateAiAssistant) {
13809 return Promise.resolve(window.desktopModeCreateAiAssistant);
13810 }
13811 return new Promise((resolve2, reject) => {
13812 const existing = document.querySelector(
13813 `script[data-desktop-mode-ai="1"]`
13814 );
13815 const finish = () => {
13816 const factory = window.desktopModeCreateAiAssistant;
13817 if (!factory) {
13818 reject(
13819 new Error(
13820 "[desktop-mode] ai-assistant bundle loaded but did not register desktopModeCreateAiAssistant"
13821 )
13822 );
13823 return;
13824 }
13825 resolve2(factory);
13826 };
13827 if (existing) {
13828 if (window.desktopModeCreateAiAssistant) {
13829 finish();
13830 } else {
13831 existing.addEventListener("load", finish);
13832 existing.addEventListener(
13833 "error",
13834 () => reject(new Error("failed to load ai-assistant bundle"))
13835 );
13836 }
13837 return;
13838 }
13839 const s = document.createElement("script");
13840 s.src = scriptUrl;
13841 s.async = true;
13842 s.dataset.desktopModeAi = "1";
13843 s.addEventListener("load", finish);
13844 s.addEventListener(
13845 "error",
13846 () => reject(new Error("failed to load ai-assistant bundle"))
13847 );
13848 document.head.appendChild(s);
13849 });
13850 }
13851 class AiAssistantStub {
13852 constructor(config, scriptUrl) {
13853 this._real = null;
13854 this._loadPromise = null;
13855 this._pendingAsk = null;
13856 this._intendOpen = false;
13857 this.ask = (...args) => {
13858 return this._ensure().then((r) => r.ask(...args));
13859 };
13860 this._config = config;
13861 this._scriptUrl = scriptUrl;
13862 }
13863 _ensure() {
13864 if (this._loadPromise) {
13865 return this._loadPromise;
13866 }
13867 this._loadPromise = loadImpl(this._scriptUrl).then((factory) => {
13868 const real = factory(this._config);
13869 if (this._pendingAsk) {
13870 real.attachAsk(this._pendingAsk);
13871 }
13872 this._real = real;
13873 return real;
13874 });
13875 return this._loadPromise;
13876 }
13877 open() {
13878 this._intendOpen = true;
13879 void this._ensure().then((r) => r.open());
13880 }
13881 close() {
13882 this._intendOpen = false;
13883 if (this._real) {
13884 this._real.close();
13885 }
13886 }
13887 toggle() {
13888 if (this.isOpen) {
13889 this.close();
13890 } else {
13891 this.open();
13892 }
13893 }
13894 get isOpen() {
13895 return this._real ? this._real.isOpen : this._intendOpen;
13896 }
13897 /**
13898 * Late-bind the programmatic `ask` callback. Mirrors the real
13899 * class's `attachAsk` signature so `desktop.ts`'s call site is
13900 * identical whether it's wiring the stub or the impl.
13901 */
13902 attachAsk(fn) {
13903 this._pendingAsk = fn;
13904 if (this._real) {
13905 this._real.attachAsk(fn);
13906 }
13907 }
13908 }
13909 const isAbortError = (err) => {
13910 if (!err || typeof err !== "object") {
13911 return false;
13912 }
13913 return err.name === "AbortError";
13914 };
13915 const normaliseToolsOpt = (tools) => {
13916 if (!tools) {
13917 return [];
13918 }
13919 const all2 = listAiCallableCommands();
13920 if (tools === true || tools === "aiCallable") {
13921 return all2;
13922 }
13923 if (Array.isArray(tools)) {
13924 const allowed = new Set(tools.map((s) => s.toLowerCase()));
13925 return all2.filter((c) => allowed.has(c.slug));
13926 }
13927 if (typeof tools === "function") {
13928 return all2.filter((c) => {
13929 try {
13930 return tools(c.slug) === true;
13931 } catch {
13932 return false;
13933 }
13934 });
13935 }
13936 return [];
13937 };
13938 const normaliseSystemPrompt = (sp) => {
13939 if (!sp) {
13940 return null;
13941 }
13942 if (typeof sp === "string") {
13943 return { text: sp, mode: "append" };
13944 }
13945 if (typeof sp === "object" && typeof sp.text === "string" && sp.text !== "") {
13946 return {
13947 text: sp.text,
13948 mode: sp.mode === "replace" ? "replace" : "append"
13949 };
13950 }
13951 return null;
13952 };
13953 function liftMessage(payloadMessage, result) {
13954 const seed2 = payloadMessage ?? "";
13955 if (seed2 !== "") {
13956 return seed2;
13957 }
13958 if (typeof result === "string" && result !== "") {
13959 return result;
13960 }
13961 if (result && typeof result === "object" && "message" in result && typeof result.message === "string") {
13962 return result.message;
13963 }
13964 return "";
13965 }
13966 function serialiseOutcome(result) {
13967 if (result === void 0) {
13968 return { value: null };
13969 }
13970 if (typeof result === "object" && result !== null) {
13971 return result;
13972 }
13973 return { value: result };
13974 }
13975 function createAsk(deps2) {
13976 const postToSearch = async (body, signal) => {
13977 const config = deps2.config();
13978 const url = config.aiSearchUrl ?? "";
13979 const nonce = config.restNonce ?? "";
13980 if (!url || !nonce) {
13981 throw new Error(
13982 "[desktop-mode] wp.desktop.ai.ask: aiSearchUrl / restNonce missing from config. AI Copilot may not be enabled."
13983 );
13984 }
13985 try {
13986 return await trackedFetch$1(
13987 url,
13988 {
13989 method: "POST",
13990 credentials: "same-origin",
13991 headers: {
13992 "Content-Type": "application/json",
13993 "X-WP-Nonce": nonce
13994 },
13995 body: JSON.stringify(body),
13996 signal
13997 },
13998 { source: "desktop-mode/ai-ask" }
13999 );
14000 } catch (err) {
14001 if (isAbortError(err)) {
14002 throw err;
14003 }
14004 throw new Error(
14005 `[desktop-mode] wp.desktop.ai.ask: network error — ${String(
14006 err?.message ?? err
14007 )}`
14008 );
14009 }
14010 };
14011 const dispatchToolCall = async (payload, opts) => {
14012 const slug = payload.tool?.slug ?? "";
14013 const args = payload.tool?.args ?? "";
14014 const cmd = findCommand(slug);
14015 if (!cmd) {
14016 return {
14017 ok: false,
14018 response: {
14019 answer_type: "tool_call",
14020 message: `Command /${slug} was not registered on this page.`,
14021 entity: null,
14022 admin_links: null,
14023 toolCall: {
14024 slug,
14025 args,
14026 result: { error: "command_not_found" }
14027 },
14028 request_id: payload.request_id
14029 }
14030 };
14031 }
14032 const ctx = opts.commandContext ?? deps2.fallbackContext();
14033 let result;
14034 try {
14035 result = await Promise.resolve(cmd.run(args, ctx));
14036 } catch (err) {
14037 result = { error: String(err?.message ?? err) };
14038 }
14039 return { ok: true, slug, args, result };
14040 };
14041 const composeFollowUp = async (text, slug, args, result, sp, signal) => {
14042 const body = {
14043 query: text,
14044 follow_up: {
14045 tool: { slug, args },
14046 result: serialiseOutcome(result)
14047 }
14048 };
14049 if (sp) {
14050 body.system_prompt_text = sp.text;
14051 body.system_prompt_mode = sp.mode;
14052 }
14053 let res;
14054 try {
14055 res = await postToSearch(body, signal);
14056 } catch (err) {
14057 if (isAbortError(err)) {
14058 throw err;
14059 }
14060 return null;
14061 }
14062 if (!res.ok) {
14063 return null;
14064 }
14065 const payload = await res.json().catch(() => ({}));
14066 const message = typeof payload.message === "string" ? payload.message.trim() : "";
14067 return message !== "" ? payload.message ?? null : null;
14068 };
14069 return async function ask(query, opts = {}) {
14070 const text = (query ?? "").trim();
14071 if (text === "") {
14072 const hasMeaningfulOpts = opts.tools !== void 0 || opts.systemPrompt !== void 0 || opts.followUp === true || opts.resumeTool !== void 0 || opts.commandContext !== void 0;
14073 if (hasMeaningfulOpts) {
14074 throw new Error(
14075 "[desktop-mode] wp.desktop.ai.ask: empty query passed with non-default options — likely a caller bug. Provide a query or call without options."
14076 );
14077 }
14078 return {
14079 answer_type: "chat",
14080 message: "",
14081 entity: null,
14082 admin_links: null
14083 };
14084 }
14085 const commandTools = normaliseToolsOpt(opts.tools);
14086 const sp = normaliseSystemPrompt(opts.systemPrompt);
14087 const body = { query: text };
14088 if (opts.resumeTool) {
14089 body.resume_tool = opts.resumeTool;
14090 }
14091 if (typeof opts.startOffset === "number") {
14092 body.start_offset = opts.startOffset;
14093 }
14094 if (commandTools.length > 0) {
14095 body.command_tools = commandTools;
14096 }
14097 if (sp) {
14098 body.system_prompt_text = sp.text;
14099 body.system_prompt_mode = sp.mode;
14100 }
14101 const res = await postToSearch(body, opts.signal);
14102 if (!res.ok) {
14103 const detail = await res.json().catch(() => ({ message: res.statusText }));
14104 throw new Error(
14105 `[desktop-mode] wp.desktop.ai.ask: HTTP ${res.status} — ${detail.message ?? res.statusText}`
14106 );
14107 }
14108 const payload = await res.json();
14109 if (payload.answer_type !== "tool_call" || !payload.tool) {
14110 return {
14111 answer_type: payload.answer_type,
14112 message: payload.message ?? "",
14113 entity: payload.entity ?? null,
14114 admin_links: payload.admin_links ?? null,
14115 request_id: payload.request_id,
14116 continue: payload.continue ?? null
14117 };
14118 }
14119 const dispatch2 = await dispatchToolCall(payload, opts);
14120 if (!dispatch2.ok) {
14121 return dispatch2.response;
14122 }
14123 const { slug, args, result } = dispatch2;
14124 let message = liftMessage(payload.message, result);
14125 if (opts.followUp === true) {
14126 const composed = await composeFollowUp(
14127 text,
14128 slug,
14129 args,
14130 result,
14131 sp,
14132 opts.signal
14133 );
14134 if (composed !== null) {
14135 message = composed;
14136 }
14137 }
14138 return {
14139 answer_type: "tool_call",
14140 message,
14141 entity: null,
14142 admin_links: null,
14143 toolCall: { slug, args, result },
14144 request_id: payload.request_id
14145 };
14146 };
14147 }
14148 const EVENT_NAME = "desktop-mode-broadcast";
14149 const POSTMESSAGE_TYPE = "desktop-mode-broadcast";
14150 const ORIGIN = window.location.origin;
14151 let _manager = null;
14152 function attachBroadcastBus(manager) {
14153 _manager = manager;
14154 }
14155 function broadcast(topic, payload) {
14156 const filteredTopic = String(
14157 applyFilters("desktop-mode.broadcast.topic", topic, { payload }) ?? topic
14158 );
14159 const filteredPayload = applyFilters(
14160 "desktop-mode.broadcast.payload",
14161 payload,
14162 { topic: filteredTopic }
14163 );
14164 const detail = {
14165 topic: filteredTopic,
14166 payload: filteredPayload
14167 };
14168 document.dispatchEvent(new CustomEvent(EVENT_NAME, { detail }));
14169 doAction(HOOKS.BROADCAST, detail);
14170 activity.publish(
14171 filteredTopic,
14172 filteredPayload
14173 );
14174 if (!_manager) {
14175 return;
14176 }
14177 const message = {
14178 type: POSTMESSAGE_TYPE,
14179 topic: filteredTopic,
14180 payload: filteredPayload
14181 };
14182 for (const win of _manager._stack) {
14183 const target2 = win.iframe?.contentWindow;
14184 if (!target2) {
14185 continue;
14186 }
14187 try {
14188 target2.postMessage(message, ORIGIN);
14189 } catch (err) {
14190 }
14191 }
14192 }
14193 function subscribe$2(topic, cb) {
14194 const handler = (e) => {
14195 const detail = e.detail;
14196 if (!detail) {
14197 return;
14198 }
14199 if (topic !== "*" && detail.topic !== topic) {
14200 return;
14201 }
14202 try {
14203 cb(detail.payload, { topic: detail.topic });
14204 } catch (err) {
14205 doAction(HOOKS.SHELL_ERROR, {
14206 scope: "broadcast-subscriber",
14207 topic: detail.topic,
14208 error: err
14209 });
14210 }
14211 };
14212 document.addEventListener(EVENT_NAME, handler);
14213 return () => document.removeEventListener(EVENT_NAME, handler);
14214 }
14215 function installBroadcastReceiver() {
14216 window.addEventListener("message", (e) => {
14217 if (e.origin !== ORIGIN) {
14218 return;
14219 }
14220 const data = e.data;
14221 if (!data || data.type !== POSTMESSAGE_TYPE) {
14222 return;
14223 }
14224 if (data._fromParent) {
14225 return;
14226 }
14227 if (typeof data.topic !== "string") {
14228 return;
14229 }
14230 broadcast(data.topic, data.payload);
14231 });
14232 }
14233 const LOG_PREFIX = "[desktop-mode-bin badge]";
14234 function log(...args) {
14235 try {
14236 if (window.localStorage?.getItem("desktopModeBinDebug")) {
14237 console.info(LOG_PREFIX, ...args);
14238 }
14239 } catch {
14240 }
14241 }
14242 function warn(...args) {
14243 console.warn(LOG_PREFIX, ...args);
14244 }
14245 const TARGET_ID = "desktop-mode-recycle-bin";
14246 const HEARTBEAT_FIELD$1 = "desktop_mode_recycle_bin_seen_ts";
14247 function getDesktopApi() {
14248 return window.wp?.desktop;
14249 }
14250 const store$3 = createSharedStore(
14251 "desktop-mode/recycle-bin/badge",
14252 () => ({
14253 current: 0,
14254 seenTs: 0,
14255 started: false,
14256 countUrl: ""
14257 })
14258 );
14259 function setRecycleBinBadge(next) {
14260 const safe = Math.max(0, Math.floor(next));
14261 const prev = store$3.state.current;
14262 store$3.state.current = safe;
14263 log("setRecycleBinBadge", { prev, next: safe });
14264 paintBadge(safe);
14265 }
14266 function adjustRecycleBinBadge(delta) {
14267 setRecycleBinBadge(store$3.state.current + delta);
14268 }
14269 function _currentRecycleBinBadge() {
14270 return store$3.state.current;
14271 }
14272 function paintBadge(count) {
14273 const desktop = getDesktopApi();
14274 const active2 = isBinWindowActive();
14275 const visible = active2 ? 0 : count;
14276 log("paintBadge", { count, visible, active: active2 });
14277 desktop?.dock?.setBadge?.(TARGET_ID, visible);
14278 desktop?.taskbar?.setBadge?.(TARGET_ID, visible);
14279 desktop?.icons?.setBadge?.(TARGET_ID, visible);
14280 }
14281 function isBinWindowActive() {
14282 return !!getDesktopApi()?.windowManager?.isActive?.(TARGET_ID);
14283 }
14284 function startRecycleBinBadge(initialRaw, countUrl = "") {
14285 const initial = Number(initialRaw) || 0;
14286 const cfg = window.desktopModeConfig;
14287 const cfgCount = cfg?.recycleBinCount;
14288 const cfgUrl = cfg?.recycleBinCountUrl;
14289 const cfgDebug = cfg?.desktopModeBinDebug;
14290 log("startRecycleBinBadge entry", {
14291 initial,
14292 countUrl,
14293 alreadyStarted: store$3.state.started,
14294 cfgCount,
14295 cfgUrl,
14296 cfgDebug,
14297 readyState: document.readyState
14298 });
14299 const cfgCountNum = Number(cfgCount);
14300 const cfgCountIsHealthy = (typeof cfgCount === "number" || typeof cfgCount === "string") && Number.isFinite(cfgCountNum);
14301 if (!cfgCountIsHealthy) {
14302 warn(
14303 "desktopModeConfig.recycleBinCount is missing — PHP filter `desktop_mode_shell_config` did not deliver. Check your PHP error log for `[desktop-mode-bin debug]` lines.",
14304 { cfg }
14305 );
14306 }
14307 if (store$3.state.started) {
14308 setRecycleBinBadge(initial);
14309 return;
14310 }
14311 store$3.state.started = true;
14312 store$3.state.countUrl = countUrl;
14313 store$3.state.seenTs = Date.now();
14314 setRecycleBinBadge(initial);
14315 wireDockTileSignal();
14316 wireDesktopIconsSignal();
14317 wireBroadcastDeltas();
14318 wirePostMessageFastPath();
14319 wireHeartbeatProbe();
14320 wireWindowLifecycleSignals();
14321 }
14322 function wireWindowLifecycleSignals() {
14323 const ns = "desktop-mode/recycle-bin/badge-lifecycle";
14324 const repaint = (payload) => {
14325 const detail = payload;
14326 if (detail?.windowId !== TARGET_ID) {
14327 return;
14328 }
14329 paintBadge(store$3.state.current);
14330 };
14331 addAction(HOOKS.WINDOW_OPENED, ns, repaint);
14332 addAction(HOOKS.WINDOW_FOCUSED, ns, repaint);
14333 addAction(HOOKS.WINDOW_BLURRED, ns, repaint);
14334 addAction(HOOKS.WINDOW_MINIMIZED, ns, repaint);
14335 addAction(HOOKS.WINDOW_RESTORED, ns, repaint);
14336 addAction(HOOKS.WINDOW_CLOSED, ns, repaint);
14337 addAction(HOOKS.WINDOW_REOPENED, ns, repaint);
14338 }
14339 function wireDockTileSignal() {
14340 addAction(
14341 HOOKS.DOCK_ITEM_APPENDED,
14342 "desktop-mode/recycle-bin/badge",
14343 (payload) => {
14344 if (payload?.id === TARGET_ID) {
14345 paintBadge(store$3.state.current);
14346 }
14347 }
14348 );
14349 }
14350 function wireDesktopIconsSignal() {
14351 addAction(
14352 HOOKS.DESKTOP_ICONS_RENDERED,
14353 "desktop-mode/recycle-bin/badge",
14354 (payload) => {
14355 if (payload?.ids?.includes(TARGET_ID)) {
14356 paintBadge(store$3.state.current);
14357 }
14358 }
14359 );
14360 }
14361 function wireBroadcastDeltas() {
14362 const onDomain = (payload) => {
14363 const detail = payload;
14364 if (!detail) {
14365 return;
14366 }
14367 const ids = Array.isArray(detail.ids) ? detail.ids.length : 0;
14368 switch (detail.action) {
14369 case "trashed":
14370 adjustRecycleBinBadge(+ids);
14371 break;
14372 case "untrashed":
14373 case "deleted":
14374 adjustRecycleBinBadge(-ids);
14375 break;
14376 }
14377 };
14378 subscribe$2("desktop-mode.post.changed", onDomain);
14379 subscribe$2("desktop-mode.page.changed", onDomain);
14380 subscribe$2("desktop-mode.attachment.changed", onDomain);
14381 subscribe$2("desktop-mode.comment.changed", onDomain);
14382 subscribe$2("desktop-mode.placement.changed", onDomain);
14383 subscribe$2("desktop-mode.shortcut.changed", onDomain);
14384 subscribe$2("desktop-mode.folder.changed", onDomain);
14385 }
14386 function wirePostMessageFastPath() {
14387 const expectedOrigin = window.location.origin;
14388 window.addEventListener("message", (e) => {
14389 if (e.origin !== expectedOrigin) {
14390 return;
14391 }
14392 const data = e.data;
14393 if (!data || data.type !== "desktop-mode-recycle-bin-changed") {
14394 return;
14395 }
14396 const ts = typeof data.ts === "number" ? data.ts : Date.now();
14397 if (ts <= store$3.state.seenTs) {
14398 log("postMessage skipped (ts <= seenTs)", { ts, seenTs: store$3.state.seenTs });
14399 return;
14400 }
14401 log("postMessage triggers refetch", { ts, prevSeenTs: store$3.state.seenTs });
14402 store$3.state.seenTs = ts;
14403 void refetchCount();
14404 });
14405 }
14406 function wireHeartbeatProbe() {
14407 const $ = window.jQuery;
14408 if (!$) {
14409 warn("wireHeartbeatProbe: window.jQuery not available — heartbeat path disabled");
14410 return;
14411 }
14412 log("wireHeartbeatProbe: jQuery + heartbeat hooks attached");
14413 $(document).on("heartbeat-send", (...args) => {
14414 const data = args[1];
14415 if (data) {
14416 data[HEARTBEAT_FIELD$1] = store$3.state.seenTs;
14417 }
14418 });
14419 $(document).on("heartbeat-tick", (...args) => {
14420 const response = args[1];
14421 const block = response?.desktop_mode_recycle_bin;
14422 log("heartbeat-tick", { hasBlock: !!block, block });
14423 if (!block) {
14424 return;
14425 }
14426 if (typeof block.ts === "number" && block.ts > store$3.state.seenTs) {
14427 store$3.state.seenTs = block.ts;
14428 }
14429 if (typeof block.count === "number") {
14430 setRecycleBinBadge(block.count);
14431 }
14432 });
14433 }
14434 async function refetchCount() {
14435 if (!store$3.state.countUrl) {
14436 log("refetchCount: no countUrl, skip");
14437 return;
14438 }
14439 log("refetchCount: hitting", store$3.state.countUrl);
14440 try {
14441 const response = await fetch(store$3.state.countUrl, {
14442 credentials: "same-origin",
14443 headers: { Accept: "application/json" }
14444 });
14445 if (!response.ok) {
14446 warn("refetchCount: non-OK", response.status, response.statusText);
14447 return;
14448 }
14449 const json = await response.json();
14450 log("refetchCount: response", json);
14451 if (typeof json.count === "number") {
14452 setRecycleBinBadge(json.count);
14453 }
14454 } catch (err) {
14455 warn("refetchCount: fetch failed", err);
14456 }
14457 }
14458 const OS_SETTINGS_ID = "desktop-mode-os-settings";
14459 const RECYCLE_BIN_ID = "desktop-mode-recycle-bin";
14460 function registerBuiltInPeekRenderers(opts) {
14461 const wpHooks = getWpHooks();
14462 if (!wpHooks) {
14463 return;
14464 }
14465 wpHooks.addFilter(
14466 "desktop-mode.dock.peek-card-content",
14467 "desktop-mode/built-in-peek-renderers",
14468 (body, ctx) => {
14469 const context = ctx;
14470 const id = context.window.id;
14471 if (id === OS_SETTINGS_ID) {
14472 return renderOsSettings();
14473 }
14474 if (id === RECYCLE_BIN_ID) {
14475 return renderRecycleBin(context, opts.getRecycleBinCount);
14476 }
14477 return body;
14478 }
14479 );
14480 }
14481 function renderOsSettings(_ctx) {
14482 const root = document.createElement("span");
14483 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--os-settings";
14484 root.setAttribute("aria-hidden", "true");
14485 const hero = document.createElement("span");
14486 hero.className = "desktop-mode-dock-peek__os-hero dashicons dashicons-admin-generic";
14487 root.appendChild(hero);
14488 const subtitle = document.createElement("span");
14489 subtitle.className = "desktop-mode-dock-peek__os-subtitle";
14490 subtitle.textContent = __("System Preferences");
14491 root.appendChild(subtitle);
14492 const tabs = document.createElement("span");
14493 tabs.className = "desktop-mode-dock-peek__os-tabs";
14494 for (const cls of [
14495 "dashicons-art",
14496 "dashicons-admin-customizer",
14497 "dashicons-editor-help"
14498 ]) {
14499 const tab = document.createElement("span");
14500 tab.className = `desktop-mode-dock-peek__os-tab dashicons ${cls}`;
14501 tabs.appendChild(tab);
14502 }
14503 root.appendChild(tabs);
14504 return root;
14505 }
14506 function renderRecycleBin(_ctx, getCount) {
14507 const root = document.createElement("span");
14508 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--recycle-bin";
14509 root.setAttribute("aria-hidden", "true");
14510 const count = Math.max(0, Math.floor(getCount() || 0));
14511 root.dataset.empty = count === 0 ? "true" : "false";
14512 const stage = document.createElement("span");
14513 stage.className = "desktop-mode-dock-peek__bin-stage";
14514 const stack = document.createElement("span");
14515 stack.className = "desktop-mode-dock-peek__bin-stack";
14516 for (let i = 0; i < 3; i++) {
14517 const slip = document.createElement("span");
14518 slip.className = "desktop-mode-dock-peek__bin-slip";
14519 stack.appendChild(slip);
14520 }
14521 stage.appendChild(stack);
14522 const icon = document.createElement("span");
14523 icon.className = `desktop-mode-dock-peek__bin-icon dashicons ${count === 0 ? "dashicons-trash" : "dashicons-trash"}`;
14524 stage.appendChild(icon);
14525 root.appendChild(stage);
14526 const label = document.createElement("span");
14527 label.className = "desktop-mode-dock-peek__bin-label";
14528 if (count === 0) {
14529 label.textContent = __("Recycle Bin — empty");
14530 } else if (count === 1) {
14531 label.textContent = __("1 item");
14532 } else if (count > 99) {
14533 label.textContent = "99+ items";
14534 } else {
14535 label.textContent = `${count} items`;
14536 }
14537 root.appendChild(label);
14538 return root;
14539 }
14540 function getWpHooks() {
14541 const wp = window.wp;
14542 return wp?.hooks ?? null;
14543 }
14544 const BUG_REPORT_WINDOW_ID = "desktop-mode-bug-report";
14545 const REPO_OWNER = "WordPress";
14546 const REPO_NAME = "desktop-mode";
14547 const MAX_BODY_LENGTH = 6e3;
14548 function renderBugReport(body) {
14549 body.classList.add("desktop-mode-bug-report");
14550 body.replaceChildren();
14551 const form = document.createElement("form");
14552 form.className = "desktop-mode-bug-report__form";
14553 form.setAttribute("novalidate", "");
14554 const intro = document.createElement("p");
14555 intro.className = "desktop-mode-bug-report__intro";
14556 intro.textContent = __(
14557 "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."
14558 );
14559 form.appendChild(intro);
14560 form.appendChild(buildTypeField());
14561 form.appendChild(buildTextField("title", __("Title"), {
14562 placeholder: __("A short summary"),
14563 required: true
14564 }));
14565 form.appendChild(buildTextareaField("description", __("What happened? What did you expect?"), {
14566 placeholder: __("Describe the issue or the feature you have in mind."),
14567 rows: 5,
14568 required: true
14569 }));
14570 form.appendChild(buildTextareaField("steps", __("Steps to reproduce (bug only)"), {
14571 placeholder: __("One step per line"),
14572 rows: 4
14573 }));
14574 const meta = buildMetadataPreview();
14575 form.appendChild(meta);
14576 const actions = document.createElement("div");
14577 actions.className = "desktop-mode-bug-report__actions";
14578 const submit = document.createElement("button");
14579 submit.type = "submit";
14580 submit.className = "desktop-mode-bug-report__submit";
14581 submit.textContent = __("Open issue on GitHub");
14582 actions.appendChild(submit);
14583 const hint = document.createElement("span");
14584 hint.className = "desktop-mode-bug-report__hint";
14585 hint.textContent = __("You will review and submit on GitHub.");
14586 actions.appendChild(hint);
14587 form.appendChild(actions);
14588 form.addEventListener("submit", (e) => {
14589 e.preventDefault();
14590 const state2 = readFormState(form);
14591 if (!state2.title.trim() || !state2.description.trim()) {
14592 showInlineError(form, __("Title and description are both required."));
14593 return;
14594 }
14595 const url = buildGithubIssueUrl(state2);
14596 window.open(url, "_blank", "noopener");
14597 });
14598 body.appendChild(form);
14599 }
14600 function buildTypeField() {
14601 const wrap = document.createElement("div");
14602 wrap.className = "desktop-mode-bug-report__field desktop-mode-bug-report__field--type";
14603 const label = document.createElement("span");
14604 label.className = "desktop-mode-bug-report__label";
14605 label.textContent = __("Type");
14606 wrap.appendChild(label);
14607 const group = document.createElement("div");
14608 group.className = "desktop-mode-bug-report__radio-group";
14609 group.setAttribute("role", "radiogroup");
14610 const options = [
14611 { value: "bug", label: __("Bug"), checked: true },
14612 { value: "feature", label: __("Feature request") },
14613 { value: "question", label: __("Question") }
14614 ];
14615 for (const opt of options) {
14616 const radioLabel = document.createElement("label");
14617 radioLabel.className = "desktop-mode-bug-report__radio";
14618 const input = document.createElement("input");
14619 input.type = "radio";
14620 input.name = "type";
14621 input.value = opt.value;
14622 if (opt.checked) {
14623 input.checked = true;
14624 }
14625 radioLabel.appendChild(input);
14626 const text = document.createElement("span");
14627 text.textContent = opt.label;
14628 radioLabel.appendChild(text);
14629 group.appendChild(radioLabel);
14630 }
14631 wrap.appendChild(group);
14632 return wrap;
14633 }
14634 function buildTextField(name, labelText, opts = {}) {
14635 const wrap = document.createElement("div");
14636 wrap.className = "desktop-mode-bug-report__field";
14637 const label = document.createElement("label");
14638 label.className = "desktop-mode-bug-report__label";
14639 label.textContent = labelText;
14640 wrap.appendChild(label);
14641 const input = document.createElement("input");
14642 input.type = "text";
14643 input.name = name;
14644 input.className = "desktop-mode-bug-report__input";
14645 if (opts.placeholder) {
14646 input.placeholder = opts.placeholder;
14647 }
14648 if (opts.required) {
14649 input.setAttribute("aria-required", "true");
14650 }
14651 label.appendChild(input);
14652 return wrap;
14653 }
14654 function buildTextareaField(name, labelText, opts = {}) {
14655 const wrap = document.createElement("div");
14656 wrap.className = "desktop-mode-bug-report__field";
14657 const label = document.createElement("label");
14658 label.className = "desktop-mode-bug-report__label";
14659 label.textContent = labelText;
14660 wrap.appendChild(label);
14661 const textarea = document.createElement("textarea");
14662 textarea.name = name;
14663 textarea.className = "desktop-mode-bug-report__textarea";
14664 textarea.rows = opts.rows ?? 4;
14665 if (opts.placeholder) {
14666 textarea.placeholder = opts.placeholder;
14667 }
14668 if (opts.required) {
14669 textarea.setAttribute("aria-required", "true");
14670 }
14671 label.appendChild(textarea);
14672 return wrap;
14673 }
14674 function buildMetadataPreview() {
14675 const details = document.createElement("details");
14676 details.className = "desktop-mode-bug-report__metadata";
14677 const summary = document.createElement("summary");
14678 summary.textContent = __("Environment included with the report");
14679 details.appendChild(summary);
14680 const pre = document.createElement("pre");
14681 pre.className = "desktop-mode-bug-report__metadata-body";
14682 pre.textContent = formatMetadata(collectMetadata());
14683 details.appendChild(pre);
14684 return details;
14685 }
14686 function showInlineError(form, msg) {
14687 let banner = form.querySelector(".desktop-mode-bug-report__error");
14688 if (!banner) {
14689 banner = document.createElement("div");
14690 banner.className = "desktop-mode-bug-report__error";
14691 banner.setAttribute("role", "alert");
14692 form.prepend(banner);
14693 }
14694 banner.textContent = msg;
14695 }
14696 function readFormState(form) {
14697 const data = new FormData(form);
14698 return {
14699 type: data.get("type") ?? "bug",
14700 title: data.get("title") ?? "",
14701 description: data.get("description") ?? "",
14702 steps: data.get("steps") ?? ""
14703 };
14704 }
14705 function buildGithubIssueUrl(state2) {
14706 const labels = labelsForType(state2.type);
14707 const body = composeIssueBody(state2);
14708 const params = new URLSearchParams();
14709 params.set("title", state2.title.trim());
14710 params.set("body", body);
14711 if (labels.length) {
14712 params.set("labels", labels.join(","));
14713 }
14714 return `https://github.com/${REPO_OWNER}/${REPO_NAME}/issues/new?${params.toString()}`;
14715 }
14716 function labelsForType(type) {
14717 switch (type) {
14718 case "bug":
14719 return ["bug"];
14720 case "feature":
14721 return ["enhancement"];
14722 case "question":
14723 return ["question"];
14724 default:
14725 return [];
14726 }
14727 }
14728 function composeIssueBody(state2) {
14729 const parts = [];
14730 parts.push(state2.description.trim());
14731 if (state2.type === "bug" && state2.steps.trim()) {
14732 parts.push("");
14733 parts.push("## Steps to reproduce");
14734 parts.push("");
14735 parts.push(state2.steps.trim());
14736 }
14737 parts.push("");
14738 parts.push("<details><summary>Environment</summary>");
14739 parts.push("");
14740 parts.push("```");
14741 parts.push(formatMetadata(collectMetadata()));
14742 parts.push("```");
14743 parts.push("");
14744 parts.push("</details>");
14745 let out = parts.join("\n");
14746 if (out.length > MAX_BODY_LENGTH) {
14747 out = out.slice(0, MAX_BODY_LENGTH) + "\n\n…(truncated to fit GitHub URL length limit)";
14748 }
14749 return out;
14750 }
14751 function collectMetadata() {
14752 const cfg = window.wp?.desktop?.config;
14753 return {
14754 pluginVersion: cfg?.pluginVersion ?? "unknown",
14755 wordpressVersion: cfg?.wordpressVersion ?? "unknown",
14756 userAgent: navigator.userAgent,
14757 viewport: `${window.innerWidth}x${window.innerHeight}`,
14758 platform: navigator.platform || "unknown",
14759 currentUrl: window.location.href
14760 };
14761 }
14762 function formatMetadata(m) {
14763 return [
14764 `Plugin version: ${m.pluginVersion}`,
14765 `WordPress version: ${m.wordpressVersion}`,
14766 `User agent: ${m.userAgent}`,
14767 `Viewport: ${m.viewport}`,
14768 `Platform: ${m.platform}`,
14769 `Current URL: ${m.currentUrl}`
14770 ].join("\n");
14771 }
14772 let _config = null;
14773 let _state = {
14774 installHintDismissed: false,
14775 notificationsEnabled: false
14776 };
14777 const _listeners = /* @__PURE__ */ new Set();
14778 function initPwaState(config) {
14779 if (!config) {
14780 _config = null;
14781 return;
14782 }
14783 _config = config;
14784 _state = { ...config.state };
14785 notify$4();
14786 }
14787 function getPwaState() {
14788 return { ..._state };
14789 }
14790 function updatePwaState(patch) {
14791 _state = { ..._state, ...patch };
14792 notify$4();
14793 if (!_config) {
14794 return getPwaState();
14795 }
14796 const body = JSON.stringify(patch);
14797 const nonce = readRestNonce$2();
14798 void fetch(_config.stateUrl, {
14799 method: "POST",
14800 credentials: "same-origin",
14801 headers: {
14802 "Content-Type": "application/json",
14803 ...nonce ? { "X-WP-Nonce": nonce } : {}
14804 },
14805 body
14806 }).catch((err) => {
14807 if (typeof console !== "undefined") {
14808 console.warn("[desktop-mode] pwa-state write failed:", err);
14809 }
14810 });
14811 return getPwaState();
14812 }
14813 function subscribePwaState(cb) {
14814 _listeners.add(cb);
14815 return () => {
14816 _listeners.delete(cb);
14817 };
14818 }
14819 function notify$4() {
14820 const snapshot = getPwaState();
14821 for (const cb of Array.from(_listeners)) {
14822 try {
14823 cb(snapshot);
14824 } catch (err) {
14825 if (typeof console !== "undefined") {
14826 console.error(
14827 "[desktop-mode] pwa-state listener threw:",
14828 err
14829 );
14830 }
14831 }
14832 }
14833 }
14834 function readRestNonce$2() {
14835 const cfg = window.desktopModeConfig;
14836 return cfg?.restNonce ?? "";
14837 }
14838 const state = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
14839 __proto__: null,
14840 getPwaState,
14841 initPwaState,
14842 subscribePwaState,
14843 updatePwaState
14844 }, Symbol.toStringTag, { value: "Module" }));
14845 let _registration = null;
14846 let _registrationFailed = false;
14847 let _controllerChangeBound = false;
14848 let _reloadingForSwUpdate = false;
14849 let _status = "pending";
14850 function bindControllerChangeReload() {
14851 if (_controllerChangeBound) {
14852 return;
14853 }
14854 _controllerChangeBound = true;
14855 const hadInitialController = !!navigator.serviceWorker.controller;
14856 navigator.serviceWorker.addEventListener("controllerchange", () => {
14857 if (!hadInitialController) {
14858 return;
14859 }
14860 if (_reloadingForSwUpdate) {
14861 return;
14862 }
14863 if (wasRecentlyReloadedForSwUpdate()) {
14864 return;
14865 }
14866 markReloadedForSwUpdate();
14867 _reloadingForSwUpdate = true;
14868 setTimeout(() => window.location.reload(), 0);
14869 });
14870 }
14871 const SW_RELOAD_THROTTLE_KEY = "wpd-sw-reload-ts";
14872 const SW_RELOAD_THROTTLE_MS = 3e4;
14873 function wasRecentlyReloadedForSwUpdate() {
14874 try {
14875 const raw = sessionStorage.getItem(SW_RELOAD_THROTTLE_KEY);
14876 const last = raw ? Number.parseInt(raw, 10) : 0;
14877 if (!Number.isFinite(last) || last <= 0) {
14878 return false;
14879 }
14880 return Date.now() - last < SW_RELOAD_THROTTLE_MS;
14881 } catch {
14882 return false;
14883 }
14884 }
14885 function markReloadedForSwUpdate() {
14886 try {
14887 sessionStorage.setItem(SW_RELOAD_THROTTLE_KEY, String(Date.now()));
14888 } catch {
14889 }
14890 }
14891 async function registerServiceWorker(config, options = {}) {
14892 if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
14893 _status = "unsupported";
14894 return null;
14895 }
14896 if (!config?.swUrl) {
14897 _status = "unsupported";
14898 return null;
14899 }
14900 if (!window.isSecureContext) {
14901 _status = "unsupported";
14902 return null;
14903 }
14904 if (_registration || _registrationFailed) {
14905 return _registration;
14906 }
14907 if (!options.forceReplace) {
14908 const existing = await navigator.serviceWorker.getRegistrations().catch(() => []);
14909 const foreign = existing.find((reg) => {
14910 const url = reg.active?.scriptURL ?? reg.installing?.scriptURL ?? "";
14911 return url !== "" && url !== config.swUrl;
14912 });
14913 if (foreign) {
14914 _status = "foreign-sw";
14915 if (typeof console !== "undefined") {
14916 console.warn(
14917 "[desktop-mode] another service worker is already registered (" + foreign.scope + "); skipping desktop-mode SW. Set desktop_mode_pwa_force_replace_sw=true to override."
14918 );
14919 }
14920 return null;
14921 }
14922 }
14923 try {
14924 _registration = await navigator.serviceWorker.register(config.swUrl, {
14925 scope: "/",
14926 updateViaCache: "none"
14927 });
14928 _status = "registered";
14929 bindControllerChangeReload();
14930 return _registration;
14931 } catch (err) {
14932 _registrationFailed = true;
14933 _status = "failed";
14934 if (typeof console !== "undefined") {
14935 console.warn("[desktop-mode] SW registration failed:", err);
14936 }
14937 return null;
14938 }
14939 }
14940 function getSwRegistrationStatus() {
14941 return _status;
14942 }
14943 const PWA_INSTALL_TILE_ID = "desktop-mode-pwa-install";
14944 function isStandaloneDisplay() {
14945 if (typeof window === "undefined") {
14946 return false;
14947 }
14948 if (window.matchMedia?.("(display-mode: standalone)").matches) {
14949 return true;
14950 }
14951 const nav = window.navigator;
14952 return nav.standalone === true;
14953 }
14954 async function isLikelyInstalled() {
14955 if (isStandaloneDisplay()) {
14956 return true;
14957 }
14958 const nav = window.navigator;
14959 if (typeof nav.getInstalledRelatedApps !== "function") {
14960 return false;
14961 }
14962 try {
14963 const apps = await nav.getInstalledRelatedApps();
14964 return Array.isArray(apps) && apps.length > 0;
14965 } catch {
14966 return false;
14967 }
14968 }
14969 let _deferred = null;
14970 function installPwaInstallAffordance(siteName, showToast2) {
14971 if (typeof window === "undefined") {
14972 return;
14973 }
14974 window.removeEventListener(
14975 "beforeinstallprompt",
14976 _handleBeforeInstall
14977 );
14978 window.addEventListener(
14979 "beforeinstallprompt",
14980 _handleBeforeInstall
14981 );
14982 window.removeEventListener("appinstalled", _handleAppInstalled);
14983 window.addEventListener("appinstalled", _handleAppInstalled);
14984 function _handleBeforeInstall(ev) {
14985 ev.preventDefault();
14986 _deferred = ev;
14987 }
14988 function _handleAppInstalled() {
14989 _deferred = null;
14990 showToast2({
14991 message: sprintf(
14992 /* translators: %s: site name */
14993 __("Installed %s as an app."),
14994 siteName
14995 )
14996 });
14997 }
14998 }
14999 function getInstallTileDef(siteName, showToast2) {
15000 return {
15001 id: PWA_INSTALL_TILE_ID,
15002 title: sprintf(
15003 /* translators: %s: site name */
15004 __("Install %s as an app"),
15005 siteName
15006 ),
15007 // Dashicons class — the dock renderer prefers Dashicons
15008 // strings. `dashicons-download` is the closest match for
15009 // "install" in the WordPress glyph set without shipping
15010 // bespoke artwork.
15011 icon: "dashicons-download",
15012 onOpen: () => {
15013 void onTileClick(siteName, showToast2);
15014 }
15015 };
15016 }
15017 async function onTileClick(siteName, showToast2) {
15018 if (_deferred) {
15019 const event = _deferred;
15020 _deferred = null;
15021 try {
15022 await event.prompt();
15023 const choice = await event.userChoice;
15024 if (choice.outcome === "dismissed") {
15025 showToast2({
15026 message: __("Install cancelled.")
15027 });
15028 }
15029 } catch (err) {
15030 if (typeof console !== "undefined") {
15031 console.warn(
15032 "[desktop-mode] install prompt failed:",
15033 err
15034 );
15035 }
15036 }
15037 return;
15038 }
15039 if (await isLikelyInstalled()) {
15040 showToast2({
15041 message: sprintf(
15042 /* translators: %s: site name */
15043 __(
15044 "%s is already installed. Open it from your apps menu or home screen."
15045 ),
15046 siteName
15047 )
15048 });
15049 return;
15050 }
15051 if (getSwRegistrationStatus() === "foreign-sw") {
15052 showToast2({
15053 message: __(
15054 "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."
15055 )
15056 });
15057 return;
15058 }
15059 showToast2({
15060 message: __(
15061 "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."
15062 )
15063 });
15064 }
15065 async function promptInstall() {
15066 if (!_deferred) {
15067 return "unavailable";
15068 }
15069 const event = _deferred;
15070 _deferred = null;
15071 try {
15072 await event.prompt();
15073 const choice = await event.userChoice;
15074 return choice.outcome;
15075 } catch {
15076 return "unavailable";
15077 }
15078 }
15079 function undismissInstallHint() {
15080 Promise.resolve().then(() => state).then((m) => {
15081 m.updatePwaState({ installHintDismissed: false });
15082 });
15083 }
15084 function notify$3(options) {
15085 const intent = activity.filter(
15086 "desktop-mode/notification-requested",
15087 { ...options }
15088 );
15089 if (!intent || intent.cancel === true || !intent.title) {
15090 return () => void 0;
15091 }
15092 let dismissed = false;
15093 let dismissNative = null;
15094 let dismissToast = null;
15095 const dismiss = () => {
15096 if (dismissed) {
15097 return;
15098 }
15099 dismissed = true;
15100 if (dismissNative) {
15101 dismissNative();
15102 }
15103 if (dismissToast) {
15104 dismissToast();
15105 }
15106 };
15107 const fallback = () => {
15108 dismissToast = showToast({
15109 message: intent.body ? intent.title + " — " + intent.body : intent.title
15110 });
15111 activity.publish("desktop-mode/notification-shown", {
15112 ...intent,
15113 fallback: "toast"
15114 });
15115 };
15116 if (typeof window === "undefined" || typeof Notification === "undefined") {
15117 fallback();
15118 return dismiss;
15119 }
15120 const perm = Notification.permission;
15121 if (perm === "granted") {
15122 dismissNative = renderNative(intent);
15123 return dismiss;
15124 }
15125 if (perm === "denied") {
15126 fallback();
15127 return dismiss;
15128 }
15129 void Notification.requestPermission().then((result) => {
15130 if (dismissed) {
15131 return;
15132 }
15133 if (result === "granted") {
15134 updatePwaState({ notificationsEnabled: true });
15135 dismissNative = renderNative(intent);
15136 return;
15137 }
15138 fallback();
15139 });
15140 return dismiss;
15141 }
15142 function renderNative(intent) {
15143 let n = null;
15144 try {
15145 n = new Notification(intent.title, {
15146 body: intent.body,
15147 icon: intent.icon,
15148 tag: intent.tag,
15149 requireInteraction: intent.requireInteraction
15150 });
15151 } catch (err) {
15152 if (typeof console !== "undefined") {
15153 console.warn("[desktop-mode] Notification ctor threw:", err);
15154 }
15155 return () => void 0;
15156 }
15157 if (intent.onClick) {
15158 const handler = intent.onClick;
15159 n.onclick = () => {
15160 try {
15161 handler(n);
15162 } catch (hErr) {
15163 if (typeof console !== "undefined") {
15164 console.error(
15165 "[desktop-mode] notification onClick threw:",
15166 hErr
15167 );
15168 }
15169 }
15170 };
15171 }
15172 activity.publish("desktop-mode/notification-shown", {
15173 ...intent,
15174 fallback: null
15175 });
15176 return () => {
15177 if (n) {
15178 n.close();
15179 }
15180 };
15181 }
15182 async function requestNotificationPermission() {
15183 if (typeof Notification === "undefined") {
15184 return "unsupported";
15185 }
15186 if (Notification.permission !== "default") {
15187 return Notification.permission;
15188 }
15189 const result = await Notification.requestPermission();
15190 if (result === "granted") {
15191 updatePwaState({ notificationsEnabled: true });
15192 }
15193 return result;
15194 }
15195 function getNotificationPermission() {
15196 if (typeof Notification === "undefined") {
15197 return "unsupported";
15198 }
15199 return Notification.permission;
15200 }
15201 function bootstrapPwa(config, showToast2) {
15202 if (!config.pwa) {
15203 return;
15204 }
15205 initPwaState(config.pwa);
15206 installPwaInstallAffordance(
15207 config.pwa.appName || "WordPress",
15208 showToast2
15209 );
15210 void registerServiceWorker(config.pwa, {
15211 forceReplace: !!config.pwa.forceReplaceSw
15212 });
15213 }
15214 const DRAG_BRIDGE_EVENTS = {
15215 START: "desktop-mode-cross-frame-drag-start",
15216 END: "desktop-mode-cross-frame-drag-end"
15217 };
15218 function isStart(m) {
15219 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-start" && !!m.payload && typeof m.payload === "object";
15220 }
15221 function isEnd(m) {
15222 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-end";
15223 }
15224 function isPayloadRequest(m) {
15225 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-payload-request";
15226 }
15227 function normalizeLegacyPayload(payload) {
15228 const obj = payload;
15229 if (obj.kind !== void 0 && obj.kind !== null) {
15230 return payload;
15231 }
15232 if (typeof obj.id === "number" && typeof obj.url === "string" && typeof obj.mime === "string") {
15233 return {
15234 kind: "attachment",
15235 id: obj.id,
15236 url: obj.url,
15237 title: typeof obj.title === "string" ? obj.title : "",
15238 alt: typeof obj.alt === "string" ? obj.alt : "",
15239 mime: obj.mime,
15240 thumbnailUrl: typeof obj.thumbnailUrl === "string" ? obj.thumbnailUrl : void 0,
15241 sizes: obj.sizes && typeof obj.sizes === "object" ? obj.sizes : void 0
15242 };
15243 }
15244 return payload;
15245 }
15246 class DragBridge {
15247 constructor() {
15248 this._payload = null;
15249 this._onMessage = (e) => {
15250 if (e.origin !== this._origin) {
15251 return;
15252 }
15253 const msg = e.data;
15254 if (isStart(msg)) {
15255 this._startDrag(msg.payload);
15256 return;
15257 }
15258 if (isEnd(msg)) {
15259 this._endDrag();
15260 return;
15261 }
15262 if (isPayloadRequest(msg) && this._payload && e.source) {
15263 try {
15264 e.source.postMessage(
15265 { type: "desktop-mode-drag-payload", payload: this._payload },
15266 this._origin
15267 );
15268 } catch {
15269 }
15270 }
15271 };
15272 this._origin = window.location.origin;
15273 window.addEventListener("message", this._onMessage);
15274 }
15275 getPayload() {
15276 return this._payload;
15277 }
15278 isDragging() {
15279 return this._payload !== null;
15280 }
15281 start(payload) {
15282 if (this._payload === payload) {
15283 return;
15284 }
15285 this._startDrag(payload);
15286 }
15287 end() {
15288 this._endDrag();
15289 }
15290 _startDrag(payload) {
15291 const normalized = normalizeLegacyPayload(payload);
15292 this._payload = normalized;
15293 document.dispatchEvent(
15294 new CustomEvent(DRAG_BRIDGE_EVENTS.START, {
15295 detail: { payload: normalized }
15296 })
15297 );
15298 }
15299 _endDrag() {
15300 if (this._payload === null) {
15301 return;
15302 }
15303 const payload = this._payload;
15304 this._payload = null;
15305 document.dispatchEvent(
15306 new CustomEvent(DRAG_BRIDGE_EVENTS.END, { detail: { payload } })
15307 );
15308 }
15309 }
15310 class DropTargetRegistry {
15311 constructor() {
15312 this._targets = /* @__PURE__ */ new Map();
15313 this._byElement = /* @__PURE__ */ new Map();
15314 }
15315 register(target2) {
15316 const prev = this._targets.get(target2.id);
15317 if (prev) {
15318 this._byElement.delete(prev.element);
15319 }
15320 this._targets.set(target2.id, target2);
15321 this._byElement.set(target2.element, target2);
15322 return () => {
15323 const cur = this._targets.get(target2.id);
15324 if (cur === target2) {
15325 this._targets.delete(target2.id);
15326 this._byElement.delete(target2.element);
15327 }
15328 };
15329 }
15330 list() {
15331 return Array.from(this._targets.values());
15332 }
15333 clear() {
15334 this._targets.clear();
15335 this._byElement.clear();
15336 }
15337 /**
15338 * Find the deepest registered target whose element is `el` or an
15339 * ancestor of `el`. Walks the DOM tree once (O(depth)).
15340 *
15341 * Window claim boundary: if the walk crosses a `.desktop-mode-window`
15342 * element BEFORE finding a registered target, hit-testing stops
15343 * there and returns null. This is the rule that makes "drag over
15344 * a Gutenberg admin window" produce reject feedback instead of
15345 * silently routing the drop to the wallpaper canvas underneath.
15346 *
15347 * A window can opt INTO accepting drops by registering a target
15348 * on its own body (e.g. Recycle Bin's `[data-desktop-mode-recycle-bin-root]`):
15349 * since that element sits inside the window, the walk hits it
15350 * before reaching the window boundary and the body's target wins.
15351 */
15352 hitTest(el) {
15353 let cur = el;
15354 while (cur) {
15355 if (cur instanceof HTMLElement) {
15356 const t = this._byElement.get(cur);
15357 if (t) {
15358 return t;
15359 }
15360 if (cur.classList.contains("desktop-mode-window")) {
15361 return null;
15362 }
15363 }
15364 cur = cur.parentElement;
15365 }
15366 return null;
15367 }
15368 /**
15369 * Convenience: pick the target at viewport `(clientX, clientY)`.
15370 * Caller is responsible for hiding any obscuring ghost element
15371 * before calling — see `GhostHandle.withHidden()`.
15372 */
15373 hitTestPoint(clientX, clientY) {
15374 const el = document.elementFromPoint(clientX, clientY);
15375 const target2 = this.hitTest(el);
15376 return { target: target2, element: el, accepted: false };
15377 }
15378 }
15379 const GHOST_CLASS = "desktop-mode-drag-ghost";
15380 const GHOST_ACCEPT_CLASS = "desktop-mode-drag-ghost--accept";
15381 const GHOST_REJECT_CLASS = "desktop-mode-drag-ghost--reject";
15382 const HINT_CLASS = "desktop-mode-drag-hint";
15383 const HINT_ACCEPT_CLASS = "desktop-mode-drag-hint--accept";
15384 const HINT_REJECT_CLASS = "desktop-mode-drag-hint--reject";
15385 const HINT_NEUTRAL_CLASS = "desktop-mode-drag-hint--neutral";
15386 const HINT_OFFSET_X = 16;
15387 const HINT_OFFSET_Y = 18;
15388 function mountGhost(payload, clientX, clientY) {
15389 const ghost = buildGhost(payload);
15390 const offsetX = payload.ghost?.offsetX ?? defaultOffsetX(payload.source);
15391 const offsetY = payload.ghost?.offsetY ?? defaultOffsetY(payload.source);
15392 ghost.classList.add(GHOST_CLASS);
15393 ghost.setAttribute("aria-hidden", "true");
15394 ghost.style.position = "fixed";
15395 ghost.style.left = "0";
15396 ghost.style.top = "0";
15397 ghost.style.margin = "0";
15398 ghost.style.pointerEvents = "none";
15399 ghost.style.zIndex = "2147483647";
15400 ghost.style.willChange = "transform";
15401 document.body.appendChild(ghost);
15402 const labels = resolveHintLabels(payload);
15403 const hint = labels ? buildHintChip() : null;
15404 if (hint) {
15405 document.body.appendChild(hint);
15406 }
15407 const handle = {
15408 get element() {
15409 return ghost;
15410 },
15411 moveTo(cx, cy) {
15412 ghost.style.transform = `translate3d(${cx - offsetX}px, ${cy - offsetY}px, 0)`;
15413 if (hint) {
15414 hint.style.transform = `translate3d(${cx + HINT_OFFSET_X}px, ${cy + HINT_OFFSET_Y}px, 0)`;
15415 }
15416 },
15417 setMode(mode, overrides) {
15418 ghost.classList.remove(GHOST_ACCEPT_CLASS, GHOST_REJECT_CLASS);
15419 if (mode === "accept") {
15420 ghost.classList.add(GHOST_ACCEPT_CLASS);
15421 } else if (mode === "reject") {
15422 ghost.classList.add(GHOST_REJECT_CLASS);
15423 }
15424 if (hint && labels) {
15425 hint.classList.remove(
15426 HINT_ACCEPT_CLASS,
15427 HINT_REJECT_CLASS,
15428 HINT_NEUTRAL_CLASS
15429 );
15430 if (mode === "accept") {
15431 hint.classList.add(HINT_ACCEPT_CLASS);
15432 hint.textContent = overrides?.acceptLabel ?? labels.accept;
15433 } else if (mode === "reject") {
15434 hint.classList.add(HINT_REJECT_CLASS);
15435 hint.textContent = labels.reject;
15436 } else {
15437 hint.classList.add(HINT_NEUTRAL_CLASS);
15438 hint.textContent = labels.neutral;
15439 }
15440 hint.hidden = !hint.textContent;
15441 }
15442 },
15443 withHidden(fn) {
15444 const prevG = ghost.style.visibility;
15445 const prevH = hint?.style.visibility ?? "";
15446 ghost.style.visibility = "hidden";
15447 if (hint) {
15448 hint.style.visibility = "hidden";
15449 }
15450 try {
15451 return fn();
15452 } finally {
15453 ghost.style.visibility = prevG;
15454 if (hint) {
15455 hint.style.visibility = prevH;
15456 }
15457 }
15458 },
15459 dispose() {
15460 if (ghost.isConnected) {
15461 ghost.remove();
15462 }
15463 if (hint?.isConnected) {
15464 hint.remove();
15465 }
15466 }
15467 };
15468 handle.moveTo(clientX, clientY);
15469 handle.setMode("neutral");
15470 return handle;
15471 }
15472 function buildHintChip() {
15473 const chip = document.createElement("div");
15474 chip.className = HINT_CLASS;
15475 chip.setAttribute("aria-hidden", "true");
15476 chip.setAttribute("role", "presentation");
15477 chip.style.position = "fixed";
15478 chip.style.left = "0";
15479 chip.style.top = "0";
15480 chip.style.margin = "0";
15481 chip.style.pointerEvents = "none";
15482 chip.style.zIndex = "2147483647";
15483 chip.style.willChange = "transform";
15484 return chip;
15485 }
15486 function resolveHintLabels(payload) {
15487 const cfg = payload.ghost?.hint;
15488 if (cfg?.hidden) {
15489 return null;
15490 }
15491 return {
15492 accept: cfg?.accept ?? defaultAcceptLabel(payload),
15493 reject: cfg?.reject ?? defaultRejectLabel(),
15494 neutral: cfg?.neutral ?? defaultNeutralLabel(payload)
15495 };
15496 }
15497 function defaultAcceptLabel(payload) {
15498 if (payload.type === "shortcut") {
15499 return __("Drop here to create shortcut", "desktop-mode");
15500 }
15501 if (payload.type === "desktop-file") {
15502 return __("Drop here to move", "desktop-mode");
15503 }
15504 return __("Drop here", "desktop-mode");
15505 }
15506 function defaultRejectLabel(_payload) {
15507 return __("Can’t drop here", "desktop-mode");
15508 }
15509 function defaultNeutralLabel(payload) {
15510 if (payload.type === "shortcut") {
15511 return __(
15512 "Drop on the desktop or a folder",
15513 "desktop-mode"
15514 );
15515 }
15516 if (payload.type === "desktop-file") {
15517 return __("Drop in a folder", "desktop-mode");
15518 }
15519 return "";
15520 }
15521 function buildGhost(payload) {
15522 if (payload.ghost?.element) {
15523 return payload.ghost.element;
15524 }
15525 const clone = payload.source.cloneNode(true);
15526 clone.removeAttribute("id");
15527 const rect = payload.source.getBoundingClientRect();
15528 clone.style.width = `${rect.width}px`;
15529 clone.style.height = `${rect.height}px`;
15530 return clone;
15531 }
15532 function defaultOffsetX(source) {
15533 return source.offsetWidth / 2;
15534 }
15535 function defaultOffsetY(source) {
15536 return source.offsetHeight / 2;
15537 }
15538 let _installed$2 = false;
15539 function installRecovery(cancelActive) {
15540 if (_installed$2) {
15541 return;
15542 }
15543 _installed$2 = true;
15544 document.addEventListener("keydown", (e) => {
15545 if (e.key === "Escape") {
15546 cancelActive("escape");
15547 }
15548 });
15549 window.addEventListener("blur", () => {
15550 cancelActive("blur");
15551 });
15552 document.addEventListener("visibilitychange", () => {
15553 if (document.hidden) {
15554 cancelActive("visibility");
15555 }
15556 });
15557 }
15558 const DRAG_THRESHOLD_PX = 4;
15559 const DRAG_EVENTS = {
15560 START: "desktop-mode.drag.start",
15561 MOVE: "desktop-mode.drag.move",
15562 ENTER: "desktop-mode.drag.enter",
15563 LEAVE: "desktop-mode.drag.leave",
15564 REJECTED: "desktop-mode.drag.rejected",
15565 COMMIT: "desktop-mode.drag.commit",
15566 CANCEL: "desktop-mode.drag.cancel",
15567 END: "desktop-mode.drag.end"
15568 };
15569 const SOURCE_DRAGGING_CLASS = "desktop-mode-file-tile--dragging";
15570 const TARGET_DROP_ACTIVE_CLASS = "desktop-mode-file-tile--drop-target";
15571 const TRASH_DROP_ACTIVE_ATTR$1 = "data-desktop-mode-trash-drop-active";
15572 const FILES_DROP_ACTIVE_ATTR = "data-files-drop-active";
15573 const BODY_DRAGGING_ATTR = "data-desktop-mode-dragging";
15574 const BODY_DRAG_TYPE_ATTR = "data-desktop-mode-drag-type";
15575 const BODY_DRAG_MODE_ATTR = "data-desktop-mode-drag-mode";
15576 class DragManager {
15577 constructor() {
15578 this._registry = new DropTargetRegistry();
15579 this._active = null;
15580 this._docListenersAttached = false;
15581 this._lastLiftedEndAt = 0;
15582 this._onPointerMove = (e) => {
15583 const session = this._active;
15584 if (!session || session._pointerId !== e.pointerId) {
15585 return;
15586 }
15587 const dx = e.clientX - session._origin.clientX;
15588 const dy = e.clientY - session._origin.clientY;
15589 if (!session._lifted) {
15590 if (Math.abs(dx) < DRAG_THRESHOLD_PX && Math.abs(dy) < DRAG_THRESHOLD_PX) {
15591 return;
15592 }
15593 this._lift(session, e);
15594 }
15595 if (!session._ghost) {
15596 return;
15597 }
15598 session._ghost.moveTo(e.clientX, e.clientY);
15599 this._updateHover(session, e.clientX, e.clientY);
15600 dispatchOnDocument(DRAG_EVENTS.MOVE, {
15601 payload: session.payload,
15602 clientX: e.clientX,
15603 clientY: e.clientY
15604 });
15605 };
15606 this._onPointerUp = (e) => {
15607 const session = this._active;
15608 if (!session || session._pointerId !== e.pointerId) {
15609 return;
15610 }
15611 if (!session._lifted) {
15612 session._finished = true;
15613 this._active = null;
15614 try {
15615 session._callbacks.onClickOnly?.();
15616 } catch (err) {
15617 console.error("[desktop-mode] drag onClickOnly threw:", err);
15618 }
15619 return;
15620 }
15621 const hit = this._hitTestNow(session, e.clientX, e.clientY);
15622 if (hit && hit.accepted && hit.target) {
15623 this._commit(session, hit.target, e.clientX, e.clientY);
15624 return;
15625 }
15626 this._cancel(session, hit && hit.target ? "rejected" : "no-target");
15627 };
15628 this._onPointerCancel = (e) => {
15629 const session = this._active;
15630 if (!session || session._pointerId !== e.pointerId) {
15631 return;
15632 }
15633 this._cancel(session, "pointercancel");
15634 };
15635 }
15636 start(opts) {
15637 if (this._active) {
15638 return null;
15639 }
15640 if (opts.origin.button !== 0) {
15641 return null;
15642 }
15643 const session = {
15644 payload: opts.payload,
15645 isFinished: () => session._finished,
15646 cancel: (reason) => this._cancel(session, reason ?? "caller"),
15647 _origin: opts.origin,
15648 _pointerId: opts.origin.pointerId,
15649 _lifted: false,
15650 _finished: false,
15651 _callbacks: {
15652 onClickOnly: opts.onClickOnly,
15653 onCancel: opts.onCancel,
15654 onCommit: opts.onCommit
15655 },
15656 _ghost: null,
15657 _currentTarget: null,
15658 _currentAccepted: false
15659 };
15660 this._active = session;
15661 this._ensureDocListeners();
15662 installRecovery((reason) => {
15663 if (this._active) {
15664 this._cancel(this._active, reason);
15665 }
15666 });
15667 return session;
15668 }
15669 registerDropTarget(target2) {
15670 return this._registry.register(target2);
15671 }
15672 isDragging() {
15673 return this._active !== null && this._active._lifted;
15674 }
15675 /**
15676 * Whether a real (lifted) drag ended within `withinMs` of now.
15677 * Surfaces that bind plain `click` listeners use this to ignore
15678 * the synthesized click that fires after a drop. 500 ms is a
15679 * generous default — browsers fire the click within 10–50 ms of
15680 * pointerup, but plugins may chain post-drag work into a
15681 * `requestAnimationFrame` and call back into a click-driven API.
15682 *
15683 * @public
15684 * @since 0.18.x
15685 */
15686 recentlyEndedDrag(withinMs = 500) {
15687 if (this._lastLiftedEndAt === 0) {
15688 return false;
15689 }
15690 return Date.now() - this._lastLiftedEndAt < withinMs;
15691 }
15692 getActive() {
15693 return this._active;
15694 }
15695 debug() {
15696 return {
15697 findOrphans: () => findOrphans(),
15698 listTargets: () => this._registry.list()
15699 };
15700 }
15701 // -----------------------------------------------------------------
15702 // Internals
15703 // -----------------------------------------------------------------
15704 _ensureDocListeners() {
15705 if (this._docListenersAttached) {
15706 return;
15707 }
15708 this._docListenersAttached = true;
15709 document.addEventListener("pointermove", this._onPointerMove, true);
15710 document.addEventListener("pointerup", this._onPointerUp, true);
15711 document.addEventListener("pointercancel", this._onPointerCancel, true);
15712 }
15713 _lift(session, e) {
15714 session._lifted = true;
15715 session.payload.source.classList.add(SOURCE_DRAGGING_CLASS);
15716 session._ghost = mountGhost(session.payload, e.clientX, e.clientY);
15717 if (typeof document !== "undefined" && document.body) {
15718 document.body.setAttribute(BODY_DRAGGING_ATTR, "");
15719 document.body.setAttribute(
15720 BODY_DRAG_TYPE_ATTR,
15721 String(session.payload.type)
15722 );
15723 document.body.setAttribute(BODY_DRAG_MODE_ATTR, "neutral");
15724 }
15725 dispatchOnDocument(DRAG_EVENTS.START, { payload: session.payload });
15726 }
15727 _hitTestNow(session, clientX, clientY) {
15728 const run = () => {
15729 const el = document.elementFromPoint(clientX, clientY);
15730 const target2 = this._registry.hitTest(el);
15731 if (!target2) {
15732 return { target: null, accepted: false };
15733 }
15734 let accepted = false;
15735 try {
15736 accepted = target2.accept(session.payload);
15737 } catch (err) {
15738 console.error("[desktop-mode] drop target accept() threw:", target2.id, err);
15739 }
15740 return { target: target2, accepted };
15741 };
15742 if (session._ghost) {
15743 return session._ghost.withHidden(run);
15744 }
15745 return run();
15746 }
15747 _updateHover(session, clientX, clientY) {
15748 const next = this._hitTestNow(session, clientX, clientY);
15749 const prevTarget = session._currentTarget;
15750 if (next.target === prevTarget && next.accepted === session._currentAccepted) {
15751 return;
15752 }
15753 if (prevTarget) {
15754 fireLeave(prevTarget, session);
15755 }
15756 session._currentTarget = next.target;
15757 session._currentAccepted = next.accepted;
15758 let mode;
15759 if (next.target) {
15760 if (next.accepted) {
15761 fireEnter(next.target, session);
15762 session._ghost?.setMode("accept", {
15763 acceptLabel: next.target.acceptLabel
15764 });
15765 mode = "accept";
15766 } else {
15767 session._ghost?.setMode("reject");
15768 dispatchOnDocument(DRAG_EVENTS.REJECTED, {
15769 payload: session.payload,
15770 targetId: next.target.id
15771 });
15772 mode = "reject";
15773 }
15774 } else {
15775 session._ghost?.setMode("reject");
15776 mode = "reject";
15777 }
15778 if (typeof document !== "undefined" && document.body) {
15779 document.body.setAttribute(BODY_DRAG_MODE_ATTR, mode);
15780 }
15781 }
15782 _commit(session, target2, clientX, clientY) {
15783 session._finished = true;
15784 this._lastLiftedEndAt = Date.now();
15785 fireLeave(target2, session);
15786 this._cleanupDom(session);
15787 const prevActive = this._active;
15788 this._active = null;
15789 try {
15790 void target2.onDrop(session, { clientX, clientY });
15791 } catch (err) {
15792 console.error("[desktop-mode] drop target onDrop threw:", target2.id, err);
15793 }
15794 try {
15795 session._callbacks.onCommit?.(target2);
15796 } catch (err) {
15797 console.error("[desktop-mode] drag onCommit threw:", err);
15798 }
15799 dispatchOnDocument(DRAG_EVENTS.COMMIT, {
15800 payload: session.payload,
15801 targetId: target2.id
15802 });
15803 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason: "commit" });
15804 if (this._active === prevActive) {
15805 this._active = null;
15806 }
15807 }
15808 _cancel(session, reason) {
15809 if (session._finished) {
15810 return;
15811 }
15812 session._finished = true;
15813 if (session._lifted) {
15814 this._lastLiftedEndAt = Date.now();
15815 }
15816 if (session._currentTarget) {
15817 fireLeave(session._currentTarget, session);
15818 }
15819 this._cleanupDom(session);
15820 this._active = null;
15821 try {
15822 session._callbacks.onCancel?.(reason);
15823 } catch (err) {
15824 console.error("[desktop-mode] drag onCancel threw:", err);
15825 }
15826 dispatchOnDocument(DRAG_EVENTS.CANCEL, { payload: session.payload, reason });
15827 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason });
15828 }
15829 _cleanupDom(session) {
15830 try {
15831 session.payload.source.classList.remove(SOURCE_DRAGGING_CLASS);
15832 } catch {
15833 }
15834 session._ghost?.dispose();
15835 session._ghost = null;
15836 session._currentTarget = null;
15837 session._currentAccepted = false;
15838 if (typeof document !== "undefined" && document.body) {
15839 document.body.removeAttribute(BODY_DRAGGING_ATTR);
15840 document.body.removeAttribute(BODY_DRAG_TYPE_ATTR);
15841 document.body.removeAttribute(BODY_DRAG_MODE_ATTR);
15842 }
15843 scrubOrphans();
15844 }
15845 }
15846 function dispatchOnDocument(type, detail) {
15847 if (typeof document === "undefined") {
15848 return;
15849 }
15850 document.dispatchEvent(new CustomEvent(type, { detail }));
15851 }
15852 function fireEnter(target2, session) {
15853 try {
15854 target2.onEnter?.(session);
15855 } catch (err) {
15856 console.error("[desktop-mode] drop target onEnter threw:", target2.id, err);
15857 }
15858 dispatchOnDocument(DRAG_EVENTS.ENTER, {
15859 payload: session.payload,
15860 targetId: target2.id
15861 });
15862 }
15863 function fireLeave(target2, session) {
15864 try {
15865 target2.onLeave?.(session);
15866 } catch (err) {
15867 console.error("[desktop-mode] drop target onLeave threw:", target2.id, err);
15868 }
15869 dispatchOnDocument(DRAG_EVENTS.LEAVE, {
15870 payload: session.payload,
15871 targetId: target2.id
15872 });
15873 }
15874 function findOrphans() {
15875 if (typeof document === "undefined") {
15876 return [];
15877 }
15878 const out = [];
15879 for (const sel of [
15880 `.${SOURCE_DRAGGING_CLASS}`,
15881 `.${TARGET_DROP_ACTIVE_CLASS}`,
15882 `[${TRASH_DROP_ACTIVE_ATTR$1}]`,
15883 `[${FILES_DROP_ACTIVE_ATTR}]`
15884 ]) {
15885 document.querySelectorAll(sel).forEach((el) => out.push(el));
15886 }
15887 return out;
15888 }
15889 function scrubOrphans() {
15890 for (const el of findOrphans()) {
15891 el.classList.remove(SOURCE_DRAGGING_CLASS, TARGET_DROP_ACTIVE_CLASS);
15892 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR$1);
15893 el.removeAttribute(FILES_DROP_ACTIVE_ATTR);
15894 }
15895 }
15896 const TARGET_ID_PREFIX = "desktop-mode-iframe-drop-";
15897 const IFRAME_SELECTOR = "iframe.desktop-mode-window__iframe";
15898 const DROP_ACTIVE_ATTR = "data-desktop-mode-iframe-drop-active";
15899 let _installed$1 = false;
15900 let _dragManager = null;
15901 const _suppressedIframes = /* @__PURE__ */ new Map();
15902 const _activeRegistrations = /* @__PURE__ */ new Map();
15903 let _bridgeInterceptPayload = null;
15904 let _lastHoveredBridgeIframe = null;
15905 function suppressIframePointerEventsBridge() {
15906 const iframes = document.querySelectorAll(
15907 IFRAME_SELECTOR
15908 );
15909 iframes.forEach((iframe) => {
15910 if (_suppressedIframes.has(iframe)) {
15911 return;
15912 }
15913 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
15914 iframe.style.pointerEvents = "none";
15915 });
15916 }
15917 function restoreIframePointerEvents() {
15918 _suppressedIframes.forEach((prev, iframe) => {
15919 iframe.style.pointerEvents = prev;
15920 });
15921 _suppressedIframes.clear();
15922 }
15923 function findIframeAtCursor(clientX, clientY) {
15924 const el = document.elementFromPoint(clientX, clientY);
15925 if (!el) {
15926 return null;
15927 }
15928 const win = el.closest(".desktop-mode-window");
15929 if (!(win instanceof HTMLElement)) {
15930 return null;
15931 }
15932 const iframe = win.querySelector(IFRAME_SELECTOR);
15933 return iframe instanceof HTMLIFrameElement ? iframe : null;
15934 }
15935 const onBridgeDragOver = (e) => {
15936 if (!_bridgeInterceptPayload) {
15937 return;
15938 }
15939 e.preventDefault();
15940 if (e.dataTransfer) {
15941 e.dataTransfer.dropEffect = "copy";
15942 }
15943 const iframe = findIframeAtCursor(e.clientX, e.clientY);
15944 if (iframe === _lastHoveredBridgeIframe) {
15945 return;
15946 }
15947 if (_lastHoveredBridgeIframe) {
15948 postIntoIframe(_lastHoveredBridgeIframe, {
15949 type: "desktop-mode-drag-leave"
15950 });
15951 }
15952 _lastHoveredBridgeIframe = iframe;
15953 if (iframe) {
15954 postIntoIframe(iframe, {
15955 type: "desktop-mode-drag-over",
15956 payload: _bridgeInterceptPayload
15957 });
15958 }
15959 };
15960 const onBridgeDrop = (e) => {
15961 if (!_bridgeInterceptPayload) {
15962 return;
15963 }
15964 e.preventDefault();
15965 e.stopPropagation();
15966 if (typeof e.stopImmediatePropagation === "function") {
15967 e.stopImmediatePropagation();
15968 }
15969 const iframe = findIframeAtCursor(e.clientX, e.clientY);
15970 const payload = _bridgeInterceptPayload;
15971 stopBridgeIntercept();
15972 if (!iframe) {
15973 return;
15974 }
15975 const rect = iframe.getBoundingClientRect();
15976 postIntoIframe(iframe, {
15977 type: "desktop-mode-drop",
15978 payload,
15979 position: {
15980 x: e.clientX - rect.left,
15981 y: e.clientY - rect.top
15982 }
15983 });
15984 };
15985 const onBridgeDragEnd = () => {
15986 stopBridgeIntercept();
15987 };
15988 function startBridgeIntercept(payload) {
15989 if (_bridgeInterceptPayload) {
15990 _bridgeInterceptPayload = payload;
15991 return;
15992 }
15993 _bridgeInterceptPayload = payload;
15994 suppressIframePointerEventsBridge();
15995 document.addEventListener("dragover", onBridgeDragOver, true);
15996 document.addEventListener("drop", onBridgeDrop, true);
15997 document.addEventListener("dragend", onBridgeDragEnd, true);
15998 }
15999 function stopBridgeIntercept() {
16000 if (!_bridgeInterceptPayload) {
16001 return;
16002 }
16003 _bridgeInterceptPayload = null;
16004 if (_lastHoveredBridgeIframe) {
16005 postIntoIframe(_lastHoveredBridgeIframe, {
16006 type: "desktop-mode-drag-leave"
16007 });
16008 _lastHoveredBridgeIframe = null;
16009 }
16010 document.removeEventListener("dragover", onBridgeDragOver, true);
16011 document.removeEventListener("drop", onBridgeDrop, true);
16012 document.removeEventListener("dragend", onBridgeDragEnd, true);
16013 restoreIframePointerEvents();
16014 }
16015 function extractBridgePayload(payload) {
16016 if (!payload || typeof payload !== "object") {
16017 return void 0;
16018 }
16019 const obj = payload;
16020 if (obj.type !== "shortcut" && obj.type !== "desktop-file") {
16021 return void 0;
16022 }
16023 const data = obj.data;
16024 return data?.bridgePayload;
16025 }
16026 function postIntoIframe(iframe, msg) {
16027 const w = iframe.contentWindow;
16028 if (!w) {
16029 return;
16030 }
16031 try {
16032 w.postMessage(msg, window.location.origin);
16033 } catch {
16034 }
16035 }
16036 function registerDropTargetFor(dragManager, iframe, target2, windowId) {
16037 return dragManager.registerDropTarget({
16038 id: `${TARGET_ID_PREFIX}${windowId}`,
16039 element: target2,
16040 accept: (payload) => !!extractBridgePayload(payload),
16041 onEnter: (session) => {
16042 const bridge = extractBridgePayload(session.payload);
16043 if (!bridge) {
16044 return;
16045 }
16046 target2.setAttribute(DROP_ACTIVE_ATTR, "");
16047 postIntoIframe(iframe, {
16048 type: "desktop-mode-drag-over",
16049 payload: bridge
16050 });
16051 },
16052 onLeave: () => {
16053 target2.removeAttribute(DROP_ACTIVE_ATTR);
16054 postIntoIframe(iframe, { type: "desktop-mode-drag-leave" });
16055 },
16056 onDrop: (session, ev) => {
16057 target2.removeAttribute(DROP_ACTIVE_ATTR);
16058 const bridge = extractBridgePayload(session.payload);
16059 if (!bridge) {
16060 return;
16061 }
16062 const rect = iframe.getBoundingClientRect();
16063 postIntoIframe(iframe, {
16064 type: "desktop-mode-drop",
16065 payload: bridge,
16066 position: {
16067 x: ev.clientX - rect.left,
16068 y: ev.clientY - rect.top
16069 }
16070 });
16071 }
16072 });
16073 }
16074 function deriveWindowIdFromIframe(iframe) {
16075 let cur = iframe.parentElement;
16076 while (cur) {
16077 if (cur.id.startsWith("wp-window-")) {
16078 return cur.id.slice("wp-window-".length);
16079 }
16080 cur = cur.parentElement;
16081 }
16082 return `unknown-${Math.random().toString(36).slice(2, 10)}`;
16083 }
16084 function onDragStart(payload) {
16085 const dragManager = _dragManager;
16086 if (!dragManager) {
16087 return;
16088 }
16089 const iframes = document.querySelectorAll(IFRAME_SELECTOR);
16090 const isBridgeable = !!extractBridgePayload(payload);
16091 console.info(
16092 "[desktop-mode] drag-start: suppressing %d iframe(s); bridgeable=%s",
16093 iframes.length,
16094 isBridgeable,
16095 payload
16096 );
16097 iframes.forEach((iframe) => {
16098 if (!_suppressedIframes.has(iframe)) {
16099 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
16100 iframe.style.pointerEvents = "none";
16101 }
16102 if (!isBridgeable) {
16103 return;
16104 }
16105 if (_activeRegistrations.has(iframe)) {
16106 return;
16107 }
16108 const dropTargetEl = iframe.parentElement;
16109 if (!dropTargetEl) {
16110 return;
16111 }
16112 const windowId = deriveWindowIdFromIframe(iframe);
16113 const deregister = registerDropTargetFor(
16114 dragManager,
16115 iframe,
16116 dropTargetEl,
16117 windowId
16118 );
16119 _activeRegistrations.set(iframe, deregister);
16120 });
16121 }
16122 function onDragEnd() {
16123 _suppressedIframes.forEach((prev, iframe) => {
16124 iframe.style.pointerEvents = prev;
16125 });
16126 _suppressedIframes.clear();
16127 _activeRegistrations.forEach((deregister) => {
16128 try {
16129 deregister();
16130 } catch {
16131 }
16132 });
16133 _activeRegistrations.clear();
16134 }
16135 function installIframeDropTargets(dragManager) {
16136 if (_installed$1) {
16137 return;
16138 }
16139 _installed$1 = true;
16140 _dragManager = dragManager;
16141 document.addEventListener(DRAG_EVENTS.START, (e) => {
16142 const detail = e.detail;
16143 onDragStart(detail?.payload);
16144 });
16145 document.addEventListener(DRAG_EVENTS.END, () => {
16146 onDragEnd();
16147 });
16148 document.addEventListener(DRAG_BRIDGE_EVENTS.START, (e) => {
16149 const detail = e.detail;
16150 if (!detail?.payload) {
16151 return;
16152 }
16153 startBridgeIntercept(detail.payload);
16154 });
16155 document.addEventListener(DRAG_BRIDGE_EVENTS.END, () => {
16156 stopBridgeIntercept();
16157 });
16158 addAction(
16159 HOOKS.WINDOW_CLOSED,
16160 "desktop-mode/drag/iframe-drop-targets-window-close",
16161 () => {
16162 for (const [iframe] of Array.from(_suppressedIframes)) {
16163 if (!iframe.isConnected) {
16164 _suppressedIframes.delete(iframe);
16165 }
16166 }
16167 for (const [iframe, deregister] of Array.from(_activeRegistrations)) {
16168 if (!iframe.isConnected) {
16169 try {
16170 deregister();
16171 } catch {
16172 }
16173 _activeRegistrations.delete(iframe);
16174 }
16175 }
16176 }
16177 );
16178 window.__desktopModeIframeDropDebug = () => ({
16179 installed: _installed$1,
16180 iframesInDom: document.querySelectorAll(IFRAME_SELECTOR).length,
16181 suppressedCount: _suppressedIframes.size,
16182 registeredCount: _activeRegistrations.size,
16183 suppressedIframeIds: Array.from(_suppressedIframes.keys()).map(
16184 deriveWindowIdFromIframe
16185 )
16186 });
16187 }
16188 function collectOpenables() {
16189 const desktop = window.wp?.desktop;
16190 if (!desktop) {
16191 return [];
16192 }
16193 const wm = desktop.windowManager;
16194 const config = desktop.config;
16195 if (!wm || !config) {
16196 return [];
16197 }
16198 const items = [];
16199 const fromMenu = (item, group) => ({
16200 id: item.id,
16201 label: item.title,
16202 description: group,
16203 icon: item.icon,
16204 open: () => wm.open({
16205 id: item.id,
16206 baseId: item.id,
16207 url: item.url,
16208 title: item.title,
16209 icon: item.icon
16210 })
16211 });
16212 for (const item of config.dockItems ?? []) {
16213 items.push(fromMenu(item, "Admin menu"));
16214 }
16215 const filtered = applyFilters(
16216 "desktop-mode.open-command.items",
16217 items
16218 );
16219 return Array.isArray(filtered) ? filtered : items;
16220 }
16221 const openCommand = {
16222 slug: "open",
16223 label: "Open",
16224 description: "Open an admin page or registered window.",
16225 hint: "[window]",
16226 icon: "dashicons-external",
16227 /**
16228 * Suggest matching windows as the user types args. Simple
16229 * case-insensitive substring match against label AND id so
16230 * "add" finds "Add New Post" and "jorvy" finds Jorvy whether
16231 * the plugin listed it with a friendly label or the slug.
16232 */
16233 suggest(args) {
16234 const q = args.trim().toLowerCase();
16235 const list2 = collectOpenables();
16236 const hits = q === "" ? list2 : list2.filter(
16237 (w) => w.label.toLowerCase().includes(q) || w.id.toLowerCase().includes(q)
16238 );
16239 return hits.slice(0, 12).map((w) => ({
16240 value: w.label,
16241 label: w.label,
16242 description: w.description,
16243 icon: w.icon ?? "dashicons-external"
16244 }));
16245 },
16246 run(args, ctx) {
16247 const q = args.trim();
16248 if (!q) {
16249 return "Type the name of a window to open, for example `/open Posts`.";
16250 }
16251 const list2 = collectOpenables();
16252 const ql = q.toLowerCase();
16253 const match = list2.find((w) => w.label.toLowerCase() === ql || w.id.toLowerCase() === ql) ?? list2.find(
16254 (w) => w.label.toLowerCase().includes(ql) || w.id.toLowerCase().includes(ql)
16255 );
16256 if (!match) {
16257 return `No window matching **${q}** — try \`/open\` alone to see available options.`;
16258 }
16259 match.open();
16260 ctx.close();
16261 }
16262 };
16263 function registerBuiltInCommands() {
16264 registerCommand(openCommand);
16265 }
16266 const palettes = [];
16267 const listeners$2 = /* @__PURE__ */ new Set();
16268 function registerPalette(p) {
16269 if (!p || typeof p.id !== "string" || p.id === "") {
16270 return () => {
16271 };
16272 }
16273 if (typeof p.open !== "function" || typeof p.close !== "function" || typeof p.isOpen !== "function") {
16274 return () => {
16275 };
16276 }
16277 const idx = palettes.findIndex((x) => x.id === p.id);
16278 if (idx >= 0) {
16279 palettes[idx] = p;
16280 } else {
16281 palettes.push(p);
16282 }
16283 notify$2();
16284 return () => {
16285 const i = palettes.findIndex((x) => x.id === p.id);
16286 if (i >= 0) {
16287 palettes.splice(i, 1);
16288 notify$2();
16289 }
16290 };
16291 }
16292 function unregisterPalette(id) {
16293 const idx = palettes.findIndex((x) => x.id === id);
16294 if (idx >= 0) {
16295 palettes.splice(idx, 1);
16296 notify$2();
16297 }
16298 }
16299 function listPalettes() {
16300 return palettes.slice();
16301 }
16302 function notify$2() {
16303 for (const cb of Array.from(listeners$2)) {
16304 try {
16305 cb();
16306 } catch (err) {
16307 if (typeof console !== "undefined") {
16308 console.error("[desktop-mode] palette-registry listener threw:", err);
16309 }
16310 }
16311 }
16312 }
16313 function cyclePalettes() {
16314 if (palettes.length === 0) {
16315 return;
16316 }
16317 const cur = palettes.findIndex((p) => {
16318 try {
16319 return p.isOpen();
16320 } catch {
16321 return false;
16322 }
16323 });
16324 if (cur === -1) {
16325 try {
16326 palettes[0].open();
16327 } catch {
16328 }
16329 return;
16330 }
16331 try {
16332 palettes[cur].close();
16333 } catch {
16334 }
16335 const next = cur + 1;
16336 if (next < palettes.length) {
16337 try {
16338 palettes[next].open();
16339 } catch {
16340 }
16341 }
16342 }
16343 function openPaletteOnly(id) {
16344 const target2 = palettes.find((p) => p.id === id);
16345 if (!target2) {
16346 return;
16347 }
16348 for (const p of palettes) {
16349 if (p.id !== id) {
16350 try {
16351 if (p.isOpen()) {
16352 p.close();
16353 }
16354 } catch {
16355 }
16356 }
16357 }
16358 try {
16359 target2.open();
16360 } catch {
16361 }
16362 }
16363 let installed$1 = false;
16364 function installPaletteShortcut() {
16365 if (installed$1) {
16366 return;
16367 }
16368 installed$1 = true;
16369 document.addEventListener(
16370 "keydown",
16371 (e) => {
16372 if (!(e.metaKey || e.ctrlKey) || e.key !== "k") {
16373 return;
16374 }
16375 if (e.shiftKey || e.altKey) {
16376 return;
16377 }
16378 e.preventDefault();
16379 e.stopImmediatePropagation();
16380 cyclePalettes();
16381 },
16382 true
16383 );
16384 const origin = window.location.origin;
16385 window.addEventListener("message", (e) => {
16386 if (e.origin !== origin) {
16387 return;
16388 }
16389 const data = e.data;
16390 if (data && data.type === "desktop-mode-palette-cycle") {
16391 cyclePalettes();
16392 }
16393 });
16394 }
16395 const suppliers = /* @__PURE__ */ new Map();
16396 const subscribers = /* @__PURE__ */ new Map();
16397 let booted$2 = false;
16398 const heartbeat = {
16399 contribute(field, supplier) {
16400 suppliers.set(field, supplier);
16401 return () => {
16402 if (suppliers.get(field) === supplier) {
16403 suppliers.delete(field);
16404 }
16405 };
16406 },
16407 subscribe(field, cb) {
16408 let set = subscribers.get(field);
16409 if (!set) {
16410 set = /* @__PURE__ */ new Set();
16411 subscribers.set(field, set);
16412 }
16413 set.add(cb);
16414 return () => {
16415 set.delete(cb);
16416 };
16417 }
16418 };
16419 function bootHeartbeatBus() {
16420 if (booted$2) {
16421 return;
16422 }
16423 booted$2 = true;
16424 const $ = window.jQuery;
16425 if (!$) {
16426 console.warn(
16427 "[desktop-mode/heartbeat] jQuery missing — Heartbeat bus disabled."
16428 );
16429 return;
16430 }
16431 $(document).on("heartbeat-send", (...args) => {
16432 const data = args[1];
16433 if (!data) {
16434 return;
16435 }
16436 for (const [field, supplier] of suppliers) {
16437 try {
16438 data[field] = supplier();
16439 } catch (err) {
16440 console.error(
16441 `[desktop-mode/heartbeat] supplier for "${field}" threw:`,
16442 err
16443 );
16444 }
16445 }
16446 });
16447 $(document).on("heartbeat-tick", (...args) => {
16448 const response = args[1];
16449 if (!response) {
16450 return;
16451 }
16452 for (const [field, set] of subscribers) {
16453 const value = response[field];
16454 if (value === void 0) {
16455 continue;
16456 }
16457 for (const cb of set) {
16458 try {
16459 cb(value);
16460 } catch (err) {
16461 console.error(
16462 `[desktop-mode/heartbeat] subscriber for "${field}" threw:`,
16463 err
16464 );
16465 }
16466 }
16467 }
16468 });
16469 }
16470 const store$2 = createSharedStore(
16471 "desktop-mode/presence",
16472 () => ({ byUser: /* @__PURE__ */ new Map(), serverTimeMs: 0 })
16473 );
16474 const ACTIVE_THRESHOLD_MS = 5 * 60 * 1e3;
16475 let lastInputMs = Date.now();
16476 let booted$1 = false;
16477 function noteUserActivity() {
16478 lastInputMs = Date.now();
16479 }
16480 function applySnapshot(block) {
16481 if (!block || !block.snapshot) {
16482 return;
16483 }
16484 const previous = store$2.state.byUser;
16485 const next = new Map(previous);
16486 const transitions = [];
16487 for (const [rawId, raw] of Object.entries(block.snapshot)) {
16488 const userId = Number(rawId);
16489 if (!Number.isFinite(userId) || userId <= 0) {
16490 continue;
16491 }
16492 const status = raw?.status ?? "offline";
16493 const entry = {
16494 status,
16495 lastSeenMs: Number(raw?.lastSeenMs ?? 0) || 0,
16496 lastActiveMs: Number(raw?.lastActiveMs ?? 0) || 0
16497 };
16498 const old = previous.get(userId);
16499 next.set(userId, entry);
16500 if (!old || old.status !== entry.status) {
16501 transitions.push({
16502 userId,
16503 oldStatus: old ? old.status : null,
16504 newStatus: entry.status,
16505 entry
16506 });
16507 }
16508 }
16509 store$2.state.byUser = next;
16510 if (typeof block.serverTimeMs === "number") {
16511 store$2.state.serverTimeMs = block.serverTimeMs;
16512 }
16513 store$2.notify();
16514 for (const t of transitions) {
16515 const detail = {
16516 userId: t.userId,
16517 oldStatus: t.oldStatus,
16518 newStatus: t.newStatus,
16519 lastSeenMs: t.entry.lastSeenMs,
16520 lastActiveMs: t.entry.lastActiveMs
16521 };
16522 document.dispatchEvent(
16523 new CustomEvent("desktop-mode-presence-changed", { detail })
16524 );
16525 activity.publish("desktop-mode/presence-changed", detail);
16526 }
16527 activity.publish("desktop-mode/presence-snapshot-applied", {
16528 applied: Object.keys(block.snapshot).length,
16529 transitions: transitions.length
16530 });
16531 }
16532 function bootPresenceProbe() {
16533 if (booted$1) {
16534 return;
16535 }
16536 booted$1 = true;
16537 document.addEventListener("pointerdown", noteUserActivity, {
16538 capture: true,
16539 passive: true
16540 });
16541 document.addEventListener("keydown", noteUserActivity, {
16542 capture: true,
16543 passive: true
16544 });
16545 document.addEventListener("visibilitychange", () => {
16546 if (!document.hidden) {
16547 noteUserActivity();
16548 }
16549 });
16550 heartbeat.contribute("desktop_mode_presence_active", () => true);
16551 heartbeat.contribute(
16552 "desktop_mode_user_active",
16553 () => Date.now() - lastInputMs < ACTIVE_THRESHOLD_MS
16554 );
16555 heartbeat.subscribe("desktop_mode_presence", (block) => {
16556 applySnapshot(block);
16557 });
16558 }
16559 function getStatus(userId) {
16560 const entry = store$2.state.byUser.get(userId);
16561 return entry ? entry.status : "offline";
16562 }
16563 function getAll() {
16564 return new Map(store$2.state.byUser);
16565 }
16566 function getEntry(userId) {
16567 return store$2.state.byUser.get(userId) ?? null;
16568 }
16569 function subscribe$1(cb) {
16570 return store$2.subscribe((s) => cb(s));
16571 }
16572 function markActive() {
16573 noteUserActivity();
16574 }
16575 function applyPresenceBatch(updates) {
16576 if (!Array.isArray(updates) || updates.length === 0) {
16577 return;
16578 }
16579 const previous = store$2.state.byUser;
16580 const next = new Map(previous);
16581 const transitions = [];
16582 for (const u of updates) {
16583 const userId = Number(u.userId);
16584 if (!Number.isFinite(userId) || userId <= 0) {
16585 continue;
16586 }
16587 const old = previous.get(userId);
16588 const entry = {
16589 status: u.status,
16590 lastSeenMs: typeof u.lastSeenMs === "number" ? u.lastSeenMs : old?.lastSeenMs ?? 0,
16591 lastActiveMs: typeof u.lastActiveMs === "number" ? u.lastActiveMs : old?.lastActiveMs ?? 0
16592 };
16593 next.set(userId, entry);
16594 if (!old || old.status !== entry.status) {
16595 transitions.push({
16596 userId,
16597 oldStatus: old ? old.status : null,
16598 newStatus: entry.status,
16599 entry
16600 });
16601 }
16602 }
16603 if (transitions.length === 0 && next.size === previous.size) {
16604 return;
16605 }
16606 store$2.state.byUser = next;
16607 store$2.notify();
16608 for (const t of transitions) {
16609 const detail = {
16610 userId: t.userId,
16611 oldStatus: t.oldStatus,
16612 newStatus: t.newStatus,
16613 lastSeenMs: t.entry.lastSeenMs,
16614 lastActiveMs: t.entry.lastActiveMs
16615 };
16616 document.dispatchEvent(
16617 new CustomEvent("desktop-mode-presence-changed", { detail })
16618 );
16619 activity.publish("desktop-mode/presence-changed", detail);
16620 }
16621 activity.publish("desktop-mode/presence-snapshot-applied", {
16622 applied: updates.length,
16623 transitions: transitions.length
16624 });
16625 }
16626 const presenceApi = Object.freeze({
16627 getStatus,
16628 getAll,
16629 getEntry,
16630 subscribe: subscribe$1,
16631 markActive,
16632 applyBatch: applyPresenceBatch
16633 });
16634 const HEARTBEAT_FIELD = "desktop_mode_nonces";
16635 const targets = /* @__PURE__ */ new Map();
16636 let booted = false;
16637 function registerNonceTarget(action, updater) {
16638 if (typeof action !== "string" || action === "") {
16639 return () => {
16640 };
16641 }
16642 let set = targets.get(action);
16643 if (!set) {
16644 set = /* @__PURE__ */ new Set();
16645 targets.set(action, set);
16646 }
16647 set.add(updater);
16648 return () => {
16649 set.delete(updater);
16650 };
16651 }
16652 function bootNonceRefresh() {
16653 if (booted) {
16654 return;
16655 }
16656 booted = true;
16657 heartbeat.subscribe(HEARTBEAT_FIELD, (payload) => {
16658 if (!payload || typeof payload !== "object") {
16659 return;
16660 }
16661 for (const [action, value] of Object.entries(payload)) {
16662 if (typeof value !== "string" || value === "") {
16663 continue;
16664 }
16665 const set = targets.get(action);
16666 if (!set) {
16667 continue;
16668 }
16669 for (const updater of set) {
16670 try {
16671 updater(value);
16672 } catch (err) {
16673 console.error(
16674 `[desktop-mode/nonce-refresh] updater for "${action}" threw:`,
16675 err
16676 );
16677 }
16678 }
16679 }
16680 });
16681 registerShellAndPluginsWindowTargets();
16682 }
16683 function registerShellAndPluginsWindowTargets() {
16684 registerNonceTarget("wp_rest", updateAllRestNonces);
16685 registerNonceTarget("desktop-mode-plugins", (fresh) => {
16686 writeWindowConfigField("desktop-mode-plugins", "ajaxNonce", fresh);
16687 });
16688 registerNonceTarget("updates", (fresh) => {
16689 writeWindowConfigField("desktop-mode-plugins", "updatesNonce", fresh);
16690 });
16691 }
16692 function updateAllRestNonces(fresh) {
16693 const cfg = readShellConfig();
16694 if (cfg && typeof cfg.restNonce === "string") {
16695 cfg.restNonce = fresh;
16696 }
16697 const windowConfigs = readWindowConfigs();
16698 if (!windowConfigs) {
16699 return;
16700 }
16701 for (const blob of Object.values(windowConfigs)) {
16702 if (blob && typeof blob === "object" && typeof blob.restNonce === "string") {
16703 blob.restNonce = fresh;
16704 }
16705 }
16706 }
16707 function writeWindowConfigField(windowId, field, value) {
16708 const blobs = readWindowConfigs();
16709 const blob = blobs?.[windowId];
16710 if (blob && typeof blob === "object") {
16711 blob[field] = value;
16712 }
16713 }
16714 function readShellConfig() {
16715 if (typeof window === "undefined") {
16716 return void 0;
16717 }
16718 return window.desktopModeConfig;
16719 }
16720 function readWindowConfigs() {
16721 if (typeof window === "undefined") {
16722 return void 0;
16723 }
16724 return window.desktopModeWindowConfig;
16725 }
16726 const VIEWPORT_CLAMP_MARGIN = 12;
16727 function findDockEntryForUrl(url, config) {
16728 const windowId = deriveWindowId(url, config.adminUrl);
16729 return (config.dockItems || []).find(
16730 (i) => deriveWindowId(i.url, config.adminUrl) === windowId || (i.submenu || []).some(
16731 (s) => deriveWindowId(s.url, config.adminUrl) === windowId
16732 )
16733 );
16734 }
16735 function clampGeometryToViewport(win, rect) {
16736 const maxW = Math.max(200, rect.width - VIEWPORT_CLAMP_MARGIN * 2);
16737 const maxH = Math.max(200, rect.height - VIEWPORT_CLAMP_MARGIN * 2);
16738 const width = Math.min(win.width, maxW);
16739 const height = Math.min(win.height, maxH);
16740 const maxX = Math.max(0, rect.width - width - VIEWPORT_CLAMP_MARGIN);
16741 const maxY = Math.max(0, rect.height - height - VIEWPORT_CLAMP_MARGIN);
16742 const x = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.x, maxX));
16743 const y = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.y, maxY));
16744 return { x, y, width, height };
16745 }
16746 const INITIAL_ORIGIN$1 = window.location.origin;
16747 function bindTopWindowLinkInterceptor(manager, config) {
16748 document.addEventListener(
16749 "click",
16750 (e) => {
16751 if (e.defaultPrevented) {
16752 return;
16753 }
16754 if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
16755 return;
16756 }
16757 const target2 = e.target;
16758 const link = target2 && target2.closest ? target2.closest("a[href]") : null;
16759 if (!link) {
16760 return;
16761 }
16762 const anchor = link;
16763 const linkTarget = anchor.getAttribute("target");
16764 if (linkTarget && linkTarget !== "" && linkTarget !== "_self") {
16765 return;
16766 }
16767 if (anchor.hasAttribute("download")) {
16768 return;
16769 }
16770 const rawHref = anchor.getAttribute("href");
16771 if (!rawHref || rawHref.charAt(0) === "#") {
16772 return;
16773 }
16774 if (/^(mailto:|tel:|javascript:|data:)/i.test(rawHref)) {
16775 return;
16776 }
16777 let url;
16778 try {
16779 url = new URL(rawHref, window.location.href);
16780 } catch (err) {
16781 if (typeof console !== "undefined") {
16782 console.warn(
16783 "[desktop-mode] Couldn’t parse href; letting the browser handle the click:",
16784 rawHref,
16785 err
16786 );
16787 }
16788 return;
16789 }
16790 if (url.origin !== INITIAL_ORIGIN$1) {
16791 return;
16792 }
16793 let adminPath;
16794 try {
16795 adminPath = new URL(config.adminUrl).pathname;
16796 } catch (err) {
16797 if (typeof console !== "undefined") {
16798 console.error(
16799 "[desktop-mode] config.adminUrl is not a valid URL; falling back to /wp-admin/:",
16800 config.adminUrl,
16801 err
16802 );
16803 }
16804 adminPath = "/wp-admin/";
16805 }
16806 if (!url.pathname.startsWith(adminPath)) {
16807 return;
16808 }
16809 if (/\/(admin-post|admin-ajax)\.php$/.test(url.pathname)) {
16810 return;
16811 }
16812 if (url.searchParams.has("action") && url.searchParams.get("action") === "logout") {
16813 return;
16814 }
16815 if (url.searchParams.has("desktop_mode_classic")) {
16816 return;
16817 }
16818 e.preventDefault();
16819 e.stopPropagation();
16820 if (tryNativeUrlRemap(url.href)) {
16821 return;
16822 }
16823 const windowId = deriveWindowId(url.href, config.adminUrl);
16824 const dockEntry = findDockEntryForUrl(url.href, config);
16825 const fallbackTitle = (anchor.textContent || "").trim() || dockEntry?.title || "";
16826 const isAdminBarNew = !!anchor.closest("#wp-admin-bar-new-content");
16827 const openOpts = {
16828 id: windowId,
16829 baseId: windowId,
16830 multi: !!dockEntry?.multi || isAdminBarNew,
16831 url: url.href,
16832 parentUrl: dockEntry?.url ?? url.href,
16833 title: dockEntry?.title || fallbackTitle,
16834 icon: dockEntry?.icon || "dashicons-admin-generic",
16835 submenu: dockEntry?.submenu
16836 };
16837 if (isAdminBarNew) {
16838 void manager.openNew(openOpts);
16839 return;
16840 }
16841 void manager.open(openOpts);
16842 },
16843 true
16844 );
16845 }
16846 const REGISTRY_CHANGED_EVENT = "desktop-mode-registry-changed";
16847 function diffIds(prev, next) {
16848 const prevIds = /* @__PURE__ */ new Set();
16849 if (Array.isArray(prev)) {
16850 for (const item of prev) {
16851 if (item && typeof item.id === "string") {
16852 prevIds.add(item.id);
16853 }
16854 }
16855 }
16856 const nextIds = /* @__PURE__ */ new Set();
16857 for (const item of next) {
16858 if (item && typeof item.id === "string") {
16859 nextIds.add(item.id);
16860 }
16861 }
16862 const added = [];
16863 for (const id of nextIds) {
16864 if (!prevIds.has(id)) {
16865 added.push(id);
16866 }
16867 }
16868 const removed = [];
16869 for (const id of prevIds) {
16870 if (!nextIds.has(id)) {
16871 removed.push(id);
16872 }
16873 }
16874 return { added, removed };
16875 }
16876 function emitRegistryChanged(registry2, prev, next) {
16877 const { added, removed } = diffIds(prev, next);
16878 if (added.length === 0 && removed.length === 0) {
16879 return;
16880 }
16881 if (typeof document === "undefined") {
16882 return;
16883 }
16884 const detail = { registry: registry2, added, removed };
16885 document.dispatchEvent(
16886 new CustomEvent(REGISTRY_CHANGED_EVENT, { detail })
16887 );
16888 }
16889 function createApplyPayload(deps2) {
16890 const {
16891 applyDockItems,
16892 config,
16893 syncNativeWindows,
16894 syncServerWidgets,
16895 syncServerWallpapers,
16896 syncServerCommands,
16897 syncServerSettingsTabs,
16898 syncServerTitleBarButtons,
16899 syncServerDockRailRenderers,
16900 renderIcons
16901 } = deps2;
16902 return function applyPayload(payload) {
16903 const dockItems = payload.dockItems;
16904 const nativeWindows = payload.nativeWindows;
16905 const serverWidgets = payload.serverWidgets;
16906 const serverWallpapers = payload.serverWallpapers;
16907 const serverCommandScripts = payload.serverCommandScripts;
16908 const serverCommands = payload.serverCommands;
16909 const serverSettingsTabScripts = payload.serverSettingsTabScripts;
16910 const serverSettingsTabs = payload.serverSettingsTabs;
16911 const serverDockRailRendererScripts = payload.serverDockRailRendererScripts;
16912 const serverTitleBarButtonScripts = payload.serverTitleBarButtonScripts;
16913 const serverWindowNotices = payload.serverWindowNotices;
16914 const desktopIcons = payload.desktopIcons;
16915 if (!Array.isArray(dockItems) || dockItems.length === 0) {
16916 return;
16917 }
16918 const prevDockItems = config.dockItems;
16919 applyDockItems(dockItems);
16920 config.dockItems = dockItems;
16921 emitRegistryChanged(
16922 "dock-items",
16923 prevDockItems,
16924 dockItems
16925 );
16926 if (Array.isArray(nativeWindows)) {
16927 const prevNativeWindows = config.nativeWindows;
16928 void syncNativeWindows(
16929 nativeWindows
16930 );
16931 config.nativeWindows = nativeWindows;
16932 emitRegistryChanged(
16933 "native-windows",
16934 prevNativeWindows,
16935 nativeWindows
16936 );
16937 }
16938 if (Array.isArray(serverWidgets)) {
16939 void syncServerWidgets(
16940 serverWidgets
16941 );
16942 config.serverWidgets = serverWidgets;
16943 }
16944 if (Array.isArray(serverWallpapers)) {
16945 void syncServerWallpapers(
16946 serverWallpapers
16947 );
16948 config.serverWallpapers = serverWallpapers;
16949 }
16950 if (Array.isArray(serverCommandScripts)) {
16951 void syncServerCommands(
16952 serverCommandScripts,
16953 Array.isArray(serverCommands) ? serverCommands : void 0
16954 );
16955 config.serverCommandScripts = serverCommandScripts;
16956 if (Array.isArray(serverCommands)) {
16957 config.serverCommands = serverCommands;
16958 }
16959 }
16960 if (Array.isArray(serverSettingsTabScripts)) {
16961 void syncServerSettingsTabs(
16962 serverSettingsTabScripts,
16963 Array.isArray(serverSettingsTabs) ? serverSettingsTabs : void 0
16964 );
16965 config.serverSettingsTabScripts = serverSettingsTabScripts;
16966 if (Array.isArray(serverSettingsTabs)) {
16967 config.serverSettingsTabs = serverSettingsTabs;
16968 }
16969 }
16970 if (Array.isArray(serverTitleBarButtonScripts)) {
16971 void syncServerTitleBarButtons(
16972 serverTitleBarButtonScripts
16973 );
16974 config.serverTitleBarButtonScripts = serverTitleBarButtonScripts;
16975 }
16976 if (Array.isArray(serverDockRailRendererScripts)) {
16977 void syncServerDockRailRenderers(
16978 serverDockRailRendererScripts
16979 );
16980 config.serverDockRailRendererScripts = serverDockRailRendererScripts;
16981 }
16982 if (Array.isArray(serverWindowNotices)) {
16983 applyServerWindowNotices(
16984 serverWindowNotices
16985 );
16986 config.serverWindowNotices = serverWindowNotices;
16987 }
16988 if (Array.isArray(desktopIcons)) {
16989 const prevDesktopIcons = config.desktopIcons;
16990 renderIcons(desktopIcons);
16991 config.desktopIcons = desktopIcons;
16992 emitRegistryChanged(
16993 "desktop-icons",
16994 prevDesktopIcons,
16995 desktopIcons
16996 );
16997 }
16998 };
16999 }
17000 const MENU_REFRESH_TIMEOUT_MS = 8e3;
17001 function bindMenuRefresh(deps2) {
17002 const {
17003 layoutDispatcher,
17004 config,
17005 syncNativeWindows,
17006 syncServerWidgets,
17007 syncServerWallpapers,
17008 syncServerCommands,
17009 syncServerSettingsTabs,
17010 syncServerTitleBarButtons,
17011 syncServerDockRailRenderers,
17012 renderIcons
17013 } = deps2;
17014 const applyPayload = createApplyPayload({
17015 applyDockItems: (items) => layoutDispatcher?.applyDockItems(items),
17016 config,
17017 syncNativeWindows,
17018 syncServerWidgets,
17019 syncServerWallpapers,
17020 syncServerCommands,
17021 syncServerSettingsTabs,
17022 syncServerTitleBarButtons,
17023 syncServerDockRailRenderers,
17024 renderIcons
17025 });
17026 window.addEventListener("message", (e) => {
17027 if (e.origin !== INITIAL_ORIGIN$1) {
17028 return;
17029 }
17030 const data = e.data;
17031 if (!data || data.type !== "desktop-mode-plugins-changed") {
17032 return;
17033 }
17034 if (data.payload) {
17035 applyPayload(data.payload);
17036 }
17037 });
17038 const refresh = () => {
17039 if (!config.adminUrl) {
17040 return Promise.resolve();
17041 }
17042 const probeUrl = (() => {
17043 try {
17044 const url = new URL("admin.php", config.adminUrl);
17045 url.searchParams.set("desktop_mode_chromeless", "1");
17046 url.searchParams.set("desktop_mode_menu_refresh", "1");
17047 return url.toString();
17048 } catch (_err) {
17049 return null;
17050 }
17051 })();
17052 if (!probeUrl) {
17053 return Promise.resolve();
17054 }
17055 return new Promise((resolve2) => {
17056 const iframe = document.createElement("iframe");
17057 iframe.setAttribute("aria-hidden", "true");
17058 iframe.tabIndex = -1;
17059 iframe.style.cssText = "position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;border:0;opacity:0;pointer-events:none;";
17060 iframe.src = probeUrl;
17061 let done = false;
17062 const cleanup = () => {
17063 if (done) {
17064 return;
17065 }
17066 done = true;
17067 window.clearTimeout(timeoutId);
17068 window.removeEventListener("message", onMessage);
17069 if (iframe.parentNode) {
17070 iframe.parentNode.removeChild(iframe);
17071 }
17072 resolve2();
17073 };
17074 const onMessage = (e) => {
17075 if (e.source !== iframe.contentWindow) {
17076 return;
17077 }
17078 const data = e.data;
17079 if (!data || data.type !== "desktop-mode-plugins-changed") {
17080 return;
17081 }
17082 cleanup();
17083 };
17084 const timeoutId = window.setTimeout(() => {
17085 doAction(HOOKS.SHELL_ERROR, {
17086 scope: "menu-refresh",
17087 error: new Error("menu refresh probe timed out")
17088 });
17089 cleanup();
17090 }, MENU_REFRESH_TIMEOUT_MS);
17091 window.addEventListener("message", onMessage);
17092 document.body.appendChild(iframe);
17093 });
17094 };
17095 return refresh;
17096 }
17097 function hasRestorableSession(session) {
17098 if (!session) {
17099 return false;
17100 }
17101 if (Array.isArray(session.windows) && session.windows.length > 0) {
17102 return true;
17103 }
17104 if (typeof session.updated !== "number" || session.updated <= 0 || !Array.isArray(session.desktops) || session.desktops.length === 0) {
17105 return false;
17106 }
17107 if (session.desktops.length > 1) {
17108 return true;
17109 }
17110 const onlyDesktop = session.desktops[0];
17111 if (onlyDesktop?.id && onlyDesktop.id !== "desktop-1") {
17112 return true;
17113 }
17114 return !!session.activeDesktop && session.activeDesktop !== "desktop-1";
17115 }
17116 async function restoreSession(manager, config, desktopArea) {
17117 const rect = desktopArea.getBoundingClientRect();
17118 if (Array.isArray(config.session.desktops) && config.session.desktops.length > 0) {
17119 manager.seedDesktops(
17120 config.session.desktops,
17121 config.session.activeDesktop || config.session.desktops[0].id
17122 );
17123 }
17124 for (const win of config.session.windows) {
17125 const clamped = clampGeometryToViewport(win, rect);
17126 const dockEntry = findDockEntryForUrl(win.url, config);
17127 const opened = await manager.open({
17128 id: win.id,
17129 baseId: win.baseId || win.id,
17130 desktopId: win.desktopId,
17131 multi: !!dockEntry?.multi,
17132 url: win.url,
17133 // `dockEntry?.url` is the parent menu's landing page —
17134 // recover it so the synthetic "back to parent" tab in
17135 // the in-window strip points at the dock URL even when
17136 // the saved `win.url` is a sub-page (e.g. theme-install.php
17137 // under Appearance, or a deep wc-admin route under
17138 // WooCommerce). Without this the dedup check in
17139 // `dom.ts` sees the iframe URL match a submenu entry
17140 // and suppresses the parent tab — losing the only
17141 // affordance to navigate back.
17142 parentUrl: dockEntry?.url ?? win.url,
17143 title: win.title,
17144 icon: win.icon || "dashicons-admin-generic",
17145 x: clamped.x,
17146 y: clamped.y,
17147 width: clamped.width,
17148 height: clamped.height,
17149 initialState: win.state,
17150 submenu: dockEntry?.submenu
17151 });
17152 if (Array.isArray(win.externalTabs)) {
17153 for (const ext of win.externalTabs) {
17154 if (ext && typeof ext.url === "string" && ext.url !== "") {
17155 opened.addExternalTab(
17156 ext.url,
17157 typeof ext.label === "string" && ext.label !== "" ? ext.label : ext.url
17158 );
17159 }
17160 }
17161 }
17162 }
17163 if (config.session.focused) {
17164 const focused = manager.getById(config.session.focused);
17165 if (focused) {
17166 manager.focus(focused);
17167 }
17168 }
17169 }
17170 async function openCurrentPage(manager, config) {
17171 if (tryNativeUrlRemap(config.currentPage)) {
17172 return;
17173 }
17174 const windowId = deriveWindowId(config.currentPage, config.adminUrl);
17175 const dockEntry = findDockEntryForUrl(config.currentPage, config);
17176 await manager.open({
17177 id: windowId,
17178 baseId: windowId,
17179 multi: !!dockEntry?.multi,
17180 url: config.currentPage,
17181 parentUrl: dockEntry?.url ?? config.currentPage,
17182 title: config.currentTitle,
17183 icon: config.currentIcon,
17184 submenu: dockEntry?.submenu
17185 });
17186 }
17187 function shouldAutoOpenCurrentPage(inputs) {
17188 const suppress = inputs.fromPortal && !inputs.fromPortalIntent && (inputs.hasSession || !inputs.defaultEnabled || inputs.isNativeDefault);
17189 return !suppress;
17190 }
17191 function trackedFetch(manager, input, requestInit, opts) {
17192 const finalInit = injectRestNonce(input, requestInit);
17193 const promise = window.fetch(input, finalInit);
17194 if (opts?.silent) {
17195 return promise;
17196 }
17197 let target2 = opts?.window;
17198 if (!target2 && opts?.windowId) {
17199 target2 = manager.getById(opts.windowId) ?? null;
17200 }
17201 if (!target2) {
17202 target2 = manager.getFocused();
17203 }
17204 if (target2 && typeof target2.trackActivity === "function") {
17205 void target2.trackActivity(promise).catch(() => {
17206 });
17207 }
17208 return promise;
17209 }
17210 const SESSION_SAVE_DEBOUNCE_MS = 500;
17211 function createSessionSaver(manager, config) {
17212 let debounceTimer = null;
17213 let inFlight = false;
17214 const doSave = async () => {
17215 if (inFlight) {
17216 return;
17217 }
17218 const payload = manager.snapshot();
17219 inFlight = true;
17220 try {
17221 await trackedFetch(
17222 manager,
17223 config.sessionUrl,
17224 {
17225 method: "POST",
17226 credentials: "same-origin",
17227 headers: {
17228 "Content-Type": "application/json",
17229 "X-WP-Nonce": config.restNonce
17230 },
17231 body: JSON.stringify({ session: payload }),
17232 // Best-effort: we don't block the UI on persistence.
17233 keepalive: true
17234 },
17235 { silent: true }
17236 );
17237 } catch (err) {
17238 doAction(HOOKS.SHELL_ERROR, { scope: "session-save", error: err });
17239 } finally {
17240 inFlight = false;
17241 }
17242 };
17243 const flushImmediately = () => {
17244 if (debounceTimer !== null) {
17245 clearTimeout(debounceTimer);
17246 debounceTimer = null;
17247 }
17248 const payload = manager.snapshot();
17249 const body = new Blob(
17250 [JSON.stringify({ session: payload })],
17251 { type: "application/json" }
17252 );
17253 const beaconUrl = config.sessionUrl + (config.sessionUrl.includes("?") ? "&" : "?") + "_wpnonce=" + encodeURIComponent(config.restNonce);
17254 if (navigator.sendBeacon && navigator.sendBeacon(beaconUrl, body)) {
17255 return;
17256 }
17257 void doSave();
17258 };
17259 const schedule = () => {
17260 if (debounceTimer !== null) {
17261 clearTimeout(debounceTimer);
17262 }
17263 debounceTimer = window.setTimeout(() => {
17264 debounceTimer = null;
17265 void doSave();
17266 }, SESSION_SAVE_DEBOUNCE_MS);
17267 };
17268 window.addEventListener("pagehide", flushImmediately);
17269 document.addEventListener("visibilitychange", () => {
17270 if (document.visibilityState === "hidden") {
17271 flushImmediately();
17272 }
17273 });
17274 return schedule;
17275 }
17276 const SHELL_RESIZE_DEBOUNCE_MS = 120;
17277 function wireSessionEvents(save) {
17278 document.addEventListener("desktop-mode-window-opened", save);
17279 document.addEventListener("desktop-mode-window-closed", save);
17280 document.addEventListener("desktop-mode-window-focused", save);
17281 document.addEventListener("desktop-mode-window-changed", save);
17282 addAction(HOOKS.DESKTOP_CREATED, "desktop-mode/session-save", save);
17283 addAction(HOOKS.DESKTOP_CLOSED, "desktop-mode/session-save", save);
17284 addAction(HOOKS.DESKTOP_SWITCHED, "desktop-mode/session-save", save);
17285 }
17286 function bindShellLifecycle() {
17287 const shellEl = document.getElementById("desktop-mode-shell");
17288 let resizeTimer = null;
17289 const fireShellResize = () => {
17290 resizeTimer = null;
17291 const rect = shellEl ? shellEl.getBoundingClientRect() : null;
17292 doAction(HOOKS.SHELL_RESIZED, {
17293 width: rect ? Math.round(rect.width) : window.innerWidth,
17294 height: rect ? Math.round(rect.height) : window.innerHeight
17295 });
17296 };
17297 window.addEventListener("resize", () => {
17298 if (resizeTimer !== null) {
17299 window.clearTimeout(resizeTimer);
17300 }
17301 resizeTimer = window.setTimeout(
17302 fireShellResize,
17303 SHELL_RESIZE_DEBOUNCE_MS
17304 );
17305 });
17306 document.addEventListener("visibilitychange", () => {
17307 doAction(HOOKS.SHELL_VISIBILITY, {
17308 state: document.hidden ? "hidden" : "visible"
17309 });
17310 });
17311 }
17312 function applyTileClasses(baseClasses, item, ctx) {
17313 const fullCtx = {
17314 rail: ctx.rail ?? "dock",
17315 orientation: ctx.orientation,
17316 dockId: ctx.dockId,
17317 container: ctx.container ?? document.body,
17318 item,
17319 isSystem: ctx.isSystem
17320 };
17321 return applyFilters(
17322 HOOKS.DOCK_TILE_CLASS,
17323 baseClasses,
17324 fullCtx
17325 );
17326 }
17327 function applyTileElement(tile2, item, ctx) {
17328 const fullCtx = {
17329 rail: ctx.rail ?? "dock",
17330 orientation: ctx.orientation,
17331 dockId: ctx.dockId,
17332 container: ctx.container ?? document.body,
17333 item,
17334 isSystem: ctx.isSystem
17335 };
17336 return applyFilters(
17337 HOOKS.DOCK_TILE_ELEMENT,
17338 tile2,
17339 fullCtx
17340 );
17341 }
17342 function applyTileTooltip(label, item, ctx) {
17343 const fullCtx = {
17344 rail: ctx.rail ?? "dock",
17345 orientation: ctx.orientation,
17346 dockId: ctx.dockId,
17347 container: ctx.container ?? document.body,
17348 item,
17349 isSystem: ctx.isSystem
17350 };
17351 return applyFilters(
17352 HOOKS.DOCK_TILE_TOOLTIP,
17353 label,
17354 fullCtx
17355 );
17356 }
17357 function dispatchTileRendered(el, item, ctx) {
17358 const fullCtx = {
17359 rail: ctx.rail ?? "dock",
17360 orientation: ctx.orientation,
17361 dockId: ctx.dockId,
17362 container: ctx.container ?? document.body,
17363 item,
17364 isSystem: ctx.isSystem
17365 };
17366 doAction(HOOKS.DOCK_TILE_RENDERED, { ...fullCtx, el });
17367 }
17368 const DEFAULT_DOCK_SELECTOR = [
17369 ".desktop-mode-dock",
17370 "#desktop-mode-dock",
17371 "#desktop-mode-side-dock",
17372 ".desktop-mode-dock__tooltip",
17373 ".desktop-mode-dock-submenu"
17374 ].join(",");
17375 const customSelectors = /* @__PURE__ */ new Set();
17376 function isDockElement(target2) {
17377 if (!target2 || typeof target2.closest !== "function") {
17378 return false;
17379 }
17380 const el = target2;
17381 if (el.closest(DEFAULT_DOCK_SELECTOR)) {
17382 return true;
17383 }
17384 for (const selector of customSelectors) {
17385 if (el.closest(selector)) {
17386 return true;
17387 }
17388 }
17389 return false;
17390 }
17391 function registerDockSelector(selector) {
17392 if (typeof selector !== "string" || selector.trim() === "") {
17393 return () => void 0;
17394 }
17395 customSelectors.add(selector);
17396 return () => {
17397 customSelectors.delete(selector);
17398 };
17399 }
17400 const states = /* @__PURE__ */ new Map();
17401 const INITIAL_ORIGIN = window.location.origin;
17402 function ensureState(windowId) {
17403 let s = states.get(windowId);
17404 if (!s) {
17405 s = {
17406 headers: /* @__PURE__ */ new Map(),
17407 observers: /* @__PURE__ */ new Set(),
17408 observeCount: 0,
17409 loadHandler: null,
17410 loadHandlerTarget: null
17411 };
17412 states.set(windowId, s);
17413 }
17414 ensureLoadHandler(windowId, s);
17415 return s;
17416 }
17417 function ensureLoadHandler(windowId, s) {
17418 const iframe = findIframe(windowId);
17419 if (!iframe) {
17420 return;
17421 }
17422 if (s.loadHandlerTarget === iframe && s.loadHandler) {
17423 return;
17424 }
17425 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
17426 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
17427 }
17428 if (typeof iframe.addEventListener !== "function") {
17429 return;
17430 }
17431 const handler = () => {
17432 queueMicrotask(() => pushInstrumentation(windowId));
17433 };
17434 iframe.addEventListener("load", handler);
17435 s.loadHandler = handler;
17436 s.loadHandlerTarget = iframe;
17437 }
17438 function detachLoadHandler(s) {
17439 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
17440 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
17441 }
17442 s.loadHandler = null;
17443 s.loadHandlerTarget = null;
17444 }
17445 function findIframe(windowId) {
17446 const wpd = window.wp?.desktop?.windowManager;
17447 if (wpd && typeof wpd.getById === "function") {
17448 const win = wpd.getById(windowId);
17449 if (win?.iframe) {
17450 return win.iframe;
17451 }
17452 if (win?.element) {
17453 const synth = win.element.querySelector("iframe");
17454 if (synth) {
17455 return synth;
17456 }
17457 }
17458 }
17459 const fallback = document.getElementById(`wp-window-${windowId}`);
17460 return fallback?.querySelector("iframe") ?? null;
17461 }
17462 function snapshotHeaders(s) {
17463 const out = {};
17464 for (const [name, contributions] of s.headers) {
17465 const parts = [];
17466 for (const c of contributions) {
17467 let v;
17468 try {
17469 v = typeof c.value === "function" ? c.value() : c.value;
17470 } catch {
17471 continue;
17472 }
17473 if (typeof v === "string" && v !== "") {
17474 parts.push(v);
17475 }
17476 }
17477 if (parts.length > 0) {
17478 out[name] = parts.join(", ");
17479 }
17480 }
17481 return out;
17482 }
17483 function pushInstrumentation(windowId) {
17484 const iframe = findIframe(windowId);
17485 if (!iframe || !iframe.contentWindow) {
17486 return;
17487 }
17488 const s = states.get(windowId);
17489 const headers = s ? snapshotHeaders(s) : {};
17490 const observe = !!s && s.observeCount > 0;
17491 try {
17492 iframe.contentWindow.postMessage(
17493 {
17494 type: "desktop-mode-instrument-set",
17495 headers,
17496 observe
17497 },
17498 INITIAL_ORIGIN
17499 );
17500 } catch {
17501 }
17502 }
17503 addAction(HOOKS.IFRAME_READY, "desktop-mode/devtools/replay", (payload) => {
17504 const p = payload;
17505 if (p && typeof p.windowId === "string" && states.has(p.windowId)) {
17506 pushInstrumentation(p.windowId);
17507 }
17508 });
17509 addAction(
17510 HOOKS.IFRAME_NETWORK_COMPLETED,
17511 "desktop-mode/devtools/dispatch",
17512 (payload) => {
17513 const p = payload;
17514 if (!p || typeof p.windowId !== "string") {
17515 return;
17516 }
17517 const s = states.get(p.windowId);
17518 if (!s) {
17519 return;
17520 }
17521 for (const cb of s.observers) {
17522 try {
17523 cb(p);
17524 } catch {
17525 }
17526 }
17527 }
17528 );
17529 const sessions = /* @__PURE__ */ new Map();
17530 const POLL_INTERVAL_MS = 1e3;
17531 function pollOnce(sessionId, restUrl2, restNonce) {
17532 const sp = sessions.get(sessionId);
17533 if (!sp || sp.inflight) {
17534 return;
17535 }
17536 sp.inflight = true;
17537 const u = new URL(restUrl2 + "desktop-mode/v1/debug", window.location.origin);
17538 u.searchParams.set("sessionId", sessionId);
17539 u.searchParams.set("since", String(sp.cursor));
17540 for (const ch of sp.channels.keys()) {
17541 u.searchParams.append("channels[]", ch);
17542 }
17543 const url = u.toString();
17544 fetch(url, {
17545 credentials: "same-origin",
17546 headers: { "X-WP-Nonce": restNonce }
17547 }).then((r) => r.ok ? r.json() : { events: [], cursor: sp.cursor }).then((body) => {
17548 sp.inflight = false;
17549 if (!sessions.has(sessionId)) {
17550 return;
17551 }
17552 if (typeof body.cursor === "number") {
17553 sp.cursor = body.cursor;
17554 }
17555 for (const ev of body.events || []) {
17556 const bucket2 = sp.channels.get(ev.channel);
17557 if (!bucket2) {
17558 continue;
17559 }
17560 for (const cb of bucket2) {
17561 try {
17562 cb(ev);
17563 } catch {
17564 }
17565 }
17566 }
17567 }).catch(() => {
17568 sp.inflight = false;
17569 }).finally(() => {
17570 const stillThere = sessions.get(sessionId);
17571 if (stillThere && stillThere.channels.size > 0) {
17572 stillThere.timer = setTimeout(
17573 () => pollOnce(sessionId, restUrl2, restNonce),
17574 POLL_INTERVAL_MS
17575 );
17576 }
17577 });
17578 }
17579 function getRestEndpoint() {
17580 const cfg = window.desktopModeConfig;
17581 if (!cfg || !cfg.restUrl || !cfg.restNonce) {
17582 return null;
17583 }
17584 return { restUrl: cfg.restUrl, restNonce: cfg.restNonce };
17585 }
17586 function dispatchLocal(sessionId, ev) {
17587 const sp = sessions.get(sessionId);
17588 if (!sp) {
17589 return;
17590 }
17591 const bucket2 = sp.channels.get(ev.channel);
17592 if (!bucket2) {
17593 return;
17594 }
17595 for (const cb of bucket2) {
17596 try {
17597 cb(ev);
17598 } catch {
17599 }
17600 }
17601 }
17602 let _localEventCounter = 0;
17603 const debugBus = {
17604 startSession() {
17605 const cryptoApi = window.crypto;
17606 if (cryptoApi && typeof cryptoApi.randomUUID === "function") {
17607 return cryptoApi.randomUUID();
17608 }
17609 return "wpdbg-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
17610 },
17611 publish(sessionId, channel, payload) {
17612 dispatchLocal(sessionId, {
17613 id: ++_localEventCounter,
17614 t: Date.now(),
17615 channel,
17616 payload
17617 });
17618 },
17619 subscribe(sessionId, channel, cb) {
17620 let sp = sessions.get(sessionId);
17621 const startedFresh = !sp;
17622 if (!sp) {
17623 sp = {
17624 channels: /* @__PURE__ */ new Map(),
17625 cursor: 0,
17626 timer: null,
17627 inflight: false
17628 };
17629 sessions.set(sessionId, sp);
17630 }
17631 let bucket2 = sp.channels.get(channel);
17632 if (!bucket2) {
17633 bucket2 = /* @__PURE__ */ new Set();
17634 sp.channels.set(channel, bucket2);
17635 }
17636 bucket2.add(cb);
17637 if (startedFresh) {
17638 const ep = getRestEndpoint();
17639 if (ep) {
17640 pollOnce(sessionId, ep.restUrl, ep.restNonce);
17641 }
17642 }
17643 return () => {
17644 const cur = sessions.get(sessionId);
17645 if (!cur) {
17646 return;
17647 }
17648 const b = cur.channels.get(channel);
17649 if (b) {
17650 b.delete(cb);
17651 if (b.size === 0) {
17652 cur.channels.delete(channel);
17653 }
17654 }
17655 if (cur.channels.size === 0) {
17656 if (cur.timer) {
17657 clearTimeout(cur.timer);
17658 }
17659 sessions.delete(sessionId);
17660 }
17661 };
17662 }
17663 };
17664 const devtools = {
17665 addRequestHeader(windowId, name, value) {
17666 if (typeof windowId !== "string" || windowId === "") {
17667 return () => {
17668 };
17669 }
17670 if (typeof name !== "string" || name === "") {
17671 return () => {
17672 };
17673 }
17674 const s = ensureState(windowId);
17675 const contribution = { value };
17676 let bucket2 = s.headers.get(name);
17677 if (!bucket2) {
17678 bucket2 = [];
17679 s.headers.set(name, bucket2);
17680 }
17681 bucket2.push(contribution);
17682 pushInstrumentation(windowId);
17683 return () => {
17684 const cur = states.get(windowId);
17685 if (!cur) {
17686 return;
17687 }
17688 const b = cur.headers.get(name);
17689 if (!b) {
17690 return;
17691 }
17692 const i = b.indexOf(contribution);
17693 if (i >= 0) {
17694 b.splice(i, 1);
17695 }
17696 if (b.length === 0) {
17697 cur.headers.delete(name);
17698 }
17699 pushInstrumentation(windowId);
17700 gcWindowState(windowId);
17701 };
17702 },
17703 onRequest(windowId, cb, opts) {
17704 if (typeof windowId !== "string" || windowId === "") {
17705 return () => {
17706 };
17707 }
17708 if (typeof cb !== "function") {
17709 return () => {
17710 };
17711 }
17712 const s = ensureState(windowId);
17713 s.observers.add(cb);
17714 const wantsObserve = !!opts?.observe;
17715 if (wantsObserve) {
17716 s.observeCount++;
17717 pushInstrumentation(windowId);
17718 }
17719 return () => {
17720 const cur = states.get(windowId);
17721 if (!cur) {
17722 return;
17723 }
17724 cur.observers.delete(cb);
17725 if (wantsObserve) {
17726 cur.observeCount = Math.max(0, cur.observeCount - 1);
17727 pushInstrumentation(windowId);
17728 }
17729 gcWindowState(windowId);
17730 };
17731 },
17732 reloadWithDebugSession(windowId, sessionId, opts) {
17733 if (typeof windowId !== "string" || windowId === "" || typeof sessionId !== "string" || sessionId === "") {
17734 return null;
17735 }
17736 const iframe = findIframe(windowId);
17737 if (!iframe) {
17738 return null;
17739 }
17740 const headerName = opts?.headerName || "X-WP-Debug-Session";
17741 const queryArg = opts?.queryArg || "wp_debug_session";
17742 const stopHeader = devtools.addRequestHeader(windowId, headerName, sessionId);
17743 try {
17744 const currentSrc = iframe.getAttribute("src") || iframe.src || "";
17745 const u = new URL(currentSrc, window.location.origin);
17746 u.searchParams.set(queryArg, sessionId);
17747 iframe.src = u.toString();
17748 } catch {
17749 }
17750 return {
17751 dispose: () => {
17752 stopHeader();
17753 }
17754 };
17755 },
17756 debug: debugBus
17757 };
17758 function gcWindowState(windowId) {
17759 const s = states.get(windowId);
17760 if (!s) {
17761 return;
17762 }
17763 if (s.headers.size === 0 && s.observers.size === 0) {
17764 detachLoadHandler(s);
17765 states.delete(windowId);
17766 }
17767 }
17768 async function wpdConfirm(options) {
17769 await ensureShellOverlaysLoaded(shellOverlaysBundleUrl());
17770 return new Promise((resolve2) => {
17771 const dialog2 = document.createElement("wpd-confirm-dialog");
17772 dialog2.setAttribute("open", "");
17773 if (options.title) {
17774 dialog2.setAttribute("title", options.title);
17775 }
17776 dialog2.setAttribute("message", options.message);
17777 if (options.confirmLabel) {
17778 dialog2.setAttribute("confirm-label", options.confirmLabel);
17779 }
17780 if (options.cancelLabel) {
17781 dialog2.setAttribute("cancel-label", options.cancelLabel);
17782 }
17783 if (options.danger) {
17784 dialog2.setAttribute("danger", "");
17785 }
17786 if (options.hideCancel) {
17787 dialog2.setAttribute("hide-cancel", "");
17788 }
17789 if (options.dismissable) {
17790 dialog2.setAttribute("dismissable", "");
17791 }
17792 const cleanup = (ok) => {
17793 dialog2.remove();
17794 resolve2(ok);
17795 };
17796 dialog2.addEventListener("wpd-confirm", () => cleanup(true));
17797 dialog2.addEventListener("wpd-cancel", () => cleanup(false));
17798 document.body.appendChild(dialog2);
17799 const inner = dialog2.shadowRoot?.querySelector(".dialog");
17800 (inner ?? dialog2).focus?.();
17801 });
17802 }
17803 function collectWallpaperSurfaces(manager) {
17804 const seed2 = [];
17805 for (const w of manager.getVisibleRects()) {
17806 if (w.state === "minimized") {
17807 continue;
17808 }
17809 if (w.element.offsetParent === null) {
17810 continue;
17811 }
17812 const r = w.element.getBoundingClientRect();
17813 seed2.push({
17814 id: `window:${w.windowId}`,
17815 kind: "window",
17816 rect: rectFromDom(r),
17817 face: "top",
17818 element: w.element
17819 });
17820 }
17821 const shellEl = document.getElementById("desktop-mode-shell");
17822 if (shellEl) {
17823 const r = shellEl.getBoundingClientRect();
17824 seed2.push({
17825 id: "shell:floor",
17826 kind: "shell",
17827 rect: {
17828 x: r.left,
17829 y: r.bottom - 1,
17830 width: r.width,
17831 height: 1
17832 },
17833 face: "top",
17834 element: shellEl
17835 });
17836 }
17837 const dockEls = document.querySelectorAll(
17838 ".desktop-mode-dock"
17839 );
17840 let dockIndex = 0;
17841 for (const dockEl of Array.from(dockEls)) {
17842 const r = dockEl.getBoundingClientRect();
17843 if (r.width <= 0 || r.height <= 0) {
17844 continue;
17845 }
17846 const placement = dockEl.getAttribute("data-desktop-mode-dock-placement") ?? "bottom";
17847 const id = dockIndex === 0 ? "dock:edge" : `dock:edge:${dockIndex}`;
17848 dockIndex++;
17849 if (placement === "bottom") {
17850 seed2.push({
17851 id,
17852 kind: "dock",
17853 rect: { x: r.left, y: r.top, width: r.width, height: 1 },
17854 face: "top",
17855 element: dockEl
17856 });
17857 } else if (placement === "right") {
17858 seed2.push({
17859 id,
17860 kind: "dock",
17861 rect: { x: r.left, y: r.top, width: 1, height: r.height },
17862 face: "left",
17863 element: dockEl
17864 });
17865 } else {
17866 seed2.push({
17867 id,
17868 kind: "dock",
17869 rect: {
17870 x: r.right - 1,
17871 y: r.top,
17872 width: 1,
17873 height: r.height
17874 },
17875 face: "right",
17876 element: dockEl
17877 });
17878 }
17879 }
17880 const widgetCards = document.querySelectorAll(
17881 ".desktop-mode-widgets__card"
17882 );
17883 let widgetIndex = 0;
17884 widgetCards.forEach((card) => {
17885 const r = card.getBoundingClientRect();
17886 if (r.width === 0 && r.height === 0) {
17887 return;
17888 }
17889 const id = card.dataset.widgetId ?? String(widgetIndex++);
17890 seed2.push({
17891 id: `widget:${id}`,
17892 kind: "widget",
17893 rect: rectFromDom(r),
17894 face: "top",
17895 element: card
17896 });
17897 });
17898 const filtered = applyFilters(HOOKS.WALLPAPER_SURFACES, seed2);
17899 return Array.isArray(filtered) ? filtered : seed2;
17900 }
17901 function rectFromDom(r) {
17902 return {
17903 x: r.left,
17904 y: r.top,
17905 width: r.width,
17906 height: r.height
17907 };
17908 }
17909 const NODE_KEY_PROP = "__desktop_modeKeyedListKey";
17910 const NODE_DATA_PROP = "__desktop_modeKeyedListData";
17911 function getHostState(host) {
17912 const cached = host.__desktop_modeKeyedList;
17913 if (cached) {
17914 return cached;
17915 }
17916 const fresh = { byKey: /* @__PURE__ */ new Map() };
17917 host.__desktop_modeKeyedList = fresh;
17918 return fresh;
17919 }
17920 function renderKeyedList(host, items, opts) {
17921 const state2 = getHostState(host);
17922 const prev = state2.byKey;
17923 const next = /* @__PURE__ */ new Map();
17924 const ordered = [];
17925 const seenKeys = /* @__PURE__ */ new Set();
17926 for (const item of items) {
17927 const key = String(opts.keyOf(item));
17928 if (seenKeys.has(key)) {
17929 console.warn(
17930 "[desktop-mode/keyed-list] duplicate key — only the last item with this key will render:",
17931 key
17932 );
17933 }
17934 seenKeys.add(key);
17935 const reused = prev.get(key);
17936 if (reused) {
17937 const prevData = reused.data;
17938 opts.updateItem?.(reused.el, item, prevData);
17939 reused.data = item;
17940 next.set(key, reused);
17941 ordered.push(reused.el);
17942 continue;
17943 }
17944 const el = opts.buildItem(item);
17945 el[NODE_KEY_PROP] = key;
17946 el[NODE_DATA_PROP] = item;
17947 next.set(key, { el, data: item });
17948 ordered.push(el);
17949 }
17950 for (const [key, entry] of prev) {
17951 if (!next.has(key)) {
17952 entry.el.remove();
17953 }
17954 }
17955 for (let i = 0; i < ordered.length; i++) {
17956 const desired = ordered[i];
17957 const live = host.children[i];
17958 if (live === desired) {
17959 continue;
17960 }
17961 host.insertBefore(desired, live ?? null);
17962 }
17963 state2.byKey = next;
17964 }
17965 function clearKeyedList(host) {
17966 const cached = host.__desktop_modeKeyedList;
17967 if (!cached) {
17968 return;
17969 }
17970 for (const entry of cached.byKey.values()) {
17971 entry.el.remove();
17972 }
17973 cached.byKey.clear();
17974 delete host.__desktop_modeKeyedList;
17975 }
17976 function createInfiniteList(options) {
17977 const {
17978 root,
17979 fetchPage,
17980 getId,
17981 renderItem,
17982 rootMargin = "200px",
17983 initialCursor = null,
17984 onLoadingChange = () => void 0,
17985 onError = (err) => {
17986 if (typeof console !== "undefined") {
17987 console.error("[desktop-mode] createInfiniteList:", err);
17988 }
17989 }
17990 } = options;
17991 let sentinel = options.sentinel ?? null;
17992 if (!sentinel) {
17993 sentinel = document.createElement("div");
17994 sentinel.dataset.wpdInfiniteListSentinel = "";
17995 sentinel.style.height = "1px";
17996 root.appendChild(sentinel);
17997 }
17998 const seen = /* @__PURE__ */ new Set();
17999 let cursor = initialCursor;
18000 let hasMoreInternal = true;
18001 let loading = false;
18002 let controller = null;
18003 let renderedCount = 0;
18004 let destroyed = false;
18005 let observer = null;
18006 const setLoading = (next) => {
18007 if (loading === next) {
18008 return;
18009 }
18010 loading = next;
18011 try {
18012 onLoadingChange(next);
18013 } catch (err) {
18014 onError(err);
18015 }
18016 };
18017 const detachObserver = () => {
18018 if (observer) {
18019 observer.disconnect();
18020 observer = null;
18021 }
18022 };
18023 const ensureObserver = () => {
18024 if (observer || !sentinel || destroyed) {
18025 return;
18026 }
18027 observer = new IntersectionObserver(
18028 (entries) => {
18029 for (const entry of entries) {
18030 if (entry.isIntersecting) {
18031 void loadMore();
18032 }
18033 }
18034 },
18035 { rootMargin }
18036 );
18037 observer.observe(sentinel);
18038 };
18039 const loadMore = async () => {
18040 if (destroyed || loading || !hasMoreInternal) {
18041 return;
18042 }
18043 setLoading(true);
18044 controller = new AbortController();
18045 const localController = controller;
18046 try {
18047 const page = await fetchPage(cursor, localController.signal);
18048 if (destroyed || localController !== controller) {
18049 return;
18050 }
18051 let appended = 0;
18052 const frag = document.createDocumentFragment();
18053 for (const item of page.items ?? []) {
18054 const key = String(getId(item));
18055 if (seen.has(key)) {
18056 continue;
18057 }
18058 seen.add(key);
18059 const el = renderItem(item, renderedCount + appended);
18060 frag.appendChild(el);
18061 appended++;
18062 }
18063 if (appended > 0) {
18064 if (sentinel && sentinel.parentNode === root) {
18065 root.insertBefore(frag, sentinel);
18066 } else {
18067 root.appendChild(frag);
18068 }
18069 renderedCount += appended;
18070 }
18071 cursor = page.nextCursor ?? null;
18072 if (!cursor) {
18073 hasMoreInternal = false;
18074 detachObserver();
18075 }
18076 } catch (err) {
18077 if (err?.name === "AbortError") {
18078 return;
18079 }
18080 onError(err);
18081 } finally {
18082 if (localController === controller) {
18083 setLoading(false);
18084 controller = null;
18085 }
18086 }
18087 };
18088 const reset = () => {
18089 if (destroyed) {
18090 return;
18091 }
18092 controller?.abort();
18093 controller = null;
18094 seen.clear();
18095 cursor = initialCursor;
18096 hasMoreInternal = true;
18097 renderedCount = 0;
18098 const sentinelInRoot = sentinel && sentinel.parentNode === root;
18099 while (root.firstChild) {
18100 root.removeChild(root.firstChild);
18101 }
18102 if (sentinelInRoot && sentinel) {
18103 root.appendChild(sentinel);
18104 }
18105 setLoading(false);
18106 ensureObserver();
18107 void loadMore();
18108 };
18109 const destroy = () => {
18110 if (destroyed) {
18111 return;
18112 }
18113 destroyed = true;
18114 detachObserver();
18115 controller?.abort();
18116 controller = null;
18117 if (!options.sentinel && sentinel && sentinel.parentNode === root) {
18118 root.removeChild(sentinel);
18119 }
18120 sentinel = null;
18121 setLoading(false);
18122 };
18123 ensureObserver();
18124 void loadMore();
18125 return {
18126 reset,
18127 loadMore,
18128 hasMore: () => hasMoreInternal,
18129 isLoading: () => loading,
18130 destroy
18131 };
18132 }
18133 const POPUP_DEFAULT_WIDTH = 520;
18134 const POPUP_DEFAULT_HEIGHT = 720;
18135 const POPUP_CLOSE_POLL_MS = 500;
18136 function startOAuth(service, options = {}) {
18137 if (typeof service !== "string" || service === "") {
18138 return Promise.reject(
18139 new Error("[desktop-mode] startOAuth requires a non-empty service slug.")
18140 );
18141 }
18142 const restRoot2 = readRestRoot$1();
18143 const restNonce = readRestNonce$1();
18144 return trackedFetch$1(
18145 joinRestUrl(restRoot2, "desktop-mode/v1/oauth/start"),
18146 {
18147 method: "POST",
18148 headers: {
18149 "Content-Type": "application/json",
18150 "X-WP-Nonce": restNonce ?? ""
18151 },
18152 body: JSON.stringify({ service })
18153 },
18154 { source: "desktop-mode/oauth-start" }
18155 ).then(async (res) => {
18156 if (!res.ok) {
18157 const text = await res.text().catch(() => "");
18158 throw new Error(
18159 `[desktop-mode] OAuth start failed (${res.status}): ${text}`
18160 );
18161 }
18162 return await res.json();
18163 }).then((startBody) => openPopupAndWait(startBody, service, options));
18164 }
18165 function openPopupAndWait(body, service, options) {
18166 return new Promise((resolve2, reject) => {
18167 const width = options.width ?? POPUP_DEFAULT_WIDTH;
18168 const height = options.height ?? POPUP_DEFAULT_HEIGHT;
18169 const left = Math.max(0, Math.floor((window.screen.width - width) / 2));
18170 const top = Math.max(0, Math.floor((window.screen.height - height) / 2));
18171 const features = [
18172 `width=${width}`,
18173 `height=${height}`,
18174 `left=${left}`,
18175 `top=${top}`,
18176 "menubar=no",
18177 "toolbar=no",
18178 "location=yes",
18179 "status=no",
18180 "resizable=yes",
18181 "scrollbars=yes"
18182 ].join(",");
18183 const popup = window.open(
18184 body.authorize_url,
18185 `desktop-mode-oauth-${service}`,
18186 features
18187 );
18188 if (!popup) {
18189 reject(
18190 new Error(
18191 "[desktop-mode] OAuth popup blocked. Tell users to allow popups for this site."
18192 )
18193 );
18194 return;
18195 }
18196 const expectedOrigin = window.location.origin;
18197 let pollTimer = null;
18198 let detached = false;
18199 const cleanup = () => {
18200 if (detached) {
18201 return;
18202 }
18203 detached = true;
18204 window.removeEventListener("message", onMessage);
18205 if (pollTimer !== null) {
18206 window.clearInterval(pollTimer);
18207 pollTimer = null;
18208 }
18209 };
18210 const onMessage = (e) => {
18211 if (e.origin !== expectedOrigin) {
18212 return;
18213 }
18214 const data = e.data;
18215 if (!data || data.type !== "desktop-mode-oauth-callback") {
18216 return;
18217 }
18218 const payload = data.payload;
18219 cleanup();
18220 if (payload && payload.ok) {
18221 resolve2(payload);
18222 } else {
18223 const reason = payload?.reason ?? "unknown";
18224 const message = payload?.message ?? "OAuth flow failed";
18225 const err = new Error(
18226 `[desktop-mode] startOAuth(${service}) failed: ${reason} — ${message}`
18227 );
18228 err.cause = payload;
18229 reject(err);
18230 }
18231 };
18232 window.addEventListener("message", onMessage);
18233 pollTimer = window.setInterval(() => {
18234 if (popup.closed) {
18235 cleanup();
18236 reject(
18237 new Error(
18238 `[desktop-mode] startOAuth(${service}) cancelled — popup closed before completing.`
18239 )
18240 );
18241 }
18242 }, POPUP_CLOSE_POLL_MS);
18243 });
18244 }
18245 function readDesktopConfig() {
18246 return window.desktopModeConfig ?? {};
18247 }
18248 function readRestRoot$1() {
18249 const root = readDesktopConfig().restRoot;
18250 if (typeof root === "string" && root !== "") {
18251 return root;
18252 }
18253 return `${window.location.origin}/wp-json/`;
18254 }
18255 function readRestNonce$1() {
18256 const nonce = readDesktopConfig().restNonce;
18257 return typeof nonce === "string" && nonce !== "" ? nonce : null;
18258 }
18259 const RESERVED_NAMESPACE_KEYS = /* @__PURE__ */ new Set([
18260 "windowManager",
18261 "dock",
18262 "taskbar",
18263 "icons",
18264 "saveSession",
18265 "hooks",
18266 "HOOKS",
18267 "isActive",
18268 "registerWallpaper",
18269 "registerWidget",
18270 "widgetLayer",
18271 "widgets",
18272 "registerSystemTile",
18273 "registerWindow",
18274 "openWindow",
18275 "cloneTemplate",
18276 "onWindow",
18277 "loadVendorScript",
18278 "getWallpaperSurfaces",
18279 "registerModule",
18280 "loadModules",
18281 "whenReady",
18282 "ready",
18283 "isReady",
18284 "setDefaultWindow",
18285 "refreshMenu",
18286 "config",
18287 "ai",
18288 "dragBridge",
18289 "dragManager",
18290 "registerCommand",
18291 "unregisterCommand",
18292 "listCommands",
18293 "registerDestructiveAdminAction",
18294 "unregisterDestructiveAdminAction",
18295 "listDestructiveAdminActions",
18296 "registerSettingsTab",
18297 "unregisterSettingsTab",
18298 "listSettingsTabs",
18299 "registerDockRailRenderer",
18300 "unregisterDockRailRenderer",
18301 "listDockRailRenderers",
18302 "openOsSettings",
18303 "getOsSettings",
18304 "subscribeOsSettings",
18305 "updateOsSettings",
18306 "deriveWindowId",
18307 "listSystemTiles",
18308 "getSystemTile",
18309 "getMenuItems",
18310 "renderIcon",
18311 "applyTileClasses",
18312 "applyTileElement",
18313 "applyTileTooltip",
18314 "dispatchTileRendered",
18315 "isDockElement",
18316 "registerDockSelector",
18317 "registerTitleBarButton",
18318 "unregisterTitleBarButton",
18319 "listTitleBarButtons",
18320 "registerWindowTheme",
18321 "unregisterWindowTheme",
18322 "listWindowThemes",
18323 "applyWindowTheme",
18324 "registerWindowControl",
18325 "unregisterWindowControl",
18326 "listWindowControls",
18327 "applyWindowControls",
18328 "registerWindowSlot",
18329 "unregisterWindowSlot",
18330 "listWindowSlots",
18331 "applyWindowSlot",
18332 "registerWindowNotice",
18333 "unregisterWindowNotice",
18334 "listWindowNotices",
18335 "dismissWindowNotice",
18336 "undismissWindowNotice",
18337 "registerWindowChrome",
18338 "unregisterWindowChrome",
18339 "listWindowChromes",
18340 "applyWindowChrome",
18341 "connect",
18342 "broadcast",
18343 "subscribe",
18344 "registerPalette",
18345 "unregisterPalette",
18346 "listPalettes",
18347 "openPalette",
18348 "devtools",
18349 "createSharedStore",
18350 "presence",
18351 "activity",
18352 "heartbeat",
18353 "showToast",
18354 "renderKeyedList",
18355 "clearKeyedList",
18356 "registerNamespace",
18357 "notify",
18358 "pwa",
18359 "getWindowConfig",
18360 "debug",
18361 "fetch"
18362 ]);
18363 function buildPublicApi(deps2) {
18364 const {
18365 manager,
18366 dock,
18367 layoutDispatcher,
18368 osSettings,
18369 iconsApi: iconsApi2,
18370 filesApi: filesApi2,
18371 saveSession,
18372 widgetLayer,
18373 registerWindow,
18374 openWindowById,
18375 openNewWindowById,
18376 placeSystemTile,
18377 setDefaultWindow,
18378 refreshMenu,
18379 openOsSettings,
18380 aiAssistant,
18381 dragBridge,
18382 dragManager,
18383 connect,
18384 getConnection,
18385 config
18386 } = deps2;
18387 const desktopApi = {
18388 windowManager: manager,
18389 dock,
18390 sideDock: layoutDispatcher?.getSide() ?? null,
18391 desktopLayout: osSettings.getOsSettingsSnapshot().desktopLayout,
18392 icons: iconsApi2,
18393 files: filesApi2,
18394 confirm: wpdConfirm,
18395 saveSession,
18396 hooks: rawHooks(),
18397 HOOKS,
18398 isActive: () => !!document.getElementById("desktop-mode-shell"),
18399 registerWallpaper: (def) => {
18400 register$2(def);
18401 osSettings.apply();
18402 },
18403 registerWidget: (def) => {
18404 register(def);
18405 },
18406 widgetLayer,
18407 widgets: {
18408 redock: (id) => {
18409 widgetLayer?.redock(id);
18410 }
18411 },
18412 loadVendorScript,
18413 getWallpaperSurfaces: () => collectWallpaperSurfaces(manager),
18414 registerWindow,
18415 openWindow: openWindowById,
18416 openNewWindow: openNewWindowById,
18417 fetch: (input, requestInit, opts) => trackedFetch(manager, input, requestInit, opts),
18418 repaintLoadingOverlays,
18419 cloneTemplate,
18420 onWindow,
18421 createInfiniteList,
18422 startOAuth,
18423 registerSystemTile: (item) => {
18424 placeSystemTile(item);
18425 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: item.id });
18426 },
18427 registerModule,
18428 loadModules,
18429 whenReady,
18430 ready: whenReady,
18431 isReady,
18432 setDefaultWindow,
18433 refreshMenu,
18434 config,
18435 ai: aiAssistant,
18436 dragBridge,
18437 dragManager,
18438 registerCommand,
18439 unregisterCommand,
18440 listCommands,
18441 registerDestructiveAdminAction,
18442 unregisterDestructiveAdminAction,
18443 listDestructiveAdminActions,
18444 registerSettingsTab,
18445 unregisterSettingsTab,
18446 listSettingsTabs,
18447 registerDockRailRenderer: register$1,
18448 unregisterDockRailRenderer: unregister$1,
18449 listDockRailRenderers: list,
18450 openOsSettings,
18451 getOsSettings: () => osSettings.getOsSettingsSnapshot(),
18452 subscribeOsSettings: (cb) => osSettings.subscribeOsSettings(cb),
18453 updateOsSettings: (patch, opts = {}) => {
18454 if (typeof patch.wallpaper === "string") {
18455 osSettings.state.wallpaper = patch.wallpaper;
18456 }
18457 if (typeof patch.accent === "string") {
18458 osSettings.state.accent = patch.accent;
18459 }
18460 if (typeof patch.dockSize === "string") {
18461 osSettings.state.dockSize = patch.dockSize;
18462 }
18463 if (typeof patch.desktopLayout === "string") {
18464 osSettings.state.desktopLayout = patch.desktopLayout;
18465 }
18466 if (typeof patch.dockRailRenderer === "string") {
18467 osSettings.state.dockRailRenderer = patch.dockRailRenderer;
18468 }
18469 if (patch.ai && typeof patch.ai === "object") {
18470 osSettings.state.ai = { ...osSettings.state.ai, ...patch.ai };
18471 }
18472 if (typeof patch.nativePostsEnabled === "boolean") {
18473 osSettings.state.nativePostsEnabled = patch.nativePostsEnabled;
18474 }
18475 if (typeof patch.nativePagesEnabled === "boolean") {
18476 osSettings.state.nativePagesEnabled = patch.nativePagesEnabled;
18477 }
18478 if (typeof patch.nativeUsersEnabled === "boolean") {
18479 osSettings.state.nativeUsersEnabled = patch.nativeUsersEnabled;
18480 }
18481 if (typeof patch.nativePluginsEnabled === "boolean") {
18482 osSettings.state.nativePluginsEnabled = patch.nativePluginsEnabled;
18483 }
18484 if (typeof patch.nativeCommentsEnabled === "boolean") {
18485 osSettings.state.nativeCommentsEnabled = patch.nativeCommentsEnabled;
18486 }
18487 if (typeof patch.foldersSharingEnabled === "boolean") {
18488 osSettings.state.foldersSharingEnabled = patch.foldersSharingEnabled;
18489 }
18490 if (Array.isArray(patch.nativePostsHiddenColumns)) {
18491 osSettings.state.nativePostsHiddenColumns = patch.nativePostsHiddenColumns.filter(
18492 (v) => typeof v === "string" && v !== ""
18493 ).slice(0, 32);
18494 }
18495 if (patch.itemVisibility && typeof patch.itemVisibility === "object") {
18496 const allowed = ["both", "dock", "desktop", "hidden"];
18497 const next = {};
18498 for (const [k, v] of Object.entries(
18499 patch.itemVisibility
18500 )) {
18501 if (typeof k !== "string" || k === "") {
18502 continue;
18503 }
18504 if (typeof v !== "string" || !allowed.includes(v)) {
18505 continue;
18506 }
18507 next[k] = v;
18508 }
18509 osSettings.state.itemVisibility = next;
18510 }
18511 if (Array.isArray(patch.dockOrder)) {
18512 osSettings.state.dockOrder = patch.dockOrder.filter(
18513 (v) => typeof v === "string" && v !== ""
18514 ).slice(0, 256);
18515 }
18516 if (patch.dockPromotedPositions && typeof patch.dockPromotedPositions === "object") {
18517 const MAX_COORD = 1e5;
18518 const next = {};
18519 for (const [k, v] of Object.entries(
18520 patch.dockPromotedPositions
18521 )) {
18522 if (typeof k !== "string" || k === "") {
18523 continue;
18524 }
18525 if (!v || typeof v !== "object") {
18526 continue;
18527 }
18528 const pos = v;
18529 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) {
18530 continue;
18531 }
18532 next[k] = { x: pos.x, y: pos.y };
18533 if (Object.keys(next).length >= 256) {
18534 break;
18535 }
18536 }
18537 osSettings.state.dockPromotedPositions = next;
18538 }
18539 osSettings.save(opts);
18540 if (patch.itemVisibility || patch.dockOrder) {
18541 layoutDispatcher?.refresh();
18542 }
18543 },
18544 deriveWindowId: (url, overrideAdminUrl) => deriveWindowId(url, overrideAdminUrl ?? config.adminUrl),
18545 listSystemTiles: () => layoutDispatcher?.listSystemTiles() ?? [],
18546 getSystemTile: (id) => layoutDispatcher?.getSystemTile(id) ?? null,
18547 getMenuItems: () => layoutDispatcher?.getMenuItems() ?? [],
18548 renderIcon,
18549 applyTileClasses,
18550 applyTileElement,
18551 applyTileTooltip,
18552 dispatchTileRendered,
18553 isDockElement,
18554 registerDockSelector,
18555 registerTitleBarButton,
18556 unregisterTitleBarButton,
18557 listTitleBarButtons,
18558 registerWindowTheme,
18559 unregisterWindowTheme,
18560 listWindowThemes,
18561 applyWindowTheme: (windowId, override) => {
18562 const win = manager.getById(windowId);
18563 if (!win) {
18564 return;
18565 }
18566 win.setAppearanceTheme(override);
18567 },
18568 registerWindowControl,
18569 unregisterWindowControl,
18570 listWindowControls,
18571 applyWindowControls: (windowId, override) => {
18572 const win = manager.getById(windowId);
18573 if (!win) {
18574 return;
18575 }
18576 win.setAppearanceControls(override);
18577 },
18578 registerWindowSlot,
18579 unregisterWindowSlot,
18580 listWindowSlots,
18581 applyWindowSlot: (windowId, slot, slotConfig) => {
18582 const win = manager.getById(windowId);
18583 if (!win) {
18584 return;
18585 }
18586 win.setAppearanceSlot(slot, slotConfig);
18587 },
18588 registerWindowNotice,
18589 unregisterWindowNotice,
18590 listWindowNotices,
18591 dismissWindowNotice,
18592 undismissWindowNotice,
18593 registerWindowChrome,
18594 unregisterWindowChrome,
18595 listWindowChromes,
18596 applyWindowChrome: (windowId, chromeId) => {
18597 const win = manager.getById(windowId);
18598 if (!win) {
18599 return;
18600 }
18601 win.setAppearanceChrome(chromeId);
18602 },
18603 connect,
18604 getConnection,
18605 broadcast,
18606 subscribe: subscribe$2,
18607 registerPalette,
18608 unregisterPalette,
18609 listPalettes,
18610 openPalette: openPaletteOnly,
18611 devtools,
18612 createSharedStore,
18613 presence: presenceApi,
18614 activity,
18615 heartbeat,
18616 showToast,
18617 notify: notify$3,
18618 pwa: {
18619 promptInstall,
18620 undismissInstallHint,
18621 getState: getPwaState,
18622 subscribe: subscribePwaState,
18623 requestNotificationPermission,
18624 getNotificationPermission
18625 },
18626 renderKeyedList,
18627 clearKeyedList,
18628 registerNamespace: (name, api) => {
18629 if (typeof name !== "string" || name === "") {
18630 console.warn(
18631 "[desktop-mode] registerNamespace: name must be a non-empty string"
18632 );
18633 return;
18634 }
18635 if (!api || typeof api !== "object") {
18636 console.warn(
18637 `[desktop-mode] registerNamespace("${name}"): api must be an object`
18638 );
18639 return;
18640 }
18641 if (RESERVED_NAMESPACE_KEYS.has(name)) {
18642 console.warn(
18643 `[desktop-mode] registerNamespace("${name}"): name is reserved by the shell — pick a plugin-specific key`
18644 );
18645 return;
18646 }
18647 desktopApi[name] = api;
18648 },
18649 getWindowConfig: (id) => {
18650 const store2 = window.desktopModeWindowConfig;
18651 if (!store2 || typeof store2 !== "object") {
18652 return void 0;
18653 }
18654 const value = store2[id];
18655 return value === void 0 ? void 0 : value;
18656 },
18657 debug: {
18658 window: (id) => {
18659 const entry = (config.nativeWindows ?? []).find(
18660 (e) => e.id === id
18661 );
18662 if (!entry) {
18663 return null;
18664 }
18665 const url = entry.scriptUrl || "";
18666 let loadPath = "unknown";
18667 let tagInDom = false;
18668 if (url) {
18669 const lazyTag = document.querySelector(
18670 `script[data-desktop-mode-vendor="${url.replace(/"/g, '\\"')}"]`
18671 );
18672 if (lazyTag) {
18673 loadPath = "lazy";
18674 tagInDom = true;
18675 } else {
18676 const eagerTag = Array.from(
18677 document.querySelectorAll(
18678 "script[src]"
18679 )
18680 ).find((s) => s.src === url);
18681 if (eagerTag) {
18682 loadPath = "eager";
18683 tagInDom = true;
18684 }
18685 }
18686 }
18687 const cfgStore = window.desktopModeWindowConfig;
18688 const configPresent = !!(cfgStore && typeof cfgStore === "object" && Object.prototype.hasOwnProperty.call(cfgStore, id));
18689 return {
18690 id,
18691 scriptHandle: entry.scriptHandle || "",
18692 scriptUrl: url,
18693 loadPath,
18694 tagInDom,
18695 configPresent,
18696 extras: {
18697 hasTranslations: !!entry.scriptTranslations,
18698 l10nCount: (entry.scriptL10n ?? []).length,
18699 beforeCount: (entry.scriptBefore ?? []).length,
18700 afterCount: (entry.scriptAfter ?? []).length
18701 }
18702 };
18703 }
18704 }
18705 };
18706 return desktopApi;
18707 }
18708 function installPublicApi(api) {
18709 if (!window.wp) {
18710 window.wp = {};
18711 }
18712 if (!window.wp.desktop) {
18713 window.wp.desktop = api;
18714 return;
18715 }
18716 Object.assign(
18717 window.wp.desktop,
18718 api
18719 );
18720 }
18721 const store$1 = createSharedStore("desktop-mode/layout", () => ({
18722 // Default mirrors the OsSettingsSnapshot default; the shell
18723 // re-publishes the persisted value as soon as it boots.
18724 layout: "classic"
18725 }));
18726 function setCurrentLayout(layout) {
18727 if (store$1.state.layout === layout) {
18728 return;
18729 }
18730 store$1.state.layout = layout;
18731 store$1.notify();
18732 }
18733 class DesktopFile {
18734 constructor(shape) {
18735 this.shape = shape;
18736 }
18737 /** Title shown under the tile. Defaults to `shape.title`. */
18738 title() {
18739 return this.shape.title;
18740 }
18741 /** Dashicon class or data URI. Defaults to `shape.icon`. */
18742 icon() {
18743 return this.shape.icon;
18744 }
18745 /** Optional preview-image URL. Defaults to `shape.previewUrl`. */
18746 previewUrl() {
18747 return this.shape.previewUrl;
18748 }
18749 /** Reference (id, URL, …). */
18750 ref() {
18751 return this.shape.ref;
18752 }
18753 /** Whether the underlying entity still exists. */
18754 exists() {
18755 return this.shape.exists;
18756 }
18757 }
18758 class DefaultDesktopFile extends DesktopFile {
18759 constructor(shape, typeSlug) {
18760 super(shape);
18761 this.typeSlug = typeSlug;
18762 }
18763 type() {
18764 return this.typeSlug;
18765 }
18766 }
18767 const seed$1 = /* @__PURE__ */ new Map();
18768 const listeners$1 = /* @__PURE__ */ new Set();
18769 function registerType(def) {
18770 if (!def.type) {
18771 throw new Error("[desktop-mode] registerType: `type` is required.");
18772 }
18773 if (!def.label) {
18774 throw new Error("[desktop-mode] registerType: `label` is required.");
18775 }
18776 seed$1.set(def.type, {
18777 type: def.type,
18778 label: def.label,
18779 sort: typeof def.sort === "number" ? def.sort : 100,
18780 DesktopFile: def.DesktopFile
18781 });
18782 doAction("desktop-mode.files.type-registered", def.type, def);
18783 notify$1();
18784 }
18785 function unregisterType(typeSlug) {
18786 if (seed$1.delete(typeSlug)) {
18787 doAction("desktop-mode.files.type-unregistered", typeSlug);
18788 notify$1();
18789 }
18790 }
18791 function getType(typeSlug) {
18792 const entry = seed$1.get(typeSlug);
18793 return entry ? entry : null;
18794 }
18795 function getTypes() {
18796 const list2 = Array.from(seed$1.values()).slice();
18797 const filtered = applyFilters(
18798 "desktop-mode.files.types",
18799 list2
18800 );
18801 const arr = Array.isArray(filtered) ? filtered : list2;
18802 arr.sort((a, b) => {
18803 if (a.sort !== b.sort) {
18804 return a.sort - b.sort;
18805 }
18806 return a.label.localeCompare(b.label);
18807 });
18808 return arr;
18809 }
18810 function resolve(shape) {
18811 const entry = seed$1.get(shape.type);
18812 if (entry?.DesktopFile) {
18813 return new entry.DesktopFile(shape);
18814 }
18815 return new DefaultDesktopFile(shape, shape.type);
18816 }
18817 function subscribe(cb) {
18818 listeners$1.add(cb);
18819 return () => listeners$1.delete(cb);
18820 }
18821 function notify$1() {
18822 for (const cb of listeners$1) {
18823 try {
18824 cb();
18825 } catch (err) {
18826 console.error("[desktop-mode] files registry subscriber threw:", err);
18827 }
18828 }
18829 }
18830 const seed = /* @__PURE__ */ new Map();
18831 const listeners = /* @__PURE__ */ new Set();
18832 let userAssociations = {};
18833 function setUserAssociations(map) {
18834 userAssociations = { ...map };
18835 notify();
18836 }
18837 function getUserAssociations() {
18838 return { ...userAssociations };
18839 }
18840 function registerOpener(def) {
18841 if (!def.id) {
18842 throw new Error("[desktop-mode] registerOpener: `id` is required.");
18843 }
18844 if (!def.label) {
18845 throw new Error("[desktop-mode] registerOpener: `label` is required.");
18846 }
18847 if (!Array.isArray(def.types) || def.types.length === 0) {
18848 throw new Error("[desktop-mode] registerOpener: `types` must be a non-empty array.");
18849 }
18850 if (!def.handler || typeof def.handler !== "object") {
18851 throw new Error("[desktop-mode] registerOpener: `handler` is required.");
18852 }
18853 seed.set(def.id, {
18854 id: def.id,
18855 label: def.label,
18856 types: def.types.slice(),
18857 isDefault: !!def.isDefault,
18858 sort: typeof def.sort === "number" ? def.sort : 100,
18859 handler: def.handler
18860 });
18861 doAction("desktop-mode.files.opener-registered", def.id, def);
18862 notify();
18863 }
18864 function unregisterOpener(id) {
18865 if (seed.delete(id)) {
18866 doAction("desktop-mode.files.opener-unregistered", id);
18867 notify();
18868 }
18869 }
18870 function getOpener(id) {
18871 return seed.get(id) ?? null;
18872 }
18873 function getOpeners() {
18874 const list2 = Array.from(seed.values()).slice();
18875 const filtered = applyFilters(
18876 "desktop-mode.files.openers",
18877 list2
18878 );
18879 const arr = Array.isArray(filtered) ? filtered : list2;
18880 arr.sort((a, b) => {
18881 const sa = typeof a.sort === "number" ? a.sort : 100;
18882 const sb = typeof b.sort === "number" ? b.sort : 100;
18883 if (sa !== sb) {
18884 return sa - sb;
18885 }
18886 return a.label.localeCompare(b.label);
18887 });
18888 return arr;
18889 }
18890 function getOpenersForType(type) {
18891 return getOpeners().filter((e) => e.types.includes(type));
18892 }
18893 function resolveOpener(type) {
18894 const candidates = getOpenersForType(type);
18895 if (candidates.length === 0) {
18896 return null;
18897 }
18898 const override = userAssociations[type];
18899 let resolved = null;
18900 if (override) {
18901 resolved = candidates.find((e) => e.id === override) ?? null;
18902 }
18903 if (!resolved) {
18904 resolved = candidates.find((e) => e.isDefault) ?? null;
18905 }
18906 if (!resolved) {
18907 resolved = candidates[0];
18908 }
18909 const filtered = applyFilters(
18910 "desktop-mode.files.resolve-opener",
18911 resolved,
18912 type
18913 );
18914 return filtered ?? null;
18915 }
18916 function subscribeOpeners(cb) {
18917 listeners.add(cb);
18918 return () => listeners.delete(cb);
18919 }
18920 function notify() {
18921 for (const cb of listeners) {
18922 try {
18923 cb();
18924 } catch (err) {
18925 console.error("[desktop-mode] openers subscriber threw:", err);
18926 }
18927 }
18928 }
18929 let deps$1 = null;
18930 function installOpenDeps(next) {
18931 deps$1 = next;
18932 }
18933 async function openFile(file, ctx) {
18934 if (!deps$1) {
18935 console.warn(
18936 "[desktop-mode] wp.desktop.files.open() called before the shell installed open deps. The file will not open."
18937 );
18938 return false;
18939 }
18940 const opener = resolveOpener(file.type());
18941 if (!opener) {
18942 doAction("desktop-mode.files.open-failed", {
18943 reason: "no-opener",
18944 type: file.type(),
18945 ref: file.ref()
18946 });
18947 return false;
18948 }
18949 doAction("desktop-mode.files.opening", { file, openerId: opener.id });
18950 try {
18951 const handler = opener.handler;
18952 if (handler.kind === "url") {
18953 const url = await handler.url(file);
18954 if (!url) {
18955 return false;
18956 }
18957 const id = handler.windowId ? handler.windowId(file) : deps$1.deriveWindowId(url);
18958 const title = handler.title ? handler.title(file) : file.title();
18959 const icon = file.icon();
18960 const opened = deps$1.openUrl({ id, url, title, icon });
18961 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "url" });
18962 return opened;
18963 }
18964 if (handler.kind === "window") {
18965 const config = handler.config ? handler.config(file) : void 0;
18966 const opened = deps$1.openNativeWindow(handler.windowId, config);
18967 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "window" });
18968 return opened;
18969 }
18970 await handler.open(file, ctx);
18971 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "js" });
18972 return true;
18973 } catch (err) {
18974 doAction("desktop-mode.files.open-failed", {
18975 reason: "handler-threw",
18976 type: file.type(),
18977 ref: file.ref(),
18978 openerId: opener.id,
18979 error: err
18980 });
18981 console.error("[desktop-mode] file opener threw:", err);
18982 return false;
18983 }
18984 }
18985 function registerBuiltInFileTypes() {
18986 registerType({ type: "shortcut", label: "Plugin shortcut", sort: 1 });
18987 registerType({ type: "folder", label: "Folder", sort: 5 });
18988 registerType({ type: "post", label: "Post", sort: 10 });
18989 registerType({ type: "attachment", label: "Media", sort: 20 });
18990 registerType({ type: "user", label: "User", sort: 30 });
18991 registerType({ type: "term", label: "Taxonomy term", sort: 40 });
18992 registerType({ type: "comment", label: "Comment", sort: 50 });
18993 registerType({ type: "bookmark", label: "Bookmark", sort: 60 });
18994 registerType({ type: "link", label: "Web link", sort: 70 });
18995 registerType({ type: "embed", label: "Embedded web window", sort: 80 });
18996 }
18997 let deps = null;
18998 function installRestDeps(next) {
18999 deps = next;
19000 }
19001 function ensureDeps() {
19002 if (!deps) {
19003 throw new Error("[desktop-mode] files REST client called before installRestDeps().");
19004 }
19005 return deps;
19006 }
19007 class FilesConflictError extends Error {
19008 constructor(detail) {
19009 super(
19010 `Row was changed by ${detail.actor.name || "another session"} (parent="${detail.current.parentName}")`
19011 );
19012 this.name = "FilesConflictError";
19013 this.status = 409;
19014 this.detail = detail;
19015 }
19016 }
19017 async function call(path, init2) {
19018 const { baseUrl, nonce } = ensureDeps();
19019 const url = joinRestUrl(baseUrl, path);
19020 const headers = new Headers(init2.headers ?? {});
19021 headers.set("X-WP-Nonce", nonce);
19022 if (init2.body && !headers.has("Content-Type")) {
19023 headers.set("Content-Type", "application/json");
19024 }
19025 const res = await trackedFetch$1(
19026 url,
19027 { ...init2, headers, credentials: "same-origin" },
19028 { source: "desktop-mode/files" }
19029 );
19030 const text = await res.text();
19031 let body = null;
19032 let parseError = null;
19033 if (text) {
19034 try {
19035 body = JSON.parse(text);
19036 } catch (e) {
19037 body = null;
19038 parseError = e;
19039 }
19040 }
19041 if (!res.ok) {
19042 if (res.status === 409) {
19043 const data = body?.data?.data ?? body?.data;
19044 if (data && typeof data === "object") {
19045 throw new FilesConflictError(data);
19046 }
19047 }
19048 const err = body;
19049 throw new Error(
19050 `[desktop-mode] files REST ${res.status}: ${err?.code ?? ""} ${err?.message ?? ""}`.trim()
19051 );
19052 }
19053 if (null === body) {
19054 if (parseError && text) {
19055 const head = text.slice(0, 120).replace(/\s+/g, " ");
19056 throw new Error(
19057 `[desktop-mode] files REST ${res.status} returned non-JSON body — ${parseError.message}. First 120 chars: ${head}`
19058 );
19059 }
19060 throw new Error(
19061 `[desktop-mode] files REST ${res.status}: empty or unparseable body.`
19062 );
19063 }
19064 return body;
19065 }
19066 function listPlacements(folderId = 0) {
19067 return call(
19068 `/placements?folder=${encodeURIComponent(String(folderId))}`,
19069 { method: "GET" }
19070 );
19071 }
19072 function createPlacement(body) {
19073 return call("/placements", {
19074 method: "POST",
19075 body: JSON.stringify(body)
19076 });
19077 }
19078 function updatePlacement(id, body, ifMatchMs) {
19079 const headers = {};
19080 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
19081 headers["If-Match"] = String(ifMatchMs);
19082 }
19083 return call(`/placements/${id}`, {
19084 method: "PATCH",
19085 body: JSON.stringify(body),
19086 headers
19087 });
19088 }
19089 function deletePlacement(id) {
19090 return call(`/placements/${id}`, { method: "DELETE" });
19091 }
19092 async function restoreTrashedItem(id, type) {
19093 const { baseUrl, nonce } = ensureDeps();
19094 const root = baseUrl.replace(/\/files\/?$/, "");
19095 const url = `${root}/recycle-bin/restore`;
19096 const res = await trackedFetch$1(
19097 url,
19098 {
19099 method: "POST",
19100 headers: {
19101 "Content-Type": "application/json",
19102 "X-WP-Nonce": nonce
19103 },
19104 credentials: "same-origin",
19105 body: JSON.stringify({ items: [{ id, type }] })
19106 },
19107 { source: "desktop-mode/files" }
19108 );
19109 if (!res.ok) {
19110 throw new Error(`[desktop-mode] restore ${res.status}`);
19111 }
19112 return await res.json();
19113 }
19114 function listFolders() {
19115 return call("/folders", { method: "GET" });
19116 }
19117 function createFolder(body) {
19118 return call("/folders", {
19119 method: "POST",
19120 body: JSON.stringify(body)
19121 });
19122 }
19123 function updateFolder(id, body, ifMatchMs) {
19124 const headers = {};
19125 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
19126 headers["If-Match"] = String(ifMatchMs);
19127 }
19128 return call(`/folders/${id}`, {
19129 method: "PATCH",
19130 body: JSON.stringify(body),
19131 headers
19132 });
19133 }
19134 function deleteFolder(id) {
19135 return call(`/folders/${id}`, { method: "DELETE" });
19136 }
19137 function saveAssociations(associations) {
19138 return call("/associations", {
19139 method: "PUT",
19140 body: JSON.stringify({ associations })
19141 });
19142 }
19143 function listShares(folderId) {
19144 return call(`/folders/${folderId}/shares`, { method: "GET" });
19145 }
19146 function inviteShare(folderId, body) {
19147 return call(`/folders/${folderId}/shares`, {
19148 method: "POST",
19149 body: JSON.stringify(body)
19150 });
19151 }
19152 function updateShareCapability(folderId, shareId, capability) {
19153 return call(`/folders/${folderId}/shares/${shareId}`, {
19154 method: "PATCH",
19155 body: JSON.stringify({ capability })
19156 });
19157 }
19158 function revokeShare(folderId, shareId) {
19159 return call(`/folders/${folderId}/shares/${shareId}`, {
19160 method: "DELETE"
19161 });
19162 }
19163 function acceptShare(folderId, shareId) {
19164 return call(`/folders/${folderId}/shares/${shareId}/accept`, {
19165 method: "POST"
19166 });
19167 }
19168 function denyShare(folderId, shareId) {
19169 return call(`/folders/${folderId}/shares/${shareId}/deny`, {
19170 method: "POST"
19171 });
19172 }
19173 function leaveShare(folderId) {
19174 return call(`/folders/${folderId}/leave`, {
19175 method: "POST"
19176 });
19177 }
19178 function purgeFolderSharingTables() {
19179 return call(
19180 "/folder-sharing-tables/purge",
19181 { method: "POST" }
19182 );
19183 }
19184 const filesRest = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
19185 __proto__: null,
19186 FilesConflictError,
19187 acceptShare,
19188 createFolder,
19189 createPlacement,
19190 deleteFolder,
19191 deletePlacement,
19192 denyShare,
19193 installRestDeps,
19194 inviteShare,
19195 leaveShare,
19196 listFolders,
19197 listPlacements,
19198 listShares,
19199 purgeFolderSharingTables,
19200 restoreTrashedItem,
19201 revokeShare,
19202 saveAssociations,
19203 updateFolder,
19204 updatePlacement,
19205 updateShareCapability
19206 }, Symbol.toStringTag, { value: "Module" }));
19207 const STORE_KEY = "desktop-mode/files";
19208 function getFilesStore() {
19209 return createSharedStore(STORE_KEY, () => ({
19210 placementsByFolder: /* @__PURE__ */ new Map(),
19211 folders: /* @__PURE__ */ new Map(),
19212 hydratedFolders: /* @__PURE__ */ new Set()
19213 }));
19214 }
19215 function fireChanged(detail) {
19216 if (typeof document === "undefined") {
19217 return;
19218 }
19219 document.dispatchEvent(
19220 new CustomEvent("desktop-mode-files-changed", {
19221 detail: { source: "local", ...detail }
19222 })
19223 );
19224 }
19225 function setFolderPlacements(folderId, placements) {
19226 const store2 = getFilesStore();
19227 const next = new Map(store2.state.placementsByFolder);
19228 next.set(folderId, placements.slice());
19229 const hydrated = new Set(store2.state.hydratedFolders);
19230 hydrated.add(folderId);
19231 store2.state = { ...store2.state, placementsByFolder: next, hydratedFolders: hydrated };
19232 store2.notify();
19233 fireChanged({ kind: "placements-set", folderId });
19234 }
19235 function upsertPlacement(placement, source = "local") {
19236 if (!placement || typeof placement.id !== "number") {
19237 console.warn(
19238 "[desktop-mode] upsertPlacement called with a non-placement value; ignoring.",
19239 placement
19240 );
19241 return;
19242 }
19243 const store2 = getFilesStore();
19244 const next = new Map(store2.state.placementsByFolder);
19245 for (const [folderId, list2] of next) {
19246 const idx2 = list2.findIndex((p) => p && p.id === placement.id);
19247 if (idx2 >= 0 && folderId !== placement.parentId) {
19248 const copy = list2.filter(Boolean);
19249 const removeAt = copy.findIndex((p) => p.id === placement.id);
19250 if (removeAt >= 0) {
19251 copy.splice(removeAt, 1);
19252 }
19253 next.set(folderId, copy);
19254 }
19255 }
19256 const rawTarget = next.get(placement.parentId)?.slice() ?? [];
19257 const target2 = rawTarget.filter(Boolean);
19258 const idx = target2.findIndex((p) => p.id === placement.id);
19259 if (idx >= 0) {
19260 target2[idx] = placement;
19261 } else {
19262 target2.push(placement);
19263 }
19264 next.set(placement.parentId, target2);
19265 store2.state = { ...store2.state, placementsByFolder: next };
19266 store2.notify();
19267 fireChanged({ kind: "placement-upserted", placementId: placement.id, folderId: placement.parentId, source });
19268 }
19269 function removePlacement(placementId, source = "local") {
19270 const store2 = getFilesStore();
19271 const next = new Map(store2.state.placementsByFolder);
19272 let touchedFolder;
19273 for (const [folderId, list2] of next) {
19274 const idx = list2.findIndex((p) => p && p.id === placementId);
19275 if (idx >= 0) {
19276 const copy = list2.filter(Boolean).filter(
19277 (p) => p.id !== placementId
19278 );
19279 next.set(folderId, copy);
19280 touchedFolder = folderId;
19281 }
19282 }
19283 if (touchedFolder === void 0) {
19284 return;
19285 }
19286 store2.state = { ...store2.state, placementsByFolder: next };
19287 store2.notify();
19288 fireChanged({ kind: "placement-removed", placementId, folderId: touchedFolder, source });
19289 }
19290 function setFolders(folders) {
19291 const store2 = getFilesStore();
19292 const next = /* @__PURE__ */ new Map();
19293 for (const f of folders) {
19294 next.set(f.id, f);
19295 }
19296 store2.state = { ...store2.state, folders: next };
19297 store2.notify();
19298 fireChanged({ kind: "folders-set" });
19299 }
19300 function upsertFolder(folder, source = "local") {
19301 const store2 = getFilesStore();
19302 const next = new Map(store2.state.folders);
19303 next.set(folder.id, folder);
19304 store2.state = { ...store2.state, folders: next };
19305 store2.notify();
19306 fireChanged({ kind: "folder-upserted", folderRowId: folder.id, source });
19307 }
19308 function removeFolder(folderId, source = "local") {
19309 const store2 = getFilesStore();
19310 const folders = new Map(store2.state.folders);
19311 folders.delete(folderId);
19312 const placements = new Map(store2.state.placementsByFolder);
19313 placements.delete(folderId);
19314 store2.state = { ...store2.state, folders, placementsByFolder: placements };
19315 store2.notify();
19316 fireChanged({ kind: "folder-removed", folderRowId: folderId, source });
19317 }
19318 function subscribeFilesStore(cb) {
19319 const store2 = getFilesStore();
19320 const off = store2.subscribe(cb);
19321 return off;
19322 }
19323 function getFilesState() {
19324 return getFilesStore().getState();
19325 }
19326 const store = {
19327 getState: getFilesState,
19328 subscribe: subscribeFilesStore,
19329 setFolderPlacements,
19330 upsertPlacement,
19331 upsertFolder,
19332 removePlacement,
19333 removeFolder
19334 };
19335 const styles$5 = css`:host{display:inline-block}`;
19336 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 )}`;
19337 const _WpdRibbon = class _WpdRibbon extends Component {
19338 render() {
19339 return html`<span class="banner" part="banner"><slot></slot></span>`;
19340 }
19341 };
19342 _WpdRibbon.props = ["placement", "tone"];
19343 _WpdRibbon.styles = [styles$4];
19344 _WpdRibbon.help = {
19345 title: "Ribbon",
19346 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.",
19347 status: "experimental",
19348 since: "0.20.0",
19349 props: [
19350 {
19351 name: "placement",
19352 type: '"top-end" | "top-start" | "bottom-end" | "bottom-start"',
19353 description: "Which corner of the parent the ribbon hugs. Defaults to `top-end` (logical right in LTR, left in RTL)."
19354 },
19355 {
19356 name: "tone",
19357 type: '"primary" | "success" | "warning" | "danger" | "info" | "neutral"',
19358 description: "Background color tone. Defaults to `primary` (the admin theme accent)."
19359 }
19360 ],
19361 slots: [{ name: "(default)", description: "Ribbon label text. Keep short." }],
19362 cssProps: [
19363 { name: "--wpd-ribbon-size", default: "90px", description: "Square clipping window edge." },
19364 { name: "--wpd-ribbon-banner-width", default: "140px", description: "Width of the rotated strip." },
19365 { name: "--wpd-ribbon-banner-offset", default: "20px", description: "Distance from corner to strip center." },
19366 { name: "--wpd-ribbon-banner-pull", default: "-36px", description: "How far the strip overhangs the clip edge." },
19367 { name: "--wpd-ribbon-bg", default: "var(--wp-admin-theme-color, #2271b1)" },
19368 { name: "--wpd-ribbon-fg", default: "#fff" },
19369 { name: "--wpd-ribbon-shadow", default: "0 2px 4px rgba(0,0,0,0.2)" },
19370 { name: "--wpd-ribbon-padding", default: "4px 0" },
19371 { name: "--wpd-ribbon-font", default: "700 10px/1.4 system-ui" },
19372 { name: "--wpd-ribbon-tracking", default: "0.06em" },
19373 { name: "--wpd-ribbon-z", default: "2" }
19374 ],
19375 example: html`
19376 <div
19377 style="position: relative; width: 240px; height: 120px;
19378 border: 1px solid #ccc; border-radius: 8px;
19379 padding: 16px; box-sizing: border-box;"
19380 >
19381 <wpd-ribbon>Featured</wpd-ribbon>
19382 Card body…
19383 </div>
19384 `
19385 };
19386 let WpdRibbon = _WpdRibbon;
19387 defineComponent("wpd-ribbon", WpdRibbon);
19388 const TILE_CLASS = "desktop-mode-file-tile";
19389 const STATUS_LABEL = {
19390 draft: "Draft",
19391 pending: "Pending",
19392 private: "Private",
19393 future: "Scheduled"
19394 };
19395 function statusRibbonsEnabled() {
19396 const get2 = window.wp?.desktop?.getOsSettings;
19397 if (typeof get2 !== "function") {
19398 return true;
19399 }
19400 try {
19401 return get2()?.showPostStatusRibbons !== false;
19402 } catch {
19403 return true;
19404 }
19405 }
19406 function getDragManager$1() {
19407 const api = window.wp?.desktop?.dragManager;
19408 return api ?? null;
19409 }
19410 const REACTIVE_PROPS = [
19411 "type",
19412 "ref",
19413 "label",
19414 "icon",
19415 "thumbnail",
19416 "kind",
19417 "status",
19418 "selected",
19419 "missing",
19420 "access-gated",
19421 "drag-kind",
19422 "drag-title",
19423 "drag-icon"
19424 ];
19425 const _WpdTile = class _WpdTile extends Component {
19426 constructor() {
19427 super(...arguments);
19428 this._pointerdownHandler = null;
19429 this._keydownHandler = null;
19430 }
19431 connectedCallback() {
19432 super.connectedCallback();
19433 if (!this._keydownHandler) {
19434 this._keydownHandler = (e) => {
19435 if (e.key === "Enter" || e.key === " ") {
19436 e.preventDefault();
19437 this.click();
19438 }
19439 };
19440 this.addEventListener("keydown", this._keydownHandler);
19441 }
19442 this._paint();
19443 }
19444 disconnectedCallback() {
19445 if (this._pointerdownHandler) {
19446 this.removeEventListener(
19447 "pointerdown",
19448 this._pointerdownHandler
19449 );
19450 this._pointerdownHandler = null;
19451 }
19452 if (this._keydownHandler) {
19453 this.removeEventListener(
19454 "keydown",
19455 this._keydownHandler
19456 );
19457 this._keydownHandler = null;
19458 }
19459 }
19460 /**
19461 * Bypass the templated render loop. Lit-html's `render(template,
19462 * root)` would wipe the host's light-DOM children every tick —
19463 * including the visual / label / ribbon `_paint()` just
19464 * inserted. We override `requestUpdate` directly so attribute
19465 * changes call `_paint` (idempotent) without lit-html getting
19466 * involved.
19467 */
19468 requestUpdate() {
19469 if (!this.isConnected) {
19470 return;
19471 }
19472 this._paint();
19473 }
19474 render() {
19475 return html``;
19476 }
19477 _paint() {
19478 const type = this.getAttribute("type") ?? "";
19479 const ref = this.getAttribute("ref") ?? "";
19480 const label = this.getAttribute("label") ?? "";
19481 const icon = this.getAttribute("icon") ?? "";
19482 const thumbnail = this.getAttribute("thumbnail") ?? "";
19483 const kind = this.getAttribute("kind") ?? "entry";
19484 const status = this.getAttribute("status") ?? "";
19485 const selected = this.hasAttribute("selected");
19486 const missing = this.hasAttribute("missing");
19487 const accessGated = this.hasAttribute("access-gated");
19488 const ownedClasses = [
19489 TILE_CLASS,
19490 `${TILE_CLASS}--folder`,
19491 `${TILE_CLASS}--missing`,
19492 `${TILE_CLASS}--access-gated`,
19493 `${TILE_CLASS}--selected`
19494 ];
19495 for (const c of ownedClasses) {
19496 this.classList.remove(c);
19497 }
19498 this.classList.add(TILE_CLASS);
19499 if (kind === "folder") {
19500 this.classList.add(`${TILE_CLASS}--folder`);
19501 }
19502 if (missing) {
19503 this.classList.add(`${TILE_CLASS}--missing`);
19504 }
19505 if (accessGated) {
19506 this.classList.add(`${TILE_CLASS}--access-gated`);
19507 }
19508 if (selected) {
19509 this.classList.add(`${TILE_CLASS}--selected`);
19510 }
19511 this.dataset.fileType = type;
19512 this.dataset.fileRef = ref;
19513 if (kind) {
19514 this.dataset.role = kind;
19515 }
19516 this.setAttribute("role", "listitem");
19517 this.setAttribute("aria-label", label);
19518 if (!this.hasAttribute("tabindex")) {
19519 this.setAttribute("tabindex", "0");
19520 }
19521 const accessGatedTitle = "You don’t have permission to open this — ask the folder owner for access.";
19522 if (accessGated) {
19523 this.title = accessGatedTitle;
19524 this.setAttribute("aria-disabled", "true");
19525 } else {
19526 this.removeAttribute("aria-disabled");
19527 if (this.title === accessGatedTitle) {
19528 this.removeAttribute("title");
19529 }
19530 }
19531 const SLOTS = [
19532 `${TILE_CLASS}__visual`,
19533 `${TILE_CLASS}__label`,
19534 `${TILE_CLASS}__lock`
19535 ];
19536 for (const cls of SLOTS) {
19537 this.querySelectorAll(`:scope > .${cls}`).forEach(
19538 (n) => n.remove()
19539 );
19540 }
19541 this.querySelectorAll(":scope > wpd-ribbon").forEach(
19542 (n) => n.remove()
19543 );
19544 const visual = document.createElement("span");
19545 visual.className = `${TILE_CLASS}__visual`;
19546 if (thumbnail) {
19547 const img = document.createElement("img");
19548 img.src = thumbnail;
19549 img.alt = "";
19550 img.loading = "lazy";
19551 img.decoding = "async";
19552 img.className = `${TILE_CLASS}__preview`;
19553 img.draggable = false;
19554 visual.appendChild(img);
19555 } else if (icon) {
19556 const iconNode = renderIcon(icon, {
19557 title: label,
19558 className: `${TILE_CLASS}__icon`
19559 });
19560 visual.appendChild(iconNode);
19561 }
19562 this.appendChild(visual);
19563 const labelNode = document.createElement("span");
19564 labelNode.className = `${TILE_CLASS}__label`;
19565 labelNode.textContent = label;
19566 this.appendChild(labelNode);
19567 if (accessGated) {
19568 const lock = document.createElement("span");
19569 lock.className = `${TILE_CLASS}__lock dashicons dashicons-lock`;
19570 lock.setAttribute("aria-hidden", "true");
19571 this.appendChild(lock);
19572 }
19573 if (status && status !== "publish" && STATUS_LABEL[status] && statusRibbonsEnabled()) {
19574 const ribbon = document.createElement("wpd-ribbon");
19575 ribbon.setAttribute("placement", "top-end");
19576 ribbon.setAttribute("tone", ribbonToneFor(status));
19577 ribbon.textContent = STATUS_LABEL[status];
19578 this.appendChild(ribbon);
19579 }
19580 applyTileEntryStagger(this);
19581 doAction("desktop-mode.tile.rendered", { tile: this });
19582 this._wireDragOut();
19583 }
19584 _wireDragOut() {
19585 if (this._pointerdownHandler) {
19586 this.removeEventListener(
19587 "pointerdown",
19588 this._pointerdownHandler
19589 );
19590 this._pointerdownHandler = null;
19591 }
19592 const dragKind = this.getAttribute("drag-kind");
19593 if (!dragKind) {
19594 return;
19595 }
19596 const handler = (e) => {
19597 if (e.button !== 0) {
19598 return;
19599 }
19600 const dragManager = getDragManager$1();
19601 if (!dragManager) {
19602 return;
19603 }
19604 const ref = this.getAttribute("ref") ?? "";
19605 const title = this.getAttribute("drag-title") ?? this.getAttribute("label") ?? void 0;
19606 const icon = this.getAttribute("drag-icon") ?? this.getAttribute("icon") ?? void 0;
19607 const rect = this.getBoundingClientRect();
19608 dragManager.start({
19609 payload: {
19610 type: "shortcut",
19611 source: this,
19612 data: {
19613 kind: dragKind,
19614 ref,
19615 title,
19616 icon
19617 },
19618 ghost: {
19619 offsetX: e.clientX - rect.left,
19620 offsetY: e.clientY - rect.top
19621 }
19622 },
19623 origin: e
19624 });
19625 };
19626 this._pointerdownHandler = handler;
19627 this.addEventListener("pointerdown", handler);
19628 }
19629 };
19630 _WpdTile.shadow = false;
19631 _WpdTile.props = REACTIVE_PROPS;
19632 _WpdTile.styles = [styles$5];
19633 _WpdTile.help = {
19634 title: "Tile",
19635 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.",
19636 status: "experimental",
19637 since: "0.21.0",
19638 props: [
19639 { name: "type", type: "string" },
19640 { name: "ref", type: "string" },
19641 { name: "label", type: "string" },
19642 { name: "icon", type: "string", description: "Dashicon class / URL / data URI. Ignored when `thumbnail` is set." },
19643 { name: "thumbnail", type: "string", description: "Preview image URL. Renders as `<img>` and wins over `icon`." },
19644 { name: "kind", type: "`entry` | `folder`" },
19645 { name: "status", type: "`draft` | `pending` | `private` | `future` | `publish`" },
19646 { name: "selected", type: "boolean" },
19647 { name: "missing", type: "boolean" },
19648 { name: "access-gated", type: "boolean" },
19649 { name: "drag-kind", type: "string", description: "When set, the component wires pointerdown → DragManager." },
19650 { name: "drag-title", type: "string" },
19651 { name: "drag-icon", type: "string" }
19652 ]
19653 };
19654 let WpdTile = _WpdTile;
19655 function ribbonToneFor(status) {
19656 switch (status) {
19657 case "draft":
19658 return "warning";
19659 case "pending":
19660 return "info";
19661 case "private":
19662 return "danger";
19663 case "future":
19664 return "primary";
19665 default:
19666 return "primary";
19667 }
19668 }
19669 defineComponent("wpd-tile", WpdTile);
19670 function buildTileFromSpec(spec) {
19671 const tile2 = document.createElement("wpd-tile");
19672 tile2.setAttribute("type", spec.type);
19673 tile2.setAttribute("ref", spec.ref);
19674 tile2.setAttribute("label", spec.label);
19675 if (spec.icon) {
19676 tile2.setAttribute("icon", spec.icon);
19677 }
19678 if (spec.thumbnail) {
19679 tile2.setAttribute("thumbnail", spec.thumbnail);
19680 }
19681 if (spec.role) {
19682 tile2.setAttribute("kind", spec.role);
19683 }
19684 if (spec.status) {
19685 tile2.setAttribute("status", spec.status);
19686 }
19687 if (spec.missing) {
19688 tile2.setAttribute("missing", "");
19689 }
19690 if (spec.accessGated) {
19691 tile2.setAttribute("access-gated", "");
19692 }
19693 if (spec.dataset) {
19694 for (const [key, raw] of Object.entries(spec.dataset)) {
19695 if (raw === void 0 || raw === null) {
19696 continue;
19697 }
19698 tile2.dataset[key] = String(raw);
19699 }
19700 }
19701 if (Array.isArray(spec.extraClasses)) {
19702 for (const c of spec.extraClasses) {
19703 if (c) {
19704 tile2.classList.add(c);
19705 }
19706 }
19707 }
19708 const classFiltered = applyFilters(
19709 "desktop-mode.tile.class",
19710 tile2.className,
19711 spec
19712 );
19713 if (classFiltered && classFiltered !== tile2.className) {
19714 tile2.className = classFiltered;
19715 }
19716 if (typeof spec.x === "number" && typeof spec.y === "number") {
19717 tile2.style.position = "absolute";
19718 tile2.style.left = `${spec.x}px`;
19719 tile2.style.top = `${spec.y}px`;
19720 }
19721 return tile2;
19722 }
19723 function placementToSpec(placement, folderId) {
19724 const file = resolve(placement.file);
19725 const previewUrl = file.previewUrl();
19726 const metaName = placement.meta && typeof placement.meta.name === "string" ? placement.meta.name.trim() : "";
19727 const label = metaName !== "" ? metaName : file.title();
19728 const metaIconUrl = placement.meta && typeof placement.meta.iconUrl === "string" ? placement.meta.iconUrl.trim() : "";
19729 return {
19730 type: placement.file.type,
19731 ref: placement.file.ref,
19732 label,
19733 // Preview wins over icon (matches the previous behavior).
19734 thumbnail: previewUrl || void 0,
19735 icon: previewUrl ? void 0 : metaIconUrl || file.icon(),
19736 x: placement.x,
19737 y: placement.y,
19738 dataset: {
19739 placementId: placement.id,
19740 folderId
19741 },
19742 meta: placement.meta,
19743 missing: !placement.file.exists,
19744 accessGated: Boolean(placement.accessGated),
19745 ariaLabel: label
19746 };
19747 }
19748 function buildTile(placement, folderId) {
19749 const file = resolve(placement.file);
19750 const tile2 = buildTileFromSpec(placementToSpec(placement, folderId));
19751 const classFiltered = applyFilters(
19752 "desktop-mode.files.tile-class",
19753 TILE_CLASS,
19754 placement
19755 );
19756 if (classFiltered && classFiltered !== TILE_CLASS) {
19757 tile2.className = classFiltered;
19758 }
19759 const extra = applyFilters(
19760 "desktop-mode.files.tile-element",
19761 null,
19762 placement
19763 );
19764 if (extra instanceof Element) {
19765 tile2.appendChild(extra);
19766 }
19767 tile2.addEventListener("dblclick", (e) => {
19768 e.preventDefault();
19769 e.stopPropagation();
19770 if (placement.accessGated) {
19771 showToast({
19772 message: `You don’t have permission to open "${placement.file.title || file.title()}". Ask the folder owner if you need access to this item.`,
19773 duration: 6e3
19774 });
19775 return;
19776 }
19777 void openFile(file, {
19778 placement: {
19779 id: placement.id,
19780 x: placement.x,
19781 y: placement.y,
19782 meta: placement.meta
19783 }
19784 });
19785 });
19786 doAction("desktop-mode.files.tile-rendered", { tile: tile2, placement });
19787 return tile2;
19788 }
19789 function setTilePosition(tile2, x, y) {
19790 tile2.style.left = `${x}px`;
19791 tile2.style.top = `${y}px`;
19792 }
19793 function attachDismissable(host, options) {
19794 const onAway = (e) => {
19795 if (e.target instanceof Node && host.contains(e.target)) {
19796 return;
19797 }
19798 if (e.target instanceof Node) {
19799 for (const sel of options.siblingSelectors ?? []) {
19800 const matches = Array.from(
19801 document.querySelectorAll(sel)
19802 );
19803 for (const m of matches) {
19804 if (m.contains(e.target)) {
19805 return;
19806 }
19807 }
19808 }
19809 }
19810 if (options.excludeOutsideTarget && e.target instanceof Node && options.excludeOutsideTarget.contains(e.target)) {
19811 return;
19812 }
19813 options.close();
19814 };
19815 const onKey = (e) => {
19816 if (e.key === "Escape") {
19817 options.close();
19818 }
19819 };
19820 document.addEventListener("mousedown", onAway, { capture: true });
19821 document.addEventListener("keydown", onKey);
19822 return () => {
19823 document.removeEventListener("mousedown", onAway, { capture: true });
19824 document.removeEventListener("keydown", onKey);
19825 };
19826 }
19827 const MENU_CLASS$2 = "desktop-mode-wallpaper-menu";
19828 let activeMenu$2 = null;
19829 function closeTileMenu() {
19830 if (!activeMenu$2) {
19831 return;
19832 }
19833 activeMenu$2.dispatchEvent(new CustomEvent("tile-menu-closed"));
19834 activeMenu$2.remove();
19835 activeMenu$2 = null;
19836 doAction("desktop-mode.files.tile-menu.closed", {});
19837 }
19838 let openGeneration$1 = 0;
19839 function openTileMenu(pos, opts) {
19840 closeTileMenu();
19841 const myGen = ++openGeneration$1;
19842 openWithShellOverlays(
19843 () => myGen === openGeneration$1,
19844 () => openTileMenuImmediate(pos, opts)
19845 );
19846 }
19847 function openTileMenuImmediate(pos, { placement, items }) {
19848 const list2 = applyFilters(
19849 "desktop-mode.files.tile-menu",
19850 items.slice(),
19851 placement
19852 );
19853 const sorted = (Array.isArray(list2) ? list2 : items).slice().sort((a, b) => {
19854 const sa = typeof a.sort === "number" ? a.sort : 100;
19855 const sb = typeof b.sort === "number" ? b.sort : 100;
19856 if (sa !== sb) {
19857 return sa - sb;
19858 }
19859 return a.label.localeCompare(b.label);
19860 });
19861 if (sorted.length === 0) {
19862 return;
19863 }
19864 const menu = document.createElement("wpd-context-menu");
19865 menu.setAttribute("open", "");
19866 menu.classList.add(MENU_CLASS$2);
19867 menu.dataset.placementId = String(placement.id);
19868 menu.style.left = `${pos.x}px`;
19869 menu.style.top = `${pos.y}px`;
19870 const itemById = /* @__PURE__ */ new Map();
19871 for (const item of sorted) {
19872 itemById.set(item.id, item);
19873 const opt = document.createElement("wpd-context-menu-option");
19874 opt.dataset.menuItemId = item.id;
19875 opt.setAttribute("value", item.id);
19876 if (item.danger) {
19877 opt.setAttribute("danger", "");
19878 }
19879 if (item.disabled) {
19880 opt.setAttribute("disabled", "");
19881 }
19882 if (item.icon) {
19883 opt.setAttribute("icon", sanitizeClass$2(item.icon));
19884 }
19885 opt.textContent = item.label;
19886 menu.appendChild(opt);
19887 }
19888 menu.addEventListener("wpd-context-menu-pick", (e) => {
19889 const detail = e.detail;
19890 const item = itemById.get(detail.id);
19891 if (!item) {
19892 return;
19893 }
19894 closeTileMenu();
19895 void item.onClick(new MouseEvent("click"));
19896 });
19897 document.body.appendChild(menu);
19898 activeMenu$2 = menu;
19899 const rect = menu.getBoundingClientRect();
19900 if (rect.right > window.innerWidth) {
19901 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
19902 }
19903 if (rect.bottom > window.innerHeight) {
19904 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
19905 }
19906 const detach = attachDismissable(menu, {
19907 close: () => closeTileMenu()
19908 });
19909 menu.addEventListener("tile-menu-closed", detach);
19910 doAction("desktop-mode.files.tile-menu.opened", {
19911 placementId: placement.id,
19912 items: sorted.map((i) => i.id)
19913 });
19914 }
19915 function sanitizeClass$2(raw) {
19916 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
19917 }
19918 const ROOT_CLASS$3 = "desktop-mode-create-folder-dialog";
19919 let active$1 = null;
19920 function closeCreateFolderDialog() {
19921 if (!active$1) {
19922 return;
19923 }
19924 active$1.dispatchEvent(new CustomEvent("create-folder-dialog-closed"));
19925 active$1.remove();
19926 active$1 = null;
19927 doAction("desktop-mode.files.create-folder.closed", {});
19928 }
19929 function openCreateFolderDialog(options) {
19930 closeCreateFolderDialog();
19931 const decision = applyFilters(
19932 "desktop-mode.files.create-folder.dialog",
19933 null,
19934 options
19935 );
19936 if (decision === false) {
19937 return;
19938 }
19939 const initial = (options.initialName ?? "Untitled folder").trim();
19940 const overlay = document.createElement("div");
19941 overlay.className = `${ROOT_CLASS$3}__overlay`;
19942 overlay.setAttribute("role", "presentation");
19943 const dialog2 = document.createElement("div");
19944 dialog2.className = ROOT_CLASS$3;
19945 dialog2.setAttribute("role", "dialog");
19946 dialog2.setAttribute("aria-modal", "true");
19947 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS$3}-title`);
19948 const title = document.createElement("h2");
19949 title.id = `${ROOT_CLASS$3}-title`;
19950 title.className = `${ROOT_CLASS$3}__title`;
19951 title.textContent = options.title ?? "New folder";
19952 dialog2.appendChild(title);
19953 const label = document.createElement("label");
19954 label.className = `${ROOT_CLASS$3}__label`;
19955 label.htmlFor = `${ROOT_CLASS$3}-input`;
19956 label.textContent = options.label ?? "Folder name";
19957 dialog2.appendChild(label);
19958 const input = document.createElement("input");
19959 input.type = "text";
19960 input.id = `${ROOT_CLASS$3}-input`;
19961 input.className = `${ROOT_CLASS$3}__input`;
19962 input.value = initial;
19963 input.setAttribute("autocomplete", "off");
19964 input.setAttribute("spellcheck", "false");
19965 dialog2.appendChild(input);
19966 const error = document.createElement("p");
19967 error.className = `${ROOT_CLASS$3}__error`;
19968 error.hidden = true;
19969 error.setAttribute("role", "alert");
19970 dialog2.appendChild(error);
19971 const actions = document.createElement("div");
19972 actions.className = `${ROOT_CLASS$3}__actions`;
19973 const cancel = document.createElement("button");
19974 cancel.type = "button";
19975 cancel.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--secondary`;
19976 cancel.textContent = "Cancel";
19977 const submit = document.createElement("button");
19978 submit.type = "button";
19979 submit.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--primary`;
19980 submit.textContent = options.submitLabel ?? "Create";
19981 actions.appendChild(cancel);
19982 actions.appendChild(submit);
19983 dialog2.appendChild(actions);
19984 overlay.appendChild(dialog2);
19985 document.body.appendChild(overlay);
19986 active$1 = overlay;
19987 input.focus();
19988 input.select();
19989 doAction("desktop-mode.files.create-folder.opened", {});
19990 const setBusy = (busy) => {
19991 input.disabled = busy;
19992 cancel.disabled = busy;
19993 submit.disabled = busy;
19994 dialog2.classList.toggle(`${ROOT_CLASS$3}--busy`, busy);
19995 };
19996 const showError = (msg) => {
19997 error.textContent = msg;
19998 error.hidden = false;
19999 };
20000 const doCancel = () => {
20001 closeCreateFolderDialog();
20002 options.onCancel?.();
20003 };
20004 const doSubmit = async () => {
20005 const name = input.value.trim();
20006 if (!name) {
20007 showError("Please enter a name.");
20008 input.focus();
20009 return;
20010 }
20011 error.hidden = true;
20012 setBusy(true);
20013 try {
20014 await options.onSubmit(name);
20015 closeCreateFolderDialog();
20016 } catch (err) {
20017 setBusy(false);
20018 showError(
20019 err instanceof Error ? err.message : "Could not create the folder."
20020 );
20021 input.focus();
20022 input.select();
20023 }
20024 };
20025 cancel.addEventListener("click", () => doCancel());
20026 submit.addEventListener("click", () => void doSubmit());
20027 overlay.addEventListener("click", (e) => {
20028 if (e.target === overlay) {
20029 doCancel();
20030 }
20031 });
20032 const onKey = (e) => {
20033 if (e.key === "Escape") {
20034 e.preventDefault();
20035 doCancel();
20036 } else if (e.key === "Enter" && !e.isComposing) {
20037 e.preventDefault();
20038 void doSubmit();
20039 }
20040 };
20041 dialog2.addEventListener("keydown", onKey);
20042 overlay.addEventListener("create-folder-dialog-closed", () => {
20043 dialog2.removeEventListener("keydown", onKey);
20044 });
20045 }
20046 const GRID_PADDING = 16;
20047 const GRID_CELL_W = 96;
20048 const GRID_CELL_H = 110;
20049 function pointToCell(x, y) {
20050 const col = Math.max(0, Math.round((x - GRID_PADDING) / GRID_CELL_W));
20051 const row = Math.max(0, Math.round((y - GRID_PADDING) / GRID_CELL_H));
20052 return cellToPos(col, row);
20053 }
20054 function cellToPos(col, row) {
20055 return {
20056 col,
20057 row,
20058 x: GRID_PADDING + col * GRID_CELL_W,
20059 y: GRID_PADDING + row * GRID_CELL_H
20060 };
20061 }
20062 function snapToEmptyCell(x, y, occupied, host) {
20063 const target2 = pointToCell(x, y);
20064 if (!occupied.has(cellKey(target2.col, target2.row))) {
20065 return target2;
20066 }
20067 const maxRows = host ? Math.max(1, Math.floor((host.clientHeight - GRID_PADDING) / GRID_CELL_H)) : 999;
20068 for (let col = 0; col < 999; col++) {
20069 for (let row = 0; row < maxRows; row++) {
20070 if (!occupied.has(cellKey(col, row))) {
20071 return cellToPos(col, row);
20072 }
20073 }
20074 }
20075 return target2;
20076 }
20077 function nextRowMajorCell(occupied, host) {
20078 const cols = host ? Math.max(
20079 1,
20080 Math.floor((host.clientWidth - GRID_PADDING) / GRID_CELL_W)
20081 ) : 4;
20082 const maxCols = Math.max(1, cols);
20083 for (let row = 0; row < 999; row++) {
20084 for (let col = 0; col < maxCols; col++) {
20085 if (!occupied.has(cellKey(col, row))) {
20086 return cellToPos(col, row);
20087 }
20088 }
20089 }
20090 return cellToPos(0, 0);
20091 }
20092 function buildOccupiedSet(placements, excludeId) {
20093 const out = /* @__PURE__ */ new Set();
20094 for (const p of placements) {
20095 const cell = pointToCell(p.x, p.y);
20096 out.add(cellKey(cell.col, cell.row));
20097 }
20098 return out;
20099 }
20100 function cellKey(col, row) {
20101 return `${col},${row}`;
20102 }
20103 function isConflict(err) {
20104 return err instanceof FilesConflictError;
20105 }
20106 function buildReason(err) {
20107 const actor = err.detail.actor.name || "Someone else";
20108 const where = err.detail.current.parentName || "another folder";
20109 if (err.detail.reason === "trashed") {
20110 return "This item is in the recycle bin.";
20111 }
20112 if (err.detail.reason === "forbidden") {
20113 return "You no longer have access.";
20114 }
20115 if (err.detail.reason === "gone") {
20116 return "This item was deleted.";
20117 }
20118 return `${actor} moved this to "${where}".`;
20119 }
20120 function showConflictToast(err) {
20121 const reason = buildReason(err);
20122 const targetParentId = err.detail.current.parentId;
20123 let action;
20124 if (targetParentId > 0) {
20125 action = {
20126 label: "View folder",
20127 onClick: () => {
20128 const winId = `desktop-mode-folder-${targetParentId}`;
20129 const mgr = window.desktopMode?.windowManager;
20130 if (mgr?.focus) {
20131 const w = mgr.focus(winId);
20132 if (w) {
20133 return;
20134 }
20135 }
20136 if (mgr?.open) {
20137 void mgr.open(winId);
20138 }
20139 }
20140 };
20141 }
20142 showToast({
20143 message: reason,
20144 action,
20145 duration: 7e3
20146 });
20147 }
20148 function broadcastFilesChange(kind, action, ids) {
20149 const api = window.wp?.desktop;
20150 api?.broadcast?.(`desktop-mode.${kind}.changed`, {
20151 source: "desktop-files",
20152 action,
20153 ids
20154 });
20155 }
20156 function showTrashErrorToast(err) {
20157 const api = window.wp?.desktop;
20158 if (!api?.showToast) {
20159 return;
20160 }
20161 const raw = err instanceof Error ? err.message : String(err);
20162 const friendly = raw.replace(/^\[desktop-mode\][^:]*:\s*/, "").replace(/^desktop_mode_files_[a-z_]+\s*/, "");
20163 api.showToast({
20164 message: friendly || "Could not move this item to the recycle bin.",
20165 duration: 5e3
20166 });
20167 }
20168 function showTrashedToast(message, onUndo) {
20169 const api = window.wp?.desktop;
20170 if (!api?.showToast) {
20171 return;
20172 }
20173 api.showToast({
20174 message,
20175 duration: 6e3,
20176 action: {
20177 label: "Undo",
20178 onClick: onUndo
20179 }
20180 });
20181 }
20182 async function trashPlacementWithUndo(placement) {
20183 const placementId = placement.id;
20184 const parentId = placement.parentId;
20185 const title = placement.file?.title ?? "Item";
20186 const kind = placement.file?.type === "shortcut" ? "shortcut" : "placement";
20187 store.removePlacement(placementId);
20188 try {
20189 await deletePlacement(placementId);
20190 broadcastFilesChange(kind, "trashed", [placementId]);
20191 showTrashedToast(`"${title}" moved to Trash`, async () => {
20192 try {
20193 await restoreTrashedItem(placementId, "placement");
20194 const res = await listPlacements(parentId);
20195 store.setFolderPlacements(parentId, res.placements);
20196 broadcastFilesChange(kind, "untrashed", [placementId]);
20197 } catch (err) {
20198 console.error("[desktop-mode] restore failed:", err);
20199 }
20200 });
20201 } catch (err) {
20202 console.error("[desktop-mode] deletePlacement failed:", err);
20203 showTrashErrorToast(err);
20204 void listPlacements(parentId).then((res) => {
20205 store.setFolderPlacements(parentId, res.placements);
20206 });
20207 }
20208 }
20209 async function trashFolderWithUndo(placement) {
20210 const folderId = parseInt(placement.file.ref, 10);
20211 if (!folderId) {
20212 return;
20213 }
20214 const placementId = placement.id;
20215 const parentId = placement.parentId;
20216 const title = placement.file?.title ?? "Folder";
20217 store.removePlacement(placementId);
20218 store.removeFolder(folderId);
20219 try {
20220 await deleteFolder(folderId);
20221 broadcastFilesChange("folder", "trashed", [folderId]);
20222 showTrashedToast(`"${title}" moved to Trash`, async () => {
20223 try {
20224 await restoreTrashedItem(folderId, "folder");
20225 const res = await listPlacements(parentId);
20226 store.setFolderPlacements(parentId, res.placements);
20227 broadcastFilesChange("folder", "untrashed", [folderId]);
20228 } catch (err) {
20229 console.error("[desktop-mode] restore folder failed:", err);
20230 }
20231 });
20232 } catch (err) {
20233 console.error("[desktop-mode] deleteFolder failed:", err);
20234 showTrashErrorToast(err);
20235 void listPlacements(parentId).then((res) => {
20236 store.setFolderPlacements(parentId, res.placements);
20237 });
20238 }
20239 }
20240 function trashByFileType(placement) {
20241 if (placement.file?.type === "folder") {
20242 return trashFolderWithUndo(placement);
20243 }
20244 return trashPlacementWithUndo(placement);
20245 }
20246 function buildBridgePayloadFromPlacement(placement) {
20247 const file = placement.file;
20248 if (!file) {
20249 return void 0;
20250 }
20251 const id = parseInt(String(file.ref ?? ""), 10);
20252 if (!Number.isFinite(id) || id <= 0) {
20253 return void 0;
20254 }
20255 const title = String(file.title ?? "");
20256 if (file.type === "attachment") {
20257 const url = String(file.sourceUrl ?? file.previewUrl ?? "");
20258 return {
20259 kind: "attachment",
20260 id,
20261 url,
20262 title,
20263 alt: String(file.alt ?? ""),
20264 mime: String(file.mime ?? ""),
20265 thumbnailUrl: file.previewUrl ? String(file.previewUrl) : void 0
20266 };
20267 }
20268 if (file.type === "post") {
20269 return {
20270 kind: "post",
20271 id,
20272 postType: String(file.postType ?? "post"),
20273 url: String(file.link ?? ""),
20274 title
20275 };
20276 }
20277 if (file.type === "user") {
20278 return {
20279 kind: "user",
20280 id,
20281 url: String(file.link ?? ""),
20282 title
20283 };
20284 }
20285 return void 0;
20286 }
20287 function getDragManager() {
20288 const api = window.wp?.desktop?.dragManager;
20289 return api ?? null;
20290 }
20291 const LAYER_CLASS = "desktop-mode-files-layer";
20292 function mountFilesLayer(host, folderId = 0) {
20293 const container = document.createElement("div");
20294 container.className = LAYER_CLASS;
20295 container.setAttribute("role", "list");
20296 container.dataset.folderId = String(folderId);
20297 host.appendChild(container);
20298 let lastFingerprint = "";
20299 let selectedId = null;
20300 const selectionListeners = /* @__PURE__ */ new Set();
20301 const notifySelection = (placement) => {
20302 for (const cb of selectionListeners) {
20303 try {
20304 cb(placement);
20305 } catch (err) {
20306 console.error(
20307 "[desktop-mode] files: selection listener threw:",
20308 err
20309 );
20310 }
20311 }
20312 };
20313 const setSelected = (placement) => {
20314 const newId = placement ? placement.id : null;
20315 if (newId === selectedId) {
20316 return;
20317 }
20318 container.querySelectorAll(`.${TILE_CLASS}--selected`).forEach((n) => n.removeAttribute("selected"));
20319 if (placement) {
20320 const tile2 = container.querySelector(
20321 `[data-placement-id="${placement.id}"]`
20322 );
20323 tile2?.setAttribute("selected", "");
20324 }
20325 selectedId = newId;
20326 notifySelection(placement);
20327 };
20328 const computeLayout = (list2) => {
20329 const pinnedSlots = /* @__PURE__ */ new Map();
20330 const occupiedCells = /* @__PURE__ */ new Set();
20331 let pinnedIdx = 0;
20332 for (const placement of list2) {
20333 if (!isPinned(placement)) {
20334 continue;
20335 }
20336 const slot = cellToPos(0, pinnedIdx);
20337 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
20338 occupiedCells.add(cellKey(slot.col, slot.row));
20339 pinnedIdx += 1;
20340 }
20341 const displaced = /* @__PURE__ */ new Map();
20342 for (const placement of list2) {
20343 if (pinnedSlots.has(placement.id)) {
20344 continue;
20345 }
20346 const target2 = pointToCell(placement.x, placement.y);
20347 const key = cellKey(target2.col, target2.row);
20348 if (!occupiedCells.has(key)) {
20349 occupiedCells.add(key);
20350 continue;
20351 }
20352 const free = snapToEmptyCell(
20353 placement.x,
20354 placement.y,
20355 occupiedCells,
20356 host
20357 );
20358 occupiedCells.add(cellKey(free.col, free.row));
20359 displaced.set(placement.id, { x: free.x, y: free.y });
20360 }
20361 return { pinnedSlots, displaced };
20362 };
20363 const applyTilePosition = (tile2, placement, pinnedSlots, displaced) => {
20364 const pinned = pinnedSlots.get(placement.id);
20365 const moved = displaced.get(placement.id);
20366 if (pinned) {
20367 setTilePosition(tile2, pinned.x, pinned.y);
20368 } else if (moved) {
20369 setTilePosition(tile2, moved.x, moved.y);
20370 } else {
20371 setTilePosition(tile2, placement.x, placement.y);
20372 }
20373 };
20374 const wireTile = (placement, pinnedSlots, displaced) => {
20375 const tile2 = buildTile(placement, folderId);
20376 const pinnedSlot = pinnedSlots.get(placement.id);
20377 if (pinnedSlot) {
20378 setTilePosition(tile2, pinnedSlot.x, pinnedSlot.y);
20379 tile2.classList.add(`${TILE_CLASS}--pinned`);
20380 attachContextMenu(tile2, placement);
20381 attachSelectOnClick(tile2, placement);
20382 if (shouldRejectTileDrops(placement)) {
20383 const dragManager = getDragManager();
20384 if (dragManager) {
20385 const deregister = dragManager.registerDropTarget({
20386 id: `desktop-mode-files-tile-${placement.id}-reject`,
20387 element: tile2,
20388 accept: () => false,
20389 onDrop: () => {
20390 }
20391 });
20392 tileRejectDeregisters.set(placement.id, deregister);
20393 }
20394 }
20395 return tile2;
20396 }
20397 const moved = displaced.get(placement.id);
20398 if (moved) {
20399 setTilePosition(tile2, moved.x, moved.y);
20400 }
20401 attachTileDrag(tile2, placement, folderId);
20402 attachContextMenu(tile2, placement);
20403 attachSelectOnClick(tile2, placement);
20404 if (placement.file.type === "folder") {
20405 const targetFolderId = parseInt(placement.file.ref, 10);
20406 if (targetFolderId > 0) {
20407 const dragManager = getDragManager();
20408 if (dragManager) {
20409 const deregister = registerFolderDropTarget(
20410 dragManager,
20411 tile2,
20412 targetFolderId
20413 );
20414 folderDropDeregisters.set(placement.id, deregister);
20415 }
20416 }
20417 } else if (shouldRejectTileDrops(placement)) {
20418 const dragManager = getDragManager();
20419 if (dragManager) {
20420 const deregister = dragManager.registerDropTarget({
20421 id: `desktop-mode-files-tile-${placement.id}-reject`,
20422 element: tile2,
20423 accept: () => false,
20424 onDrop: () => {
20425 }
20426 });
20427 tileRejectDeregisters.set(placement.id, deregister);
20428 }
20429 }
20430 return tile2;
20431 };
20432 const tryPatchIncremental = (list2) => {
20433 const existing = /* @__PURE__ */ new Map();
20434 for (const tile2 of container.querySelectorAll(
20435 "[data-placement-id]"
20436 )) {
20437 const raw = tile2.dataset.placementId ?? "";
20438 const id = parseInt(raw, 10);
20439 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
20440 return false;
20441 }
20442 existing.set(id, tile2);
20443 }
20444 const wantIds = /* @__PURE__ */ new Set();
20445 for (const placement of list2) {
20446 wantIds.add(placement.id);
20447 }
20448 for (const placement of list2) {
20449 const tile2 = existing.get(placement.id);
20450 if (!tile2) {
20451 continue;
20452 }
20453 if (tile2.dataset.fileType !== placement.file.type) {
20454 return false;
20455 }
20456 if (tile2.dataset.fileRef !== placement.file.ref) {
20457 return false;
20458 }
20459 const wasPinned = tile2.classList.contains(
20460 `${TILE_CLASS}--pinned`
20461 );
20462 if (wasPinned !== isPinned(placement)) {
20463 return false;
20464 }
20465 }
20466 for (const [id, tile2] of existing) {
20467 if (wantIds.has(id)) {
20468 continue;
20469 }
20470 const folderDereg = folderDropDeregisters.get(id);
20471 if (folderDereg) {
20472 try {
20473 folderDereg();
20474 } catch {
20475 }
20476 folderDropDeregisters.delete(id);
20477 }
20478 const rejectDereg = tileRejectDeregisters.get(id);
20479 if (rejectDereg) {
20480 try {
20481 rejectDereg();
20482 } catch {
20483 }
20484 tileRejectDeregisters.delete(id);
20485 }
20486 tile2.remove();
20487 }
20488 const { pinnedSlots, displaced } = computeLayout(list2);
20489 for (const placement of list2) {
20490 const tile2 = existing.get(placement.id);
20491 if (tile2) {
20492 applyTilePosition(tile2, placement, pinnedSlots, displaced);
20493 continue;
20494 }
20495 container.appendChild(
20496 wireTile(placement, pinnedSlots, displaced)
20497 );
20498 }
20499 if (selectedId !== null && !container.querySelector(
20500 `[data-placement-id="${selectedId}"]`
20501 )) {
20502 selectedId = null;
20503 notifySelection(null);
20504 }
20505 doAction("desktop-mode.files.grid-rendered", {
20506 folderId,
20507 count: list2.length
20508 });
20509 return true;
20510 };
20511 const repaint = (state2) => {
20512 const raw = state2.placementsByFolder.get(folderId) ?? [];
20513 const list2 = raw.slice().sort((a, b) => {
20514 const ap = isPinned(a) ? 0 : 1;
20515 const bp = isPinned(b) ? 0 : 1;
20516 return ap - bp;
20517 });
20518 const fp = fingerprint(list2);
20519 if (fp === lastFingerprint) {
20520 return;
20521 }
20522 lastFingerprint = fp;
20523 if (tryPatchPositions(list2, container, host)) {
20524 return;
20525 }
20526 if (tryPatchIncremental(list2)) {
20527 return;
20528 }
20529 container.replaceChildren();
20530 for (const [, deregister] of folderDropDeregisters) {
20531 try {
20532 deregister();
20533 } catch {
20534 }
20535 }
20536 folderDropDeregisters.clear();
20537 for (const [, deregister] of tileRejectDeregisters) {
20538 try {
20539 deregister();
20540 } catch {
20541 }
20542 }
20543 tileRejectDeregisters.clear();
20544 const { pinnedSlots, displaced } = computeLayout(list2);
20545 for (const placement of list2) {
20546 container.appendChild(
20547 wireTile(placement, pinnedSlots, displaced)
20548 );
20549 }
20550 if (selectedId !== null && !container.querySelector(`[data-placement-id="${selectedId}"]`)) {
20551 selectedId = null;
20552 notifySelection(null);
20553 } else if (selectedId !== null) {
20554 const tile2 = container.querySelector(
20555 `[data-placement-id="${selectedId}"]`
20556 );
20557 tile2?.setAttribute("selected", "");
20558 }
20559 doAction("desktop-mode.files.grid-rendered", {
20560 folderId,
20561 count: list2.length
20562 });
20563 };
20564 const dropTargetDeregisters = [];
20565 const folderDropDeregisters = /* @__PURE__ */ new Map();
20566 const tileRejectDeregisters = /* @__PURE__ */ new Map();
20567 let dropPreviewEl = null;
20568 let dropPreviewMoveHandler = null;
20569 const installCanvasDropPreview = (session) => {
20570 if (dropPreviewEl) {
20571 return;
20572 }
20573 if (session.payload.type !== "desktop-file") {
20574 return;
20575 }
20576 const previewEl = document.createElement("div");
20577 previewEl.className = "desktop-mode-files-drop-preview";
20578 previewEl.setAttribute("aria-hidden", "true");
20579 container.appendChild(previewEl);
20580 dropPreviewEl = previewEl;
20581 const ghost = session.payload.ghost;
20582 const offsetX = ghost?.offsetX ?? 0;
20583 const offsetY = ghost?.offsetY ?? 0;
20584 const data = session.payload.data;
20585 const movingId = data?.placement?.id;
20586 const updatePreview = (clientX, clientY) => {
20587 const rect = container.getBoundingClientRect();
20588 const rawX = Math.max(0, clientX - rect.left - offsetX);
20589 const rawY = Math.max(0, clientY - rect.top - offsetY);
20590 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
20591 const occupied = buildVisualOccupiedSet(peers, movingId);
20592 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
20593 previewEl.style.transform = `translate3d(${cell.x}px, ${cell.y}px, 0)`;
20594 };
20595 const sourceRect = session.payload.source.getBoundingClientRect();
20596 updatePreview(
20597 sourceRect.left + offsetX,
20598 sourceRect.top + offsetY
20599 );
20600 const moveHandler = (ev) => {
20601 updatePreview(ev.clientX, ev.clientY);
20602 };
20603 document.addEventListener("pointermove", moveHandler);
20604 dropPreviewMoveHandler = moveHandler;
20605 };
20606 const teardownCanvasDropPreview = () => {
20607 if (dropPreviewMoveHandler) {
20608 document.removeEventListener("pointermove", dropPreviewMoveHandler);
20609 dropPreviewMoveHandler = null;
20610 }
20611 if (dropPreviewEl) {
20612 dropPreviewEl.remove();
20613 dropPreviewEl = null;
20614 }
20615 };
20616 const canvasDropTarget = {
20617 id: `desktop-mode-files-canvas-${folderId}`,
20618 element: host,
20619 accept: (payload) => {
20620 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
20621 return false;
20622 }
20623 if (folderId > 0 && payload.type === "desktop-file") {
20624 const data = payload.data;
20625 if (data.placement.file?.type === "folder") {
20626 const movingFolderId = parseInt(data.placement.file.ref, 10);
20627 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, folderId)) {
20628 return false;
20629 }
20630 }
20631 }
20632 return true;
20633 },
20634 onEnter: (session) => {
20635 host.setAttribute("data-files-drop-active", "");
20636 installCanvasDropPreview(session);
20637 },
20638 onLeave: () => {
20639 host.removeAttribute("data-files-drop-active");
20640 teardownCanvasDropPreview();
20641 },
20642 onDrop: (session, ev) => {
20643 host.removeAttribute("data-files-drop-active");
20644 teardownCanvasDropPreview();
20645 const rect = container.getBoundingClientRect();
20646 const ghost = session.payload.ghost;
20647 const offsetX = ghost?.offsetX ?? 0;
20648 const offsetY = ghost?.offsetY ?? 0;
20649 const rawX = Math.max(0, ev.clientX - rect.left - offsetX);
20650 const rawY = Math.max(0, ev.clientY - rect.top - offsetY);
20651 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
20652 if (session.payload.type === "desktop-file") {
20653 const data = session.payload.data;
20654 const occupied = buildVisualOccupiedSet(peers, data.placement.id);
20655 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
20656 const next = {
20657 ...data.placement,
20658 x: cell.x,
20659 y: cell.y,
20660 parentId: folderId
20661 };
20662 store.upsertPlacement(next);
20663 doAction("desktop-mode.files.tile-manually-placed", {
20664 folderId,
20665 placementId: data.placement.id
20666 });
20667 if (isSyntheticPlacement(data.placement)) {
20668 const dockItemId = readSynthSource(data.placement);
20669 if (dockItemId) {
20670 persistDockPromotedPosition(
20671 dockItemId,
20672 cell.x,
20673 cell.y
20674 );
20675 }
20676 return;
20677 }
20678 void updatePlacement(
20679 data.placement.id,
20680 {
20681 x: cell.x,
20682 y: cell.y,
20683 parentId: folderId
20684 },
20685 data.placement.updatedAtMs
20686 ).then((server) => {
20687 store.upsertPlacement(server, "remote");
20688 }).catch((err) => {
20689 if (isConflict(err)) {
20690 showConflictToast(err);
20691 } else {
20692 console.error(
20693 "[desktop-mode] files: drag persist failed",
20694 err
20695 );
20696 }
20697 store.upsertPlacement(data.placement);
20698 });
20699 return;
20700 }
20701 if (session.payload.type === "shortcut") {
20702 const data = session.payload.data;
20703 const occupied = buildVisualOccupiedSet(peers);
20704 const cell = nextRowMajorCell(occupied, host);
20705 void createPlacement({
20706 parentId: folderId,
20707 type: data.kind,
20708 ref: data.ref,
20709 x: cell.x,
20710 y: cell.y
20711 }).then((placement) => {
20712 store.upsertPlacement(placement);
20713 doAction("desktop-mode.files.shortcut-dropped", {
20714 folderId,
20715 placement
20716 });
20717 }).catch((err) => {
20718 console.error(
20719 "[desktop-mode] shortcut drop failed:",
20720 err
20721 );
20722 });
20723 }
20724 }
20725 };
20726 const dragManagerForLayer = getDragManager();
20727 if (dragManagerForLayer) {
20728 dropTargetDeregisters.push(
20729 dragManagerForLayer.registerDropTarget(canvasDropTarget)
20730 );
20731 }
20732 const onCanvasClick = (e) => {
20733 if (e.target instanceof Element && e.target.closest(`.${TILE_CLASS}`)) {
20734 return;
20735 }
20736 setSelected(null);
20737 };
20738 host.addEventListener("click", onCanvasClick);
20739 function attachSelectOnClick(tile2, placement) {
20740 tile2.addEventListener("click", (e) => {
20741 e.stopPropagation();
20742 setSelected(placement);
20743 });
20744 }
20745 repaint(store.getState());
20746 const off = store.subscribe(repaint);
20747 let resolveHydrated = () => void 0;
20748 const hydrated = new Promise((resolve2) => {
20749 resolveHydrated = resolve2;
20750 });
20751 if (!store.getState().hydratedFolders.has(folderId)) {
20752 void listPlacements(folderId).then((res) => {
20753 store.setFolderPlacements(folderId, res.placements);
20754 }).catch((err) => {
20755 console.error("[desktop-mode] files: failed to hydrate folder", folderId, err);
20756 }).finally(() => {
20757 resolveHydrated();
20758 });
20759 } else {
20760 queueMicrotask(resolveHydrated);
20761 }
20762 const colsForWidth = () => {
20763 const w = host.clientWidth > 0 ? host.clientWidth : 4 * GRID_CELL_W;
20764 return Math.max(1, Math.floor((w - GRID_PADDING) / GRID_CELL_W));
20765 };
20766 const sortPlacements = (list2, mode) => {
20767 const sorted = list2.slice();
20768 switch (mode) {
20769 case "name-asc":
20770 sorted.sort(
20771 (a, b) => a.file.title.localeCompare(b.file.title)
20772 );
20773 break;
20774 case "name-desc":
20775 sorted.sort(
20776 (a, b) => b.file.title.localeCompare(a.file.title)
20777 );
20778 break;
20779 case "date-asc":
20780 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
20781 break;
20782 case "date-desc":
20783 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
20784 break;
20785 }
20786 return sorted;
20787 };
20788 const sort = (mode) => {
20789 const live = store.getState().placementsByFolder.get(folderId);
20790 if (!live || live.length === 0) {
20791 return;
20792 }
20793 const pinned = live.filter((p) => isPinned(p));
20794 const draggable = live.filter((p) => !isPinned(p));
20795 const sorted = sortPlacements(draggable, mode);
20796 const cols = colsForWidth();
20797 const occupied = /* @__PURE__ */ new Set();
20798 for (let i = 0; i < pinned.length; i += 1) {
20799 occupied.add(cellKey(0, i));
20800 }
20801 let idx = 0;
20802 const nextCell = () => {
20803 while (true) {
20804 const row = Math.floor(idx / cols);
20805 const col = idx % cols;
20806 idx += 1;
20807 if (!occupied.has(cellKey(col, row))) {
20808 return { col, row };
20809 }
20810 }
20811 };
20812 sorted.forEach((p, i) => {
20813 const cell = nextCell();
20814 const x = GRID_PADDING + cell.col * GRID_CELL_W;
20815 const y = GRID_PADDING + cell.row * GRID_CELL_H;
20816 const next = {
20817 ...p,
20818 x,
20819 y,
20820 sortOrder: i
20821 };
20822 store.upsertPlacement(next);
20823 if (isSyntheticPlacement(p)) {
20824 return;
20825 }
20826 void updatePlacement(p.id, { x, y, sortOrder: i }).catch((err) => {
20827 console.error(
20828 "[desktop-mode] files: sort persist failed",
20829 err
20830 );
20831 });
20832 });
20833 };
20834 const reflow = () => {
20835 const live = store.getState().placementsByFolder.get(folderId);
20836 if (!live || live.length === 0) {
20837 return;
20838 }
20839 const w = host.clientWidth > 0 ? host.clientWidth : Infinity;
20840 const overflowing = live.some((p) => {
20841 const right = p.x + GRID_CELL_W;
20842 return right > w;
20843 });
20844 if (!overflowing) {
20845 return;
20846 }
20847 const cols = colsForWidth();
20848 const pinned = live.filter((p) => isPinned(p));
20849 const draggable = live.filter((p) => !isPinned(p));
20850 const occupied = /* @__PURE__ */ new Set();
20851 for (let i = 0; i < pinned.length; i += 1) {
20852 occupied.add(cellKey(0, i));
20853 }
20854 let idx = 0;
20855 const nextCell = () => {
20856 while (true) {
20857 const row = Math.floor(idx / cols);
20858 const col = idx % cols;
20859 idx += 1;
20860 if (!occupied.has(cellKey(col, row))) {
20861 return { col, row };
20862 }
20863 }
20864 };
20865 for (const p of draggable) {
20866 const cell = nextCell();
20867 const x = GRID_PADDING + cell.col * GRID_CELL_W;
20868 const y = GRID_PADDING + cell.row * GRID_CELL_H;
20869 const tile2 = container.querySelector(
20870 `[data-placement-id="${p.id}"]`
20871 );
20872 if (tile2) {
20873 setTilePosition(tile2, x, y);
20874 }
20875 }
20876 };
20877 let lastWidth = host.clientWidth;
20878 let resizeObserver = null;
20879 if (typeof ResizeObserver !== "undefined") {
20880 resizeObserver = new ResizeObserver(() => {
20881 const w = host.clientWidth;
20882 if (w === lastWidth) {
20883 return;
20884 }
20885 lastWidth = w;
20886 reflow();
20887 });
20888 resizeObserver.observe(host);
20889 }
20890 return {
20891 host,
20892 folderId,
20893 onSelectionChange(cb) {
20894 selectionListeners.add(cb);
20895 return () => {
20896 selectionListeners.delete(cb);
20897 };
20898 },
20899 sort,
20900 reflow,
20901 hydrated,
20902 dispose() {
20903 off();
20904 resizeObserver?.disconnect();
20905 resizeObserver = null;
20906 for (const deregister of dropTargetDeregisters) {
20907 try {
20908 deregister();
20909 } catch {
20910 }
20911 }
20912 dropTargetDeregisters.length = 0;
20913 for (const deregister of folderDropDeregisters.values()) {
20914 try {
20915 deregister();
20916 } catch {
20917 }
20918 }
20919 folderDropDeregisters.clear();
20920 for (const deregister of tileRejectDeregisters.values()) {
20921 try {
20922 deregister();
20923 } catch {
20924 }
20925 }
20926 tileRejectDeregisters.clear();
20927 host.removeEventListener("click", onCanvasClick);
20928 selectionListeners.clear();
20929 container.remove();
20930 }
20931 };
20932 }
20933 function fingerprint(list2) {
20934 if (list2.length === 0) {
20935 return "0";
20936 }
20937 const parts = [];
20938 for (const p of list2) {
20939 parts.push(
20940 `${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}`
20941 );
20942 }
20943 return parts.join("|");
20944 }
20945 function isPinned(placement) {
20946 return Boolean(placement.file.pinned);
20947 }
20948 function readSynthSource(placement) {
20949 const meta = placement.meta;
20950 if (!meta || typeof meta !== "object") {
20951 return null;
20952 }
20953 const v = meta.__synthFromDockItem;
20954 return typeof v === "string" && v !== "" ? v : null;
20955 }
20956 function isSyntheticPlacement(placement) {
20957 return placement.id <= 0 || readSynthSource(placement) !== null;
20958 }
20959 const RECYCLE_BIN_REF = "desktop-mode-recycle-bin";
20960 function shouldRejectTileDrops(placement) {
20961 if (placement.file?.type === "folder") {
20962 return false;
20963 }
20964 if (placement.file?.ref === RECYCLE_BIN_REF) {
20965 return false;
20966 }
20967 return true;
20968 }
20969 function buildVisualOccupiedSet(placements, excludeId) {
20970 const sorted = placements.slice().sort((a, b) => {
20971 const ap = isPinned(a) ? 0 : 1;
20972 const bp = isPinned(b) ? 0 : 1;
20973 return ap - bp;
20974 });
20975 const set = /* @__PURE__ */ new Set();
20976 let pinnedIdx = 0;
20977 for (const p of sorted) {
20978 if (excludeId !== void 0 && p.id === excludeId) {
20979 continue;
20980 }
20981 if (isPinned(p)) {
20982 set.add(cellKey(0, pinnedIdx));
20983 pinnedIdx += 1;
20984 } else {
20985 const cell = pointToCell(p.x, p.y);
20986 set.add(cellKey(cell.col, cell.row));
20987 }
20988 }
20989 return set;
20990 }
20991 function wouldCreateFolderCycle(movingFolderId, targetParentId) {
20992 if (targetParentId <= 0 || movingFolderId <= 0) {
20993 return false;
20994 }
20995 if (movingFolderId === targetParentId) {
20996 return true;
20997 }
20998 const parentByFolderId = /* @__PURE__ */ new Map();
20999 const state2 = store.getState();
21000 for (const bucket2 of state2.placementsByFolder.values()) {
21001 for (const p of bucket2) {
21002 if (p.file?.type !== "folder") {
21003 continue;
21004 }
21005 const fid = parseInt(p.file.ref, 10);
21006 if (Number.isNaN(fid) || fid <= 0) {
21007 continue;
21008 }
21009 if (!parentByFolderId.has(fid)) {
21010 parentByFolderId.set(fid, p.parentId);
21011 }
21012 }
21013 }
21014 const visited = /* @__PURE__ */ new Set();
21015 let cursor = targetParentId;
21016 let maxDepth = 256;
21017 while (cursor > 0 && maxDepth-- > 0) {
21018 if (cursor === movingFolderId) {
21019 return true;
21020 }
21021 if (visited.has(cursor)) {
21022 return true;
21023 }
21024 visited.add(cursor);
21025 const next = parentByFolderId.get(cursor);
21026 if (next === void 0) {
21027 return false;
21028 }
21029 cursor = next;
21030 }
21031 return false;
21032 }
21033 function persistDockPromotedPosition(dockItemId, x, y) {
21034 const api = window.wp?.desktop;
21035 if (!api?.getOsSettings || !api?.updateOsSettings) {
21036 return;
21037 }
21038 const current = api.getOsSettings().dockPromotedPositions ?? {};
21039 api.updateOsSettings({
21040 dockPromotedPositions: {
21041 ...current,
21042 [dockItemId]: { x, y }
21043 }
21044 });
21045 }
21046 function tryPatchPositions(list2, container, host) {
21047 const tiles = Array.from(
21048 container.querySelectorAll("[data-placement-id]")
21049 );
21050 if (tiles.length !== list2.length) {
21051 return false;
21052 }
21053 const byId = /* @__PURE__ */ new Map();
21054 for (const tile2 of tiles) {
21055 const raw = tile2.dataset.placementId ?? "";
21056 const id = parseInt(raw, 10);
21057 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
21058 return false;
21059 }
21060 byId.set(id, tile2);
21061 }
21062 for (const placement of list2) {
21063 const tile2 = byId.get(placement.id);
21064 if (!tile2) {
21065 return false;
21066 }
21067 if (tile2.dataset.fileType !== placement.file.type) {
21068 return false;
21069 }
21070 if (tile2.dataset.fileRef !== placement.file.ref) {
21071 return false;
21072 }
21073 const wasPinned = tile2.classList.contains(`${TILE_CLASS}--pinned`);
21074 if (wasPinned !== isPinned(placement)) {
21075 return false;
21076 }
21077 }
21078 const pinnedSlots = /* @__PURE__ */ new Map();
21079 const occupiedCells = /* @__PURE__ */ new Set();
21080 let pinnedIdx = 0;
21081 for (const placement of list2) {
21082 if (!isPinned(placement)) {
21083 continue;
21084 }
21085 const slot = cellToPos(0, pinnedIdx);
21086 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
21087 occupiedCells.add(cellKey(slot.col, slot.row));
21088 pinnedIdx += 1;
21089 }
21090 const displaced = /* @__PURE__ */ new Map();
21091 for (const placement of list2) {
21092 if (pinnedSlots.has(placement.id)) {
21093 continue;
21094 }
21095 const target2 = pointToCell(placement.x, placement.y);
21096 const key = cellKey(target2.col, target2.row);
21097 if (!occupiedCells.has(key)) {
21098 occupiedCells.add(key);
21099 continue;
21100 }
21101 const free = snapToEmptyCell(
21102 placement.x,
21103 placement.y,
21104 occupiedCells,
21105 host
21106 );
21107 occupiedCells.add(cellKey(free.col, free.row));
21108 displaced.set(placement.id, { x: free.x, y: free.y });
21109 }
21110 for (const placement of list2) {
21111 const tile2 = byId.get(placement.id);
21112 if (!tile2) {
21113 continue;
21114 }
21115 const pinned = pinnedSlots.get(placement.id);
21116 const disp = displaced.get(placement.id);
21117 if (pinned) {
21118 setTilePosition(tile2, pinned.x, pinned.y);
21119 } else if (disp) {
21120 setTilePosition(tile2, disp.x, disp.y);
21121 } else {
21122 setTilePosition(tile2, placement.x, placement.y);
21123 }
21124 }
21125 return true;
21126 }
21127 function hidePromotedDockItem(dockItemId) {
21128 const api = window.wp?.desktop;
21129 if (!api?.getOsSettings || !api?.updateOsSettings) {
21130 return;
21131 }
21132 const current = api.getOsSettings().itemVisibility ?? {};
21133 const next = { ...current, [dockItemId]: "dock" };
21134 api.updateOsSettings({ itemVisibility: next });
21135 }
21136 function registerFolderDropTarget(dragManager, tile2, targetFolderId, currentFolderId) {
21137 const target2 = {
21138 id: `desktop-mode-files-folder-${targetFolderId}-tile-${tile2.dataset.placementId ?? "?"}`,
21139 element: tile2,
21140 accept: (payload) => {
21141 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
21142 return false;
21143 }
21144 if (payload.type === "desktop-file") {
21145 const data = payload.data;
21146 if (data.placement.file.type === "folder" && parseInt(data.placement.file.ref, 10) === targetFolderId) {
21147 return false;
21148 }
21149 if (data.placement.parentId === targetFolderId) {
21150 return false;
21151 }
21152 if (isSyntheticPlacement(data.placement)) {
21153 return false;
21154 }
21155 if (data.placement.file.type === "folder") {
21156 const movingFolderId = parseInt(data.placement.file.ref, 10);
21157 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, targetFolderId)) {
21158 return false;
21159 }
21160 }
21161 }
21162 return true;
21163 },
21164 onEnter: () => {
21165 tile2.classList.add(`${TILE_CLASS}--drop-target`);
21166 },
21167 onLeave: () => {
21168 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
21169 },
21170 onDrop: (session) => {
21171 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
21172 if (session.payload.type === "desktop-file") {
21173 const data = session.payload.data;
21174 const next = {
21175 ...data.placement,
21176 parentId: targetFolderId
21177 };
21178 store.upsertPlacement(next);
21179 void updatePlacement(
21180 data.placement.id,
21181 { parentId: targetFolderId },
21182 data.placement.updatedAtMs
21183 ).then((server) => {
21184 store.upsertPlacement(server, "remote");
21185 }).catch((err) => {
21186 if (isConflict(err)) {
21187 showConflictToast(err);
21188 } else {
21189 console.error(
21190 "[desktop-mode] files: move-into-folder persist failed",
21191 err
21192 );
21193 }
21194 store.upsertPlacement(data.placement);
21195 });
21196 return;
21197 }
21198 if (session.payload.type === "shortcut") {
21199 const data = session.payload.data;
21200 const peers = store.getState().placementsByFolder.get(targetFolderId) ?? [];
21201 const cell = nextRowMajorCell(buildVisualOccupiedSet(peers));
21202 void createPlacement({
21203 parentId: targetFolderId,
21204 type: data.kind,
21205 ref: data.ref,
21206 x: cell.x,
21207 y: cell.y
21208 }).then((placement) => {
21209 store.upsertPlacement(placement);
21210 doAction("desktop-mode.files.shortcut-dropped", {
21211 folderId: targetFolderId,
21212 placement
21213 });
21214 }).catch((err) => {
21215 console.error(
21216 "[desktop-mode] shortcut drop into folder failed:",
21217 err
21218 );
21219 });
21220 }
21221 }
21222 };
21223 return dragManager.registerDropTarget(target2);
21224 }
21225 function attachTileDrag(tile2, placement, folderId) {
21226 tile2.addEventListener("pointerdown", (e) => {
21227 if (e.button !== 0) {
21228 return;
21229 }
21230 const dragManager = getDragManager();
21231 if (!dragManager) {
21232 return;
21233 }
21234 const liveBucket = store.getState().placementsByFolder.get(folderId);
21235 const livePlacement = liveBucket?.find((p) => p.id === placement.id) ?? placement;
21236 parseFloat(tile2.style.left) || livePlacement.x;
21237 parseFloat(tile2.style.top) || livePlacement.y;
21238 dragManager.start({
21239 payload: {
21240 type: "desktop-file",
21241 source: tile2,
21242 data: {
21243 placement: livePlacement,
21244 sourceFolderId: folderId,
21245 // Synthesize a cross-frame bridge payload from the
21246 // placement's file shape so a wallpaper-placed
21247 // shortcut can be dropped into an open Gutenberg
21248 // iframe and inserted as the matching block. The
21249 // PHP serialize() methods (`Desktop_Mode_Post_File`,
21250 // `Desktop_Mode_User_File`, `Desktop_Mode_Attachment_File`)
21251 // surface the URL fields this needs.
21252 bridgePayload: buildBridgePayloadFromPlacement(livePlacement)
21253 },
21254 ghost: {
21255 offsetX: e.clientX - tile2.getBoundingClientRect().left,
21256 offsetY: e.clientY - tile2.getBoundingClientRect().top
21257 }
21258 },
21259 origin: e
21260 // `onClickOnly` intentionally empty — a tile click is
21261 // handled by the dedicated `attachSelectOnClick` listener
21262 // below, which fires from the regular `click` event after
21263 // a sub-threshold pointerup. The manager won't fire a
21264 // `click` itself; the browser does.
21265 });
21266 });
21267 }
21268 function attachContextMenu(tile2, placement) {
21269 tile2.addEventListener("contextmenu", (e) => {
21270 e.preventDefault();
21271 e.stopPropagation();
21272 const items = [
21273 {
21274 id: "open",
21275 label: "Open",
21276 icon: "dashicons-external",
21277 sort: 10,
21278 onClick: () => {
21279 const file = resolve(placement.file);
21280 void openFile(file);
21281 }
21282 }
21283 ];
21284 if (placement.file.type === "post") {
21285 items.push({
21286 id: "navigate-into",
21287 label: "Navigate into",
21288 icon: "dashicons-category",
21289 sort: 20,
21290 onClick: () => {
21291 const postId = parseInt(placement.file.ref, 10);
21292 if (!postId) {
21293 return;
21294 }
21295 const api = window.wp?.desktop?.myWordpress;
21296 const postType = typeof placement.file.postType === "string" ? placement.file.postType : "post";
21297 const entityId = postType === "page" ? "pages" : "posts";
21298 api?.openDetail({
21299 entityId,
21300 postId,
21301 postTitle: placement.file.title || `#${postId}`
21302 });
21303 }
21304 });
21305 }
21306 const isFolder = placement.file.type === "folder";
21307 if (isFolder) {
21308 items.push({
21309 id: "rename-folder",
21310 label: "Rename…",
21311 icon: "dashicons-edit",
21312 sort: 30,
21313 onClick: () => {
21314 const folderId = parseInt(placement.file.ref, 10);
21315 if (!folderId) {
21316 return;
21317 }
21318 openCreateFolderDialog({
21319 title: "Rename folder",
21320 label: "New name",
21321 submitLabel: "Rename",
21322 initialName: placement.file.title,
21323 onSubmit: async (name) => {
21324 const trimmed = name.trim();
21325 if (!trimmed || trimmed === placement.file.title) {
21326 return;
21327 }
21328 const previousTitle = placement.file.title;
21329 const optimistic = {
21330 ...placement,
21331 file: { ...placement.file, title: trimmed }
21332 };
21333 store.upsertPlacement(optimistic);
21334 try {
21335 const folderUpdatedAtMs = store.getState().folders.get(folderId)?.updatedAtMs ?? 0;
21336 const updated = await updateFolder(
21337 folderId,
21338 { name: trimmed },
21339 folderUpdatedAtMs
21340 );
21341 store.upsertFolder(updated);
21342 const refreshed = await listPlacements(
21343 placement.parentId
21344 );
21345 store.setFolderPlacements(
21346 placement.parentId,
21347 refreshed.placements
21348 );
21349 } catch (err) {
21350 console.error(
21351 "[desktop-mode] rename folder failed:",
21352 err
21353 );
21354 store.upsertPlacement({
21355 ...placement,
21356 file: {
21357 ...placement.file,
21358 title: previousTitle
21359 }
21360 });
21361 }
21362 }
21363 });
21364 }
21365 });
21366 if (placement.canTrash !== false) {
21367 items.push({
21368 id: "delete-folder",
21369 label: "Move folder to Trash",
21370 icon: "dashicons-trash",
21371 sort: 90,
21372 danger: true,
21373 onClick: () => trashFolderWithUndo(placement)
21374 });
21375 }
21376 } else {
21377 const synthFromDockItem = readSynthSource(placement);
21378 const isRegisteredIcon = placement.file.type === "shortcut";
21379 if (synthFromDockItem || isRegisteredIcon) {
21380 const hideId = synthFromDockItem ?? placement.file.ref;
21381 items.push({
21382 id: "hide-from-desktop",
21383 label: "Hide from desktop",
21384 icon: "dashicons-hidden",
21385 sort: 90,
21386 onClick: () => hidePromotedDockItem(hideId)
21387 });
21388 } else if (placement.canTrash !== false) {
21389 items.push({
21390 id: "remove",
21391 label: "Move to Trash",
21392 icon: "dashicons-trash",
21393 sort: 90,
21394 danger: true,
21395 onClick: () => trashPlacementWithUndo(placement)
21396 });
21397 }
21398 }
21399 openTileMenu({ x: e.clientX, y: e.clientY }, { placement, items });
21400 });
21401 }
21402 const STATUS_BAR_CLASS = "desktop-mode-folder-status-bar";
21403 const ROOT_CLASS$2 = STATUS_BAR_CLASS;
21404 function mountFolderStatusBar(host, folderId) {
21405 const bar = document.createElement("div");
21406 bar.className = ROOT_CLASS$2;
21407 bar.setAttribute("role", "status");
21408 bar.dataset.folderId = String(folderId);
21409 host.appendChild(bar);
21410 const repaint = () => {
21411 const list2 = getFilesState().placementsByFolder.get(folderId) ?? [];
21412 const folders = list2.filter((p) => p.file.type === "folder").length;
21413 const files = list2.length - folders;
21414 const ctx = {
21415 folderId,
21416 totals: { files, folders, total: list2.length }
21417 };
21418 const segments = computeSegments(ctx);
21419 render(bar, segments);
21420 };
21421 repaint();
21422 const off = subscribeFilesStore(() => repaint());
21423 return {
21424 dispose() {
21425 off();
21426 bar.remove();
21427 }
21428 };
21429 }
21430 function computeSegments(ctx) {
21431 const { folders, files } = ctx.totals;
21432 const builtIns = [
21433 {
21434 id: "count",
21435 label: pluralize(files, "file", "files") + (folders > 0 ? `, ${pluralize(folders, "folder", "folders")}` : ""),
21436 align: "start",
21437 sort: 10
21438 }
21439 ];
21440 const filtered = applyFilters(
21441 "desktop-mode.files.folder-window.status-bar",
21442 builtIns,
21443 ctx
21444 );
21445 return Array.isArray(filtered) ? filtered : builtIns;
21446 }
21447 function render(bar, segments) {
21448 const sort = (a, b) => {
21449 const sa = typeof a.sort === "number" ? a.sort : 100;
21450 const sb = typeof b.sort === "number" ? b.sort : 100;
21451 if (sa !== sb) {
21452 return sa - sb;
21453 }
21454 return a.label.localeCompare(b.label);
21455 };
21456 const start = segments.filter((s) => (s.align ?? "start") === "start").sort(sort);
21457 const end = segments.filter((s) => s.align === "end").sort(sort);
21458 bar.replaceChildren();
21459 bar.appendChild(buildCluster("start", start));
21460 bar.appendChild(buildCluster("end", end));
21461 }
21462 function buildCluster(align, segs) {
21463 const cluster = document.createElement("div");
21464 cluster.className = `${ROOT_CLASS$2}__cluster ${ROOT_CLASS$2}__cluster--${align}`;
21465 for (const seg of segs) {
21466 cluster.appendChild(buildSegment(seg));
21467 }
21468 return cluster;
21469 }
21470 function buildSegment(seg) {
21471 const interactive = typeof seg.onClick === "function";
21472 const el = document.createElement(interactive ? "button" : "span");
21473 el.className = `${ROOT_CLASS$2}__segment`;
21474 el.dataset.segmentId = seg.id;
21475 if (interactive) {
21476 el.type = "button";
21477 el.addEventListener("click", (e) => seg.onClick(e));
21478 }
21479 if (seg.icon) {
21480 const icon = document.createElement("span");
21481 icon.className = `${ROOT_CLASS$2}__icon dashicons ${seg.icon.replace(/[^a-zA-Z0-9_-]/g, "")}`;
21482 icon.setAttribute("aria-hidden", "true");
21483 el.appendChild(icon);
21484 }
21485 const label = document.createElement("span");
21486 label.className = `${ROOT_CLASS$2}__label`;
21487 label.textContent = seg.label;
21488 el.appendChild(label);
21489 return el;
21490 }
21491 function pluralize(n, singular, plural) {
21492 return `${n} ${n === 1 ? singular : plural}`;
21493 }
21494 const MENU_CLASS$1 = "desktop-mode-icon-canvas-menu";
21495 let activeMenu$1 = null;
21496 let activeFlyout = null;
21497 let activeCanvas = null;
21498 let outsideHandler = null;
21499 let escHandler = null;
21500 function attachIconCanvasMenu(canvas, deps2) {
21501 deps2.openOnBackgroundClick !== false;
21502 const onContextMenu = (e) => {
21503 if (isInsideTile(e.target) || isInsideMenu(e.target)) {
21504 return;
21505 }
21506 e.preventDefault();
21507 toggle(e.clientX, e.clientY);
21508 };
21509 let toggleGen = 0;
21510 const toggle = (x, y) => {
21511 if (activeCanvas === canvas && activeMenu$1) {
21512 closeMenu();
21513 return;
21514 }
21515 const items = buildItems(deps2);
21516 const filtered = applyFilters(
21517 "desktop-mode.icon-canvas.menu",
21518 items,
21519 deps2.scope
21520 );
21521 const finalItems = Array.isArray(filtered) ? filtered : items;
21522 const myGen = ++toggleGen;
21523 openWithShellOverlays(
21524 () => myGen === toggleGen,
21525 () => openMenu(finalItems, { x, y }, canvas)
21526 );
21527 };
21528 canvas.addEventListener("contextmenu", onContextMenu);
21529 return {
21530 dispose: () => {
21531 canvas.removeEventListener("contextmenu", onContextMenu);
21532 closeMenu();
21533 }
21534 };
21535 }
21536 function isInsideTile(target2) {
21537 if (!(target2 instanceof Element)) {
21538 return false;
21539 }
21540 return target2.closest(".desktop-mode-file-tile") !== null;
21541 }
21542 function isInsideMenu(target2) {
21543 if (!(target2 instanceof Element)) {
21544 return false;
21545 }
21546 return target2.closest(`.${MENU_CLASS$1}`) !== null;
21547 }
21548 function buildItems(deps2) {
21549 const sortItem = {
21550 id: "sort-by",
21551 label: __("Sort by", "desktop-mode"),
21552 icon: "dashicons-sort",
21553 sort: 10,
21554 children: [
21555 {
21556 id: "sort-name-asc",
21557 label: __("Name (A → Z)", "desktop-mode"),
21558 sort: 10,
21559 onClick: () => deps2.onSort("name-asc")
21560 },
21561 {
21562 id: "sort-name-desc",
21563 label: __("Name (Z → A)", "desktop-mode"),
21564 sort: 20,
21565 onClick: () => deps2.onSort("name-desc")
21566 },
21567 {
21568 id: "sort-date-desc",
21569 label: __("Newest first", "desktop-mode"),
21570 sort: 30,
21571 onClick: () => deps2.onSort("date-desc")
21572 },
21573 {
21574 id: "sort-date-asc",
21575 label: __("Oldest first", "desktop-mode"),
21576 sort: 40,
21577 onClick: () => deps2.onSort("date-asc")
21578 }
21579 ]
21580 };
21581 const items = [sortItem];
21582 if (Array.isArray(deps2.extraItems)) {
21583 items.push(...deps2.extraItems);
21584 }
21585 return items;
21586 }
21587 function sortItems(items) {
21588 return items.slice().sort((a, b) => {
21589 const sa = typeof a.sort === "number" ? a.sort : 100;
21590 const sb = typeof b.sort === "number" ? b.sort : 100;
21591 if (sa !== sb) {
21592 return sa - sb;
21593 }
21594 return a.label.localeCompare(b.label);
21595 });
21596 }
21597 function openMenu(items, pos, canvas) {
21598 closeMenu();
21599 if (items.length === 0) {
21600 return;
21601 }
21602 activeCanvas = canvas;
21603 const sorted = sortItems(items);
21604 const menu = document.createElement("wpd-context-menu");
21605 menu.setAttribute("open", "");
21606 menu.classList.add(MENU_CLASS$1);
21607 menu.style.left = `${pos.x}px`;
21608 menu.style.top = `${pos.y}px`;
21609 const itemById = /* @__PURE__ */ new Map();
21610 for (const item of sorted) {
21611 itemById.set(item.id, item);
21612 const opt = appendOption(menu, item);
21613 if (hasChildren(item)) {
21614 opt.addEventListener("mouseenter", () => {
21615 openFlyout(item, opt);
21616 });
21617 }
21618 }
21619 menu.addEventListener("wpd-context-menu-pick", (e) => {
21620 const detail = e.detail;
21621 const item = itemById.get(detail.id);
21622 if (!item) {
21623 return;
21624 }
21625 if (hasChildren(item)) {
21626 e.stopPropagation();
21627 const anchor = menu.querySelector(
21628 `[data-menu-item-id="${item.id}"]`
21629 );
21630 if (anchor) {
21631 openFlyout(item, anchor);
21632 }
21633 return;
21634 }
21635 closeMenu();
21636 item.onClick?.();
21637 });
21638 document.body.appendChild(menu);
21639 activeMenu$1 = menu;
21640 clampToViewport(menu);
21641 queueMicrotask(() => {
21642 outsideHandler = (e) => {
21643 if (isInsideMenu(e.target)) {
21644 return;
21645 }
21646 closeMenu();
21647 };
21648 escHandler = (e) => {
21649 if (e.key === "Escape") {
21650 closeMenu();
21651 }
21652 };
21653 document.addEventListener("mousedown", outsideHandler);
21654 document.addEventListener("keydown", escHandler);
21655 });
21656 }
21657 function appendOption(host, item) {
21658 const opt = document.createElement("wpd-context-menu-option");
21659 opt.dataset.menuItemId = item.id;
21660 opt.setAttribute("value", item.id);
21661 if (item.heading) {
21662 opt.setAttribute("heading", "");
21663 }
21664 if (item.disabled) {
21665 opt.setAttribute("disabled", "");
21666 }
21667 if (item.icon) {
21668 opt.setAttribute("icon", sanitizeClass$1(item.icon));
21669 }
21670 if (hasChildren(item)) {
21671 opt.setAttribute("has-children", "");
21672 }
21673 opt.textContent = item.label;
21674 host.appendChild(opt);
21675 return opt;
21676 }
21677 function openFlyout(parent, anchor) {
21678 closeFlyout();
21679 if (!hasChildren(parent)) {
21680 return;
21681 }
21682 const fly = document.createElement("wpd-context-menu");
21683 fly.setAttribute("open", "");
21684 fly.classList.add(MENU_CLASS$1, `${MENU_CLASS$1}--flyout`);
21685 const childById = /* @__PURE__ */ new Map();
21686 for (const child of sortItems(parent.children ?? [])) {
21687 childById.set(child.id, child);
21688 appendOption(fly, child);
21689 }
21690 fly.addEventListener("wpd-context-menu-pick", (e) => {
21691 const detail = e.detail;
21692 const child = childById.get(detail.id);
21693 if (!child) {
21694 return;
21695 }
21696 e.stopPropagation();
21697 closeMenu();
21698 child.onClick?.();
21699 });
21700 document.body.appendChild(fly);
21701 activeFlyout = fly;
21702 positionFlyout(fly, anchor);
21703 }
21704 function positionFlyout(fly, anchor) {
21705 const ar = anchor.getBoundingClientRect();
21706 fly.style.position = "fixed";
21707 fly.style.left = `${ar.right}px`;
21708 fly.style.top = `${ar.top}px`;
21709 const fr = fly.getBoundingClientRect();
21710 if (fr.right > window.innerWidth) {
21711 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
21712 }
21713 if (fr.bottom > window.innerHeight) {
21714 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
21715 }
21716 }
21717 function clampToViewport(menu) {
21718 const rect = menu.getBoundingClientRect();
21719 if (rect.right > window.innerWidth) {
21720 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
21721 }
21722 if (rect.bottom > window.innerHeight) {
21723 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
21724 }
21725 }
21726 function hasChildren(item) {
21727 return Array.isArray(item.children) && item.children.length > 0;
21728 }
21729 function closeFlyout() {
21730 if (activeFlyout) {
21731 activeFlyout.remove();
21732 activeFlyout = null;
21733 }
21734 }
21735 function closeMenu() {
21736 closeFlyout();
21737 if (activeMenu$1) {
21738 activeMenu$1.remove();
21739 activeMenu$1 = null;
21740 }
21741 activeCanvas = null;
21742 if (outsideHandler) {
21743 document.removeEventListener("mousedown", outsideHandler);
21744 outsideHandler = null;
21745 }
21746 if (escHandler) {
21747 document.removeEventListener("keydown", escHandler);
21748 escHandler = null;
21749 }
21750 }
21751 function sanitizeClass$1(raw) {
21752 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
21753 }
21754 const ROOT_CLASS$1 = "desktop-mode-breadcrumbs";
21755 function renderBreadcrumbs(host, segments, opts = {}) {
21756 host.replaceChildren();
21757 host.classList.add(ROOT_CLASS$1);
21758 if (opts.onBack) {
21759 const back = document.createElement("button");
21760 back.type = "button";
21761 back.className = `${ROOT_CLASS$1}__back`;
21762 back.setAttribute("aria-label", __("Back", "desktop-mode"));
21763 back.title = __("Back", "desktop-mode");
21764 const arrow = document.createElement("span");
21765 arrow.className = "dashicons dashicons-arrow-left-alt2";
21766 arrow.setAttribute("aria-hidden", "true");
21767 back.appendChild(arrow);
21768 if (opts.backDisabled) {
21769 back.disabled = true;
21770 }
21771 const onBack = opts.onBack;
21772 back.addEventListener("click", () => {
21773 if (back.disabled) {
21774 return;
21775 }
21776 onBack();
21777 });
21778 host.appendChild(back);
21779 }
21780 const nav = document.createElement("nav");
21781 nav.className = `${ROOT_CLASS$1}__crumbs`;
21782 nav.setAttribute("aria-label", __("Breadcrumb", "desktop-mode"));
21783 segments.forEach((seg, idx) => {
21784 if (idx > 0) {
21785 const sep = document.createElement("span");
21786 sep.className = `${ROOT_CLASS$1}__sep`;
21787 sep.setAttribute("aria-hidden", "true");
21788 sep.textContent = "›";
21789 nav.appendChild(sep);
21790 }
21791 if (!seg.onClick) {
21792 const here = document.createElement("span");
21793 here.className = `${ROOT_CLASS$1}__crumb ${ROOT_CLASS$1}__crumb--current`;
21794 here.setAttribute("aria-current", "page");
21795 here.textContent = seg.label;
21796 nav.appendChild(here);
21797 return;
21798 }
21799 const btn = document.createElement("button");
21800 btn.type = "button";
21801 btn.className = `${ROOT_CLASS$1}__crumb`;
21802 btn.textContent = seg.label;
21803 const onClick = seg.onClick;
21804 btn.addEventListener("click", () => {
21805 onClick();
21806 });
21807 nav.appendChild(btn);
21808 });
21809 host.appendChild(nav);
21810 }
21811 async function getJson(url, init2 = {}) {
21812 const response = await trackedFetch$1(url, {
21813 credentials: "same-origin",
21814 headers: {
21815 Accept: "application/json",
21816 "X-WP-Nonce": readRestNonce(),
21817 ...init2.headers ?? {}
21818 },
21819 ...init2
21820 });
21821 if (!response.ok) {
21822 throw new Error(`${response.status} ${response.statusText}`);
21823 }
21824 return await response.json();
21825 }
21826 function readRestNonce() {
21827 const cfg = window.wp?.desktop?.config;
21828 return cfg?.restNonce ?? "";
21829 }
21830 function readRestRoot() {
21831 const cfg = window.wp?.desktop?.config;
21832 if (cfg?.restUrl) {
21833 return cfg.restUrl.endsWith("/") ? cfg.restUrl : cfg.restUrl + "/";
21834 }
21835 return `${window.location.origin}/wp-json/`;
21836 }
21837 function restUrl(path) {
21838 return joinRestUrl(readRestRoot(), path);
21839 }
21840 function renderPlacementPreview(placement, host) {
21841 const filtered = applyFilters(
21842 "desktop-mode.files.preview",
21843 null,
21844 placement
21845 );
21846 if (filtered instanceof HTMLElement) {
21847 host.replaceChildren(filtered);
21848 return;
21849 }
21850 if (placement.accessGated) {
21851 host.replaceChildren(renderAccessGated(placement));
21852 return;
21853 }
21854 host.replaceChildren(renderLoading());
21855 void renderByType(placement).then((node) => {
21856 host.replaceChildren(node);
21857 }).catch((err) => {
21858 host.replaceChildren(renderError(err));
21859 });
21860 }
21861 function renderAccessGated(placement) {
21862 const wrap = document.createElement("div");
21863 wrap.className = "desktop-mode-files__access-gated";
21864 const ring = document.createElement("div");
21865 ring.className = "desktop-mode-files__access-gated-ring";
21866 const glyph = document.createElement("span");
21867 glyph.className = "dashicons dashicons-lock desktop-mode-files__access-gated-glyph";
21868 glyph.setAttribute("aria-hidden", "true");
21869 ring.appendChild(glyph);
21870 wrap.appendChild(ring);
21871 const title = document.createElement("h2");
21872 title.className = "desktop-mode-files__access-gated-title";
21873 title.textContent = "No permission to view";
21874 wrap.appendChild(title);
21875 const sub = document.createElement("p");
21876 sub.className = "desktop-mode-files__access-gated-sub";
21877 const target2 = placement.file.title || placement.file.type;
21878 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.`;
21879 wrap.appendChild(sub);
21880 const hint = document.createElement("p");
21881 hint.className = "desktop-mode-files__access-gated-hint";
21882 hint.textContent = "Ask the owner to grant access on the underlying item, or to remove it from the shared folder.";
21883 wrap.appendChild(hint);
21884 return wrap;
21885 }
21886 async function renderByType(placement) {
21887 const file = placement.file;
21888 switch (file.type) {
21889 case "post":
21890 return renderPostPreview(file.ref, file);
21891 case "folder":
21892 return renderFolderPreview(file);
21893 case "shortcut":
21894 return renderShortcutPreview(file);
21895 case "attachment":
21896 return renderAttachmentPreview(file.ref, file);
21897 case "user":
21898 return renderUserSummary(file.ref, file);
21899 case "term":
21900 return renderTermSummary(file);
21901 case "comment":
21902 return renderCommentSummary(file.ref, file);
21903 case "bookmark":
21904 return renderBookmarkPreview(file);
21905 default:
21906 return renderGenericPreview(file);
21907 }
21908 }
21909 async function renderPostPreview(ref, file) {
21910 const id = parseInt(ref, 10);
21911 if (!id) {
21912 return renderGenericPreview(file);
21913 }
21914 let data = null;
21915 for (const path of ["wp/v2/posts", "wp/v2/pages"]) {
21916 try {
21917 data = await getJson(
21918 restUrl(
21919 `${path}/${id}?_fields=id,title,content,date,link,status`
21920 )
21921 );
21922 break;
21923 } catch {
21924 }
21925 }
21926 if (!data) {
21927 return renderGenericPreview(file);
21928 }
21929 const wrap = articleShell();
21930 const h = document.createElement("h2");
21931 h.className = "desktop-mode-my-wordpress__article-title";
21932 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
21933 wrap.appendChild(h);
21934 const meta = document.createElement("p");
21935 meta.className = "desktop-mode-my-wordpress__article-meta";
21936 const parts = [];
21937 parts.push(formatDate(data.date));
21938 if (data.status && data.status !== "publish") {
21939 parts.push(data.status);
21940 }
21941 meta.textContent = parts.join(" · ");
21942 wrap.appendChild(meta);
21943 if (data.content?.rendered) {
21944 const body = document.createElement("div");
21945 body.className = "desktop-mode-my-wordpress__article-content";
21946 body.innerHTML = data.content.rendered;
21947 wrap.appendChild(body);
21948 }
21949 const footer = document.createElement("footer");
21950 footer.className = "desktop-mode-my-wordpress__article-footer";
21951 const myWordpressApi = window.wp?.desktop?.myWordpress;
21952 if (myWordpressApi) {
21953 const exploreBtn = document.createElement("wpd-button");
21954 exploreBtn.setAttribute("variant", "secondary");
21955 exploreBtn.textContent = __("Explore details", "desktop-mode");
21956 exploreBtn.title = __(
21957 "See author, comments, categories, tags, attached media, and revisions for this entry.",
21958 "desktop-mode"
21959 );
21960 exploreBtn.addEventListener("click", () => {
21961 const postType = typeof file.postType === "string" ? file.postType : "post";
21962 myWordpressApi.openDetail({
21963 entityId: postType === "page" ? "pages" : "posts",
21964 postId: id,
21965 postTitle: stripTags(data.title.rendered) || `#${id}`
21966 });
21967 });
21968 footer.appendChild(exploreBtn);
21969 }
21970 const editBtn = document.createElement("wpd-button");
21971 editBtn.setAttribute("variant", "primary");
21972 editBtn.textContent = __("Open in editor", "desktop-mode");
21973 editBtn.addEventListener("click", () => {
21974 const adminUrl = window.wp?.desktop?.config?.adminUrl;
21975 if (!adminUrl) {
21976 return;
21977 }
21978 const editUrl = `${adminUrl}post.php?post=${id}&action=edit`;
21979 const wm = window.wp?.desktop?.windowManager;
21980 const postType = typeof file.postType === "string" ? file.postType : "post";
21981 const entityId = postType === "page" ? "pages" : "posts";
21982 wm?.open({
21983 id: `${entityId}-edit-${id}`,
21984 url: editUrl,
21985 title: stripTags(data.title.rendered),
21986 icon: file.icon
21987 });
21988 });
21989 footer.appendChild(editBtn);
21990 wrap.appendChild(footer);
21991 return wrap;
21992 }
21993 async function renderUserSummary(ref, file) {
21994 const id = parseInt(ref, 10);
21995 if (!id) {
21996 return renderGenericPreview(file);
21997 }
21998 let data = null;
21999 try {
22000 data = await getJson(
22001 restUrl(`desktop-mode/v1/user-stats/${id}`)
22002 );
22003 } catch {
22004 return renderGenericPreview(file);
22005 }
22006 const wrap = articleShell("desktop-mode-my-wordpress__user");
22007 const header = document.createElement("header");
22008 header.className = "desktop-mode-my-wordpress__user-header";
22009 if (data.profile.avatarUrl) {
22010 const img = document.createElement("img");
22011 img.className = "desktop-mode-my-wordpress__user-avatar";
22012 img.src = data.profile.avatarUrl;
22013 img.alt = "";
22014 header.appendChild(img);
22015 }
22016 const head = document.createElement("div");
22017 head.className = "desktop-mode-my-wordpress__user-headline";
22018 const h = document.createElement("h2");
22019 h.className = "desktop-mode-my-wordpress__article-title";
22020 h.textContent = data.profile.name || file.title || `#${id}`;
22021 head.appendChild(h);
22022 if (data.profile.roleLabels && data.profile.roleLabels.length > 0) {
22023 const roles = document.createElement("div");
22024 roles.className = "desktop-mode-my-wordpress__user-roles";
22025 for (const r of data.profile.roleLabels) {
22026 const badge = document.createElement("span");
22027 badge.className = "desktop-mode-my-wordpress__user-role";
22028 badge.textContent = r;
22029 roles.appendChild(badge);
22030 }
22031 head.appendChild(roles);
22032 }
22033 header.appendChild(head);
22034 wrap.appendChild(header);
22035 if (data.profile.description) {
22036 const bio = document.createElement("div");
22037 bio.className = "desktop-mode-my-wordpress__user-bio";
22038 bio.textContent = data.profile.description;
22039 wrap.appendChild(bio);
22040 }
22041 const cards = document.createElement("div");
22042 cards.className = "desktop-mode-my-wordpress__user-stats";
22043 cards.appendChild(
22044 statCard(
22045 data.counts.posts.total.toLocaleString(),
22046 __("Posts", "desktop-mode")
22047 )
22048 );
22049 cards.appendChild(
22050 statCard(
22051 data.counts.pages.total.toLocaleString(),
22052 __("Pages", "desktop-mode")
22053 )
22054 );
22055 cards.appendChild(
22056 statCard(
22057 data.counts.commentsReceived.toLocaleString(),
22058 __("Comments received", "desktop-mode")
22059 )
22060 );
22061 wrap.appendChild(cards);
22062 return wrap;
22063 }
22064 async function renderTermSummary(file) {
22065 const id = parseInt(file.ref, 10);
22066 const taxonomy = typeof file.taxonomy === "string" && file.taxonomy ? file.taxonomy : "category";
22067 if (!id) {
22068 return renderGenericPreview(file);
22069 }
22070 let data = null;
22071 try {
22072 data = await getJson(
22073 restUrl(`desktop-mode/v1/term-stats/${taxonomy}/${id}`)
22074 );
22075 } catch {
22076 return renderGenericPreview(file);
22077 }
22078 const wrap = articleShell();
22079 const h = document.createElement("h2");
22080 h.className = "desktop-mode-my-wordpress__article-title";
22081 h.textContent = data.profile.name || file.title || `#${id}`;
22082 wrap.appendChild(h);
22083 const meta = document.createElement("p");
22084 meta.className = "desktop-mode-my-wordpress__article-meta";
22085 meta.textContent = data.profile.taxonomyLabel || data.profile.taxonomy;
22086 wrap.appendChild(meta);
22087 if (data.profile.description) {
22088 const desc = document.createElement("div");
22089 desc.className = "desktop-mode-my-wordpress__article-content";
22090 desc.innerHTML = data.profile.description;
22091 wrap.appendChild(desc);
22092 }
22093 const cards = document.createElement("div");
22094 cards.className = "desktop-mode-my-wordpress__user-stats";
22095 cards.appendChild(
22096 statCard(
22097 data.counts.posts.total.toLocaleString(),
22098 __("Posts", "desktop-mode")
22099 )
22100 );
22101 cards.appendChild(
22102 statCard(
22103 data.counts.commentsReceived.toLocaleString(),
22104 __("Comments", "desktop-mode")
22105 )
22106 );
22107 cards.appendChild(
22108 statCard(
22109 data.counts.distinctAuthors.toLocaleString(),
22110 __("Authors", "desktop-mode")
22111 )
22112 );
22113 wrap.appendChild(cards);
22114 return wrap;
22115 }
22116 async function renderCommentSummary(ref, file) {
22117 const id = parseInt(ref, 10);
22118 if (!id) {
22119 return renderGenericPreview(file);
22120 }
22121 let data = null;
22122 try {
22123 data = await getJson(
22124 restUrl(`desktop-mode/v1/comment-stats/${id}`)
22125 );
22126 } catch {
22127 return renderGenericPreview(file);
22128 }
22129 const wrap = articleShell();
22130 const header = document.createElement("header");
22131 header.className = "desktop-mode-my-wordpress__user-header";
22132 if (data.author.avatarUrl) {
22133 const img = document.createElement("img");
22134 img.className = "desktop-mode-my-wordpress__user-avatar";
22135 img.src = data.author.avatarUrl;
22136 img.alt = "";
22137 header.appendChild(img);
22138 }
22139 const head = document.createElement("div");
22140 head.className = "desktop-mode-my-wordpress__user-headline";
22141 const h = document.createElement("h2");
22142 h.className = "desktop-mode-my-wordpress__article-title";
22143 h.textContent = data.author.name;
22144 head.appendChild(h);
22145 const sub = document.createElement("p");
22146 sub.className = "desktop-mode-my-wordpress__article-meta";
22147 sub.textContent = `${formatDate(data.comment.date)} · ${data.comment.status}`;
22148 head.appendChild(sub);
22149 header.appendChild(head);
22150 wrap.appendChild(header);
22151 const body = document.createElement("div");
22152 body.className = "desktop-mode-my-wordpress__article-content";
22153 body.innerHTML = data.comment.rendered;
22154 wrap.appendChild(body);
22155 if (data.post) {
22156 const card = document.createElement("div");
22157 card.className = "desktop-mode-my-wordpress__comment-post";
22158 const link = document.createElement("a");
22159 link.className = "desktop-mode-my-wordpress__comment-post-title";
22160 link.href = data.post.link;
22161 link.target = "_blank";
22162 link.rel = "noopener noreferrer";
22163 link.textContent = data.post.title;
22164 card.appendChild(link);
22165 wrap.appendChild(card);
22166 }
22167 return wrap;
22168 }
22169 async function renderAttachmentPreview(ref, file) {
22170 const id = parseInt(ref, 10);
22171 if (!id) {
22172 return renderGenericPreview(file);
22173 }
22174 let data = null;
22175 try {
22176 data = await getJson(
22177 restUrl(
22178 `wp/v2/media/${id}?_fields=id,title,source_url,mime_type,alt_text,media_details`
22179 )
22180 );
22181 } catch {
22182 return renderGenericPreview(file);
22183 }
22184 const wrap = articleShell();
22185 const h = document.createElement("h2");
22186 h.className = "desktop-mode-my-wordpress__article-title";
22187 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
22188 wrap.appendChild(h);
22189 const meta = document.createElement("p");
22190 meta.className = "desktop-mode-my-wordpress__article-meta";
22191 meta.textContent = data.mime_type;
22192 wrap.appendChild(meta);
22193 if (data.mime_type.startsWith("image/")) {
22194 const img = document.createElement("img");
22195 img.className = "desktop-mode-my-wordpress__article-hero";
22196 const sizes = data.media_details?.sizes;
22197 img.src = sizes?.large?.source_url ?? sizes?.medium?.source_url ?? data.source_url;
22198 img.alt = data.alt_text ?? "";
22199 wrap.appendChild(img);
22200 } else {
22201 const p = document.createElement("p");
22202 const a = document.createElement("a");
22203 a.href = data.source_url;
22204 a.textContent = data.source_url;
22205 a.target = "_blank";
22206 a.rel = "noopener noreferrer";
22207 p.appendChild(a);
22208 wrap.appendChild(p);
22209 }
22210 return wrap;
22211 }
22212 function renderFolderPreview(file) {
22213 const wrap = articleShell();
22214 const h = document.createElement("h2");
22215 h.className = "desktop-mode-my-wordpress__article-title";
22216 h.textContent = file.title || __("(folder)", "desktop-mode");
22217 wrap.appendChild(h);
22218 const meta = document.createElement("p");
22219 meta.className = "desktop-mode-my-wordpress__article-meta";
22220 meta.textContent = __("Double-click to open.", "desktop-mode");
22221 wrap.appendChild(meta);
22222 return wrap;
22223 }
22224 function renderShortcutPreview(file) {
22225 const wrap = articleShell();
22226 const h = document.createElement("h2");
22227 h.className = "desktop-mode-my-wordpress__article-title";
22228 h.textContent = file.title || __("Shortcut", "desktop-mode");
22229 wrap.appendChild(h);
22230 const meta = document.createElement("p");
22231 meta.className = "desktop-mode-my-wordpress__article-meta";
22232 meta.textContent = __("Plugin shortcut. Double-click to open.", "desktop-mode");
22233 wrap.appendChild(meta);
22234 return wrap;
22235 }
22236 function renderBookmarkPreview(file) {
22237 const wrap = articleShell();
22238 const h = document.createElement("h2");
22239 h.className = "desktop-mode-my-wordpress__article-title";
22240 h.textContent = file.title || __("Bookmark", "desktop-mode");
22241 wrap.appendChild(h);
22242 const url = typeof file.url === "string" ? file.url : "";
22243 if (url) {
22244 const a = document.createElement("a");
22245 a.href = url;
22246 a.textContent = url;
22247 a.target = "_blank";
22248 a.rel = "noopener noreferrer";
22249 wrap.appendChild(a);
22250 }
22251 return wrap;
22252 }
22253 function renderGenericPreview(file) {
22254 const wrap = articleShell();
22255 const h = document.createElement("h2");
22256 h.className = "desktop-mode-my-wordpress__article-title";
22257 h.textContent = file.title || file.type;
22258 wrap.appendChild(h);
22259 const meta = document.createElement("p");
22260 meta.className = "desktop-mode-my-wordpress__article-meta";
22261 meta.textContent = sprintf(
22262 // translators: %s is a file-type slug.
22263 __("Type: %s", "desktop-mode"),
22264 file.type
22265 );
22266 wrap.appendChild(meta);
22267 if (!file.exists) {
22268 const warn2 = document.createElement("p");
22269 warn2.className = "desktop-mode-my-wordpress__article-meta";
22270 warn2.textContent = __(
22271 "The underlying entity is no longer available.",
22272 "desktop-mode"
22273 );
22274 wrap.appendChild(warn2);
22275 }
22276 return wrap;
22277 }
22278 function articleShell(extraClass = "") {
22279 const article = document.createElement("article");
22280 article.className = "desktop-mode-my-wordpress__article" + (extraClass ? " " + extraClass : "");
22281 return article;
22282 }
22283 function statCard(value, label) {
22284 const card = document.createElement("div");
22285 card.className = "desktop-mode-my-wordpress__user-stat";
22286 const v = document.createElement("span");
22287 v.className = "desktop-mode-my-wordpress__user-stat-value";
22288 v.textContent = value;
22289 card.appendChild(v);
22290 const l = document.createElement("span");
22291 l.className = "desktop-mode-my-wordpress__user-stat-label";
22292 l.textContent = label;
22293 card.appendChild(l);
22294 return card;
22295 }
22296 function renderLoading() {
22297 const wrap = document.createElement("div");
22298 wrap.className = "desktop-mode-my-wordpress__preview-loading";
22299 const spinner = document.createElement("wpd-spinner");
22300 wrap.appendChild(spinner);
22301 return wrap;
22302 }
22303 function renderError(err) {
22304 const wrap = document.createElement("div");
22305 wrap.className = "desktop-mode-my-wordpress__error";
22306 wrap.textContent = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
22307 return wrap;
22308 }
22309 function stripTags(html2) {
22310 const div = document.createElement("div");
22311 div.innerHTML = html2;
22312 return (div.textContent ?? "").trim();
22313 }
22314 function formatDate(iso) {
22315 if (!iso) {
22316 return "";
22317 }
22318 try {
22319 return new Date(iso).toLocaleString();
22320 } catch {
22321 return iso;
22322 }
22323 }
22324 function renderPreviewEmpty() {
22325 const wrap = document.createElement("div");
22326 wrap.className = "desktop-mode-my-wordpress__preview-empty";
22327 wrap.textContent = __(
22328 "Select an item to preview it here.",
22329 "desktop-mode"
22330 );
22331 return wrap;
22332 }
22333 const ID_PREFIX = "desktop-mode-embed-";
22334 const DEFAULT_W = 800;
22335 const DEFAULT_H = 600;
22336 const MIN_W = 360;
22337 const MIN_H = 240;
22338 const PADDING = 16;
22339 const lastPersisted = /* @__PURE__ */ new Map();
22340 function openEmbedWindow(file, ctx) {
22341 const url = file.ref();
22342 if (!url) {
22343 return;
22344 }
22345 const wm = window.wp?.desktop?.windowManager;
22346 if (!wm) {
22347 return;
22348 }
22349 const placement = ctx?.placement;
22350 const meta = placement?.meta ?? null;
22351 const windowId = placement ? `${ID_PREFIX}${placement.id}` : `${ID_PREFIX}anon-${hash(url)}`;
22352 const customName = meta?.name?.trim() ?? "";
22353 const title = customName !== "" ? customName : file.title();
22354 const cfg = {
22355 id: windowId,
22356 baseId: windowId,
22357 url,
22358 title,
22359 icon: file.icon(),
22360 minWidth: MIN_W,
22361 minHeight: MIN_H
22362 };
22363 const saved = meta?.window;
22364 const area = document.getElementById("desktop-mode-area");
22365 const aw = area?.clientWidth ?? window.innerWidth;
22366 const ah = area?.clientHeight ?? window.innerHeight;
22367 if (saved && Number.isFinite(saved.width) && Number.isFinite(saved.height)) {
22368 const { x, y, width, height } = clampGeometry(saved, aw, ah);
22369 cfg.x = x;
22370 cfg.y = y;
22371 cfg.width = width;
22372 cfg.height = height;
22373 } else {
22374 cfg.width = Math.min(DEFAULT_W, Math.max(MIN_W, aw - PADDING * 2));
22375 cfg.height = Math.min(DEFAULT_H, Math.max(MIN_H, ah - PADDING * 2));
22376 }
22377 if (placement) {
22378 if (saved) {
22379 lastPersisted.set(windowId, { ...saved });
22380 }
22381 }
22382 wm.open(cfg);
22383 }
22384 let installed = false;
22385 function installEmbedPersistence() {
22386 if (installed) {
22387 return;
22388 }
22389 installed = true;
22390 const onChange = (payload) => {
22391 const p = payload;
22392 const id = p?.windowId;
22393 if (!id || !id.startsWith(ID_PREFIX)) {
22394 return;
22395 }
22396 const placementIdStr = id.slice(ID_PREFIX.length);
22397 const placementId = parseInt(placementIdStr, 10);
22398 if (!placementId) {
22399 return;
22400 }
22401 const wm = window.wp?.desktop?.windowManager;
22402 const win = wm?.getById?.(id);
22403 const el = win?.element;
22404 if (!el) {
22405 return;
22406 }
22407 const next = {
22408 x: el.offsetLeft,
22409 y: el.offsetTop,
22410 width: el.offsetWidth,
22411 height: el.offsetHeight
22412 };
22413 const prev = lastPersisted.get(id);
22414 if (prev && prev.x === next.x && prev.y === next.y && prev.width === next.width && prev.height === next.height) {
22415 return;
22416 }
22417 lastPersisted.set(id, next);
22418 void persist(placementId, next);
22419 };
22420 addAction(HOOKS.WINDOW_DRAG_END, "desktop-mode-embed-persist", onChange);
22421 addAction(HOOKS.WINDOW_RESIZE_END, "desktop-mode-embed-persist", onChange);
22422 }
22423 async function persist(placementId, geo) {
22424 try {
22425 const list2 = await listPlacements(0);
22426 const row = list2.placements.find((p) => p.id === placementId);
22427 const prevMeta = row?.meta ?? {};
22428 const nextMeta = {
22429 ...prevMeta,
22430 window: geo
22431 };
22432 await updatePlacement(placementId, { meta: nextMeta });
22433 } catch (err) {
22434 console.warn("[desktop-mode] embed window persist failed:", err);
22435 }
22436 }
22437 function clampGeometry(g, areaW, areaH) {
22438 const width = Math.max(MIN_W, Math.min(g.width, areaW - PADDING));
22439 const height = Math.max(MIN_H, Math.min(g.height, areaH - PADDING));
22440 const x = Math.max(0, Math.min(g.x, Math.max(0, areaW - width)));
22441 const y = Math.max(0, Math.min(g.y, Math.max(0, areaH - height)));
22442 return { x, y, width, height };
22443 }
22444 function hash(s) {
22445 let h = 0;
22446 for (let i = 0; i < s.length; i++) {
22447 h = (Math.imul(h, 31) + s.charCodeAt(i)) % 2147483647;
22448 }
22449 return Math.abs(h).toString(36);
22450 }
22451 function adminBase() {
22452 const cfg = window.wp?.desktop?.config;
22453 const url = cfg?.adminUrl ?? "/wp-admin/";
22454 return url.endsWith("/") ? url : `${url}/`;
22455 }
22456 function registerBuiltInFileOpeners() {
22457 registerOpener({
22458 id: "wp-post-editor",
22459 label: "Block Editor",
22460 types: ["post"],
22461 isDefault: true,
22462 sort: 10,
22463 handler: {
22464 kind: "url",
22465 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
22466 }
22467 });
22468 registerOpener({
22469 id: "wp-media-editor",
22470 label: "Media editor",
22471 types: ["attachment"],
22472 isDefault: true,
22473 sort: 10,
22474 handler: {
22475 kind: "url",
22476 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
22477 }
22478 });
22479 registerOpener({
22480 id: "wp-user-profile",
22481 label: "User profile",
22482 types: ["user"],
22483 isDefault: true,
22484 sort: 10,
22485 handler: {
22486 kind: "url",
22487 url: (file) => `${adminBase()}user-edit.php?user_id=${encodeURIComponent(file.ref())}`
22488 }
22489 });
22490 registerOpener({
22491 id: "wp-term-editor",
22492 label: "Term editor",
22493 types: ["term"],
22494 isDefault: true,
22495 sort: 10,
22496 handler: {
22497 kind: "url",
22498 url: (file) => {
22499 const [taxonomy, termId] = file.ref().split(":");
22500 return `${adminBase()}term.php?taxonomy=${encodeURIComponent(taxonomy ?? "")}&tag_ID=${encodeURIComponent(termId ?? "")}`;
22501 }
22502 }
22503 });
22504 registerOpener({
22505 id: "wp-comment-editor",
22506 label: "Comment editor",
22507 types: ["comment"],
22508 isDefault: true,
22509 sort: 10,
22510 handler: {
22511 kind: "url",
22512 url: (file) => `${adminBase()}comment.php?action=editcomment&c=${encodeURIComponent(file.ref())}`
22513 }
22514 });
22515 registerOpener({
22516 id: "desktop-mode-folder-window",
22517 label: "Open folder",
22518 types: ["folder"],
22519 isDefault: true,
22520 sort: 10,
22521 handler: {
22522 kind: "js",
22523 open: (file) => {
22524 const folderId = parseInt(file.ref(), 10);
22525 if (!folderId) {
22526 return;
22527 }
22528 const wm = window.wp?.desktop?.windowManager;
22529 if (!wm) {
22530 return;
22531 }
22532 const id = `desktop-mode-folder-${folderId}`;
22533 const folderRow = store.getState().folders.get(folderId);
22534 const viewerId2 = Number(window.desktopModeConfig?.currentUserId ?? 0);
22535 const isRecipient = !!folderRow && folderRow.ownerId > 0 && folderRow.ownerId !== viewerId2;
22536 const baseTitle = file.title();
22537 const titleWithCue = isRecipient ? `${baseTitle} · Shared` : baseTitle;
22538 wm.open({
22539 id,
22540 baseId: id,
22541 url: `#folder-${folderId}`,
22542 title: titleWithCue,
22543 icon: file.icon(),
22544 native: true,
22545 render: (body) => {
22546 body.replaceChildren();
22547 body.classList.add("desktop-mode-folder-window");
22548 const routes = [
22549 { folderId, title: file.title() }
22550 ];
22551 let currentDispose = null;
22552 const breadcrumbsHost = document.createElement("header");
22553 body.appendChild(breadcrumbsHost);
22554 const bodyHost = document.createElement("div");
22555 bodyHost.style.cssText = "flex:1 1 auto;min-height:0;display:flex;flex-direction:column;";
22556 body.appendChild(bodyHost);
22557 const paintBreadcrumbs = () => {
22558 const segments = routes.map(
22559 (route, idx) => {
22560 const isCurrent = idx === routes.length - 1;
22561 if (isCurrent) {
22562 return { label: route.title };
22563 }
22564 return {
22565 label: route.title,
22566 onClick: () => {
22567 routes.length = idx + 1;
22568 mountCurrent();
22569 }
22570 };
22571 }
22572 );
22573 renderBreadcrumbs(breadcrumbsHost, segments, {
22574 onBack: () => {
22575 if (routes.length <= 1) {
22576 return;
22577 }
22578 routes.pop();
22579 mountCurrent();
22580 },
22581 backDisabled: routes.length <= 1
22582 });
22583 };
22584 const mountCurrent = () => {
22585 currentDispose?.();
22586 currentDispose = null;
22587 bodyHost.replaceChildren();
22588 const split = document.createElement("div");
22589 split.className = "desktop-mode-folder-window__split";
22590 bodyHost.appendChild(split);
22591 const layerHost = document.createElement("div");
22592 layerHost.className = "desktop-mode-folder-window__layer";
22593 split.appendChild(layerHost);
22594 const previewPane = document.createElement("div");
22595 previewPane.className = "desktop-mode-folder-window__preview";
22596 previewPane.appendChild(renderPreviewEmpty());
22597 split.appendChild(previewPane);
22598 const route = routes[routes.length - 1];
22599 const layer = mountFilesLayer(
22600 layerHost,
22601 route.folderId
22602 );
22603 const offSelection = layer.onSelectionChange(
22604 (placement) => {
22605 if (!placement) {
22606 previewPane.replaceChildren(
22607 renderPreviewEmpty()
22608 );
22609 return;
22610 }
22611 renderPlacementPreview(
22612 placement,
22613 previewPane
22614 );
22615 }
22616 );
22617 const dblClickHandler = (e) => {
22618 if (!(e.target instanceof Element)) {
22619 return;
22620 }
22621 const tile2 = e.target.closest(
22622 ".desktop-mode-file-tile"
22623 );
22624 if (!tile2) {
22625 return;
22626 }
22627 if (tile2.dataset.fileType !== "folder") {
22628 return;
22629 }
22630 const subId = parseInt(
22631 tile2.dataset.fileRef ?? "",
22632 10
22633 );
22634 if (!subId) {
22635 return;
22636 }
22637 e.preventDefault();
22638 e.stopPropagation();
22639 const subTitle = tile2.querySelector(
22640 ".desktop-mode-file-tile__label"
22641 )?.textContent ?? `#${subId}`;
22642 routes.push({
22643 folderId: subId,
22644 title: subTitle
22645 });
22646 mountCurrent();
22647 };
22648 layerHost.addEventListener(
22649 "dblclick",
22650 dblClickHandler,
22651 true
22652 );
22653 const menu = attachIconCanvasMenu(layerHost, {
22654 scope: `desktop-mode-folder:${route.folderId}`,
22655 onSort: (mode) => layer.sort(mode),
22656 extraItems: [
22657 {
22658 id: "new-folder",
22659 label: "New folder",
22660 icon: "dashicons-portfolio",
22661 sort: 5,
22662 onClick: () => {
22663 openCreateFolderDialog({
22664 onSubmit: async (name) => {
22665 const folder = await createFolder({
22666 name
22667 });
22668 const peers = store.getState().placementsByFolder.get(
22669 route.folderId
22670 ) ?? [];
22671 const occupied = buildOccupiedSet(peers);
22672 const cell = snapToEmptyCell(
22673 GRID_PADDING,
22674 GRID_PADDING,
22675 occupied,
22676 layerHost
22677 );
22678 const placement = await createPlacement({
22679 type: "folder",
22680 ref: String(folder.id),
22681 parentId: route.folderId,
22682 x: cell.x,
22683 y: cell.y
22684 });
22685 store.upsertFolder(folder);
22686 store.upsertPlacement(
22687 placement
22688 );
22689 }
22690 });
22691 }
22692 }
22693 ]
22694 });
22695 const status = mountFolderStatusBar(
22696 bodyHost,
22697 route.folderId
22698 );
22699 currentDispose = () => {
22700 offSelection();
22701 menu.dispose();
22702 status.dispose();
22703 layerHost.removeEventListener(
22704 "dblclick",
22705 dblClickHandler,
22706 true
22707 );
22708 layer.dispose();
22709 };
22710 paintBreadcrumbs();
22711 };
22712 mountCurrent();
22713 },
22714 width: 720,
22715 height: 480,
22716 minWidth: 360,
22717 minHeight: 240
22718 });
22719 }
22720 }
22721 });
22722 registerOpener({
22723 id: "desktop-mode-shortcut-opener",
22724 label: "Open shortcut",
22725 types: ["shortcut"],
22726 isDefault: true,
22727 sort: 10,
22728 handler: {
22729 kind: "js",
22730 open: (file) => {
22731 const extras = file.shape;
22732 const wp = window.wp?.desktop;
22733 if (!wp) {
22734 return;
22735 }
22736 if (extras.shortcutWindow && wp.openWindow) {
22737 wp.openWindow(extras.shortcutWindow);
22738 return;
22739 }
22740 if (extras.shortcutUrl && wp.windowManager) {
22741 try {
22742 const u = new URL(extras.shortcutUrl, window.location.origin);
22743 if (u.origin !== window.location.origin) {
22744 window.open(u.toString(), "_blank", "noopener,noreferrer");
22745 return;
22746 }
22747 const adminUrl = wp.config?.adminUrl;
22748 const id = adminUrl ? deriveWindowId(u.toString(), adminUrl) : `desktop-icon-${file.ref()}`;
22749 wp.windowManager.open({
22750 id,
22751 baseId: id,
22752 url: u.toString(),
22753 title: file.title(),
22754 icon: file.icon()
22755 });
22756 } catch {
22757 }
22758 }
22759 }
22760 }
22761 });
22762 registerOpener({
22763 id: "browser-navigate",
22764 label: "Open in browser",
22765 types: ["bookmark"],
22766 isDefault: true,
22767 sort: 10,
22768 handler: {
22769 kind: "js",
22770 open: (file) => {
22771 const url = file.ref();
22772 if (!url) {
22773 return;
22774 }
22775 window.open(url, "_blank", "noopener,noreferrer");
22776 }
22777 }
22778 });
22779 registerOpener({
22780 id: "desktop-mode-link-opener",
22781 label: "Open in browser",
22782 types: ["link"],
22783 isDefault: true,
22784 sort: 10,
22785 handler: {
22786 kind: "js",
22787 open: (file) => {
22788 const url = file.ref();
22789 if (!url) {
22790 return;
22791 }
22792 window.open(url, "_blank", "noopener,noreferrer");
22793 }
22794 }
22795 });
22796 registerOpener({
22797 id: "desktop-mode-embed-opener",
22798 label: "Open as window",
22799 types: ["embed"],
22800 isDefault: true,
22801 sort: 10,
22802 handler: {
22803 kind: "js",
22804 open: (file, ctx) => {
22805 openEmbedWindow(file, ctx);
22806 }
22807 }
22808 });
22809 }
22810 const TAB_ID = "desktop-mode-file-associations";
22811 function registerFileAssociationsTab() {
22812 registerSettingsTab({
22813 id: TAB_ID,
22814 label: "File Associations",
22815 order: 50,
22816 render(body) {
22817 renderTab(body);
22818 }
22819 });
22820 }
22821 function renderTab(body) {
22822 body.replaceChildren();
22823 const types = getTypes();
22824 if (types.length === 0) {
22825 const empty = document.createElement("p");
22826 empty.className = "desktop-mode-file-associations__empty";
22827 empty.textContent = "No file types are registered.";
22828 body.appendChild(empty);
22829 return;
22830 }
22831 const intro = document.createElement("p");
22832 intro.className = "desktop-mode-file-associations__intro";
22833 intro.textContent = "Pick which app opens each kind of file when you double-click it on the desktop.";
22834 body.appendChild(intro);
22835 const associations = getUserAssociations();
22836 const list2 = document.createElement("div");
22837 list2.className = "desktop-mode-file-associations__list";
22838 list2.setAttribute("role", "list");
22839 for (const type of types) {
22840 list2.appendChild(buildRow(type.type, type.label, associations));
22841 }
22842 body.appendChild(list2);
22843 }
22844 function buildRow(typeSlug, typeLabel, associations) {
22845 const row = document.createElement("div");
22846 row.className = "desktop-mode-file-associations__row";
22847 row.setAttribute("role", "listitem");
22848 row.dataset.fileType = typeSlug;
22849 const label = document.createElement("label");
22850 label.className = "desktop-mode-file-associations__label";
22851 label.textContent = typeLabel;
22852 row.appendChild(label);
22853 const candidates = getOpenersForType(typeSlug);
22854 if (candidates.length === 0) {
22855 const empty = document.createElement("span");
22856 empty.className = "desktop-mode-file-associations__none";
22857 empty.textContent = "No app available";
22858 row.appendChild(empty);
22859 return row;
22860 }
22861 const resolved = resolveOpener(typeSlug);
22862 const currentId = associations[typeSlug] ?? resolved?.id ?? "";
22863 const select = document.createElement("wpd-select");
22864 select.setAttribute("value", currentId);
22865 select.setAttribute("aria-label", `Default app for ${typeLabel}`);
22866 select.className = "desktop-mode-file-associations__select";
22867 label.htmlFor = `assoc-${typeSlug}`;
22868 select.id = `assoc-${typeSlug}`;
22869 for (const o of candidates) {
22870 const opt = document.createElement("wpd-option");
22871 opt.setAttribute("value", o.id);
22872 opt.textContent = o.isDefault ? `${o.label} (default)` : o.label;
22873 select.appendChild(opt);
22874 }
22875 select.addEventListener("wpd-pick", (e) => {
22876 const next = e.detail?.value;
22877 if (!next) {
22878 return;
22879 }
22880 const merged = { ...getUserAssociations(), [typeSlug]: next };
22881 setUserAssociations(merged);
22882 void saveAssociations(merged).catch((err) => {
22883 console.error("[desktop-mode] saveAssociations failed:", err);
22884 });
22885 });
22886 row.appendChild(select);
22887 return row;
22888 }
22889 let _store$1 = null;
22890 function sharesStore() {
22891 if (!_store$1) {
22892 _store$1 = createSharedStore("desktop-files/shares", () => ({
22893 byFolder: /* @__PURE__ */ new Map(),
22894 pending: [],
22895 sharesVersion: 0,
22896 deniedFolders: /* @__PURE__ */ new Set()
22897 }));
22898 }
22899 return _store$1;
22900 }
22901 function setSharesForFolder(folderId, shares) {
22902 const s = sharesStore();
22903 s.state.byFolder.set(folderId, shares);
22904 s.notify();
22905 }
22906 function upsertShare(share) {
22907 if (!share || typeof share.folderId !== "number") {
22908 return;
22909 }
22910 const s = sharesStore();
22911 const existing = s.state.byFolder.get(share.folderId) ?? [];
22912 const next = existing.filter((r) => r.id !== share.id);
22913 next.push(share);
22914 s.state.byFolder.set(share.folderId, next);
22915 s.notify();
22916 }
22917 function removeShare(folderId, shareId) {
22918 const s = sharesStore();
22919 const existing = s.state.byFolder.get(folderId) ?? [];
22920 s.state.byFolder.set(
22921 folderId,
22922 existing.filter((r) => r.id !== shareId)
22923 );
22924 s.notify();
22925 }
22926 function inviteEquals(a, b) {
22927 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;
22928 }
22929 function ingestPendingInvites(invites) {
22930 const s = sharesStore();
22931 const existingById = new Map(s.state.pending.map((p) => [p.id, p]));
22932 let mutated = false;
22933 for (const inv of invites) {
22934 if (s.state.deniedFolders.has(inv.folderId)) {
22935 continue;
22936 }
22937 const existing = existingById.get(inv.id);
22938 if (existing) {
22939 if (inviteEquals(existing, inv)) {
22940 continue;
22941 }
22942 s.state.pending = s.state.pending.map((p) => p.id === inv.id ? inv : p);
22943 } else {
22944 s.state.pending.push(inv);
22945 }
22946 if (inv.invitedAtMs > s.state.sharesVersion) {
22947 s.state.sharesVersion = inv.invitedAtMs;
22948 }
22949 mutated = true;
22950 }
22951 if (mutated) {
22952 s.notify();
22953 }
22954 }
22955 function dropPending(shareId, opts = {}) {
22956 const s = sharesStore();
22957 s.state.pending = s.state.pending.filter((p) => p.id !== shareId);
22958 if (opts.denied && typeof opts.folderId === "number") {
22959 s.state.deniedFolders.add(opts.folderId);
22960 }
22961 s.notify();
22962 }
22963 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}`;
22964 const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
22965 const _WpdModal = class _WpdModal extends Component {
22966 constructor() {
22967 super(...arguments);
22968 this._prevFocus = null;
22969 this._onKey = (e) => {
22970 if (e.key === "Escape" && !this.hasAttribute("mandatory")) {
22971 e.preventDefault();
22972 this._cancel();
22973 return;
22974 }
22975 if (e.key === "Tab") {
22976 const f = this._focusables();
22977 if (f.length === 0) {
22978 return;
22979 }
22980 const first = f[0];
22981 const last = f[f.length - 1];
22982 const doc = this.ownerDocument;
22983 const fallback = doc ? doc.activeElement : null;
22984 const active2 = e.composedPath()[0] || fallback;
22985 if (e.shiftKey && active2 === first) {
22986 e.preventDefault();
22987 last.focus();
22988 } else if (!e.shiftKey && active2 === last) {
22989 e.preventDefault();
22990 first.focus();
22991 }
22992 }
22993 };
22994 this._onBackdrop = (e) => {
22995 if (this.hasAttribute("mandatory")) {
22996 return;
22997 }
22998 const path = e.composedPath();
22999 const original = path.length > 0 ? path[0] : e.target;
23000 if (original === this) {
23001 this._cancel();
23002 }
23003 };
23004 }
23005 connectedCallback() {
23006 super.connectedCallback();
23007 this.setAttribute("role", "dialog");
23008 this.setAttribute("aria-modal", "true");
23009 this.addEventListener("keydown", this._onKey);
23010 this.addEventListener("click", this._onBackdrop);
23011 }
23012 disconnectedCallback() {
23013 this.removeEventListener("keydown", this._onKey);
23014 this.removeEventListener("click", this._onBackdrop);
23015 }
23016 attributeChangedCallback(name, oldValue, newValue) {
23017 super.attributeChangedCallback?.(name, oldValue, newValue);
23018 if (name === "open") {
23019 if (newValue !== null) {
23020 const doc = this.ownerDocument;
23021 this._prevFocus = doc ? doc.activeElement : null;
23022 queueMicrotask(() => this._focusFirst());
23023 } else if (this._prevFocus) {
23024 try {
23025 this._prevFocus.focus();
23026 } catch (e) {
23027 }
23028 this._prevFocus = null;
23029 }
23030 }
23031 }
23032 showModal() {
23033 this.setAttribute("open", "");
23034 }
23035 hideModal() {
23036 this.removeAttribute("open");
23037 }
23038 _focusables() {
23039 const root = this.shadowRoot;
23040 if (!root) {
23041 return [];
23042 }
23043 const slotted = Array.from(this.querySelectorAll(FOCUSABLE));
23044 const inShadow = Array.from(root.querySelectorAll(FOCUSABLE));
23045 return [...slotted, ...inShadow].filter((el) => el.offsetParent !== null || el.tagName === "BUTTON");
23046 }
23047 _focusFirst() {
23048 const f = this._focusables();
23049 if (f.length > 0) {
23050 f[0].focus();
23051 } else {
23052 const inner = this.shadowRoot?.querySelector(".dialog");
23053 inner?.focus?.();
23054 }
23055 }
23056 _cancel() {
23057 const ev = new CustomEvent("wpd-modal-cancel", {
23058 bubbles: true,
23059 cancelable: true,
23060 composed: true
23061 });
23062 const allowed = this.dispatchEvent(ev);
23063 if (allowed) {
23064 this.hideModal();
23065 }
23066 }
23067 render() {
23068 const title = this.getAttribute("title") ?? "";
23069 const mandatory = this.hasAttribute("mandatory");
23070 return html`
23071 <div class="dialog" tabindex="-1">
23072 ${title ? html`
23073 <div class="header">
23074 <h2 class="title">${title}</h2>
23075 <div class="header-actions">
23076 <slot name="header-actions"></slot>
23077 ${mandatory ? html`` : html`<button
23078 type="button"
23079 class="close"
23080 aria-label="Close"
23081 @click=${() => this._cancel()}
23082 >×</button>`}
23083 </div>
23084 </div>
23085 ` : html``}
23086 <div class="body">
23087 <slot></slot>
23088 </div>
23089 <div class="footer">
23090 <slot name="footer"></slot>
23091 </div>
23092 </div>
23093 `;
23094 }
23095 };
23096 _WpdModal.props = ["open", "title", "size", "mandatory"];
23097 _WpdModal.styles = [modalStyles];
23098 _WpdModal.help = {
23099 title: "Modal overlay",
23100 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.",
23101 status: "experimental",
23102 since: "0.18.0",
23103 props: [
23104 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
23105 { name: "title", type: "string", description: "Heading shown at the top of the dialog." },
23106 { name: "size", type: "'sm' | 'md' | 'lg'", default: "md", description: "Width preset." },
23107 {
23108 name: "mandatory",
23109 type: "boolean attribute",
23110 description: "Disables ESC, click-outside and the close button."
23111 }
23112 ],
23113 slots: [
23114 { name: "(default)", description: "Body content." },
23115 { name: "footer", description: "Footer button row, right-aligned." },
23116 { name: "header-actions", description: "Extra actions next to the close button." }
23117 ],
23118 events: [
23119 {
23120 name: "wpd-modal-cancel",
23121 description: "Fires when the user dismisses the modal (ESC, click-outside, close button). Cancelable; calling `preventDefault()` keeps the modal open."
23122 }
23123 ]
23124 };
23125 let WpdModal = _WpdModal;
23126 defineComponent("wpd-modal", WpdModal);
23127 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}`;
23128 const _WpdUserSearch = class _WpdUserSearch extends Component {
23129 constructor() {
23130 super(...arguments);
23131 this._timer = null;
23132 this._abort = null;
23133 this._results = [];
23134 this._query = "";
23135 this._open = false;
23136 this._phase = "idle";
23137 this._error = "";
23138 this._dropdownStyle = "";
23139 this._onScrollOrResize = () => void 0;
23140 this._onInput = (e) => {
23141 const value = e.target.value;
23142 this._query = value;
23143 this._scheduleSearch(value);
23144 };
23145 this._onFocus = () => {
23146 if (this._results.length === 0 && this._phase === "idle") {
23147 this._scheduleSearch(this._query);
23148 return;
23149 }
23150 this._open = true;
23151 this._positionDropdown();
23152 this.requestUpdate();
23153 };
23154 this._onBlur = () => {
23155 setTimeout(() => {
23156 this._open = false;
23157 this.requestUpdate();
23158 }, 150);
23159 };
23160 this._pick = (user) => {
23161 this.emit("wpd-user-pick", { user });
23162 this._results = [];
23163 this._open = false;
23164 this._phase = "idle";
23165 this._query = "";
23166 const input = this.shadowRoot?.querySelector(".input");
23167 if (input) {
23168 input.value = "";
23169 }
23170 this.requestUpdate();
23171 };
23172 }
23173 connectedCallback() {
23174 super.connectedCallback();
23175 this._onScrollOrResize = () => {
23176 if (this._open) {
23177 this._positionDropdown();
23178 this.requestUpdate();
23179 }
23180 };
23181 window.addEventListener("resize", this._onScrollOrResize);
23182 window.addEventListener("scroll", this._onScrollOrResize, true);
23183 }
23184 disconnectedCallback() {
23185 if (this._timer) {
23186 clearTimeout(this._timer);
23187 }
23188 if (this._abort) {
23189 this._abort.abort();
23190 }
23191 window.removeEventListener("resize", this._onScrollOrResize);
23192 window.removeEventListener("scroll", this._onScrollOrResize, true);
23193 }
23194 _endpoint() {
23195 const attr = this.getAttribute("endpoint");
23196 if (attr) {
23197 return attr;
23198 }
23199 return window.desktopModeConfig?.filesUsersSearchUrl || "";
23200 }
23201 _scheduleSearch(q) {
23202 if (this._timer) {
23203 clearTimeout(this._timer);
23204 }
23205 this._phase = "loading";
23206 this._open = true;
23207 this._positionDropdown();
23208 this.requestUpdate();
23209 this._timer = setTimeout(() => this._runSearch(q), 200);
23210 }
23211 async _runSearch(q) {
23212 const url = this._endpoint();
23213 if (!url) {
23214 this._phase = "error";
23215 this._error = "Search endpoint is not configured.";
23216 this._results = [];
23217 this._open = true;
23218 this.requestUpdate();
23219 return;
23220 }
23221 if (this._abort) {
23222 this._abort.abort();
23223 }
23224 const ctrl = new AbortController();
23225 this._abort = ctrl;
23226 const exclude = this.getAttribute("exclude") || "";
23227 const full = url + "?q=" + encodeURIComponent(q) + "&exclude=" + encodeURIComponent(exclude);
23228 try {
23229 const init2 = {
23230 signal: ctrl.signal,
23231 credentials: "same-origin"
23232 };
23233 const res = await trackedFetch$1(full, init2, {
23234 source: "desktop-mode/files-user-search",
23235 silent: true
23236 });
23237 if (!res.ok) {
23238 throw new Error(`HTTP ${res.status}`);
23239 }
23240 const json = await res.json();
23241 this._results = json && Array.isArray(json.users) ? json.users : [];
23242 this._phase = "ready";
23243 this._error = "";
23244 this._open = true;
23245 } catch (e) {
23246 if (e.name === "AbortError") {
23247 return;
23248 }
23249 this._results = [];
23250 this._phase = "error";
23251 this._error = e.message || "Search failed.";
23252 this._open = true;
23253 }
23254 this._positionDropdown();
23255 this.requestUpdate();
23256 }
23257 _positionDropdown() {
23258 const input = this.shadowRoot?.querySelector(".input");
23259 if (!input) {
23260 return;
23261 }
23262 const rect = input.getBoundingClientRect();
23263 const top = rect.bottom + 4;
23264 const left = rect.left;
23265 const width = rect.width;
23266 const viewportH = window.innerHeight;
23267 const spaceBelow = viewportH - rect.bottom;
23268 const spaceAbove = rect.top;
23269 const maxHeight = Math.max(120, Math.min(280, Math.max(spaceBelow, spaceAbove) - 16));
23270 if (spaceBelow < 200 && spaceAbove > spaceBelow) {
23271 this._dropdownStyle = [
23272 "position:fixed",
23273 `left:${left}px`,
23274 `top:${rect.top - 4 - maxHeight}px`,
23275 `width:${width}px`,
23276 `max-height:${maxHeight}px`
23277 ].join(";");
23278 } else {
23279 this._dropdownStyle = [
23280 "position:fixed",
23281 `left:${left}px`,
23282 `top:${top}px`,
23283 `width:${width}px`,
23284 `max-height:${maxHeight}px`
23285 ].join(";");
23286 }
23287 }
23288 _dropdownContent() {
23289 if (this._phase === "loading") {
23290 return html`<div class="empty">Searching…</div>`;
23291 }
23292 if (this._phase === "error") {
23293 return html`<div class="empty error">${this._error}</div>`;
23294 }
23295 if (this._results.length === 0) {
23296 const message = this._query ? "No matches." : "No users available.";
23297 return html`<div class="empty">${message}</div>`;
23298 }
23299 return this._results.map(
23300 (u) => html`
23301 <button
23302 type="button"
23303 class="item"
23304 role="option"
23305 @mousedown=${(e) => e.preventDefault()}
23306 @click=${() => this._pick(u)}
23307 >
23308 <img class="avatar" src=${u.avatarUrl} alt="" />
23309 <div>
23310 <div class="name">${u.name}</div>
23311 <div class="slug">${u.slug}</div>
23312 </div>
23313 </button>
23314 `
23315 );
23316 }
23317 render() {
23318 const placeholder = this.getAttribute("placeholder") || "Search users…";
23319 return html`
23320 <input
23321 class="input"
23322 type="search"
23323 placeholder=${placeholder}
23324 autocomplete="off"
23325 @input=${this._onInput}
23326 @focus=${this._onFocus}
23327 @blur=${this._onBlur}
23328 .value=${this._query}
23329 />
23330 ${this._open ? html`
23331 <div class="dropdown" role="listbox" style=${this._dropdownStyle}>
23332 ${this._dropdownContent()}
23333 </div>
23334 ` : html``}
23335 `;
23336 }
23337 };
23338 _WpdUserSearch.props = ["placeholder", "exclude", "endpoint"];
23339 _WpdUserSearch.styles = [userSearchStyles];
23340 _WpdUserSearch.help = {
23341 title: "User autocomplete",
23342 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.",
23343 status: "experimental",
23344 since: "0.18.0",
23345 props: [
23346 { name: "placeholder", type: "string", description: "Input placeholder text." },
23347 {
23348 name: "exclude",
23349 type: "csv user ids",
23350 description: "Already-picked user ids to suppress in results."
23351 },
23352 {
23353 name: "endpoint",
23354 type: "URL",
23355 description: "Override the search URL (defaults to desktopModeConfig.filesUsersSearchUrl)."
23356 }
23357 ],
23358 events: [
23359 { name: "wpd-user-pick", description: "Emitted on pick. Detail: `{ user: SearchUser }`." }
23360 ]
23361 };
23362 let WpdUserSearch = _WpdUserSearch;
23363 defineComponent("wpd-user-search", WpdUserSearch);
23364 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}`;
23365 const _WpdRolePicker = class _WpdRolePicker extends Component {
23366 constructor() {
23367 super(...arguments);
23368 this._onToggle = (slug) => {
23369 const selected = !this._selectedSet().has(slug);
23370 this.emit("wpd-role-toggle", { slug, selected });
23371 };
23372 }
23373 _selectedSet() {
23374 const raw = this.getAttribute("selected") || "";
23375 return new Set(
23376 raw.split(",").map((s) => s.trim()).filter((s) => s !== "")
23377 );
23378 }
23379 _roles() {
23380 const attr = this.getAttribute("roles");
23381 if (attr) {
23382 try {
23383 const parsed = JSON.parse(attr);
23384 if (Array.isArray(parsed)) {
23385 return parsed;
23386 }
23387 } catch (e) {
23388 }
23389 }
23390 return window.desktopModeConfig?.shareEligibleRoles || [];
23391 }
23392 render() {
23393 const roles = this._roles();
23394 if (roles.length === 0) {
23395 return html`<span class="empty">No eligible roles.</span>`;
23396 }
23397 const set = this._selectedSet();
23398 return html`
23399 ${roles.map((r) => {
23400 const isSelected = set.has(r.slug);
23401 return html`
23402 <button
23403 type="button"
23404 class="chip"
23405 aria-pressed=${isSelected ? "true" : "false"}
23406 @click=${() => this._onToggle(r.slug)}
23407 >${r.name}</button>
23408 `;
23409 })}
23410 `;
23411 }
23412 };
23413 _WpdRolePicker.props = ["selected", "roles"];
23414 _WpdRolePicker.styles = [rolePickerStyles];
23415 _WpdRolePicker.help = {
23416 title: "Role picker",
23417 summary: "Chip multi-select for WordPress roles. Reads eligible roles from desktopModeConfig.shareEligibleRoles; emits wpd-role-toggle { slug, selected } on every change.",
23418 status: "experimental",
23419 since: "0.18.0",
23420 props: [
23421 {
23422 name: "selected",
23423 type: "csv role slugs",
23424 description: "Comma-separated role slugs that are currently selected."
23425 },
23426 {
23427 name: "roles",
23428 type: "JSON",
23429 description: "Override the source of eligible roles (defaults to the global config)."
23430 }
23431 ],
23432 events: [
23433 {
23434 name: "wpd-role-toggle",
23435 description: "Emitted on every click. Detail: `{ slug, selected }`."
23436 }
23437 ]
23438 };
23439 let WpdRolePicker = _WpdRolePicker;
23440 defineComponent("wpd-role-picker", WpdRolePicker);
23441 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}`;
23442 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}`;
23443 const _WpdSegment = class _WpdSegment extends Component {
23444 render() {
23445 this.setAttribute("role", "radio");
23446 return html`
23447 <button type="button" @click=${() => this._onPick()}>
23448 <slot></slot>
23449 </button>
23450 `;
23451 }
23452 _onPick() {
23453 this.emit("wpd-segment-pick", {
23454 value: this.value
23455 });
23456 }
23457 };
23458 _WpdSegment.props = ["value"];
23459 _WpdSegment.styles = [segmentStyles];
23460 _WpdSegment.help = {
23461 title: "Segment",
23462 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
23463 status: "stable",
23464 since: "0.9.0",
23465 props: [
23466 {
23467 name: "value",
23468 type: "string",
23469 description: "Identifier this segment contributes to the parent group selection."
23470 }
23471 ],
23472 slots: [
23473 { name: "(default)", description: "Visible segment label." }
23474 ],
23475 events: [
23476 {
23477 name: "wpd-segment-pick",
23478 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
23479 detail: "{ value: string }"
23480 }
23481 ]
23482 };
23483 let WpdSegment = _WpdSegment;
23484 defineComponent("wpd-segment", WpdSegment);
23485 const _WpdSegmented = class _WpdSegmented extends Component {
23486 connectedCallback() {
23487 super.connectedCallback();
23488 this.addEventListener("wpd-segment-pick", (e) => {
23489 const detail = e.detail;
23490 e.stopPropagation();
23491 this.value = detail.value;
23492 this.emit("wpd-pick", { value: detail.value });
23493 });
23494 }
23495 /**
23496 * Declarative item-list setter. Replaces the existing
23497 * `<wpd-segment>` children with a fresh set built from a
23498 * `{ value, label }` array; preserves the current selection
23499 * when the value still matches an entry, otherwise falls back
23500 * to the first item.
23501 *
23502 * Collapses the pre-0.11 imperative dance (clear children,
23503 * `createElement`, set `textContent`, `appendChild`, then
23504 * `setAttribute('value', …)` on the group — order matters) to
23505 * a single assignment:
23506 *
23507 * ```js
23508 * segmented.items = [
23509 * { value: 'm', label: 'm' },
23510 * { value: 'km', label: 'km' },
23511 * ];
23512 * ```
23513 *
23514 * @since 0.11.0
23515 */
23516 set items(list2) {
23517 const existing = this.querySelectorAll(":scope > wpd-segment");
23518 for (const el of Array.from(existing)) {
23519 el.remove();
23520 }
23521 for (const item of list2) {
23522 const seg = document.createElement("wpd-segment");
23523 seg.setAttribute("value", item.value);
23524 seg.textContent = item.label;
23525 this.appendChild(seg);
23526 }
23527 const current = this.value;
23528 const stillValid = current !== null && list2.some((i) => i.value === current);
23529 if (!stillValid && list2.length > 0) {
23530 this.value = list2[0].value;
23531 } else {
23532 this.requestUpdate();
23533 }
23534 }
23535 render() {
23536 const label = this.label || "";
23537 if (label) {
23538 this.setAttribute("aria-label", label);
23539 }
23540 this.setAttribute("role", "radiogroup");
23541 const current = this.value;
23542 queueMicrotask(() => {
23543 const segs = this.querySelectorAll("wpd-segment");
23544 for (const seg of Array.from(segs)) {
23545 const v = seg.getAttribute("value");
23546 seg.setAttribute(
23547 "aria-checked",
23548 v === current ? "true" : "false"
23549 );
23550 }
23551 });
23552 return html`<slot></slot>`;
23553 }
23554 };
23555 _WpdSegmented.props = ["value", "label"];
23556 _WpdSegmented.styles = [segmentedStyles];
23557 _WpdSegmented.help = {
23558 title: "Segmented",
23559 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
23560 status: "stable",
23561 since: "0.9.0",
23562 props: [
23563 {
23564 name: "value",
23565 type: "string",
23566 description: "Currently selected segment value. Mirrored onto child aria-checked."
23567 },
23568 {
23569 name: "label",
23570 type: "string",
23571 description: "aria-label for the radiogroup."
23572 }
23573 ],
23574 slots: [
23575 { name: "(default)", description: '<wpd-segment value="…"> children.' }
23576 ],
23577 events: [
23578 {
23579 name: "wpd-pick",
23580 description: "Fires when the selected segment changes.",
23581 detail: "{ value: string }"
23582 }
23583 ],
23584 cssProps: [
23585 { name: "--desktop-mode-window-bg", description: "Pill background." },
23586 { name: "--desktop-mode-text", description: "Active label colour." },
23587 { name: "--desktop-mode-muted", description: "Inactive label colour." }
23588 ],
23589 example: html`
23590 <wpd-segmented value="md" label="Dock size">
23591 <wpd-segment value="sm">Small</wpd-segment>
23592 <wpd-segment value="md">Medium</wpd-segment>
23593 <wpd-segment value="lg">Large</wpd-segment>
23594 </wpd-segmented>
23595 `
23596 };
23597 let WpdSegmented = _WpdSegmented;
23598 defineComponent("wpd-segmented", WpdSegmented);
23599 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}`;
23600 const _WpdButton = class _WpdButton extends Component {
23601 render() {
23602 const disabled = this.disabled !== null;
23603 const type = this.type || "button";
23604 return html`
23605 <button part="button" type=${type} ?disabled=${disabled}>
23606 <slot></slot>
23607 </button>
23608 `;
23609 }
23610 };
23611 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
23612 _WpdButton.styles = [styles$3];
23613 _WpdButton.help = {
23614 title: "Button",
23615 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
23616 status: "stable",
23617 since: "0.9.0",
23618 props: [
23619 {
23620 name: "variant",
23621 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
23622 default: "ghost",
23623 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
23624 },
23625 {
23626 name: "disabled",
23627 type: "boolean attribute",
23628 description: "Disable pointer + keyboard interaction and dim the chrome."
23629 },
23630 {
23631 name: "type",
23632 type: "'button' | 'submit' | 'reset'",
23633 default: "button",
23634 description: "Forwarded to the underlying native <button>."
23635 },
23636 {
23637 name: "busy",
23638 type: "boolean attribute",
23639 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
23640 },
23641 {
23642 name: "fill-cell",
23643 type: "boolean attribute",
23644 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
23645 }
23646 ],
23647 slots: [{ name: "(default)", description: "Button label." }],
23648 parts: [{ name: "button", description: "Underlying <button> element." }],
23649 cssProps: [
23650 { name: "--wpd-button-bg", description: "Background color." },
23651 { name: "--wpd-button-fg", description: "Text color." },
23652 { name: "--wpd-button-border", description: "Border shorthand." },
23653 { name: "--wpd-button-border-radius", default: "6px" },
23654 { name: "--wpd-button-padding", default: "6px 12px" },
23655 {
23656 name: "--wpd-button-min-height",
23657 description: "Minimum height when fill-cell is set."
23658 }
23659 ],
23660 example: html`
23661 <wpd-cluster gap="8">
23662 <wpd-button variant="primary">Primary</wpd-button>
23663 <wpd-button variant="secondary">Secondary</wpd-button>
23664 <wpd-button variant="ghost">Ghost</wpd-button>
23665 <wpd-button variant="danger">Danger</wpd-button>
23666 <wpd-button variant="link">Link</wpd-button>
23667 </wpd-cluster>
23668 `
23669 };
23670 let WpdButton = _WpdButton;
23671 defineComponent("wpd-button", WpdButton);
23672 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}`;
23673 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}}`;
23674 const _WpdToastContainer = class _WpdToastContainer extends Component {
23675 connectedCallback() {
23676 super.connectedCallback();
23677 this.setAttribute("aria-live", "polite");
23678 }
23679 render() {
23680 return html`<slot></slot>`;
23681 }
23682 };
23683 _WpdToastContainer.styles = [containerStyles];
23684 _WpdToastContainer.help = {
23685 title: "Toast container",
23686 summary: "Singleton stack beneath <body> that hosts transient <wpd-toast> notifications in the top-right. Created lazily by showToast(); authors rarely place one themselves.",
23687 status: "stable",
23688 since: "0.9.0",
23689 slots: [
23690 { name: "(default)", description: "<wpd-toast> children, stacked vertically." }
23691 ],
23692 cssProps: [
23693 { name: "--desktop-mode-z-fullscreen", description: "z-index base — toasts sit above fullscreen windows." }
23694 ],
23695 example: html`
23696 <wpd-toast-container>
23697 <wpd-toast state="in">Settings saved.</wpd-toast>
23698 <wpd-toast state="in" action="Undo">Theme changed.</wpd-toast>
23699 </wpd-toast-container>
23700 `
23701 };
23702 let WpdToastContainer = _WpdToastContainer;
23703 defineComponent("wpd-toast-container", WpdToastContainer);
23704 const _WpdToast = class _WpdToast extends Component {
23705 connectedCallback() {
23706 super.connectedCallback();
23707 if (!this.hasAttribute("role")) {
23708 this.setAttribute("role", "status");
23709 }
23710 }
23711 render() {
23712 const action = this.action || "";
23713 return html`
23714 <span class="wpd-toast__label"><slot></slot></span>
23715 <button
23716 type="button"
23717 ?hidden=${!action}
23718 @click=${(e) => this._onAction(e)}
23719 >
23720 ${action}
23721 </button>
23722 `;
23723 }
23724 _onAction(e) {
23725 e.preventDefault();
23726 e.stopPropagation();
23727 this.emit("wpd-toast-action", {});
23728 }
23729 };
23730 _WpdToast.props = ["action", "state"];
23731 _WpdToast.styles = [toastStyles];
23732 _WpdToast.help = {
23733 title: "Toast",
23734 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.',
23735 status: "stable",
23736 since: "0.9.0",
23737 props: [
23738 {
23739 name: "action",
23740 type: "string",
23741 description: "Optional action button label. When set, a button renders on the right and emits wpd-toast-action on click."
23742 },
23743 {
23744 name: "state",
23745 type: "'in' | 'out'",
23746 description: 'Drives the CSS fade transition. Set to "in" when rendered, flip to "out" before removal.'
23747 }
23748 ],
23749 slots: [
23750 { name: "(default)", description: "Message text." }
23751 ],
23752 events: [
23753 {
23754 name: "wpd-toast-action",
23755 description: "Fires when the action button is clicked.",
23756 detail: "{}"
23757 }
23758 ],
23759 example: html`
23760 <wpd-toast state="in" action="Undo">Post moved to trash.</wpd-toast>
23761 `
23762 };
23763 let WpdToast = _WpdToast;
23764 defineComponent("wpd-toast", WpdToast);
23765 function buildCapSegmented(initial, onChange) {
23766 const segmented = document.createElement("wpd-segmented");
23767 segmented.setAttribute("value", initial);
23768 segmented.setAttribute("label", "Capability");
23769 segmented.style.setProperty("--wpd-segmented-bg", "rgba(255,255,255,0.06)");
23770 segmented.style.setProperty(
23771 "--desktop-mode-window-bg",
23772 "var(--wp-admin-theme-color, #2271b1)"
23773 );
23774 segmented.style.setProperty("--desktop-mode-text", "#fff");
23775 segmented.style.setProperty("--desktop-mode-muted", "rgba(255,255,255,0.65)");
23776 const segRead = document.createElement("wpd-segment");
23777 segRead.setAttribute("value", "read");
23778 segRead.textContent = "Read";
23779 segmented.appendChild(segRead);
23780 const segWrite = document.createElement("wpd-segment");
23781 segWrite.setAttribute("value", "write");
23782 segWrite.textContent = "Read + Write";
23783 segmented.appendChild(segWrite);
23784 segmented.addEventListener("wpd-pick", (e) => {
23785 const detail = e.detail;
23786 onChange(detail.value);
23787 });
23788 return segmented;
23789 }
23790 function buildIconButton(label, onClick, opts = {}) {
23791 const btn = document.createElement("wpd-button");
23792 btn.setAttribute("variant", "ghost");
23793 btn.setAttribute("aria-label", opts.danger ? "Remove" : "Dismiss");
23794 btn.textContent = label;
23795 const fg = opts.danger ? "#ff8080" : "rgba(255,255,255,0.75)";
23796 const border = opts.danger ? "1px solid rgba(255,128,128,0.45)" : "1px solid rgba(255,255,255,0.18)";
23797 btn.style.setProperty("--wpd-button-fg", fg);
23798 btn.style.setProperty("--wpd-button-border", border);
23799 btn.style.setProperty("--wpd-button-padding", "6px 12px");
23800 btn.style.setProperty("--wpd-button-border-radius", "7px");
23801 btn.style.setProperty("--wpd-button-min-height", "34px");
23802 btn.style.minWidth = "34px";
23803 btn.style.fontSize = "18px";
23804 btn.style.lineHeight = "1";
23805 btn.addEventListener("click", onClick);
23806 return btn;
23807 }
23808 async function openShareSettingsModal(opts) {
23809 const modal = document.createElement("wpd-modal");
23810 modal.setAttribute("open", "");
23811 modal.setAttribute("size", "lg");
23812 modal.setAttribute("title", `Share "${opts.folderName}"`);
23813 document.body.appendChild(modal);
23814 let shares = [];
23815 let pendingPicks = [];
23816 const renderBody = () => {
23817 modal.innerHTML = "";
23818 const owner = document.createElement("div");
23819 owner.style.cssText = "opacity:0.7;margin-bottom:14px;font-size:12px;";
23820 owner.textContent = opts.ownerName ? `Owner: ${opts.ownerName} — cannot be changed` : "Owner cannot be changed";
23821 modal.appendChild(owner);
23822 const addPeople = document.createElement("div");
23823 addPeople.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
23824 const addPeopleLabel = document.createElement("div");
23825 addPeopleLabel.textContent = "Add people";
23826 addPeopleLabel.style.cssText = "font-weight:600;";
23827 addPeople.appendChild(addPeopleLabel);
23828 const userSearch = document.createElement("wpd-user-search");
23829 const excludedUserIds = shares.filter((s) => s.principalType === "user").map((s) => s.principalRef).concat(pendingPicks.filter((p) => p.kind === "user").map((p) => p.ref));
23830 userSearch.setAttribute("exclude", excludedUserIds.join(","));
23831 userSearch.setAttribute("placeholder", "Search users…");
23832 userSearch.addEventListener("wpd-user-pick", (e) => {
23833 const detail = e.detail;
23834 pendingPicks.push({
23835 kind: "user",
23836 ref: String(detail.user.id),
23837 label: detail.user.name,
23838 cap: "read"
23839 });
23840 renderBody();
23841 });
23842 addPeople.appendChild(userSearch);
23843 modal.appendChild(addPeople);
23844 const addRoles = document.createElement("div");
23845 addRoles.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
23846 const addRolesLabel = document.createElement("div");
23847 addRolesLabel.textContent = "Add roles";
23848 addRolesLabel.style.cssText = "font-weight:600;";
23849 addRoles.appendChild(addRolesLabel);
23850 const rolePicker = document.createElement("wpd-role-picker");
23851 const grantedRoles = shares.filter((s) => s.principalType === "role").map((s) => s.principalRef);
23852 const pickedRoles = pendingPicks.filter((p) => p.kind === "role").map((p) => p.ref);
23853 rolePicker.setAttribute("selected", [...grantedRoles, ...pickedRoles].join(","));
23854 rolePicker.addEventListener("wpd-role-toggle", (e) => {
23855 const detail = e.detail;
23856 const existing = shares.find(
23857 (s) => s.principalType === "role" && s.principalRef === detail.slug
23858 );
23859 if (existing) {
23860 if (!detail.selected) {
23861 void revoke(existing);
23862 }
23863 return;
23864 }
23865 if (detail.selected) {
23866 const eligible = (window.desktopModeConfig?.shareEligibleRoles ?? []).find(
23867 (r) => r.slug === detail.slug
23868 );
23869 pendingPicks.push({
23870 kind: "role",
23871 ref: detail.slug,
23872 label: eligible ? eligible.name : detail.slug,
23873 cap: "read"
23874 });
23875 } else {
23876 pendingPicks = pendingPicks.filter(
23877 (p) => !(p.kind === "role" && p.ref === detail.slug)
23878 );
23879 }
23880 renderBody();
23881 });
23882 addRoles.appendChild(rolePicker);
23883 modal.appendChild(addRoles);
23884 if (pendingPicks.length > 0) {
23885 const pendingBlock = document.createElement("div");
23886 pendingBlock.style.cssText = "border:1px dashed rgba(255,255,255,0.18);border-radius:8px;padding:10px;margin-bottom:14px;";
23887 const pendingTitle = document.createElement("div");
23888 pendingTitle.textContent = "New invites (not sent yet)";
23889 pendingTitle.style.cssText = "font-weight:600;margin-bottom:6px;font-size:12px;";
23890 pendingBlock.appendChild(pendingTitle);
23891 for (const pick of pendingPicks) {
23892 const row = document.createElement("div");
23893 row.style.cssText = "display:flex;align-items:center;gap:8px;padding:4px 0;font-size:13px;";
23894 const tag = document.createElement("span");
23895 tag.textContent = pick.kind === "role" ? `Role: ${pick.label}` : pick.label;
23896 tag.style.flex = "1";
23897 row.appendChild(tag);
23898 const capSeg = buildCapSegmented(pick.cap, (next) => {
23899 pick.cap = next;
23900 });
23901 row.appendChild(capSeg);
23902 const removeBtn = buildIconButton("×", () => {
23903 pendingPicks = pendingPicks.filter(
23904 (p) => !(p.kind === pick.kind && p.ref === pick.ref)
23905 );
23906 renderBody();
23907 });
23908 row.appendChild(removeBtn);
23909 pendingBlock.appendChild(row);
23910 }
23911 const sendBtn = document.createElement("wpd-button");
23912 sendBtn.setAttribute("variant", "primary");
23913 sendBtn.textContent = `Send ${pendingPicks.length} invite${pendingPicks.length === 1 ? "" : "s"}`;
23914 sendBtn.style.marginTop = "8px";
23915 sendBtn.addEventListener("click", async () => {
23916 if (pendingPicks.length === 0) {
23917 return;
23918 }
23919 sendBtn.setAttribute("busy", "");
23920 sendBtn.setAttribute("disabled", "");
23921 const snapshot = pendingPicks.slice();
23922 let succeeded = 0;
23923 let firstError = null;
23924 for (const pick of snapshot) {
23925 try {
23926 await inviteShare(opts.folderId, {
23927 principalType: pick.kind,
23928 principalRef: pick.ref,
23929 capability: pick.cap
23930 });
23931 succeeded++;
23932 } catch (err) {
23933 firstError = err;
23934 break;
23935 }
23936 }
23937 if (succeeded > 0) {
23938 pendingPicks = pendingPicks.slice(succeeded);
23939 }
23940 try {
23941 await refresh();
23942 } catch (_e) {
23943 }
23944 if (firstError) {
23945 showToast({
23946 message: `Could not send invites: ${firstError.message}`
23947 });
23948 } else {
23949 showToast({
23950 message: 1 === succeeded ? "Invite sent." : `${succeeded} invites sent.`
23951 });
23952 }
23953 sendBtn.removeAttribute("busy");
23954 sendBtn.removeAttribute("disabled");
23955 renderBody();
23956 });
23957 pendingBlock.appendChild(sendBtn);
23958 modal.appendChild(pendingBlock);
23959 }
23960 const listTitle = document.createElement("div");
23961 listTitle.textContent = "Who has access";
23962 listTitle.style.cssText = "font-weight:600;margin:8px 0 6px;";
23963 modal.appendChild(listTitle);
23964 if (shares.length === 0) {
23965 const empty = document.createElement("div");
23966 empty.textContent = "Only you can see this folder.";
23967 empty.style.cssText = "opacity:0.6;font-size:12px;";
23968 modal.appendChild(empty);
23969 } else {
23970 for (const s of shares) {
23971 const row = document.createElement("div");
23972 row.style.cssText = "display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.04);";
23973 const label = document.createElement("div");
23974 label.style.flex = "1";
23975 label.textContent = s.principalType === "role" ? `Role: ${s.displayName}` : s.displayName;
23976 if (s.state === "pending") {
23977 const tag = document.createElement("span");
23978 tag.textContent = " · pending";
23979 tag.style.cssText = "opacity:0.6;font-size:12px;";
23980 label.appendChild(tag);
23981 } else if (s.state === "denied") {
23982 const tag = document.createElement("span");
23983 tag.textContent = " · denied";
23984 tag.style.cssText = "color:#d63638;font-size:12px;";
23985 label.appendChild(tag);
23986 }
23987 row.appendChild(label);
23988 const cap = s.capability === "write" ? "write" : "read";
23989 const capSeg = buildCapSegmented(cap, (next) => {
23990 void changeCap(s, next);
23991 });
23992 row.appendChild(capSeg);
23993 const removeBtn = buildIconButton(
23994 "×",
23995 () => {
23996 void revoke(s);
23997 },
23998 { danger: true }
23999 );
24000 row.appendChild(removeBtn);
24001 modal.appendChild(row);
24002 }
24003 }
24004 const footer = document.createElement("div");
24005 footer.setAttribute("slot", "footer");
24006 footer.style.display = "flex";
24007 footer.style.justifyContent = "flex-end";
24008 footer.style.gap = "10px";
24009 footer.style.flexWrap = "wrap";
24010 const doneBtn = document.createElement("wpd-button");
24011 doneBtn.setAttribute("variant", "secondary");
24012 doneBtn.textContent = "Done";
24013 doneBtn.addEventListener("click", () => modal.remove());
24014 footer.appendChild(doneBtn);
24015 modal.appendChild(footer);
24016 };
24017 const refresh = async () => {
24018 try {
24019 const res = await listShares(opts.folderId);
24020 shares = res.shares;
24021 setSharesForFolder(opts.folderId, shares);
24022 } catch (err) {
24023 showToast({
24024 message: `Could not load shares: ${err.message}`
24025 });
24026 }
24027 renderBody();
24028 };
24029 const revoke = async (s) => {
24030 try {
24031 await revokeShare(opts.folderId, s.id);
24032 removeShare(opts.folderId, s.id);
24033 await refresh();
24034 showToast({ message: "Access revoked." });
24035 } catch (err) {
24036 showToast({
24037 message: `Could not revoke: ${err.message}`
24038 });
24039 }
24040 };
24041 const changeCap = async (s, cap) => {
24042 try {
24043 const next = await updateShareCapability(opts.folderId, s.id, cap);
24044 upsertShare(next);
24045 await refresh();
24046 } catch (err) {
24047 showToast({
24048 message: `Could not update capability: ${err.message}`
24049 });
24050 }
24051 };
24052 modal.addEventListener("wpd-modal-cancel", () => modal.remove());
24053 renderBody();
24054 await refresh();
24055 }
24056 function openPendingInviteModal(invite) {
24057 return new Promise((resolve2) => {
24058 const modal = document.createElement("wpd-modal");
24059 modal.setAttribute("open", "");
24060 modal.setAttribute("title", invite.folderName ? `${invite.ownerName ?? "Someone"} shared "${invite.folderName}" with you` : "Folder shared with you");
24061 const body = document.createElement("div");
24062 const capLabel = invite.capability === "write" ? "Read + Write" : "Read";
24063 body.innerHTML = `
24064 <p style="margin: 0 0 12px;">Accept the invite to add this folder to your desktop.</p>
24065 <p style="margin: 0; opacity: 0.75;">Access level: <strong>${capLabel}</strong></p>
24066 `;
24067 modal.appendChild(body);
24068 const footer = document.createElement("div");
24069 footer.setAttribute("slot", "footer");
24070 footer.style.display = "flex";
24071 footer.style.justifyContent = "flex-end";
24072 footer.style.gap = "10px";
24073 footer.style.flexWrap = "wrap";
24074 const laterBtn = document.createElement("wpd-button");
24075 laterBtn.setAttribute("variant", "secondary");
24076 laterBtn.textContent = "Decide later";
24077 laterBtn.addEventListener("click", () => {
24078 modal.remove();
24079 resolve2("dismissed");
24080 });
24081 const denyBtn = document.createElement("wpd-button");
24082 denyBtn.setAttribute("variant", "danger");
24083 denyBtn.textContent = "Deny";
24084 denyBtn.addEventListener("click", async () => {
24085 denyBtn.setAttribute("busy", "");
24086 denyBtn.setAttribute("disabled", "");
24087 try {
24088 await denyShare(invite.folderId, invite.id);
24089 sharesStore().state.deniedFolders.add(invite.folderId);
24090 sharesStore().notify();
24091 modal.remove();
24092 resolve2("denied");
24093 } catch (err) {
24094 showToast({
24095 message: `Could not deny: ${err.message}`
24096 });
24097 denyBtn.removeAttribute("busy");
24098 denyBtn.removeAttribute("disabled");
24099 }
24100 });
24101 const acceptBtn = document.createElement("wpd-button");
24102 acceptBtn.setAttribute("variant", "primary");
24103 acceptBtn.textContent = "Accept";
24104 acceptBtn.addEventListener("click", async () => {
24105 acceptBtn.setAttribute("busy", "");
24106 acceptBtn.setAttribute("disabled", "");
24107 try {
24108 await acceptShare(invite.folderId, invite.id);
24109 try {
24110 const res = await listPlacements(0);
24111 setFolderPlacements(0, res.placements);
24112 } catch (_e) {
24113 }
24114 modal.remove();
24115 resolve2("accepted");
24116 } catch (err) {
24117 showToast({
24118 message: `Could not accept: ${err.message}`
24119 });
24120 acceptBtn.removeAttribute("busy");
24121 acceptBtn.removeAttribute("disabled");
24122 }
24123 });
24124 footer.appendChild(laterBtn);
24125 footer.appendChild(denyBtn);
24126 footer.appendChild(acceptBtn);
24127 modal.appendChild(footer);
24128 modal.addEventListener("wpd-modal-cancel", () => {
24129 modal.remove();
24130 resolve2("dismissed");
24131 });
24132 document.body.appendChild(modal);
24133 });
24134 }
24135 function viewerId() {
24136 return Number(window.desktopModeConfig?.currentUserId ?? 0);
24137 }
24138 function sharingEnabled$1() {
24139 const settings = window.wp?.desktop?.getOsSettings?.();
24140 if (!settings) {
24141 return true;
24142 }
24143 return settings.foldersSharingEnabled !== false;
24144 }
24145 function folderOwnerId(folderId) {
24146 const folder = getFilesState().folders.get(folderId);
24147 return folder ? Number(folder.ownerId) : 0;
24148 }
24149 function folderIdFromBaseId(baseId) {
24150 if (typeof baseId !== "string") {
24151 return null;
24152 }
24153 const m = /^desktop-mode-folder-(\d+)$/.exec(baseId);
24154 return m ? Number(m[1]) : null;
24155 }
24156 function placementFolderId(placement) {
24157 if (placement.file.type !== "folder") {
24158 return null;
24159 }
24160 const ref = Number(placement.file.ref);
24161 if (!Number.isFinite(ref) || ref <= 0) {
24162 return null;
24163 }
24164 return ref;
24165 }
24166 function placementOwnerId(placement) {
24167 return Number(placement.file.ownerId ?? 0);
24168 }
24169 function installShareMenuItems() {
24170 addFilter(
24171 "desktop-mode.files.tile-menu",
24172 "desktop-mode/folder-share",
24173 (items, placement) => {
24174 if (!sharingEnabled$1()) {
24175 return items;
24176 }
24177 const folderId = placementFolderId(placement);
24178 if (folderId === null) {
24179 return items;
24180 }
24181 const ownerId = folderOwnerId(folderId) || placementOwnerId(placement);
24182 const viewer = viewerId();
24183 if (ownerId === viewer) {
24184 const shared = !!placement.file.shareSummary?.shared;
24185 const label = shared ? "Manage sharing…" : "Share folder…";
24186 items.push({
24187 id: "desktop-mode/folder-share",
24188 label,
24189 icon: "dashicons-share",
24190 sort: 30,
24191 onClick: () => {
24192 void openShareSettingsModal({
24193 folderId,
24194 folderName: placement.file.title || `Folder ${folderId}`
24195 });
24196 }
24197 });
24198 } else if (ownerId > 0) {
24199 items.push({
24200 id: "desktop-mode/folder-leave",
24201 label: "Leave shared folder",
24202 icon: "dashicons-exit",
24203 sort: 80,
24204 danger: true,
24205 onClick: async () => {
24206 const ok = await wpdConfirm$1({
24207 title: "Leave this folder?",
24208 message: "The folder will be removed from your desktop. The original and its contents are not deleted; the owner keeps them.",
24209 confirmLabel: "Leave",
24210 danger: true
24211 });
24212 if (!ok) {
24213 return;
24214 }
24215 try {
24216 await leaveShare(folderId);
24217 removePlacement(placement.id);
24218 try {
24219 const res = await listPlacements(0);
24220 setFolderPlacements(0, res.placements);
24221 } catch (_e) {
24222 }
24223 const winId = `desktop-mode-folder-${folderId}`;
24224 const mgr = window.desktopMode?.windowManager;
24225 mgr?.close?.(winId);
24226 showToast({ message: "You left the shared folder." });
24227 } catch (err) {
24228 showToast({
24229 message: `Could not leave: ${err.message}`
24230 });
24231 }
24232 }
24233 });
24234 }
24235 return items;
24236 }
24237 );
24238 registerTitleBarButton({
24239 id: "desktop-mode/folder-share",
24240 label: "Share folder",
24241 icon: "dashicons-share",
24242 placement: "right",
24243 order: 50,
24244 match: (w) => {
24245 if (!sharingEnabled$1()) {
24246 return false;
24247 }
24248 const base = w.config.baseId ?? w.id;
24249 const folderId = folderIdFromBaseId(base);
24250 if (folderId === null) {
24251 return false;
24252 }
24253 return folderOwnerId(folderId) === viewerId();
24254 },
24255 onClick: (w) => {
24256 const base = w.config.baseId ?? w.id;
24257 const folderId = folderIdFromBaseId(base);
24258 if (folderId === null) {
24259 return;
24260 }
24261 void openShareSettingsModal({
24262 folderId,
24263 folderName: w.config.title || `Folder ${folderId}`
24264 });
24265 }
24266 });
24267 addAction(
24268 "desktop-mode.files.tile-rendered",
24269 "desktop-mode/folder-share",
24270 (payload) => {
24271 const { tile: tile2, placement } = payload;
24272 if (placement.file.type !== "folder") {
24273 return;
24274 }
24275 const summary = placement.file.shareSummary;
24276 if (!summary?.shared) {
24277 return;
24278 }
24279 if (tile2.querySelector(".desktop-mode-file-tile__share-badge")) {
24280 return;
24281 }
24282 const badge = document.createElement("span");
24283 badge.className = "desktop-mode-file-tile__share-badge dashicons dashicons-share";
24284 badge.setAttribute("aria-label", "Shared folder");
24285 badge.title = "Shared folder";
24286 badge.style.cssText = [
24287 "position:absolute",
24288 "top:6px",
24289 "inset-inline-end:6px",
24290 "background:rgba(0,0,0,0.55)",
24291 "color:#fff",
24292 "border-radius:50%",
24293 "width:18px",
24294 "height:18px",
24295 "font-size:12px",
24296 "line-height:18px",
24297 "text-align:center",
24298 "pointer-events:none"
24299 ].join(";");
24300 tile2.appendChild(badge);
24301 }
24302 );
24303 }
24304 const prompted = /* @__PURE__ */ new Set();
24305 function sharingEnabled() {
24306 const settings = window.wp?.desktop?.getOsSettings?.();
24307 if (!settings) {
24308 return true;
24309 }
24310 return settings.foldersSharingEnabled !== false;
24311 }
24312 function installShareInviteBanner() {
24313 const store2 = sharesStore();
24314 const handle = (state2) => {
24315 if (!sharingEnabled()) {
24316 return;
24317 }
24318 for (const invite of state2.pending) {
24319 if (prompted.has(invite.id)) {
24320 continue;
24321 }
24322 prompted.add(invite.id);
24323 void openPendingInviteModal({
24324 id: invite.id,
24325 folderId: invite.folderId,
24326 folderName: invite.folderName,
24327 ownerName: invite.ownerName,
24328 capability: invite.capability
24329 }).then((decision) => {
24330 if (decision === "accepted") {
24331 dropPending(invite.id);
24332 } else if (decision === "denied") {
24333 dropPending(invite.id, { denied: true, folderId: invite.folderId });
24334 }
24335 });
24336 }
24337 };
24338 store2.subscribe(handle);
24339 handle(store2.state);
24340 }
24341 registerBuiltInFileTypes();
24342 registerBuiltInFileOpeners();
24343 installEmbedPersistence();
24344 registerFileAssociationsTab();
24345 installShareMenuItems();
24346 const seededPending = window.desktopModeConfig?.serverPendingShares;
24347 if (Array.isArray(seededPending) && seededPending.length > 0) {
24348 ingestPendingInvites(seededPending);
24349 }
24350 installShareInviteBanner();
24351 const filesApi = {
24352 DesktopFile,
24353 registerType,
24354 unregisterType,
24355 getType,
24356 getTypes,
24357 resolve,
24358 subscribe,
24359 registerOpener,
24360 unregisterOpener,
24361 getOpener,
24362 getOpeners,
24363 getOpenersForType,
24364 resolveOpener,
24365 subscribeOpeners,
24366 getUserAssociations,
24367 open: openFile,
24368 rest: filesRest,
24369 store: {
24370 get: getFilesStore,
24371 getState: getFilesState,
24372 subscribe: subscribeFilesStore,
24373 setFolderPlacements,
24374 upsertPlacement,
24375 removePlacement,
24376 setFolders,
24377 upsertFolder,
24378 removeFolder
24379 }
24380 };
24381 const SYNTH_META_KEY = "__synthFromDockItem";
24382 function hashToNegativeId(s) {
24383 let h = 0;
24384 for (let i = 0; i < s.length; i++) {
24385 h = (h * 31 + s.charCodeAt(i)) % 2147483647;
24386 }
24387 return -(h + 1);
24388 }
24389 function buildSyntheticPlacement(item, persistedPositions) {
24390 const saved = persistedPositions[item.id];
24391 return {
24392 id: hashToNegativeId(item.id),
24393 parentId: 0,
24394 x: saved ? saved.x : 0,
24395 y: saved ? saved.y : 0,
24396 sortOrder: 9999,
24397 updatedAtMs: Date.now(),
24398 meta: { [SYNTH_META_KEY]: item.id },
24399 file: {
24400 type: "shortcut",
24401 ref: `dock-promoted:${item.id}`,
24402 title: item.title,
24403 icon: item.icon,
24404 previewUrl: "",
24405 exists: true,
24406 // The shortcut opener (built-in-openers.ts) reads these
24407 // off the file shape — `shortcutUrl` is what a dock-item
24408 // promotion naturally has.
24409 shortcutUrl: item.url
24410 }
24411 };
24412 }
24413 function readDockItems() {
24414 const api = window.wp?.desktop;
24415 if (api?.getMenuItems) {
24416 const items = api.getMenuItems();
24417 return items.map((i) => ({
24418 id: i.id,
24419 title: i.title,
24420 icon: i.icon,
24421 url: i.url,
24422 badge: i.badge ?? 0,
24423 submenu: i.submenu ?? []
24424 }));
24425 }
24426 const cfg = window.desktopModeConfig;
24427 return cfg?.dockItems ?? [];
24428 }
24429 function readServerIcons() {
24430 const cfg = window.desktopModeConfig;
24431 return cfg?.desktopIcons ?? [];
24432 }
24433 let reentrant = false;
24434 const removedServerPlacementsByRef = /* @__PURE__ */ new Map();
24435 function syncShortcutsWithVisibility(visibility, positions = {}) {
24436 if (reentrant) {
24437 return;
24438 }
24439 reentrant = true;
24440 try {
24441 const dockItems = readDockItems();
24442 const serverIcons = readServerIcons();
24443 const state2 = filesApi.store.getState();
24444 const root = state2.placementsByFolder.get(0) ?? [];
24445 const currentSynth = /* @__PURE__ */ new Map();
24446 for (const p of root) {
24447 const sourceId = (p.meta ?? null) && typeof p.meta === "object" ? p.meta[SYNTH_META_KEY] : null;
24448 if (typeof sourceId === "string") {
24449 currentSynth.set(sourceId, p);
24450 }
24451 }
24452 const realByRef = /* @__PURE__ */ new Map();
24453 const registeredIconIds = new Set(
24454 serverIcons.map((i) => i.id)
24455 );
24456 for (const p of root) {
24457 const ref = p?.file?.ref;
24458 if (typeof ref === "string" && registeredIconIds.has(ref)) {
24459 realByRef.set(ref, p);
24460 }
24461 }
24462 const desiredSynth = /* @__PURE__ */ new Set();
24463 for (const item of dockItems) {
24464 const placement = visibility[item.id];
24465 if (placement === "desktop" || placement === "both") {
24466 desiredSynth.add(item.id);
24467 if (!currentSynth.has(item.id)) {
24468 filesApi.store.upsertPlacement(
24469 buildSyntheticPlacement(item, positions)
24470 );
24471 }
24472 }
24473 }
24474 for (const [sourceId, p] of currentSynth) {
24475 if (!desiredSynth.has(sourceId)) {
24476 filesApi.store.removePlacement(p.id);
24477 }
24478 }
24479 for (const icon of serverIcons) {
24480 const placement = visibility[icon.id];
24481 const inStore = realByRef.get(icon.id);
24482 if (placement === "dock" || placement === "hidden") {
24483 if (inStore) {
24484 removedServerPlacementsByRef.set(icon.id, inStore);
24485 filesApi.store.removePlacement(inStore.id);
24486 }
24487 continue;
24488 }
24489 if (!inStore) {
24490 const cached = removedServerPlacementsByRef.get(icon.id);
24491 if (cached) {
24492 filesApi.store.upsertPlacement(cached);
24493 removedServerPlacementsByRef.delete(icon.id);
24494 }
24495 }
24496 }
24497 } finally {
24498 reentrant = false;
24499 }
24500 }
24501 function installShortcutsSync(getVisibility, getPositions = () => ({})) {
24502 queueMicrotask(
24503 () => syncShortcutsWithVisibility(getVisibility(), getPositions())
24504 );
24505 const off = filesApi.store.subscribe(() => {
24506 syncShortcutsWithVisibility(getVisibility(), getPositions());
24507 });
24508 return off;
24509 }
24510 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%}`;
24511 const DEFAULT_EVENT = "desktop-mode-os-settings-save-lifecycle";
24512 const DEFAULT_AUTO_CLEAR_SAVED_MS = 2200;
24513 const DEFAULT_AUTO_CLEAR_FAILED_MS = 6e3;
24514 const _WpdSaveStatus = class _WpdSaveStatus extends Component {
24515 constructor() {
24516 super(...arguments);
24517 this._autoTimer = null;
24518 this._docListener = null;
24519 }
24520 connectedCallback() {
24521 super.connectedCallback();
24522 if (this.auto !== null) {
24523 this._installAutoListener();
24524 }
24525 }
24526 disconnectedCallback() {
24527 this._removeAutoListener();
24528 if (this._autoTimer !== null) {
24529 window.clearTimeout(this._autoTimer);
24530 this._autoTimer = null;
24531 }
24532 }
24533 attributeChangedCallback(name, oldValue, newValue) {
24534 super.attributeChangedCallback(name, oldValue, newValue);
24535 if (name === "auto" || name === "event") {
24536 this._removeAutoListener();
24537 if (this.auto !== null) {
24538 this._installAutoListener();
24539 }
24540 }
24541 if (name === "phase") {
24542 this._scheduleAutoClear();
24543 const detail = {
24544 phase: this.phase ?? "idle",
24545 error: this.error ?? void 0
24546 };
24547 this.emit("wpd-save-status-change", detail);
24548 }
24549 }
24550 render() {
24551 const phase = this.phase ?? "idle";
24552 const mode = this.mode ?? "dot";
24553 const error = this.error ?? "";
24554 const title = error || this._labelForPhase(phase);
24555 if (title) {
24556 this.setAttribute("title", title);
24557 } else {
24558 this.removeAttribute("title");
24559 }
24560 this.setAttribute("aria-live", phase === "failed" ? "assertive" : "polite");
24561 this.setAttribute("role", phase === "failed" ? "alert" : "status");
24562 return html`
24563 <span class="wpd-save-status">
24564 <span class="wpd-save-status__indicator" aria-hidden="true">
24565 <span class="wpd-save-status__glyph">${this._renderGlyph(phase)}</span>
24566 </span>
24567 ${mode === "pill" ? html`<span class="wpd-save-status__label"
24568 >${this._labelForPhase(phase)}</span
24569 >` : html``}
24570 </span>
24571 `;
24572 }
24573 _renderGlyph(phase) {
24574 if (phase === "saved") {
24575 return _iconCheck();
24576 }
24577 if (phase === "failed") {
24578 return _iconBang();
24579 }
24580 return "";
24581 }
24582 _labelForPhase(phase) {
24583 switch (phase) {
24584 case "pending":
24585 case "saving":
24586 return this["saving-label"] ?? "Saving…";
24587 case "saved":
24588 return this["saved-label"] ?? "Saved";
24589 case "failed": {
24590 const err = this.error ?? "";
24591 return err || "Couldn’t save";
24592 }
24593 default:
24594 return this["idle-label"] ?? "";
24595 }
24596 }
24597 _installAutoListener() {
24598 const eventName = this.event || DEFAULT_EVENT;
24599 this._docListener = (e) => {
24600 const detail = e.detail;
24601 if (!detail || typeof detail.phase !== "string") {
24602 return;
24603 }
24604 this.phase = detail.phase;
24605 if (detail.error) {
24606 this.error = detail.error;
24607 } else if (detail.phase !== "failed" && this.error) {
24608 this.removeAttribute("error");
24609 }
24610 };
24611 document.addEventListener(eventName, this._docListener);
24612 }
24613 _removeAutoListener() {
24614 if (!this._docListener) {
24615 return;
24616 }
24617 const eventName = this.event || DEFAULT_EVENT;
24618 document.removeEventListener(eventName, this._docListener);
24619 this._docListener = null;
24620 }
24621 _scheduleAutoClear() {
24622 if (this._autoTimer !== null) {
24623 window.clearTimeout(this._autoTimer);
24624 this._autoTimer = null;
24625 }
24626 const phase = this.phase ?? "idle";
24627 const ms = this._autoClearMsFor(phase);
24628 if (ms <= 0) {
24629 return;
24630 }
24631 this._autoTimer = window.setTimeout(() => {
24632 this._autoTimer = null;
24633 this.phase = "idle";
24634 }, ms);
24635 }
24636 _autoClearMsFor(phase) {
24637 if (phase === "saved") {
24638 const raw = this["auto-clear-saved-ms"];
24639 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_SAVED_MS;
24640 }
24641 if (phase === "failed") {
24642 const raw = this["auto-clear-failed-ms"];
24643 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_FAILED_MS;
24644 }
24645 return 0;
24646 }
24647 };
24648 _WpdSaveStatus.props = [
24649 "phase",
24650 "mode",
24651 "animation",
24652 "auto",
24653 "event",
24654 "error",
24655 "saving-label",
24656 "saved-label",
24657 "idle-label",
24658 "auto-clear-saved-ms",
24659 "auto-clear-failed-ms"
24660 ];
24661 _WpdSaveStatus.styles = [styles$2];
24662 _WpdSaveStatus.help = {
24663 title: "Save status",
24664 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.',
24665 status: "experimental",
24666 since: "0.8.0",
24667 props: [
24668 {
24669 name: "phase",
24670 type: "'idle' | 'pending' | 'saving' | 'saved' | 'failed'",
24671 default: "idle",
24672 description: "Current lifecycle phase. Set manually for one-off integrations, or rely on `auto` to populate it from a CustomEvent."
24673 },
24674 {
24675 name: "mode",
24676 type: "'dot' | 'icon' | 'pill'",
24677 default: "dot",
24678 description: "Layout. `dot` is the smallest (10×10 colored dot); `icon` adds a glyph inside on saved/failed; `pill` adds an inline label."
24679 },
24680 {
24681 name: "animation",
24682 type: "'pulse' | 'modem'",
24683 default: "pulse",
24684 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."
24685 },
24686 {
24687 name: "auto",
24688 type: "boolean attribute",
24689 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="…"`.'
24690 },
24691 {
24692 name: "event",
24693 type: "string",
24694 default: "desktop-mode-os-settings-save-lifecycle",
24695 description: "CustomEvent name to listen on when `auto` is set."
24696 },
24697 {
24698 name: "error",
24699 type: "string",
24700 description: "Error message shown in `pill` mode and exposed as the host title attribute (so dot/icon modes still surface the message via tooltip)."
24701 },
24702 {
24703 name: "saving-label",
24704 type: "string",
24705 default: "Saving…",
24706 description: "Pill-mode label shown during `pending` / `saving`."
24707 },
24708 {
24709 name: "saved-label",
24710 type: "string",
24711 default: "Saved",
24712 description: "Pill-mode label shown during `saved`."
24713 },
24714 {
24715 name: "idle-label",
24716 type: "string",
24717 description: 'Optional pill-mode label shown during `idle` (e.g. "All changes saved"). When unset, the pill collapses to invisible while idle.'
24718 },
24719 {
24720 name: "auto-clear-saved-ms",
24721 type: "integer",
24722 default: "2200",
24723 description: "How long the `saved` phase stays visible before auto-fading back to `idle`."
24724 },
24725 {
24726 name: "auto-clear-failed-ms",
24727 type: "integer",
24728 default: "6000",
24729 description: "How long the `failed` phase stays visible before auto-fading back to `idle`."
24730 }
24731 ],
24732 events: [
24733 {
24734 name: "wpd-save-status-change",
24735 description: "Fires when the phase changes (manually or via auto-listen).",
24736 detail: "{ phase, error }"
24737 }
24738 ],
24739 cssProps: [
24740 {
24741 name: "--wpd-save-status-bg",
24742 description: "Indicator background color (saving/pending phase)."
24743 },
24744 {
24745 name: "--wpd-save-status-saved-bg",
24746 description: "Indicator background on saved."
24747 },
24748 {
24749 name: "--wpd-save-status-failed-bg",
24750 description: "Indicator background on failed."
24751 },
24752 {
24753 name: "--wpd-save-status-pill-bg",
24754 description: "Pill background (mode=pill)."
24755 },
24756 {
24757 name: "--wpd-save-status-pill-fg",
24758 description: "Pill foreground (mode=pill)."
24759 }
24760 ],
24761 example: html`
24762 <wpd-cluster gap="12">
24763 <wpd-save-status phase="pending"></wpd-save-status>
24764 <wpd-save-status phase="saving"></wpd-save-status>
24765 <wpd-save-status phase="saved"></wpd-save-status>
24766 <wpd-save-status phase="failed"></wpd-save-status>
24767 <wpd-save-status mode="pill" phase="saving"></wpd-save-status>
24768 <wpd-save-status mode="pill" phase="saved"></wpd-save-status>
24769 <wpd-save-status mode="pill" phase="failed" error="Network error."></wpd-save-status>
24770 </wpd-cluster>
24771 `
24772 };
24773 let WpdSaveStatus = _WpdSaveStatus;
24774 defineComponent("wpd-save-status", WpdSaveStatus);
24775 function _iconCheck() {
24776 return html`
24777 <svg
24778 viewBox="0 0 12 12"
24779 aria-hidden="true"
24780 focusable="false"
24781 fill="none"
24782 stroke="currentColor"
24783 stroke-width="2"
24784 stroke-linecap="round"
24785 stroke-linejoin="round"
24786 >
24787 <path d="M2.5 6 L5 8.5 L9.5 4" />
24788 </svg>
24789 `;
24790 }
24791 function _iconBang() {
24792 return html`
24793 <svg
24794 viewBox="0 0 12 12"
24795 aria-hidden="true"
24796 focusable="false"
24797 fill="currentColor"
24798 >
24799 <path
24800 d="M5 2 H7 V7 H5 z M5 8.5 H7 V10.5 H5 z"
24801 />
24802 </svg>
24803 `;
24804 }
24805 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}`;
24806 const _WpdTextarea = class _WpdTextarea extends Component {
24807 constructor() {
24808 super(...arguments);
24809 this._textareaEl = null;
24810 }
24811 connectedCallback() {
24812 super.connectedCallback();
24813 ensureAutoId(this);
24814 }
24815 render() {
24816 const label = this._attr("label") || "";
24817 const value = this._attr("value") ?? "";
24818 const placeholder = this._attr("placeholder") || "";
24819 const disabled = this._boolAttr("disabled");
24820 const readonly = this._boolAttr("readonly");
24821 const ariaLabel = this._attr("aria-label") || label;
24822 const name = this._attr("name") || "";
24823 const rows = Number(this._attr("rows")) || 3;
24824 const maxLength = this._attr("maxlength");
24825 const minLength = this._attr("minlength");
24826 const invalid = this._boolAttr("invalid");
24827 const hostId = this.id || "wpd-unnamed";
24828 const fieldId = `${hostId}__field`;
24829 return html`
24830 ${label ? html`<label class="wpd-textarea__label" for=${fieldId}>${label}</label>` : html``}
24831 <textarea
24832 id=${fieldId}
24833 part="textarea"
24834 .value=${value}
24835 placeholder=${placeholder}
24836 ?disabled=${disabled}
24837 ?readonly=${readonly}
24838 rows=${rows}
24839 maxlength=${maxLength ?? ""}
24840 minlength=${minLength ?? ""}
24841 name=${name}
24842 aria-invalid=${invalid ? "true" : "false"}
24843 aria-label=${ariaLabel || ""}
24844 @input=${(e) => this._onInput(e)}
24845 @change=${(e) => this._onChange(e)}
24846 @keydown=${(e) => this._onKeyDown(e)}
24847 ></textarea>
24848 `;
24849 }
24850 _attr(name) {
24851 return this.getAttribute(name);
24852 }
24853 _boolAttr(name) {
24854 return this.getAttribute(name) !== null;
24855 }
24856 _onInput(e) {
24857 const ta = e.target;
24858 this._textareaEl = ta;
24859 this.setAttribute("value", ta.value);
24860 this.emit("wpd-input-change", { value: ta.value });
24861 if (this._boolAttr("auto-grow")) {
24862 this._autosize(ta);
24863 }
24864 }
24865 _onChange(e) {
24866 const ta = e.target;
24867 this.emit("wpd-input-commit", { value: ta.value });
24868 }
24869 _onKeyDown(e) {
24870 if (!this._boolAttr("submit-on-enter")) {
24871 return;
24872 }
24873 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
24874 e.preventDefault();
24875 const ta = e.target;
24876 this.emit("wpd-submit", { value: ta.value });
24877 }
24878 }
24879 /**
24880 * Grow the textarea height to fit content, capped at `max-rows`.
24881 * Resets to scroll-height each input then clamps; cheap because
24882 * the browser caches layout.
24883 */
24884 _autosize(ta) {
24885 const maxRows = Number(this._attr("max-rows")) || 8;
24886 const cs = window.getComputedStyle(ta);
24887 const fontSize = parseFloat(cs.fontSize) || 13;
24888 const lineHeightRaw = cs.lineHeight;
24889 const lineHeight = lineHeightRaw === "normal" ? fontSize * 1.45 : parseFloat(lineHeightRaw) || fontSize * 1.45;
24890 const paddingTop = parseFloat(cs.paddingTop) || 0;
24891 const paddingBottom = parseFloat(cs.paddingBottom) || 0;
24892 const max = lineHeight * maxRows + paddingTop + paddingBottom;
24893 ta.style.height = "auto";
24894 const next = Math.min(ta.scrollHeight, max);
24895 ta.style.height = `${next}px`;
24896 }
24897 /** Public helper for callers that programmatically set `.value` and want autosize to re-run. */
24898 refreshAutosize() {
24899 if (this._textareaEl && this._boolAttr("auto-grow")) {
24900 this._autosize(this._textareaEl);
24901 }
24902 }
24903 /** Imperatively focus the underlying textarea. */
24904 focusInput() {
24905 const root = this.shadowRoot ?? this;
24906 const ta = root.querySelector("textarea");
24907 ta?.focus();
24908 }
24909 /** Imperatively clear the value. */
24910 clear() {
24911 this.setAttribute("value", "");
24912 const root = this.shadowRoot ?? this;
24913 const ta = root.querySelector("textarea");
24914 if (ta) {
24915 ta.value = "";
24916 if (this._boolAttr("auto-grow")) {
24917 this._autosize(ta);
24918 }
24919 }
24920 }
24921 };
24922 _WpdTextarea.props = [
24923 "label",
24924 "value",
24925 "placeholder",
24926 "disabled",
24927 "readonly",
24928 "ariaLabel",
24929 "name",
24930 "rows",
24931 "maxlength",
24932 "minlength",
24933 "invalid",
24934 "autoGrow",
24935 "maxRows",
24936 "submitOnEnter"
24937 ];
24938 _WpdTextarea.styles = [textareaStyles];
24939 _WpdTextarea.help = {
24940 title: "Textarea",
24941 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).",
24942 status: "stable",
24943 since: "0.22.0",
24944 props: [
24945 { name: "label", type: "string", description: "Visible label above the textarea." },
24946 { name: "value", type: "string", description: "Current value; reflected two-way." },
24947 { name: "placeholder", type: "string", description: "Native placeholder." },
24948 { name: "disabled", type: "boolean attribute" },
24949 { name: "readonly", type: "boolean attribute" },
24950 { name: "aria-label", type: "string", description: "Accessible label when no visible label is rendered." },
24951 { name: "name", type: "string", description: "Forwarded to native textarea for form submission." },
24952 { name: "rows", type: "integer (string)", default: "3", description: "Initial visible row count." },
24953 { name: "maxlength", type: "integer (string)" },
24954 { name: "minlength", type: "integer (string)" },
24955 { name: "invalid", type: "boolean attribute", description: "Sets aria-invalid + error styling." },
24956 { name: "auto-grow", type: "boolean attribute", description: "Grows up to max-rows as the user types." },
24957 { name: "max-rows", type: "integer (string)", default: "8" },
24958 {
24959 name: "submit-on-enter",
24960 type: "boolean attribute",
24961 description: "Enter fires wpd-submit; Shift+Enter inserts a newline."
24962 }
24963 ],
24964 events: [
24965 { name: "wpd-input-change", description: "Fires on every keystroke.", detail: "{ value: string }" },
24966 { name: "wpd-input-commit", description: "Fires on blur / native change.", detail: "{ value: string }" },
24967 {
24968 name: "wpd-submit",
24969 description: "Fires on Enter (without Shift) when submit-on-enter is set.",
24970 detail: "{ value: string }"
24971 }
24972 ],
24973 example: html`
24974 <wpd-textarea label="Message" rows="3" auto-grow max-rows="8" submit-on-enter></wpd-textarea>
24975 `
24976 };
24977 let WpdTextarea = _WpdTextarea;
24978 defineComponent("wpd-textarea", WpdTextarea);
24979 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}`;
24980 const ICONS = {
24981 minimize: '<path d="M3 6h6" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>',
24982 maximize: '<rect x="3" y="3" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.25" fill="none"/>',
24983 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"/>',
24984 "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"/>',
24985 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"/>',
24986 reload: (
24987 // Filled icon scaled from a 512×512 source into the 12×12 viewBox
24988 // shared with the other title-bar glyphs. The wrapping `<g>` does
24989 // the math; the inner path is dropped in unmodified so its
24990 // authoring tool can be re-edited and copy-pasted again.
24991 // `scale(0.021)` ≈ 90% of full fit, with `translate(0.6)` to
24992 // keep the result centered inside the 12×12 viewBox so the
24993 // glyph reads slightly smaller than min/max/close — closer to
24994 // the visual weight of the other title-bar buttons.
24995 '<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>'
24996 ),
24997 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"/>',
24998 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"/>'
24999 };
25000 const _WpdWindowButton = class _WpdWindowButton extends Component {
25001 constructor() {
25002 super(...arguments);
25003 this._activateWired = false;
25004 }
25005 render() {
25006 const iconKey = this.icon || "";
25007 const svgInner = ICONS[iconKey] || "";
25008 return html`
25009 <button type="button">
25010 <svg
25011 width="14"
25012 height="14"
25013 viewBox="0 0 12 12"
25014 aria-hidden="true"
25015 focusable="false"
25016 ></svg>
25017 <slot></slot>
25018 </button>
25019 <span data-svg-buffer style="display:none">${svgInner}</span>
25020 `;
25021 }
25022 /**
25023 * After each render, copy the raw SVG markup into the actual
25024 * `<svg>` element. The templater only writes text into slots,
25025 * so we stash the intended markup in a hidden buffer and
25026 * `innerHTML = ` the svg once here — a one-shot post-render
25027 * hook that keeps the declarative template honest.
25028 *
25029 * Also wires up the `wpd-button-activate` CustomEvent that
25030 * fires exactly once per gesture — the canonical contract
25031 * for plugin-registered title-bar buttons. Plugin authors who
25032 * use `addEventListener( 'click', cb )` directly still get
25033 * what they expect (the title bar's drag-handler now excludes
25034 * chrome buttons by class so static clicks land normally),
25035 * but `wpd-button-activate` is the documented surface that
25036 * documents the once-per-gesture contract explicitly. See
25037 * the class-level docblock for rationale.
25038 */
25039 connectedCallback() {
25040 super.connectedCallback();
25041 queueMicrotask(() => this._paintSvg());
25042 queueMicrotask(() => this._wireActivateEvent());
25043 }
25044 attributeChangedCallback(name, oldValue, newValue) {
25045 super.attributeChangedCallback(name, oldValue, newValue);
25046 queueMicrotask(() => this._paintSvg());
25047 }
25048 _paintSvg() {
25049 const root = this.shadowRoot;
25050 if (!root) {
25051 return;
25052 }
25053 const svg = root.querySelector("svg");
25054 const buffer = root.querySelector("[data-svg-buffer]");
25055 if (svg && buffer) {
25056 const markup = buffer.textContent || "";
25057 if (svg.innerHTML !== markup) {
25058 svg.innerHTML = markup;
25059 }
25060 }
25061 }
25062 _wireActivateEvent() {
25063 if (this._activateWired) {
25064 return;
25065 }
25066 const root = this.shadowRoot;
25067 if (!root) {
25068 return;
25069 }
25070 const button = root.querySelector("button");
25071 if (!button) {
25072 return;
25073 }
25074 this._activateWired = true;
25075 button.addEventListener("click", () => {
25076 this.dispatchEvent(
25077 new CustomEvent("wpd-button-activate", {
25078 bubbles: true,
25079 composed: true,
25080 cancelable: true
25081 })
25082 );
25083 });
25084 }
25085 };
25086 _WpdWindowButton.props = ["icon", "active", "danger"];
25087 _WpdWindowButton.styles = [styles$1];
25088 _WpdWindowButton.help = {
25089 title: "Window button",
25090 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.",
25091 status: "stable",
25092 since: "0.9.0",
25093 props: [
25094 {
25095 name: "icon",
25096 type: "'minimize' | 'maximize' | 'fullscreen' | 'fullscreen-exit' | 'detach' | 'reload' | 'close' | 'menu'",
25097 description: "Which built-in inline SVG to paint. Omit to supply your own via the slot."
25098 },
25099 {
25100 name: "active",
25101 type: "boolean attribute",
25102 description: "Applies the pressed-down look (used e.g. while a menu it triggers is open)."
25103 },
25104 {
25105 name: "danger",
25106 type: "boolean attribute",
25107 description: "Swaps the hover wash to red — used by the close button."
25108 }
25109 ],
25110 slots: [
25111 { name: "(default)", description: "Optional custom icon markup (inline SVG) when `icon` is omitted." }
25112 ],
25113 cssProps: [
25114 { name: "--wpd-btn-color", description: "Resting foreground." },
25115 { name: "--wpd-btn-color-hover", description: "Hover foreground." },
25116 { name: "--wpd-btn-bg-hover", description: "Hover background wash." },
25117 { name: "--wpd-btn-bg-active", description: "Pressed background." },
25118 { name: "--wpd-btn-danger-hover", description: "Hover background for danger variant." },
25119 { name: "--wpd-btn-outline", description: "Focus outline colour." }
25120 ],
25121 example: html`
25122 <wpd-cluster gap="2">
25123 <wpd-window-button icon="minimize"></wpd-window-button>
25124 <wpd-window-button icon="maximize"></wpd-window-button>
25125 <wpd-window-button icon="menu"></wpd-window-button>
25126 <wpd-window-button icon="close" danger></wpd-window-button>
25127 </wpd-cluster>
25128 `
25129 };
25130 let WpdWindowButton = _WpdWindowButton;
25131 defineComponent("wpd-window-button", WpdWindowButton);
25132 const DEFAULT_STICKY_TITLE = "Sticky Note";
25133 const LEGACY_METADATA_PREFIX = "<!-- wpworkspace-sticky:";
25134 const LEGACY_METADATA_SUFFIX = "-->";
25135 const TITLE_MAX = 64;
25136 const GENERATED_TITLE_MAX = 48;
25137 const EXCERPT_MAX = 180;
25138 function noteFromGuideline(guideline) {
25139 const title = titleField(guideline.title);
25140 const content = removeLegacyMetadataComment(
25141 textFieldValue(guideline.content, { stripHtmlForRendered: true })
25142 );
25143 const modifiedMs = modifiedTimeMs(guideline);
25144 return {
25145 localId: `guideline:${guideline.id}`,
25146 guidelineId: guideline.id,
25147 title,
25148 body: editorBody(title, content),
25149 modified: guideline.modified,
25150 ...modifiedMs > 0 ? { modifiedMs } : {},
25151 link: guideline.link,
25152 termIds: Array.isArray(guideline.wp_guideline_type) ? guideline.wp_guideline_type.filter(isFiniteNumber) : []
25153 };
25154 }
25155 function titleField(field) {
25156 const candidates = [];
25157 if (typeof field === "string") {
25158 candidates.push(field);
25159 } else if (field && typeof field === "object") {
25160 if (typeof field.raw === "string") {
25161 candidates.push(field.raw);
25162 }
25163 if (typeof field.rendered === "string") {
25164 candidates.push(stripHtml(field.rendered));
25165 }
25166 }
25167 for (const candidate of candidates) {
25168 const trimmed = stripHtml(candidate).trim();
25169 if (trimmed) {
25170 return trimmed;
25171 }
25172 }
25173 return DEFAULT_STICKY_TITLE;
25174 }
25175 function textFieldValue(field, options = {}) {
25176 if (typeof field === "string") {
25177 return field;
25178 }
25179 if (!field || typeof field !== "object") {
25180 return "";
25181 }
25182 if (typeof field.raw === "string" && field.raw.length > 0) {
25183 return field.raw;
25184 }
25185 if (typeof field.rendered === "string") {
25186 return options.stripHtmlForRendered ? stripHtml(field.rendered) : field.rendered;
25187 }
25188 return "";
25189 }
25190 function titleForBody(body) {
25191 const line = body.split(/\r?\n/).find((item) => item.trim().length > 0)?.trim();
25192 const title = line && line.length > 0 ? line : DEFAULT_STICKY_TITLE;
25193 return truncate(title, TITLE_MAX);
25194 }
25195 function generatedTitle(body) {
25196 const collapsed = body.replace(/\s+/g, " ").trim();
25197 const title = collapsed || DEFAULT_STICKY_TITLE;
25198 return truncate(title, GENERATED_TITLE_MAX);
25199 }
25200 function editorBody(title, content) {
25201 const trimmedTitle = title.trim();
25202 if (!trimmedTitle) {
25203 return content;
25204 }
25205 const firstLine = content.split(/\r?\n/)[0]?.trim();
25206 if (firstLine === trimmedTitle) {
25207 return content;
25208 }
25209 if (!content) {
25210 return trimmedTitle;
25211 }
25212 return `${trimmedTitle}
25213 ${content}`;
25214 }
25215 function noteComponentsForBody(editorValue, fallbackTitle = DEFAULT_STICKY_TITLE) {
25216 const fallback = fallbackTitle.trim() || DEFAULT_STICKY_TITLE;
25217 const title = titleForBody(editorValue);
25218 const firstNewline = editorValue.search(/\r?\n/);
25219 if (firstNewline === -1) {
25220 const resolvedTitle = title === DEFAULT_STICKY_TITLE ? fallback : title;
25221 return {
25222 title: resolvedTitle,
25223 content: "",
25224 excerpt: excerptFor(resolvedTitle)
25225 };
25226 }
25227 let content = editorValue.slice(firstNewline);
25228 content = content.replace(/^\r?\n/, "");
25229 if (content.startsWith("\n")) {
25230 content = content.slice(1);
25231 }
25232 return {
25233 title,
25234 content,
25235 excerpt: excerptFor(content.trim() ? content : title)
25236 };
25237 }
25238 function excerptFor(body) {
25239 const collapsed = body.replace(/[\n\t]+/g, " ").trim();
25240 return truncate(collapsed, EXCERPT_MAX);
25241 }
25242 function removeLegacyMetadataComment(content) {
25243 if (!content.startsWith(LEGACY_METADATA_PREFIX) || !content.includes(LEGACY_METADATA_SUFFIX)) {
25244 return content;
25245 }
25246 const end = content.indexOf(LEGACY_METADATA_SUFFIX);
25247 let body = content.slice(end + LEGACY_METADATA_SUFFIX.length);
25248 if (body.startsWith("\r\n")) {
25249 body = body.slice(2);
25250 } else if (body.startsWith("\n")) {
25251 body = body.slice(1);
25252 }
25253 return body;
25254 }
25255 function stripHtml(value) {
25256 if (typeof document !== "undefined") {
25257 const template = document.createElement("template");
25258 template.innerHTML = value;
25259 return (template.content.textContent ?? "").trim();
25260 }
25261 return value.replace(/<[^>]*>/g, "").trim();
25262 }
25263 function truncate(value, max) {
25264 return value.length > max ? `${value.slice(0, max)}...` : value;
25265 }
25266 function modifiedTimeMs(guideline) {
25267 if (typeof guideline.desktop_mode_modified_ms === "number" && Number.isFinite(guideline.desktop_mode_modified_ms)) {
25268 return guideline.desktop_mode_modified_ms;
25269 }
25270 if (!guideline.modified) {
25271 return 0;
25272 }
25273 const parsed = Date.parse(guideline.modified);
25274 return Number.isFinite(parsed) ? parsed : 0;
25275 }
25276 function isFiniteNumber(value) {
25277 return typeof value === "number" && Number.isFinite(value);
25278 }
25279 class StickyNotesRestError extends Error {
25280 constructor(message, status) {
25281 super(message);
25282 this.name = "StickyNotesRestError";
25283 this.status = status;
25284 }
25285 }
25286 async function resolveStickyTerms(config) {
25287 const terms = await fetchStickyTermCandidates(config);
25288 const picked = pickStickyTerms(
25289 [...terms.artifactTerms, ...terms.artifactsTerms],
25290 terms.noteTerms,
25291 terms.stickyTerms
25292 );
25293 if (picked) {
25294 return picked;
25295 }
25296 const artifact = await ensureTerm(config, {
25297 slug: "artifact",
25298 name: "Artifact",
25299 parent: 0
25300 });
25301 const note = await ensureTerm(config, {
25302 slug: "note",
25303 name: "Note",
25304 parent: artifact.id
25305 });
25306 const sticky = await ensureTerm(config, {
25307 slug: "sticky",
25308 name: "Sticky",
25309 parent: artifact.id
25310 });
25311 return {
25312 stickyTermId: sticky.id,
25313 termIds: uniqueNumbers([artifact.id, note.id, sticky.id])
25314 };
25315 }
25316 async function fetchStickyTermCandidates(config) {
25317 const [artifactTerms, artifactsTerms, noteTerms, stickyTerms] = await Promise.all([
25318 fetchTermsBySlug(config, "artifact"),
25319 fetchTermsBySlug(config, "artifacts"),
25320 fetchTermsBySlug(config, "note"),
25321 fetchTermsBySlug(config, "sticky")
25322 ]);
25323 return {
25324 artifactTerms,
25325 artifactsTerms,
25326 noteTerms,
25327 stickyTerms
25328 };
25329 }
25330 function pickStickyTerms(artifactTerms, noteTerms, stickyTerms) {
25331 if (stickyTerms.length === 0) {
25332 return null;
25333 }
25334 const artifact = artifactTerms.find(
25335 (term) => ["artifact", "artifacts"].includes(term.slug)
25336 ) ?? artifactTerms[0] ?? null;
25337 const sticky = artifact ? stickyTerms.find((term) => Number(term.parent) === artifact.id) ?? stickyTerms[0] : stickyTerms[0];
25338 if (!sticky) {
25339 return null;
25340 }
25341 const note = artifact ? noteTerms.find((term) => Number(term.parent) === artifact.id) ?? null : null;
25342 return {
25343 stickyTermId: sticky.id,
25344 termIds: uniqueNumbers([
25345 artifact?.id,
25346 note?.id,
25347 sticky.id
25348 ])
25349 };
25350 }
25351 async function fetchStickyNotes(config, stickyTermId) {
25352 const guidelines = await requestJson(
25353 config,
25354 pathWithQuery("wp/v2/guidelines", {
25355 context: "edit",
25356 status: "private",
25357 per_page: "100",
25358 orderby: "modified",
25359 order: "desc",
25360 wp_guideline_type: String(stickyTermId)
25361 }),
25362 void 0,
25363 true
25364 );
25365 return guidelines.filter(
25366 (guideline) => Array.isArray(guideline.wp_guideline_type) ? guideline.wp_guideline_type.includes(stickyTermId) : true
25367 ).map(noteFromGuideline);
25368 }
25369 async function saveStickyNote(config, note, terms) {
25370 const components = noteComponentsForBody(note.body, note.title);
25371 const payload = {
25372 status: "private",
25373 title: components.title,
25374 content: components.content,
25375 excerpt: components.excerpt
25376 };
25377 if (note.guidelineId === null) {
25378 payload.wp_guideline_type = terms.termIds;
25379 }
25380 const path = note.guidelineId === null ? "wp/v2/guidelines" : `wp/v2/guidelines/${note.guidelineId}`;
25381 const guideline = await requestJson(
25382 config,
25383 path,
25384 {
25385 method: "POST",
25386 headers: {
25387 "Content-Type": "application/json"
25388 },
25389 body: JSON.stringify(payload)
25390 },
25391 false
25392 );
25393 return noteFromGuideline(guideline);
25394 }
25395 function buildGuidelineEditUrl(adminUrl, guidelineId) {
25396 const url = new URL("post.php", adminUrl);
25397 url.searchParams.set("post", String(guidelineId));
25398 url.searchParams.set("action", "edit");
25399 return url.toString();
25400 }
25401 async function fetchTermsBySlug(config, slug) {
25402 try {
25403 return await requestJson(
25404 config,
25405 pathWithQuery("wp/v2/wp_guideline_type", {
25406 context: "edit",
25407 slug,
25408 per_page: "100"
25409 }),
25410 void 0,
25411 true
25412 );
25413 } catch (error) {
25414 if (error instanceof StickyNotesRestError && (error.status === 404 || error.status === 400)) {
25415 return [];
25416 }
25417 throw error;
25418 }
25419 }
25420 async function ensureTerm(config, term) {
25421 const existing = await fetchTermsBySlug(config, term.slug);
25422 const byParent = existing.find(
25423 (item) => Number(item.parent ?? 0) === term.parent
25424 );
25425 if (byParent) {
25426 return byParent;
25427 }
25428 if (existing[0]) {
25429 return existing[0];
25430 }
25431 try {
25432 return await requestJson(
25433 config,
25434 "wp/v2/wp_guideline_type",
25435 {
25436 method: "POST",
25437 headers: {
25438 "Content-Type": "application/json"
25439 },
25440 body: JSON.stringify(term)
25441 },
25442 true
25443 );
25444 } catch (error) {
25445 const fallback = await fetchTermsBySlug(config, term.slug);
25446 if (fallback[0]) {
25447 return fallback[0];
25448 }
25449 throw error;
25450 }
25451 }
25452 async function requestJson(config, path, init2, silent = true) {
25453 const response = await trackedFetch$1(
25454 joinRestUrl(restRoot(config), path),
25455 init2,
25456 {
25457 source: "desktop-mode/sticky-notes",
25458 silent
25459 }
25460 );
25461 if (!response.ok) {
25462 throw new StickyNotesRestError(
25463 response.statusText || `${DEFAULT_STICKY_TITLE} request failed`,
25464 response.status
25465 );
25466 }
25467 return await response.json();
25468 }
25469 function restRoot(config) {
25470 if (config.restUrl) {
25471 return config.restUrl;
25472 }
25473 return `${window.location.origin}/wp-json/`;
25474 }
25475 function pathWithQuery(path, query) {
25476 const params = new URLSearchParams();
25477 Object.entries(query).forEach(([key, value]) => {
25478 params.set(key, value);
25479 });
25480 return `${path}?${params.toString()}`;
25481 }
25482 function uniqueNumbers(values) {
25483 const out = [];
25484 values.forEach((value) => {
25485 if (typeof value === "number" && Number.isFinite(value) && !out.includes(value)) {
25486 out.push(value);
25487 }
25488 });
25489 return out;
25490 }
25491 const SUBSCRIBE_FIELD = "desktop_mode_sticky_notes_subscribe";
25492 const RESPONSE_FIELD = "desktop_mode_sticky_notes";
25493 let started$3 = false;
25494 let target = null;
25495 function startStickyNotesHeartbeat(nextTarget) {
25496 target = nextTarget;
25497 if (started$3) {
25498 return;
25499 }
25500 started$3 = true;
25501 heartbeat.contribute(
25502 SUBSCRIBE_FIELD,
25503 () => target?.getHeartbeatSubscription()
25504 );
25505 heartbeat.subscribe(
25506 RESPONSE_FIELD,
25507 (payload) => {
25508 target?.applyHeartbeatPayload(payload);
25509 }
25510 );
25511 }
25512 const GEOMETRY_KEY = "desktop-mode-sticky-notes-geometry";
25513 const DEFAULT_WIDTH = 264;
25514 const DEFAULT_HEIGHT = 176;
25515 const MIN_WIDTH = 180;
25516 const MIN_HEIGHT = 128;
25517 const EDGE_PADDING = 16;
25518 const SAVE_DEBOUNCE_MS = 1e3;
25519 class StickyNotesLayer {
25520 constructor(options) {
25521 this.root = null;
25522 this.terms = null;
25523 this.controllers = /* @__PURE__ */ new Map();
25524 this.contextMenuInstalled = false;
25525 this.desktopHooksInstalled = false;
25526 this.highWaterMs = 0;
25527 this.zIndexCounter = 0;
25528 this.host = options.host;
25529 this.config = options.config;
25530 this.openArtifact = options.openArtifact;
25531 this.getActiveDesktopId = options.getActiveDesktopId ?? (() => "desktop-1");
25532 this.onError = options.onError;
25533 }
25534 async boot() {
25535 try {
25536 this.terms = await resolveStickyTerms(this.config);
25537 if (!this.terms) {
25538 return;
25539 }
25540 this.installContextMenu();
25541 this.installDesktopHooks();
25542 const notes = await fetchStickyNotes(
25543 this.config,
25544 this.terms.stickyTermId
25545 );
25546 this.bumpHighWaterFromNotes(notes);
25547 startStickyNotesHeartbeat(this);
25548 if (notes.length === 0) {
25549 return;
25550 }
25551 this.ensureRoot();
25552 sortNotesByModified(notes).forEach(
25553 (note, index2) => this.upsert(note, index2)
25554 );
25555 } catch (error) {
25556 if (error instanceof Error) {
25557 console.debug("[desktop-mode] Sticky notes unavailable:", error.message);
25558 }
25559 }
25560 }
25561 createNote(body = "") {
25562 if (!this.terms) {
25563 return;
25564 }
25565 const note = {
25566 localId: `local:${Date.now()}:${Math.random().toString(36).slice(2)}`,
25567 guidelineId: null,
25568 title: body.trim() ? generatedTitle(body) : DEFAULT_STICKY_TITLE,
25569 body,
25570 termIds: this.terms.termIds
25571 };
25572 const controller = this.upsert(note, this.controllers.size, {
25573 activate: true
25574 });
25575 controller.focus();
25576 }
25577 upsert(note, index2, options = {}) {
25578 this.ensureRoot();
25579 const key = noteKey(note);
25580 const existing = this.controllers.get(key);
25581 if (existing) {
25582 existing.replace(note);
25583 if (options.activate) {
25584 this.bringToFront(existing);
25585 }
25586 return existing;
25587 }
25588 const controller = new StickyNoteController({
25589 layer: this,
25590 note,
25591 index: index2
25592 });
25593 this.controllers.set(key, controller);
25594 this.root?.appendChild(controller.element);
25595 this.assignZIndex(controller);
25596 this.applyDesktopVisibility(controller);
25597 if (options.activate) {
25598 this.bringToFront(controller);
25599 }
25600 return controller;
25601 }
25602 ensureRoot() {
25603 if (this.root) {
25604 return this.root;
25605 }
25606 const root = document.createElement("section");
25607 root.className = "desktop-mode-sticky-notes";
25608 root.setAttribute("aria-label", __("Sticky notes"));
25609 this.host.appendChild(root);
25610 this.root = root;
25611 return root;
25612 }
25613 installContextMenu() {
25614 if (this.contextMenuInstalled) {
25615 return;
25616 }
25617 this.contextMenuInstalled = true;
25618 addFilter(
25619 "desktop-mode.wallpaper-context-menu",
25620 "desktop-mode/sticky-notes",
25621 (items) => {
25622 if (!Array.isArray(items) || !this.terms) {
25623 return items;
25624 }
25625 if (items.some(
25626 (item) => item.id === "new-sticky-note"
25627 )) {
25628 return items;
25629 }
25630 return [
25631 ...items,
25632 {
25633 id: "new-sticky-note",
25634 label: __("New sticky note"),
25635 icon: "dashicons-edit-page",
25636 sort: 14,
25637 onClick: () => this.createNote()
25638 }
25639 ];
25640 }
25641 );
25642 }
25643 installDesktopHooks() {
25644 if (this.desktopHooksInstalled) {
25645 return;
25646 }
25647 this.desktopHooksInstalled = true;
25648 addAction(
25649 HOOKS.DESKTOP_SWITCHED,
25650 "desktop-mode/sticky-notes",
25651 () => this.refreshDesktopVisibility()
25652 );
25653 addAction(
25654 HOOKS.DESKTOP_CLOSED,
25655 "desktop-mode/sticky-notes",
25656 (detail) => {
25657 this.migrateDesktopAssignments(detail?.desktopId, detail?.migratedTo);
25658 this.refreshDesktopVisibility();
25659 }
25660 );
25661 }
25662 save(note) {
25663 if (!this.terms) {
25664 return Promise.reject(new Error(__("Sticky term is unavailable.")));
25665 }
25666 return saveStickyNote(this.config, note, this.terms);
25667 }
25668 getHeartbeatSubscription() {
25669 if (!this.terms) {
25670 return void 0;
25671 }
25672 return {
25673 stickyTermId: this.terms.stickyTermId,
25674 knownIds: this.knownGuidelineIds(),
25675 version: this.highWaterMs
25676 };
25677 }
25678 applyHeartbeatPayload(payload) {
25679 for (const guideline of payload.notes ?? []) {
25680 const note = noteFromGuideline(guideline);
25681 this.upsertRemote(note);
25682 }
25683 for (const id of payload.removed ?? []) {
25684 this.forgetGuidelineId(id);
25685 }
25686 if (typeof payload.serverTimeMs === "number" && Number.isFinite(payload.serverTimeMs) && payload.serverTimeMs > this.highWaterMs) {
25687 this.highWaterMs = payload.serverTimeMs;
25688 }
25689 if (payload.truncated) {
25690 void this.reloadFromServer();
25691 }
25692 }
25693 openNoteArtifact(note) {
25694 if (note.guidelineId === null) {
25695 return;
25696 }
25697 this.openArtifact(
25698 buildGuidelineEditUrl(this.config.adminUrl, note.guidelineId),
25699 note.title,
25700 note.guidelineId
25701 );
25702 }
25703 notifyError(message) {
25704 this.onError?.(message);
25705 }
25706 hostSize() {
25707 return {
25708 width: Math.max(1, this.host.clientWidth),
25709 height: Math.max(1, this.host.clientHeight)
25710 };
25711 }
25712 defaultGeometry(index2) {
25713 const { width: hostWidth, height: hostHeight } = this.hostSize();
25714 const width = Math.min(
25715 DEFAULT_WIDTH,
25716 Math.max(MIN_WIDTH, hostWidth - EDGE_PADDING * 2)
25717 );
25718 const height = Math.min(
25719 DEFAULT_HEIGHT,
25720 Math.max(MIN_HEIGHT, hostHeight - EDGE_PADDING * 2)
25721 );
25722 const offset = index2 % 8 * 28;
25723 const left = clamp(
25724 hostWidth - width - 32 - offset,
25725 EDGE_PADDING,
25726 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
25727 );
25728 const top = clamp(
25729 32 + offset,
25730 EDGE_PADDING,
25731 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
25732 );
25733 return {
25734 x: left / hostWidth,
25735 y: top / hostHeight,
25736 width,
25737 height
25738 };
25739 }
25740 forget(controller) {
25741 this.controllers.delete(noteKey(controller.note));
25742 controller.dispose();
25743 controller.element.remove();
25744 if (this.controllers.size === 0) {
25745 this.root?.remove();
25746 this.root = null;
25747 }
25748 }
25749 replaceControllerKey(oldKey, controller) {
25750 const newKey = noteKey(controller.note);
25751 this.controllers.delete(oldKey);
25752 this.controllers.set(newKey, controller);
25753 moveStoredGeometry(oldKey, newKey);
25754 this.applyDesktopVisibility(controller);
25755 }
25756 bumpHighWaterFromNote(note) {
25757 const modifiedMs = noteModifiedMs(note);
25758 if (modifiedMs > this.highWaterMs) {
25759 this.highWaterMs = modifiedMs;
25760 }
25761 }
25762 bringToFront(controller) {
25763 controller.setZIndex(this.nextZIndex());
25764 }
25765 geometryForNote(note, index2) {
25766 const key = noteKey(note);
25767 const loaded = loadGeometry(key);
25768 const desktopId = this.normalizeDesktopId(loaded?.desktopId);
25769 const geometry = loaded ? { ...loaded, desktopId } : { ...this.defaultGeometry(index2), desktopId };
25770 if (!loaded || loaded.desktopId !== geometry.desktopId) {
25771 saveGeometry(key, geometry);
25772 }
25773 return geometry;
25774 }
25775 upsertRemote(note) {
25776 const key = noteKey(note);
25777 const existing = this.controllers.get(key);
25778 if (existing) {
25779 if (!existing.shouldReplaceFromRemote(note)) {
25780 this.bumpHighWaterFromNote(note);
25781 return existing;
25782 }
25783 existing.replace(note);
25784 this.bumpHighWaterFromNote(note);
25785 return existing;
25786 }
25787 const controller = this.upsert(note, this.controllers.size);
25788 this.bumpHighWaterFromNote(note);
25789 return controller;
25790 }
25791 forgetGuidelineId(guidelineId) {
25792 for (const controller of this.controllers.values()) {
25793 if (controller.note.guidelineId === guidelineId) {
25794 this.forget(controller);
25795 return;
25796 }
25797 }
25798 }
25799 knownGuidelineIds() {
25800 const ids = [];
25801 for (const controller of this.controllers.values()) {
25802 if (controller.note.guidelineId !== null) {
25803 ids.push(controller.note.guidelineId);
25804 }
25805 }
25806 return ids;
25807 }
25808 bumpHighWaterFromNotes(notes) {
25809 notes.forEach((note) => this.bumpHighWaterFromNote(note));
25810 }
25811 assignZIndex(controller) {
25812 controller.setZIndex(this.nextZIndex());
25813 }
25814 nextZIndex() {
25815 this.zIndexCounter += 1;
25816 return this.zIndexCounter;
25817 }
25818 applyDesktopVisibility(controller) {
25819 controller.setVisible(this.isNoteOnActiveDesktop(controller.note));
25820 }
25821 refreshDesktopVisibility() {
25822 for (const controller of this.controllers.values()) {
25823 this.applyDesktopVisibility(controller);
25824 }
25825 }
25826 isNoteOnActiveDesktop(note) {
25827 const key = noteKey(note);
25828 const geometry = loadGeometry(key);
25829 const desktopId = this.normalizeDesktopId(geometry?.desktopId);
25830 if (geometry && geometry.desktopId !== desktopId) {
25831 saveGeometry(key, { ...geometry, desktopId });
25832 }
25833 return desktopId === this.activeDesktopId();
25834 }
25835 migrateDesktopAssignments(desktopId, migratedTo) {
25836 if (!desktopId || !migratedTo || desktopId === migratedTo) {
25837 return;
25838 }
25839 const map = readGeometryMap();
25840 let changed = false;
25841 Object.entries(map).forEach(([key, geometry]) => {
25842 if (geometry.desktopId === desktopId) {
25843 map[key] = {
25844 ...geometry,
25845 desktopId: this.normalizeDesktopId(migratedTo)
25846 };
25847 changed = true;
25848 }
25849 });
25850 if (changed) {
25851 writeGeometryMap(map);
25852 }
25853 }
25854 activeDesktopId() {
25855 try {
25856 const id = this.getActiveDesktopId();
25857 return typeof id === "string" && id ? id : "desktop-1";
25858 } catch {
25859 return "desktop-1";
25860 }
25861 }
25862 normalizeDesktopId(desktopId) {
25863 if (!desktopId) {
25864 return this.activeDesktopId();
25865 }
25866 return desktopId;
25867 }
25868 async reloadFromServer() {
25869 if (!this.terms) {
25870 return;
25871 }
25872 try {
25873 const notes = await fetchStickyNotes(
25874 this.config,
25875 this.terms.stickyTermId
25876 );
25877 const ids = /* @__PURE__ */ new Set();
25878 sortNotesByModified(notes).forEach((note) => {
25879 if (note.guidelineId !== null) {
25880 ids.add(note.guidelineId);
25881 }
25882 this.upsertRemote(note);
25883 });
25884 this.knownGuidelineIds().forEach((id) => {
25885 if (!ids.has(id)) {
25886 this.forgetGuidelineId(id);
25887 }
25888 });
25889 } catch {
25890 }
25891 }
25892 }
25893 class StickyNoteController {
25894 constructor(options) {
25895 this.saveTimer = null;
25896 this.geometryTimer = null;
25897 this.saving = false;
25898 this.saveAgain = false;
25899 this.resizeObserver = null;
25900 this.disposed = false;
25901 this.layer = options.layer;
25902 this.note = options.note;
25903 this.index = options.index;
25904 this.element = document.createElement("article");
25905 this.element.className = "desktop-mode-sticky-note";
25906 this.element.dataset.stickyNoteId = noteKey(this.note);
25907 this.titleEl = document.createElement("span");
25908 this.editor = document.createElement("wpd-textarea");
25909 this.statusEl = document.createElement("wpd-save-status");
25910 this.openButton = document.createElement("wpd-window-button");
25911 this.paint();
25912 this.applyGeometry(this.layer.geometryForNote(this.note, this.index));
25913 this.element.addEventListener(
25914 "pointerdown",
25915 () => this.layer.bringToFront(this),
25916 { capture: true }
25917 );
25918 this.element.addEventListener("focusin", () => this.layer.bringToFront(this));
25919 this.watchResize();
25920 }
25921 focus() {
25922 window.setTimeout(() => this.editor.focusInput?.(), 0);
25923 }
25924 replace(note) {
25925 this.note = note;
25926 this.element.dataset.stickyNoteId = noteKey(this.note);
25927 this.titleEl.textContent = this.note.title;
25928 this.editor.setAttribute("value", this.note.body);
25929 this.refreshOpenButton();
25930 }
25931 shouldReplaceFromRemote(note) {
25932 if (this.hasLocalChanges()) {
25933 return false;
25934 }
25935 const currentMs = noteModifiedMs(this.note);
25936 const incomingMs = noteModifiedMs(note);
25937 if (currentMs > 0 && incomingMs > 0 && incomingMs <= currentMs && this.note.title === note.title && this.note.body === note.body) {
25938 return false;
25939 }
25940 return true;
25941 }
25942 setZIndex(zIndex) {
25943 this.element.style.zIndex = String(zIndex);
25944 }
25945 setVisible(visible) {
25946 this.element.style.display = visible ? "" : "none";
25947 }
25948 dispose() {
25949 this.disposed = true;
25950 if (this.saveTimer !== null) {
25951 window.clearTimeout(this.saveTimer);
25952 this.saveTimer = null;
25953 }
25954 if (this.geometryTimer !== null) {
25955 window.clearTimeout(this.geometryTimer);
25956 this.geometryTimer = null;
25957 }
25958 this.resizeObserver?.disconnect();
25959 this.resizeObserver = null;
25960 }
25961 paint() {
25962 this.element.innerHTML = "";
25963 this.element.style.minWidth = `${MIN_WIDTH}px`;
25964 this.element.style.minHeight = `${MIN_HEIGHT}px`;
25965 const header = document.createElement("div");
25966 header.className = "desktop-mode-sticky-note__header";
25967 const grip = document.createElement("span");
25968 grip.className = "desktop-mode-sticky-note__grip";
25969 grip.setAttribute("aria-hidden", "true");
25970 this.titleEl.className = "desktop-mode-sticky-note__title";
25971 this.titleEl.textContent = this.note.title;
25972 this.statusEl.setAttribute("mode", "icon");
25973 this.statusEl.setAttribute("phase", "idle");
25974 this.statusEl.className = "desktop-mode-sticky-note__status";
25975 this.openButton.setAttribute("icon", "detach");
25976 this.openButton.setAttribute("title", __("Open artifact"));
25977 this.openButton.className = "desktop-mode-sticky-note__open";
25978 this.openButton.addEventListener("wpd-button-activate", () => {
25979 this.layer.openNoteArtifact(this.note);
25980 });
25981 const close = document.createElement("wpd-window-button");
25982 close.setAttribute("icon", "close");
25983 close.setAttribute("danger", "");
25984 close.setAttribute("title", __("Hide sticky note"));
25985 close.className = "desktop-mode-sticky-note__close";
25986 close.addEventListener("wpd-button-activate", () => this.close());
25987 header.append(grip, this.titleEl, this.statusEl, this.openButton, close);
25988 header.addEventListener("pointerdown", (event) => this.startDrag(event));
25989 this.editor.className = "desktop-mode-sticky-note__editor";
25990 this.editor.setAttribute("aria-label", __("Sticky note text"));
25991 this.editor.setAttribute("rows", "8");
25992 this.editor.setAttribute("value", this.note.body);
25993 this.installEditorKeyboardGuard();
25994 this.editor.addEventListener("wpd-input-change", (event) => {
25995 const detail = event.detail;
25996 this.note.body = detail.value;
25997 this.note.title = titleForBody(detail.value);
25998 this.titleEl.textContent = this.note.title;
25999 this.setPhase("pending");
26000 this.scheduleSave();
26001 });
26002 this.editor.addEventListener("wpd-input-commit", () => this.flushSave());
26003 this.element.append(header, this.editor);
26004 this.refreshOpenButton();
26005 }
26006 installEditorKeyboardGuard() {
26007 ["keydown", "keypress", "keyup"].forEach((eventName) => {
26008 this.editor.addEventListener(eventName, (event) => {
26009 event.stopPropagation();
26010 });
26011 });
26012 }
26013 refreshOpenButton() {
26014 const disabled = this.note.guidelineId === null;
26015 this.openButton.classList.toggle("is-disabled", disabled);
26016 this.openButton.setAttribute("aria-disabled", disabled ? "true" : "false");
26017 }
26018 close() {
26019 if (this.note.guidelineId === null && this.note.body.trim().length === 0) {
26020 this.layer.forget(this);
26021 return;
26022 }
26023 this.flushSave();
26024 this.layer.forget(this);
26025 }
26026 scheduleSave() {
26027 if (this.note.guidelineId === null && this.note.body.trim().length === 0) {
26028 this.setPhase("idle");
26029 return;
26030 }
26031 if (this.saveTimer !== null) {
26032 window.clearTimeout(this.saveTimer);
26033 }
26034 this.saveTimer = window.setTimeout(() => {
26035 this.saveTimer = null;
26036 void this.save();
26037 }, SAVE_DEBOUNCE_MS);
26038 }
26039 flushSave() {
26040 if (this.saveTimer !== null) {
26041 window.clearTimeout(this.saveTimer);
26042 this.saveTimer = null;
26043 }
26044 if (this.note.guidelineId !== null || this.note.body.trim().length > 0) {
26045 void this.save();
26046 }
26047 }
26048 async save() {
26049 if (this.saving) {
26050 this.saveAgain = true;
26051 this.setPhase("pending");
26052 return;
26053 }
26054 this.saving = true;
26055 this.setPhase("saving");
26056 const bodyAtSave = this.note.body;
26057 try {
26058 const saved = await this.layer.save({
26059 ...this.note,
26060 body: bodyAtSave
26061 });
26062 if (this.disposed) {
26063 return;
26064 }
26065 const oldKey = noteKey(this.note);
26066 this.note.guidelineId = saved.guidelineId;
26067 this.note.modified = saved.modified;
26068 this.note.link = saved.link;
26069 this.note.termIds = saved.termIds.length > 0 ? saved.termIds : this.note.termIds;
26070 if (this.note.body === bodyAtSave) {
26071 this.note.title = saved.title;
26072 this.titleEl.textContent = saved.title;
26073 }
26074 if (oldKey !== noteKey(this.note)) {
26075 this.element.dataset.stickyNoteId = noteKey(this.note);
26076 this.layer.replaceControllerKey(oldKey, this);
26077 }
26078 this.layer.bumpHighWaterFromNote(this.note);
26079 this.refreshOpenButton();
26080 this.setPhase("saved");
26081 } catch (error) {
26082 if (this.disposed) {
26083 return;
26084 }
26085 const message = error instanceof Error ? error.message : __("Could not save sticky note.");
26086 this.setPhase("failed", message);
26087 this.layer.notifyError(message);
26088 } finally {
26089 this.saving = false;
26090 if (!this.disposed && this.saveAgain) {
26091 this.saveAgain = false;
26092 this.scheduleSave();
26093 }
26094 }
26095 }
26096 setPhase(phase, error) {
26097 this.statusEl.setAttribute("phase", phase);
26098 if (error) {
26099 this.statusEl.setAttribute("error", error);
26100 this.statusEl.setAttribute("title", error);
26101 } else {
26102 this.statusEl.removeAttribute("error");
26103 this.statusEl.removeAttribute("title");
26104 }
26105 }
26106 hasLocalChanges() {
26107 const phase = this.statusEl.getAttribute("phase");
26108 return this.saveTimer !== null || this.saving || this.saveAgain || phase === "pending" || phase === "failed";
26109 }
26110 startDrag(event) {
26111 if (event.button !== 0) {
26112 return;
26113 }
26114 const target2 = event.target;
26115 if (target2?.closest("wpd-window-button, wpd-save-status")) {
26116 return;
26117 }
26118 event.preventDefault();
26119 const startRect = this.element.getBoundingClientRect();
26120 const hostRect = this.layerHostRect();
26121 const startLeft = startRect.left - hostRect.left;
26122 const startTop = startRect.top - hostRect.top;
26123 const startX = event.clientX;
26124 const startY = event.clientY;
26125 this.element.classList.add("desktop-mode-sticky-note--dragging");
26126 this.element.setPointerCapture?.(event.pointerId);
26127 const move = (moveEvent) => {
26128 const width = this.element.offsetWidth;
26129 const height = this.element.offsetHeight;
26130 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26131 const left = clamp(
26132 startLeft + moveEvent.clientX - startX,
26133 EDGE_PADDING,
26134 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
26135 );
26136 const top = clamp(
26137 startTop + moveEvent.clientY - startY,
26138 EDGE_PADDING,
26139 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
26140 );
26141 this.element.style.left = `${left}px`;
26142 this.element.style.top = `${top}px`;
26143 };
26144 const up = (upEvent) => {
26145 this.element.classList.remove("desktop-mode-sticky-note--dragging");
26146 this.element.releasePointerCapture?.(upEvent.pointerId);
26147 document.removeEventListener("pointermove", move);
26148 document.removeEventListener("pointerup", up);
26149 this.persistGeometry();
26150 };
26151 document.addEventListener("pointermove", move);
26152 document.addEventListener("pointerup", up);
26153 }
26154 applyGeometry(geometry) {
26155 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26156 const width = clamp(geometry.width, MIN_WIDTH, hostWidth - EDGE_PADDING * 2);
26157 const height = clamp(geometry.height, MIN_HEIGHT, hostHeight - EDGE_PADDING * 2);
26158 const left = clamp(
26159 geometry.x * hostWidth,
26160 EDGE_PADDING,
26161 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
26162 );
26163 const top = clamp(
26164 geometry.y * hostHeight,
26165 EDGE_PADDING,
26166 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
26167 );
26168 this.element.style.left = `${left}px`;
26169 this.element.style.top = `${top}px`;
26170 this.element.style.width = `${width}px`;
26171 this.element.style.height = `${height}px`;
26172 }
26173 watchResize() {
26174 if (typeof ResizeObserver === "undefined") {
26175 return;
26176 }
26177 this.resizeObserver = new ResizeObserver(() => {
26178 if (this.geometryTimer !== null) {
26179 window.clearTimeout(this.geometryTimer);
26180 }
26181 this.geometryTimer = window.setTimeout(() => {
26182 this.geometryTimer = null;
26183 this.persistGeometry();
26184 }, 150);
26185 });
26186 this.resizeObserver.observe(this.element);
26187 }
26188 persistGeometry() {
26189 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26190 const left = parseFloat(this.element.style.left) || 0;
26191 const top = parseFloat(this.element.style.top) || 0;
26192 const existing = loadGeometry(noteKey(this.note));
26193 saveGeometry(noteKey(this.note), {
26194 ...existing ?? {},
26195 x: clamp(left / hostWidth, 0, 1),
26196 y: clamp(top / hostHeight, 0, 1),
26197 width: this.element.offsetWidth,
26198 height: this.element.offsetHeight
26199 });
26200 }
26201 layerHostRect() {
26202 const parent = this.element.parentElement?.parentElement;
26203 return (parent ?? document.body).getBoundingClientRect();
26204 }
26205 }
26206 function bootStickyNotes(options) {
26207 const layer = new StickyNotesLayer(options);
26208 void layer.boot();
26209 return layer;
26210 }
26211 function noteKey(note) {
26212 return note.guidelineId === null ? note.localId : `guideline:${note.guidelineId}`;
26213 }
26214 function noteModifiedMs(note) {
26215 if (typeof note.modifiedMs === "number" && Number.isFinite(note.modifiedMs)) {
26216 return note.modifiedMs;
26217 }
26218 if (!note.modified) {
26219 return 0;
26220 }
26221 const parsed = Date.parse(note.modified);
26222 return Number.isFinite(parsed) ? parsed : 0;
26223 }
26224 function sortNotesByModified(notes) {
26225 return [...notes].sort((a, b) => noteModifiedMs(a) - noteModifiedMs(b));
26226 }
26227 function loadGeometry(key) {
26228 const map = readGeometryMap();
26229 const value = map[key];
26230 if (!value || !Number.isFinite(value.x) || !Number.isFinite(value.y) || !Number.isFinite(value.width) || !Number.isFinite(value.height)) {
26231 return null;
26232 }
26233 return value;
26234 }
26235 function saveGeometry(key, geometry) {
26236 const map = readGeometryMap();
26237 map[key] = geometry;
26238 writeGeometryMap(map);
26239 }
26240 function moveStoredGeometry(oldKey, newKey) {
26241 if (oldKey === newKey) {
26242 return;
26243 }
26244 const map = readGeometryMap();
26245 if (map[oldKey]) {
26246 map[newKey] = map[oldKey];
26247 delete map[oldKey];
26248 writeGeometryMap(map);
26249 }
26250 }
26251 function readGeometryMap() {
26252 try {
26253 const raw = window.localStorage.getItem(GEOMETRY_KEY);
26254 return raw ? JSON.parse(raw) : {};
26255 } catch {
26256 return {};
26257 }
26258 }
26259 function writeGeometryMap(map) {
26260 try {
26261 window.localStorage.setItem(GEOMETRY_KEY, JSON.stringify(map));
26262 } catch {
26263 }
26264 }
26265 function clamp(value, min, max) {
26266 if (max < min) {
26267 return min;
26268 }
26269 return Math.min(max, Math.max(min, value));
26270 }
26271 const clock = {
26272 id: "clock",
26273 // Labels/descriptions on built-in defs stay string-literal at
26274 // module-eval time so the extract-pot pass picks them up. The
26275 // values are wrapped in `__()` so they translate at runtime.
26276 get label() {
26277 return __("Clock");
26278 },
26279 get description() {
26280 return __("Local time and date, refreshed every second.");
26281 },
26282 icon: "dashicons-clock",
26283 mount: (container) => {
26284 container.classList.add("desktop-mode-widget-clock");
26285 const time = document.createElement("div");
26286 time.className = "desktop-mode-widget-clock__time";
26287 container.appendChild(time);
26288 const date = document.createElement("div");
26289 date.className = "desktop-mode-widget-clock__date";
26290 container.appendChild(date);
26291 const render2 = () => {
26292 const now = /* @__PURE__ */ new Date();
26293 time.textContent = now.toLocaleTimeString(void 0, {
26294 hour: "2-digit",
26295 minute: "2-digit"
26296 });
26297 date.textContent = now.toLocaleDateString(void 0, {
26298 weekday: "long",
26299 month: "short",
26300 day: "numeric"
26301 });
26302 };
26303 render2();
26304 const msUntilNextSecond = 1e3 - Date.now() % 1e3;
26305 let interval = null;
26306 const kickoff = window.setTimeout(() => {
26307 render2();
26308 interval = window.setInterval(render2, 1e3);
26309 }, msUntilNextSecond);
26310 return () => {
26311 window.clearTimeout(kickoff);
26312 if (interval !== null) {
26313 window.clearInterval(interval);
26314 }
26315 };
26316 }
26317 };
26318 function registerBuiltInWidgets() {
26319 register(clock);
26320 }
26321 function createWidgetRegistrySync(deps2) {
26322 const { layer } = deps2;
26323 const registered = /* @__PURE__ */ new Set();
26324 const loadedScripts = /* @__PURE__ */ new Set();
26325 const ensureScript = async (entry) => {
26326 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
26327 return;
26328 }
26329 try {
26330 await loadVendorScript(entry.scriptUrl, {
26331 translations: entry.scriptTranslations,
26332 l10n: entry.scriptL10n,
26333 before: entry.scriptBefore,
26334 after: entry.scriptAfter
26335 });
26336 } catch (err) {
26337 doAction(HOOKS.SHELL_ERROR, {
26338 scope: "widget-script-load",
26339 id: entry.id,
26340 error: err
26341 });
26342 }
26343 loadedScripts.add(entry.scriptUrl);
26344 };
26345 const buildDefFromEntry = (entry) => {
26346 const globals = window.desktopModeWidgets || {};
26347 const mount = globals[entry.id];
26348 if (!mount) {
26349 doAction(HOOKS.SHELL_ERROR, {
26350 scope: "widget-missing-mount",
26351 id: entry.id,
26352 error: new Error(
26353 `[desktop-mode] No mount callback on window.desktopModeWidgets["${entry.id}"]. Plugin script loaded but didn't register. Check the plugin's enqueue + global assignment.`
26354 )
26355 });
26356 return null;
26357 }
26358 return {
26359 id: entry.id,
26360 label: entry.label,
26361 description: entry.description,
26362 icon: entry.icon,
26363 movable: entry.movable,
26364 resizable: entry.resizable,
26365 minWidth: entry.minWidth || void 0,
26366 minHeight: entry.minHeight || void 0,
26367 maxWidth: entry.maxWidth || void 0,
26368 maxHeight: entry.maxHeight || void 0,
26369 defaultWidth: entry.defaultWidth || void 0,
26370 defaultHeight: entry.defaultHeight || void 0,
26371 mount
26372 };
26373 };
26374 const registerEntry = async (entry) => {
26375 if (registered.has(entry.id)) {
26376 return;
26377 }
26378 await ensureScript(entry);
26379 const def = buildDefFromEntry(entry);
26380 if (!def) {
26381 return;
26382 }
26383 try {
26384 register(def);
26385 } catch (err) {
26386 doAction(HOOKS.SHELL_ERROR, {
26387 scope: "widget-register",
26388 id: entry.id,
26389 error: err
26390 });
26391 return;
26392 }
26393 registered.add(entry.id);
26394 refreshWidgetPicker();
26395 if (layer) {
26396 layer.mountIfEnabled(entry.id);
26397 }
26398 };
26399 const unregisterEntry = (id) => {
26400 if (!registered.has(id)) {
26401 return;
26402 }
26403 layer?.unmount(id);
26404 unregister(id);
26405 registered.delete(id);
26406 refreshWidgetPicker();
26407 };
26408 return async (list2) => {
26409 const incoming = /* @__PURE__ */ new Set();
26410 for (const entry of list2) {
26411 incoming.add(entry.id);
26412 }
26413 for (const id of Array.from(registered)) {
26414 if (!incoming.has(id)) {
26415 unregisterEntry(id);
26416 }
26417 }
26418 for (const entry of list2) {
26419 if (!registered.has(entry.id)) {
26420 await registerEntry(entry);
26421 }
26422 }
26423 };
26424 }
26425 const WPD_COMPONENT_TAGS = [
26426 "wpd-section",
26427 "wpd-button",
26428 "wpd-swatch",
26429 "wpd-swatch-grid",
26430 "wpd-segmented",
26431 "wpd-segment",
26432 "wpd-select",
26433 "wpd-option",
26434 "wpd-multiselect",
26435 "wpd-color-field",
26436 "wpd-range-field",
26437 "wpd-text-field",
26438 "wpd-number-field",
26439 "wpd-checkbox",
26440 "wpd-checkbox-label",
26441 "wpd-toast",
26442 "wpd-toast-container",
26443 "wpd-tabs",
26444 "wpd-tab",
26445 "wpd-tabpanel",
26446 "wpd-window-button",
26447 "wpd-menu",
26448 "wpd-menu-item",
26449 "wpd-context-menu",
26450 "wpd-context-menu-option",
26451 "wpd-confirm-dialog",
26452 "wpd-modal",
26453 "wpd-user-search",
26454 "wpd-role-picker",
26455 "wpd-flyout",
26456 "wpd-tab-chip",
26457 "wpd-stack",
26458 "wpd-cluster",
26459 "wpd-icon",
26460 "wpd-body",
26461 "wpd-panel",
26462 "wpd-row",
26463 "wpd-grid",
26464 "wpd-display",
26465 "wpd-empty-state",
26466 "wpd-key",
26467 "wpd-code",
26468 "wpd-badge",
26469 "wpd-log",
26470 "wpd-steps",
26471 "wpd-step",
26472 "wpd-table",
26473 "wpd-spinner",
26474 "wpd-relative-time",
26475 "wpd-avatar",
26476 "wpd-textarea",
26477 "wpd-chip",
26478 "wpd-tag-input",
26479 "wpd-form",
26480 "wpd-save-status",
26481 "wpd-category-picker",
26482 "wpd-crumb-chain",
26483 "wpd-card",
26484 "wpd-notice"
26485 ];
26486 const KNOWN = new Set(WPD_COMPONENT_TAGS);
26487 const WARN_GRACE_MS = 2e3;
26488 const warnedTags = /* @__PURE__ */ new Set();
26489 const observedRoots = /* @__PURE__ */ new WeakSet();
26490 let started$2 = false;
26491 function distance(a, b) {
26492 const m = a.length;
26493 const n = b.length;
26494 if (m === 0) {
26495 return n;
26496 }
26497 if (n === 0) {
26498 return m;
26499 }
26500 const dp = new Array(n + 1);
26501 for (let j = 0; j <= n; j++) {
26502 dp[j] = j;
26503 }
26504 for (let i = 1; i <= m; i++) {
26505 let prev = dp[0];
26506 dp[0] = i;
26507 for (let j = 1; j <= n; j++) {
26508 const tmp = dp[j];
26509 dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
26510 prev = tmp;
26511 }
26512 }
26513 return dp[n];
26514 }
26515 function suggest(tag) {
26516 let best = null;
26517 let bestD = Infinity;
26518 for (const known of KNOWN) {
26519 const d = distance(tag, known);
26520 if (d < bestD) {
26521 bestD = d;
26522 best = known;
26523 }
26524 }
26525 return bestD > 0 && bestD <= 3 ? best : null;
26526 }
26527 function folderFor(tag) {
26528 return tag.startsWith("wpd-") ? tag.slice(4) : tag;
26529 }
26530 function warnFor(tag, sample) {
26531 if (warnedTags.has(tag)) {
26532 return;
26533 }
26534 warnedTags.add(tag);
26535 const isKnown = KNOWN.has(tag);
26536 if (isKnown) {
26537 const folder = folderFor(tag);
26538 console.error(
26539 `[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.
26540
26541 Fix — side-effect-import the component module from wherever you render it:
26542
26543 import '<rel>/ui/components/${folder}/${folder}';
26544
26545 Or pull every wpd-* component in one go (heavier — only do this from an entry bundle):
26546
26547 import '<rel>/ui/components';
26548
26549 See docs/components-reference.md for the full list.`,
26550 "\nFirst offending element:",
26551 sample
26552 );
26553 return;
26554 }
26555 const guess = suggest(tag);
26556 if (guess) {
26557 console.error(
26558 `[wp.desktop] <${tag}> is not a registered wpd-* component. Did you mean <${guess}>?
26559
26560 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'.`,
26561 "\nFirst offending element:",
26562 sample
26563 );
26564 return;
26565 }
26566 console.error(
26567 `[wp.desktop] <${tag}> looks like a wpd-* tag but no component by that name exists.
26568
26569 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.`,
26570 "\nFirst offending element:",
26571 sample
26572 );
26573 }
26574 function checkElement(el) {
26575 const tag = el.tagName.toLowerCase();
26576 if (!tag.startsWith("wpd-")) {
26577 return;
26578 }
26579 if (warnedTags.has(tag)) {
26580 return;
26581 }
26582 if (customElements.get(tag)) {
26583 return;
26584 }
26585 let settled = false;
26586 customElements.whenDefined(tag).then(() => {
26587 settled = true;
26588 });
26589 setTimeout(() => {
26590 if (settled) {
26591 return;
26592 }
26593 if (customElements.get(tag)) {
26594 return;
26595 }
26596 warnFor(tag, el);
26597 }, WARN_GRACE_MS);
26598 }
26599 function walk(root) {
26600 if (root instanceof Element) {
26601 checkElement(root);
26602 if (root.shadowRoot) {
26603 observeRoot(root.shadowRoot);
26604 }
26605 }
26606 const all2 = root.querySelectorAll("*");
26607 for (let i = 0; i < all2.length; i++) {
26608 const el = all2[i];
26609 checkElement(el);
26610 if (el.shadowRoot) {
26611 observeRoot(el.shadowRoot);
26612 }
26613 }
26614 }
26615 function observeRoot(root) {
26616 if (observedRoots.has(root)) {
26617 return;
26618 }
26619 observedRoots.add(root);
26620 walk(root);
26621 const mo = new MutationObserver((records) => {
26622 for (let i = 0; i < records.length; i++) {
26623 const added = records[i].addedNodes;
26624 for (let j = 0; j < added.length; j++) {
26625 const node = added[j];
26626 if (node.nodeType === 1) {
26627 walk(node);
26628 }
26629 }
26630 }
26631 });
26632 mo.observe(root, { childList: true, subtree: true });
26633 }
26634 function patchAttachShadow() {
26635 const proto = Element.prototype;
26636 const original = proto.attachShadow;
26637 if (original.__wpdPatched) {
26638 return;
26639 }
26640 const patched = function(init2) {
26641 const root = original.call(this, init2);
26642 if (root.mode === "open") {
26643 observeRoot(root);
26644 }
26645 return root;
26646 };
26647 patched.__wpdPatched = true;
26648 proto.attachShadow = patched;
26649 }
26650 function startMissingImportWarner() {
26651 if (started$2) {
26652 return;
26653 }
26654 if (typeof document === "undefined") {
26655 return;
26656 }
26657 started$2 = true;
26658 patchAttachShadow();
26659 observeRoot(document);
26660 }
26661 const TRASHABLE_SHORTCUT_KINDS = /* @__PURE__ */ new Set(["post"]);
26662 function getMyWordpressTrashApi() {
26663 const api = window.wp?.desktop?.myWordpress;
26664 return api && typeof api.trashEntity === "function" ? api : null;
26665 }
26666 const TRASH_DROP_ACTIVE_ATTR = "data-desktop-mode-trash-drop-active";
26667 const RECYCLE_BIN_WINDOW_ID = "desktop-mode-recycle-bin";
26668 const BIN_TILE_SELECTORS = [
26669 `.desktop-mode-file-tile[data-file-ref="${RECYCLE_BIN_WINDOW_ID}"]`,
26670 `[data-icon-id="${RECYCLE_BIN_WINDOW_ID}"]`,
26671 `[data-system-id="${RECYCLE_BIN_WINDOW_ID}"]`
26672 ];
26673 function findBinTile() {
26674 for (const sel of BIN_TILE_SELECTORS) {
26675 const el = document.querySelector(sel);
26676 if (el instanceof HTMLElement) {
26677 return el;
26678 }
26679 }
26680 return null;
26681 }
26682 let _installed = false;
26683 let _dockDeregister = null;
26684 let _windowDeregister = null;
26685 let _binMutationObserver = null;
26686 function isDesktopFilePayload(session) {
26687 return session.payload.type === "desktop-file";
26688 }
26689 function isShortcutPayload(session) {
26690 return session.payload.type === "shortcut";
26691 }
26692 function isTrashableShortcut(data) {
26693 if (!data.kind || !data.ref || !data.entityId) {
26694 return false;
26695 }
26696 if (!TRASHABLE_SHORTCUT_KINDS.has(data.kind)) {
26697 return false;
26698 }
26699 const numericRef = Number.parseInt(data.ref, 10);
26700 if (!Number.isFinite(numericRef) || numericRef <= 0) {
26701 return false;
26702 }
26703 return getMyWordpressTrashApi() !== null;
26704 }
26705 function registerOn(dragManager, id, el) {
26706 return dragManager.registerDropTarget({
26707 id,
26708 element: el,
26709 // Override the ghost-chip label: while the cursor is over
26710 // the bin the user is trashing, not creating a shortcut /
26711 // moving the placement. The DragManager swaps this in for
26712 // the payload-default "Drop here to create shortcut" /
26713 // "Drop here to move" chip text whenever this target is the
26714 // current accept-mode target.
26715 acceptLabel: __("Move to Trash", "desktop-mode"),
26716 // Reject the drop UP FRONT when the viewer can't trash the
26717 // payload's placement (e.g. an item inside a read-only
26718 // shared folder, or someone else's tile in a shared
26719 // namespace). `accept` flipping to `false` means the
26720 // drop-active highlight never lights up + onDrop never
26721 // fires + the drag manager surfaces a `rejected` outcome.
26722 // The user sees the icon snap back instead of attempting a
26723 // REST call that would 403 and only log to the console.
26724 accept: (payload) => {
26725 if (payload.type === "desktop-file") {
26726 const data = payload.data;
26727 const placement = data?.placement;
26728 if (!placement) {
26729 return false;
26730 }
26731 if (placement.file?.ref === RECYCLE_BIN_WINDOW_ID) {
26732 return false;
26733 }
26734 return placement.canTrash !== false;
26735 }
26736 if (payload.type === "shortcut") {
26737 const data = payload.data;
26738 return isTrashableShortcut(data);
26739 }
26740 return false;
26741 },
26742 onEnter: () => {
26743 el.setAttribute(TRASH_DROP_ACTIVE_ATTR, "");
26744 },
26745 onLeave: () => {
26746 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
26747 },
26748 onDrop: (session) => {
26749 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
26750 if (isDesktopFilePayload(session)) {
26751 const placement = session.payload.data.placement;
26752 void trashByFileType(placement);
26753 return;
26754 }
26755 if (isShortcutPayload(session)) {
26756 const data = session.payload.data;
26757 const api = getMyWordpressTrashApi();
26758 if (!api?.trashEntity || !data.entityId) {
26759 return;
26760 }
26761 const numericRef = Number.parseInt(data.ref, 10);
26762 if (!Number.isFinite(numericRef) || numericRef <= 0) {
26763 return;
26764 }
26765 void api.trashEntity(data.entityId, numericRef).catch(
26766 (err) => {
26767 console.error(
26768 "[desktop-mode] recycle-bin: shortcut trash failed:",
26769 err
26770 );
26771 }
26772 );
26773 }
26774 }
26775 });
26776 }
26777 function installRecycleBinDropTargets(dragManager) {
26778 if (_installed) {
26779 return;
26780 }
26781 _installed = true;
26782 const reprobeTile = () => {
26783 const el = findBinTile();
26784 if (!el) {
26785 _dockDeregister?.();
26786 _dockDeregister = null;
26787 return;
26788 }
26789 if (_dockDeregister && getRegisteredElementId(dragManager) === el) {
26790 return;
26791 }
26792 _dockDeregister?.();
26793 _dockDeregister = registerOn(dragManager, "recycle-bin-dock", el);
26794 };
26795 reprobeTile();
26796 document.addEventListener("desktop-mode-files-changed", reprobeTile);
26797 document.addEventListener("desktop-mode-desktop-icons-rendered", reprobeTile);
26798 addAction(
26799 HOOKS.DOCK_AFTER_RENDER,
26800 "desktop-mode/files/recycle-bin-dock-target",
26801 reprobeTile
26802 );
26803 if (typeof MutationObserver !== "undefined") {
26804 _binMutationObserver = new MutationObserver(() => {
26805 reprobeTile();
26806 });
26807 const desktopArea = document.getElementById("desktop-mode-area") ?? document.body;
26808 _binMutationObserver.observe(desktopArea, {
26809 childList: true,
26810 subtree: true
26811 });
26812 }
26813 addAction(
26814 HOOKS.WINDOW_OPENED,
26815 "desktop-mode/files/recycle-bin-window-target",
26816 (detail) => {
26817 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
26818 return;
26819 }
26820 _windowDeregister?.();
26821 _windowDeregister = null;
26822 const el = document.querySelector(
26823 "[data-desktop-mode-recycle-bin-root]"
26824 );
26825 if (el instanceof HTMLElement) {
26826 _windowDeregister = registerOn(
26827 dragManager,
26828 "recycle-bin-window",
26829 el
26830 );
26831 }
26832 }
26833 );
26834 addAction(
26835 HOOKS.WINDOW_CLOSED,
26836 "desktop-mode/files/recycle-bin-window-cleanup",
26837 (detail) => {
26838 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
26839 return;
26840 }
26841 _windowDeregister?.();
26842 _windowDeregister = null;
26843 }
26844 );
26845 }
26846 function getRegisteredElementId(dragManager) {
26847 const t = dragManager.debug().listTargets().find((target2) => target2.id === "recycle-bin-dock");
26848 return t ? t.element : null;
26849 }
26850 let started$1 = false;
26851 let highWaterMs = 0;
26852 function startFilesHeartbeat() {
26853 if (started$1) {
26854 return;
26855 }
26856 started$1 = true;
26857 heartbeat.contribute("desktop_mode_files_subscribe", () => {
26858 const state2 = getFilesState();
26859 const folderVersions = {};
26860 for (const [id, folder] of state2.folders) {
26861 folderVersions[String(id)] = folder.updatedAtMs;
26862 }
26863 return {
26864 folderVersions,
26865 placementsVersion: highWaterMs,
26866 sharesVersion: sharesStore().state.sharesVersion
26867 };
26868 });
26869 heartbeat.subscribe("desktop_mode_files", (payload) => {
26870 applyDelta(payload);
26871 });
26872 }
26873 function applyDelta(payload) {
26874 const folders = payload.folders ?? [];
26875 for (const folder of folders) {
26876 upsertFolder(folder, "remote");
26877 if (folder.updatedAtMs > highWaterMs) {
26878 highWaterMs = folder.updatedAtMs;
26879 }
26880 }
26881 const placements = payload.placements ?? [];
26882 for (const placement of placements) {
26883 upsertPlacement(placement, "remote");
26884 if (placement.updatedAtMs > highWaterMs) {
26885 highWaterMs = placement.updatedAtMs;
26886 }
26887 }
26888 const removed = payload.removed ?? {};
26889 for (const id of removed.folders ?? []) {
26890 removeFolder(id, "remote");
26891 }
26892 for (const id of removed.placements ?? []) {
26893 removePlacement(id, "remote");
26894 }
26895 if (typeof payload.serverTimeMs === "number" && payload.serverTimeMs > highWaterMs) {
26896 highWaterMs = payload.serverTimeMs;
26897 }
26898 const pending2 = payload.shares?.pending;
26899 if (Array.isArray(pending2) && pending2.length > 0) {
26900 ingestPendingInvites(pending2);
26901 }
26902 if (payload.truncated) {
26903 const hydrated = Array.from(getFilesState().hydratedFolders);
26904 for (const folderId of hydrated) {
26905 void listPlacements(folderId).then((res) => {
26906 setFolderPlacements(folderId, res.placements);
26907 }).catch(() => {
26908 });
26909 }
26910 }
26911 }
26912 let started = false;
26913 const unsubscribers = [];
26914 function startFilesRestoreSync() {
26915 if (started) {
26916 return;
26917 }
26918 started = true;
26919 const onChange = (payload) => {
26920 const detail = payload;
26921 if (!detail || detail.action !== "untrashed") {
26922 return;
26923 }
26924 resyncFromServer();
26925 };
26926 unsubscribers.push(
26927 subscribe$2("desktop-mode.placement.changed", onChange),
26928 subscribe$2("desktop-mode.shortcut.changed", onChange),
26929 subscribe$2("desktop-mode.folder.changed", onChange)
26930 );
26931 }
26932 function resyncFromServer() {
26933 void listFolders().then((res) => {
26934 setFolders(res.folders);
26935 }).catch((err) => {
26936 console.error(
26937 "[desktop-mode] files restore-sync: listFolders failed",
26938 err
26939 );
26940 });
26941 const hydrated = Array.from(getFilesState().hydratedFolders);
26942 for (const folderId of hydrated) {
26943 void listPlacements(folderId).then((res) => {
26944 setFolderPlacements(folderId, res.placements);
26945 }).catch((err) => {
26946 console.error(
26947 "[desktop-mode] files restore-sync: listPlacements failed for",
26948 folderId,
26949 err
26950 );
26951 });
26952 }
26953 }
26954 const MENU_CLASS = "desktop-mode-wallpaper-menu";
26955 let activeMenu = null;
26956 function isWallpaperMenuOpen() {
26957 return activeMenu !== null;
26958 }
26959 let openGeneration = 0;
26960 function openWallpaperMenu(host, pos, items, options = {}) {
26961 closeWallpaperMenu();
26962 const myGen = ++openGeneration;
26963 openWithShellOverlays(
26964 () => myGen === openGeneration,
26965 () => openWallpaperMenuImmediate(host, pos, items, options)
26966 );
26967 }
26968 function openWallpaperMenuImmediate(host, pos, items, options = {}) {
26969 if (items.length === 0) {
26970 return;
26971 }
26972 items = items.slice().sort((a, b) => {
26973 const sa = typeof a.sort === "number" ? a.sort : 100;
26974 const sb = typeof b.sort === "number" ? b.sort : 100;
26975 if (sa !== sb) {
26976 return sa - sb;
26977 }
26978 return a.label.localeCompare(b.label);
26979 });
26980 const menu = document.createElement("wpd-context-menu");
26981 menu.setAttribute("open", "");
26982 menu.classList.add(MENU_CLASS);
26983 menu.style.left = `${pos.x}px`;
26984 menu.style.top = `${pos.y}px`;
26985 const itemById = /* @__PURE__ */ new Map();
26986 let activeFlyout2 = null;
26987 let activeFlyoutParent = null;
26988 const closeActiveFlyout = () => {
26989 if (activeFlyout2) {
26990 activeFlyout2.remove();
26991 activeFlyout2 = null;
26992 activeFlyoutParent = null;
26993 }
26994 };
26995 for (const item of items) {
26996 itemById.set(item.id, item);
26997 const opt = document.createElement("wpd-context-menu-option");
26998 opt.dataset.menuItemId = item.id;
26999 opt.setAttribute("value", item.id);
27000 if (item.heading) {
27001 opt.setAttribute("heading", "");
27002 }
27003 if (item.disabled) {
27004 opt.setAttribute("disabled", "");
27005 }
27006 if (item.icon) {
27007 opt.setAttribute("icon", sanitizeClass(item.icon));
27008 }
27009 const hasChildren2 = Array.isArray(item.children) && item.children.length > 0;
27010 if (hasChildren2) {
27011 opt.setAttribute("has-children", "");
27012 }
27013 opt.textContent = item.label;
27014 opt.addEventListener("mouseenter", () => {
27015 if (hasChildren2) {
27016 openFlyout2(item, opt);
27017 return;
27018 }
27019 closeActiveFlyout();
27020 });
27021 menu.appendChild(opt);
27022 }
27023 menu.addEventListener("wpd-context-menu-pick", (e) => {
27024 const detail = e.detail;
27025 const item = itemById.get(detail.id) ?? null;
27026 if (!item) {
27027 return;
27028 }
27029 if (Array.isArray(item.children) && item.children.length > 0) {
27030 e.stopPropagation();
27031 if (activeFlyoutParent && activeFlyoutParent.id === item.id) {
27032 closeActiveFlyout();
27033 return;
27034 }
27035 const anchor = menu.querySelector(
27036 `[data-menu-item-id="${item.id}"]`
27037 );
27038 if (anchor) {
27039 openFlyout2(item, anchor);
27040 }
27041 return;
27042 }
27043 closeWallpaperMenu();
27044 void item.onClick(new MouseEvent("click"));
27045 });
27046 function openFlyout2(parent, anchor) {
27047 closeActiveFlyout();
27048 const fly = document.createElement("wpd-context-menu");
27049 fly.setAttribute("open", "");
27050 fly.classList.add(MENU_CLASS, `${MENU_CLASS}--flyout`);
27051 fly.dataset.parentId = parent.id;
27052 const sortedKids = (parent.children ?? []).slice().sort((a, b) => {
27053 const sa = typeof a.sort === "number" ? a.sort : 100;
27054 const sb = typeof b.sort === "number" ? b.sort : 100;
27055 if (sa !== sb) {
27056 return sa - sb;
27057 }
27058 return a.label.localeCompare(b.label);
27059 });
27060 for (const child of sortedKids) {
27061 const kopt = document.createElement("wpd-context-menu-option");
27062 kopt.dataset.menuItemId = child.id;
27063 kopt.setAttribute("value", child.id);
27064 if (child.icon) {
27065 kopt.setAttribute("icon", sanitizeClass(child.icon));
27066 }
27067 if (child.disabled) {
27068 kopt.setAttribute("disabled", "");
27069 }
27070 if (child.checked) {
27071 kopt.setAttribute("checked", "");
27072 }
27073 kopt.textContent = child.label;
27074 kopt.addEventListener("wpd-context-menu-pick", (e) => {
27075 e.stopPropagation();
27076 closeWallpaperMenu();
27077 void child.onClick(new MouseEvent("click"));
27078 });
27079 fly.appendChild(kopt);
27080 }
27081 document.body.appendChild(fly);
27082 activeFlyout2 = fly;
27083 activeFlyoutParent = parent;
27084 positionFlyout2(fly, anchor);
27085 }
27086 function positionFlyout2(fly, anchor) {
27087 const ar = anchor.getBoundingClientRect();
27088 fly.style.position = "fixed";
27089 fly.style.left = `${ar.right}px`;
27090 fly.style.top = `${ar.top}px`;
27091 const fr = fly.getBoundingClientRect();
27092 if (fr.right > window.innerWidth) {
27093 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
27094 }
27095 if (fr.bottom > window.innerHeight) {
27096 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
27097 }
27098 }
27099 host.appendChild(menu);
27100 activeMenu = menu;
27101 const rect = menu.getBoundingClientRect();
27102 if (rect.right > window.innerWidth) {
27103 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
27104 }
27105 if (rect.bottom > window.innerHeight) {
27106 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
27107 }
27108 const detach = attachDismissable(menu, {
27109 close: () => closeWallpaperMenu(),
27110 siblingSelectors: [`.${MENU_CLASS}--flyout`],
27111 excludeOutsideTarget: options.excludeOutsideTarget
27112 });
27113 menu.addEventListener("wallpaper-menu-closed", detach);
27114 doAction("desktop-mode.wallpaper-menu.opened", { items: items.map((i) => i.id) });
27115 }
27116 function closeWallpaperMenu() {
27117 if (!activeMenu) {
27118 return;
27119 }
27120 document.querySelectorAll(`.${MENU_CLASS}--flyout`).forEach((el) => el.remove());
27121 activeMenu.dispatchEvent(new CustomEvent("wallpaper-menu-closed"));
27122 activeMenu.remove();
27123 activeMenu = null;
27124 doAction("desktop-mode.wallpaper-menu.closed", {});
27125 }
27126 function buildMenuItems(deps2) {
27127 const builtIn = [
27128 {
27129 id: "create-folder",
27130 label: deps2.labels.createFolder,
27131 icon: "dashicons-portfolio",
27132 sort: 10,
27133 onClick: () => deps2.createFolder()
27134 },
27135 {
27136 id: "new-url",
27137 label: deps2.labels.newUrl,
27138 icon: "dashicons-admin-links",
27139 sort: 12,
27140 onClick: () => deps2.createUrl()
27141 },
27142 {
27143 id: "sort-by",
27144 label: deps2.labels.sortHeading,
27145 icon: "dashicons-sort",
27146 sort: 16,
27147 onClick: () => void 0,
27148 children: [
27149 {
27150 id: "sort-name-asc",
27151 label: deps2.labels.sortNameAsc,
27152 sort: 10,
27153 checked: deps2.currentSortMode === "name-asc",
27154 onClick: () => deps2.sortIcons("name-asc")
27155 },
27156 {
27157 id: "sort-name-desc",
27158 label: deps2.labels.sortNameDesc,
27159 sort: 20,
27160 checked: deps2.currentSortMode === "name-desc",
27161 onClick: () => deps2.sortIcons("name-desc")
27162 },
27163 {
27164 id: "sort-date-desc",
27165 label: deps2.labels.sortDateDesc,
27166 sort: 30,
27167 checked: deps2.currentSortMode === "date-desc",
27168 onClick: () => deps2.sortIcons("date-desc")
27169 },
27170 {
27171 id: "sort-date-asc",
27172 label: deps2.labels.sortDateAsc,
27173 sort: 40,
27174 checked: deps2.currentSortMode === "date-asc",
27175 onClick: () => deps2.sortIcons("date-asc")
27176 }
27177 ]
27178 },
27179 ...deps2.includeShowDesktop === false ? [] : [
27180 {
27181 id: "show-desktop",
27182 label: deps2.labels.showDesktop,
27183 icon: "dashicons-desktop",
27184 sort: 20,
27185 onClick: () => deps2.toggleShowDesktop()
27186 }
27187 ],
27188 {
27189 id: "os-settings",
27190 label: deps2.labels.osSettings,
27191 icon: "dashicons-admin-generic",
27192 sort: 30,
27193 onClick: () => deps2.openOsSettings()
27194 }
27195 ];
27196 const serverItems = (deps2.serverItems ?? []).map(
27197 (s) => serverItemToMenuItem(s, deps2)
27198 );
27199 const merged = [...builtIn, ...serverItems];
27200 const filtered = applyFilters(
27201 "desktop-mode.wallpaper-context-menu",
27202 merged
27203 );
27204 return Array.isArray(filtered) ? filtered : merged;
27205 }
27206 function serverItemToMenuItem(server, deps2) {
27207 return {
27208 id: server.id,
27209 label: server.label,
27210 icon: server.icon,
27211 sort: server.sort,
27212 disabled: server.disabled,
27213 onClick: () => {
27214 if (server.callbackId) {
27215 const cb = deps2.serverCallbacks?.[server.callbackId];
27216 if (typeof cb === "function") {
27217 return cb();
27218 }
27219 }
27220 doAction("desktop-mode.wallpaper-context-menu.activated", {
27221 id: server.id,
27222 callbackId: server.callbackId ?? ""
27223 });
27224 }
27225 };
27226 }
27227 function sanitizeClass(raw) {
27228 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
27229 }
27230 const ROOT_CLASS = "desktop-mode-url-dialog";
27231 let active = null;
27232 function closeUrlDialog() {
27233 if (!active) {
27234 return;
27235 }
27236 active.dispatchEvent(new CustomEvent("url-dialog-closed"));
27237 active.remove();
27238 active = null;
27239 doAction("desktop-mode.files.url-dialog.closed", {});
27240 }
27241 function openUrlDialog(options) {
27242 closeUrlDialog();
27243 const decision = applyFilters(
27244 "desktop-mode.files.url-dialog",
27245 null,
27246 options
27247 );
27248 if (decision === false) {
27249 return;
27250 }
27251 const overlay = document.createElement("div");
27252 overlay.className = `${ROOT_CLASS}__overlay desktop-mode-create-folder-dialog__overlay`;
27253 overlay.setAttribute("role", "presentation");
27254 const dialog2 = document.createElement("div");
27255 dialog2.className = `${ROOT_CLASS} desktop-mode-create-folder-dialog`;
27256 dialog2.setAttribute("role", "dialog");
27257 dialog2.setAttribute("aria-modal", "true");
27258 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS}-title`);
27259 const title = document.createElement("h2");
27260 title.id = `${ROOT_CLASS}-title`;
27261 title.className = "desktop-mode-create-folder-dialog__title";
27262 title.textContent = options.title;
27263 dialog2.appendChild(title);
27264 if (options.description) {
27265 const desc = document.createElement("p");
27266 desc.className = `${ROOT_CLASS}__description`;
27267 desc.textContent = options.description;
27268 dialog2.appendChild(desc);
27269 }
27270 const nameField = document.createElement("wpd-text-field");
27271 nameField.setAttribute("label", options.nameLabel ?? "Name");
27272 nameField.setAttribute("value", options.initialName ?? "");
27273 nameField.setAttribute("placeholder", "My web app");
27274 nameField.setAttribute("autocomplete", "off");
27275 dialog2.appendChild(nameField);
27276 const urlField = document.createElement("wpd-text-field");
27277 urlField.setAttribute("label", options.urlLabel ?? "URL");
27278 urlField.setAttribute("value", options.initialUrl ?? "https://");
27279 urlField.setAttribute("placeholder", "https://example.com");
27280 urlField.setAttribute("type", "url");
27281 urlField.setAttribute("autocomplete", "off");
27282 dialog2.appendChild(urlField);
27283 const error = document.createElement("p");
27284 error.className = "desktop-mode-create-folder-dialog__error";
27285 error.hidden = true;
27286 error.setAttribute("role", "alert");
27287 dialog2.appendChild(error);
27288 const actions = document.createElement("div");
27289 actions.className = "desktop-mode-create-folder-dialog__actions";
27290 const cancel = document.createElement("button");
27291 cancel.type = "button";
27292 cancel.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--secondary";
27293 cancel.textContent = "Cancel";
27294 const submit = document.createElement("button");
27295 submit.type = "button";
27296 submit.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--primary";
27297 submit.textContent = options.submitLabel ?? "Create";
27298 actions.appendChild(cancel);
27299 actions.appendChild(submit);
27300 dialog2.appendChild(actions);
27301 overlay.appendChild(dialog2);
27302 document.body.appendChild(overlay);
27303 active = overlay;
27304 queueMicrotask(() => {
27305 const input = nameField.shadowRoot?.querySelector("input");
27306 input?.focus();
27307 input?.select();
27308 });
27309 doAction("desktop-mode.files.url-dialog.opened", {});
27310 const readValue = (field) => {
27311 const v = field.value;
27312 if (typeof v === "string") {
27313 return v;
27314 }
27315 return field.shadowRoot?.querySelector("input")?.value ?? "";
27316 };
27317 const setBusy = (busy) => {
27318 nameField.disabled = busy;
27319 urlField.disabled = busy;
27320 cancel.disabled = busy;
27321 submit.disabled = busy;
27322 dialog2.classList.toggle("desktop-mode-create-folder-dialog--busy", busy);
27323 };
27324 const showError = (msg) => {
27325 error.textContent = msg;
27326 error.hidden = false;
27327 };
27328 const doCancel = () => {
27329 closeUrlDialog();
27330 options.onCancel?.();
27331 };
27332 const doSubmit = async () => {
27333 const url = readValue(urlField).trim();
27334 if (!url) {
27335 showError("Please enter a URL.");
27336 return;
27337 }
27338 const finalUrl = /^[a-z][a-z0-9+\-.]*:/i.test(url) ? url : `https://${url}`;
27339 try {
27340 new URL(finalUrl);
27341 } catch {
27342 showError("That doesn't look like a valid URL.");
27343 return;
27344 }
27345 const name = readValue(nameField).trim();
27346 error.hidden = true;
27347 setBusy(true);
27348 try {
27349 await options.onSubmit({ name, url: finalUrl });
27350 closeUrlDialog();
27351 } catch (err) {
27352 setBusy(false);
27353 showError(err instanceof Error ? err.message : "Could not save.");
27354 }
27355 };
27356 cancel.addEventListener("click", () => doCancel());
27357 submit.addEventListener("click", () => void doSubmit());
27358 overlay.addEventListener("click", (e) => {
27359 if (e.target === overlay) {
27360 doCancel();
27361 }
27362 });
27363 const onKey = (e) => {
27364 if (e.key === "Escape") {
27365 e.preventDefault();
27366 doCancel();
27367 } else if (e.key === "Enter" && !e.isComposing) {
27368 e.preventDefault();
27369 void doSubmit();
27370 }
27371 };
27372 dialog2.addEventListener("keydown", onKey);
27373 overlay.addEventListener("url-dialog-closed", () => {
27374 dialog2.removeEventListener("keydown", onKey);
27375 });
27376 }
27377 const _earlyReadyQueue = [];
27378 let _earlyReady = false;
27379 (function installEarlyDesktopShim() {
27380 const w = window;
27381 if (!w.wp) {
27382 w.wp = {};
27383 }
27384 if (w.wp.desktop) {
27385 return;
27386 }
27387 const shim = {
27388 whenReady(cb) {
27389 if (typeof cb !== "function") {
27390 return;
27391 }
27392 if (_earlyReady) {
27393 Promise.resolve().then(cb);
27394 return;
27395 }
27396 _earlyReadyQueue.push(cb);
27397 },
27398 ready(cb) {
27399 shim.whenReady(cb);
27400 },
27401 isReady() {
27402 return _earlyReady;
27403 }
27404 };
27405 w.wp.desktop = shim;
27406 })();
27407 const OS_SETTINGS_WINDOW_ID = "desktop-mode-os-settings";
27408 let _idleBootQueue = [];
27409 let _idleBootTimeout = Number.POSITIVE_INFINITY;
27410 let _idleBootScheduled = false;
27411 function scheduleIdleBoot(cb, timeout = 1500) {
27412 _idleBootQueue.push(cb);
27413 if (timeout < _idleBootTimeout) {
27414 _idleBootTimeout = timeout;
27415 }
27416 if (_idleBootScheduled) {
27417 return;
27418 }
27419 _idleBootScheduled = true;
27420 const drain = () => {
27421 const callbacks = _idleBootQueue;
27422 _idleBootQueue = [];
27423 _idleBootTimeout = Number.POSITIVE_INFINITY;
27424 _idleBootScheduled = false;
27425 for (const fn of callbacks) {
27426 try {
27427 fn();
27428 } catch (err) {
27429 if (typeof console !== "undefined") {
27430 console.error(
27431 "[desktop-mode] scheduleIdleBoot callback threw:",
27432 err
27433 );
27434 }
27435 }
27436 }
27437 };
27438 if (typeof window.requestIdleCallback === "function") {
27439 window.requestIdleCallback(drain, { timeout: _idleBootTimeout });
27440 } else {
27441 window.setTimeout(drain, 0);
27442 }
27443 }
27444 function init() {
27445 const config = window.desktopModeConfig;
27446 if (!config) {
27447 return;
27448 }
27449 const desktopArea = document.getElementById("desktop-mode-area");
27450 if (!desktopArea) {
27451 return;
27452 }
27453 const manager = new WindowManager(desktopArea);
27454 const wallpaperEl = document.getElementById("desktop-mode-wallpaper");
27455 const pluginUrl = config.pluginUrl || "";
27456 let wallpaperLayer = null;
27457 if (wallpaperEl) {
27458 wallpaperLayer = new WallpaperLayer(wallpaperEl, pluginUrl);
27459 }
27460 const widgetsEl = document.getElementById("desktop-mode-widgets");
27461 let widgetLayer = null;
27462 registerBuiltInWidgets();
27463 installDefaultDockRailRenderer();
27464 if (widgetsEl) {
27465 widgetLayer = new WidgetLayer(widgetsEl, pluginUrl);
27466 }
27467 registerModule({
27468 id: "pixijs",
27469 url: `${pluginUrl}/assets/vendor/pixi.min.js`,
27470 isReady: () => typeof window.PIXI !== "undefined"
27471 });
27472 const osSettings = new OsSettings(
27473 {
27474 mediaUrl: config.mediaUrl,
27475 restNonce: config.restNonce,
27476 canUpload: !!config.canUpload,
27477 isAdmin: !!config.currentUserIsAdmin,
27478 aiPlatformSettings: config.aiPlatformSettings ?? null,
27479 aiPlatformSettingsUrl: config.aiPlatformSettingsUrl ?? "",
27480 extendedOptions: config.extendedOptions ?? null,
27481 extendedOptionsUrl: config.extendedOptionsUrl ?? "",
27482 osSettingsPanelBundleUrl: config.osSettingsPanelBundleUrl ?? ""
27483 },
27484 wallpaperLayer ?? new WallpaperLayer(document.createElement("div"), pluginUrl)
27485 );
27486 osSettings.apply();
27487 const aiAssistant = new AiAssistantStub(
27488 {
27489 aiSearchUrl: config.aiSearchUrl ?? "",
27490 aiSearchStreamUrl: config.aiSearchStreamUrl ?? "",
27491 restNonce: config.restNonce,
27492 // Transport picker lives in OS Settings → AI Settings. Read
27493 // live (not captured at construction) so a change applies on
27494 // the next search without a page reload.
27495 getTransport: () => osSettings.getOsSettingsSnapshot().ai.transport
27496 },
27497 config.aiAssistantBundleUrl ?? ""
27498 );
27499 aiAssistant.attachAsk(
27500 createAsk({
27501 config: () => config,
27502 fallbackContext: () => ({
27503 close: () => aiAssistant.close(),
27504 openInWindow: (url, title, icon) => {
27505 manager.open({
27506 url,
27507 title,
27508 icon: icon ?? "dashicons-admin-generic"
27509 });
27510 },
27511 confirm: (msg) => wpdConfirm({ message: msg })
27512 })
27513 })
27514 );
27515 const dragBridge = new DragBridge();
27516 const dragManager = new DragManager();
27517 document.addEventListener(DRAG_EVENTS.START, (e) => {
27518 const detail = e.detail;
27519 const payload = detail?.payload;
27520 if (!payload) {
27521 return;
27522 }
27523 if (payload.type !== "shortcut" && payload.type !== "desktop-file") {
27524 return;
27525 }
27526 const bridgePayload = payload.data?.bridgePayload;
27527 if (bridgePayload) {
27528 dragBridge.start(bridgePayload);
27529 }
27530 });
27531 document.addEventListener(DRAG_EVENTS.END, () => {
27532 dragBridge.end();
27533 });
27534 scheduleIdleBoot(() => installIframeDropTargets(dragManager));
27535 window.addEventListener("message", (e) => {
27536 if (e.origin !== window.location.origin) {
27537 return;
27538 }
27539 const data = e.data;
27540 if (!data || data.type !== "desktop-mode-drop-failed") {
27541 return;
27542 }
27543 showToast({
27544 message: "Could not insert into the editor."
27545 });
27546 });
27547 registerPalette({
27548 id: "desktop-mode-ai-assistant",
27549 label: "AI Assistant",
27550 open: () => aiAssistant.open(),
27551 close: () => aiAssistant.close(),
27552 isOpen: () => aiAssistant.isOpen
27553 });
27554 installPaletteShortcut();
27555 installWindowSwitcherShortcut(manager);
27556 installDesktopArrowShortcuts(manager);
27557 scheduleIdleBoot(() => {
27558 new IframeCommandBridge({
27559 manager,
27560 adminUrl: config.adminUrl
27561 }).install();
27562 new ShellCommandHarvester({
27563 manager,
27564 adminUrl: config.adminUrl
27565 }).install();
27566 });
27567 document.addEventListener("desktop-mode-open-ai", () => {
27568 openPaletteOnly("desktop-mode-ai-assistant");
27569 });
27570 const bottomDockEl = document.getElementById("desktop-mode-dock");
27571 const shellEl = document.getElementById("desktop-mode-shell");
27572 const shellBody = shellEl?.querySelector(
27573 ".desktop-mode-shell__body"
27574 );
27575 let layoutDispatcher = null;
27576 const nativeWindows = createNativeWindowSync({
27577 manager,
27578 appendSystemTile: (item) => layoutDispatcher?.appendSystemTile(item),
27579 removeSystemTile: (id) => layoutDispatcher?.removeSystemTile(id)
27580 });
27581 const syncNativeWindows = nativeWindows.sync;
27582 bindNativeUrlRemap({
27583 getSnapshot: () => osSettings.getOsSettingsSnapshot(),
27584 openById: (id) => nativeWindows.openById(id),
27585 adminUrl: config.adminUrl
27586 });
27587 const findDockEntryForUrl2 = (url) => {
27588 const targetSlug = deriveWindowId(url, config.adminUrl);
27589 const items = layoutDispatcher ? layoutDispatcher.getMenuItems() : config.dockItems ?? [];
27590 for (const item of items) {
27591 if (deriveWindowId(item.url, config.adminUrl) === targetSlug) {
27592 return {
27593 title: item.title,
27594 icon: item.icon,
27595 url: item.url,
27596 submenu: item.submenu,
27597 multi: item.multi
27598 };
27599 }
27600 for (const sub of item.submenu ?? []) {
27601 if (deriveWindowId(sub.url, config.adminUrl) === targetSlug) {
27602 return {
27603 title: sub.title,
27604 // Sub-menu entries inherit the parent tile's
27605 // icon — that's the dock's own convention and
27606 // avoids painting a generic glyph on a window
27607 // the user knows by its parent's identity.
27608 icon: item.icon,
27609 // `url` holds the PARENT tile's landing page, so
27610 // the new window's synthetic "back to parent"
27611 // tab links to the dock URL (themes.php) rather
27612 // than to the sub-page itself.
27613 url: item.url,
27614 multi: item.multi
27615 };
27616 }
27617 }
27618 }
27619 return null;
27620 };
27621 bindAdminLinkDispatch({
27622 adminUrl: config.adminUrl,
27623 deriveSlug: (url) => deriveWindowId(url, config.adminUrl),
27624 openWindow: (windowConfig) => {
27625 void manager.open(windowConfig);
27626 },
27627 findDockEntry: findDockEntryForUrl2
27628 });
27629 registerNativeUrlRemap({
27630 id: "desktop-mode-posts",
27631 nativeWindowId: "desktop-mode-posts",
27632 matches: (_url, parsed) => {
27633 if (!parsed.pathname.endsWith("/edit.php")) {
27634 return false;
27635 }
27636 const postType = parsed.searchParams.get("post_type");
27637 return !postType || postType === "post";
27638 },
27639 enabled: (snapshot) => snapshot.nativePostsEnabled === true
27640 });
27641 registerNativeUrlRemap({
27642 id: "desktop-mode-pages",
27643 nativeWindowId: "desktop-mode-pages",
27644 matches: (_url, parsed) => {
27645 if (!parsed.pathname.endsWith("/edit.php")) {
27646 return false;
27647 }
27648 return parsed.searchParams.get("post_type") === "page";
27649 },
27650 enabled: (snapshot) => snapshot.nativePagesEnabled === true
27651 });
27652 registerNativeUrlRemap({
27653 id: "desktop-mode-users",
27654 nativeWindowId: "desktop-mode-users",
27655 matches: (_url, parsed) => parsed.pathname.endsWith("/users.php"),
27656 enabled: (snapshot) => snapshot.nativeUsersEnabled === true
27657 });
27658 registerNativeUrlRemap({
27659 id: "desktop-mode-user-edit",
27660 nativeWindowId: "desktop-mode-user-edit",
27661 matches: (_url, parsed) => {
27662 const path = parsed.pathname;
27663 if (path.endsWith("/profile.php")) {
27664 return true;
27665 }
27666 if (path.endsWith("/user-edit.php")) {
27667 return parsed.searchParams.has("user_id");
27668 }
27669 return false;
27670 },
27671 enabled: (snapshot) => snapshot.nativeUsersEnabled === true,
27672 onMatch: (_url, parsed) => {
27673 const userId = parseInt(
27674 parsed.searchParams.get("user_id") ?? "0",
27675 10
27676 );
27677 if (userId > 0) {
27678 setUserEditTarget(userId);
27679 }
27680 }
27681 });
27682 registerNativeUrlRemap({
27683 id: "desktop-mode-comments",
27684 nativeWindowId: "desktop-mode-comments",
27685 matches: (_url, parsed) => parsed.pathname.endsWith("/edit-comments.php"),
27686 enabled: (snapshot) => snapshot.nativeCommentsEnabled === true
27687 });
27688 registerNativeUrlRemap({
27689 id: "desktop-mode-plugins",
27690 nativeWindowId: "desktop-mode-plugins",
27691 matches: (_url, parsed) => {
27692 const path = parsed.pathname;
27693 return path.endsWith("/plugins.php") || path.endsWith("/plugin-install.php");
27694 },
27695 enabled: (snapshot) => snapshot.nativePluginsEnabled === true,
27696 onMatch: (_url, parsed) => {
27697 const tab = parsed.pathname.endsWith("/plugin-install.php") ? "browse" : "installed";
27698 void Promise.resolve().then(() => tabTarget).then((m) => {
27699 m.setPluginsWindowTab(tab);
27700 });
27701 }
27702 });
27703 if (bottomDockEl && shellEl && shellBody && config.dockItems) {
27704 desktopArea.classList.add("desktop-mode-area--with-dock");
27705 const initialLayout = osSettings.getOsSettingsSnapshot().desktopLayout;
27706 const renderIcons2 = (icons) => {
27707 renderDesktopIcons(desktopArea, icons, {
27708 openWindow: nativeWindows.openById,
27709 manager,
27710 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
27711 });
27712 };
27713 layoutDispatcher = createLayoutDispatcher(
27714 {
27715 shellRoot: shellEl,
27716 shellBody,
27717 bottomDockEl,
27718 desktopArea,
27719 windowManager: manager,
27720 adminUrl: config.adminUrl,
27721 renderIcons: renderIcons2,
27722 getSettings: () => {
27723 const snap = osSettings.getOsSettingsSnapshot();
27724 return {
27725 itemVisibility: snap.itemVisibility,
27726 dockOrder: snap.dockOrder
27727 };
27728 }
27729 },
27730 initialLayout,
27731 config.dockItems,
27732 config.desktopIcons
27733 );
27734 layoutDispatcher.appendSystemTile(
27735 {
27736 id: OS_SETTINGS_WINDOW_ID,
27737 title: "OS Settings",
27738 icon: "dashicons-desktop",
27739 // "Open" for the dock dot means "open on the currently
27740 // active desktop." OS Settings on another desktop
27741 // shouldn't paint the dot on the active view.
27742 isOpen: () => {
27743 const win = manager.getById(OS_SETTINGS_WINDOW_ID);
27744 if (!win) {
27745 return false;
27746 }
27747 return (win.config.desktopId || manager.getActiveDesktopId()) === manager.getActiveDesktopId();
27748 },
27749 onOpen: openOsSettings
27750 },
27751 "core"
27752 );
27753 if (!isStandaloneDisplay()) {
27754 layoutDispatcher.appendSystemTile(
27755 getInstallTileDef(
27756 config.pwa?.appName || "WordPress",
27757 showToast
27758 ),
27759 "core"
27760 );
27761 }
27762 window.matchMedia("(display-mode: standalone)").addEventListener("change", (e) => {
27763 if (e.matches) {
27764 layoutDispatcher?.removeSystemTile(
27765 "desktop-mode-pwa-install"
27766 );
27767 }
27768 });
27769 void isLikelyInstalled().then((installed2) => {
27770 if (installed2) {
27771 layoutDispatcher?.removeSystemTile(
27772 "desktop-mode-pwa-install"
27773 );
27774 }
27775 });
27776 }
27777 function openOsSettings(opts = {}) {
27778 if (opts.tabId) {
27779 osSettings.activeTabId = opts.tabId;
27780 }
27781 void manager.open({
27782 id: OS_SETTINGS_WINDOW_ID,
27783 baseId: OS_SETTINGS_WINDOW_ID,
27784 url: "#os-settings",
27785 title: "OS Settings",
27786 icon: "dashicons-desktop",
27787 native: true,
27788 render: (body) => osSettings.renderPanel(body),
27789 width: 820,
27790 height: 720,
27791 minWidth: 560,
27792 minHeight: 480
27793 });
27794 if (opts.tabId) {
27795 osSettings.focusTab(opts.tabId);
27796 }
27797 }
27798 function openBugReport() {
27799 void manager.open({
27800 id: BUG_REPORT_WINDOW_ID,
27801 baseId: BUG_REPORT_WINDOW_ID,
27802 url: `#${BUG_REPORT_WINDOW_ID}`,
27803 title: "Report a bug",
27804 icon: "dashicons-buddicons-replies",
27805 native: true,
27806 render: (body) => renderBugReport(body),
27807 width: 560,
27808 height: 620,
27809 minWidth: 420,
27810 minHeight: 480
27811 });
27812 }
27813 document.addEventListener("desktop-mode-open-bug-report", () => {
27814 openBugReport();
27815 });
27816 if (layoutDispatcher) {
27817 layoutDispatcher.appendSystemTile(
27818 {
27819 id: BUG_REPORT_WINDOW_ID,
27820 title: "Report a bug",
27821 icon: "dashicons-buddicons-replies",
27822 isOpen: () => {
27823 const win = manager.getById(BUG_REPORT_WINDOW_ID);
27824 if (!win) {
27825 return false;
27826 }
27827 return (win.config.desktopId || manager.getActiveDesktopId()) === manager.getActiveDesktopId();
27828 },
27829 onOpen: openBugReport
27830 },
27831 "core"
27832 );
27833 layoutDispatcher.appendSystemTile(
27834 getExitDesktopModeTileDef(),
27835 "core"
27836 );
27837 }
27838 const dock = layoutDispatcher?.getPrimary() ?? null;
27839 void syncNativeWindows(
27840 Array.isArray(config.nativeWindows) ? config.nativeWindows : []
27841 );
27842 const hasSession = hasRestorableSession(config.session);
27843 const sessionRestore = hasSession ? restoreSession(manager, config, desktopArea).catch((err) => {
27844 if (typeof console !== "undefined") {
27845 console.error("[desktop-mode] session restore failed:", err);
27846 }
27847 }) : Promise.resolve();
27848 const defaultEnabled = config.defaultWindow?.enabled !== false;
27849 const defaultUrlEarly = config.defaultWindow?.url ?? "";
27850 const isNativeDefault = typeof defaultUrlEarly === "string" && defaultUrlEarly.startsWith("native:");
27851 if (shouldAutoOpenCurrentPage({
27852 fromPortal: config.fromPortal,
27853 fromPortalIntent: config.fromPortalIntent,
27854 hasSession,
27855 defaultEnabled,
27856 isNativeDefault
27857 })) {
27858 void sessionRestore.then(
27859 () => openCurrentPage(manager, config).catch((err) => {
27860 if (typeof console !== "undefined") {
27861 console.error("[desktop-mode] openCurrentPage failed:", err);
27862 }
27863 })
27864 );
27865 }
27866 const saveSession = createSessionSaver(manager, config);
27867 wireSessionEvents(saveSession);
27868 const setDefaultWindow = async (url) => {
27869 try {
27870 const response = await trackedFetch(
27871 manager,
27872 config.defaultWindowUrl,
27873 {
27874 method: "POST",
27875 credentials: "same-origin",
27876 headers: {
27877 "Content-Type": "application/json",
27878 "X-WP-Nonce": config.restNonce
27879 },
27880 body: JSON.stringify({ url })
27881 },
27882 { source: "desktop-mode/default-window" }
27883 );
27884 if (!response.ok) {
27885 throw new Error(`HTTP ${response.status}`);
27886 }
27887 const data = await response.json();
27888 config.defaultWindow = data;
27889 document.dispatchEvent(
27890 new CustomEvent("desktop-mode-default-window-changed", {
27891 detail: data
27892 })
27893 );
27894 } catch (err) {
27895 doAction(HOOKS.SHELL_ERROR, { scope: "default-window-save", error: err });
27896 if (typeof console !== "undefined") {
27897 console.error(
27898 "[desktop-mode] Failed to save default window:",
27899 err
27900 );
27901 }
27902 }
27903 };
27904 manager.onToggleStartupRequested = (win) => {
27905 const currentPref = config.defaultWindow;
27906 const isNative = !!win.config.native;
27907 const winValue = isNative ? `native:${win.id}` : win.getCurrentUrl();
27908 const matchesCurrent = isNative ? currentPref?.url === winValue : urlMatchKey(currentPref?.url ?? "") === urlMatchKey(winValue);
27909 const alreadyDefault = !!currentPref?.enabled && matchesCurrent;
27910 void setDefaultWindow(alreadyDefault ? null : winValue);
27911 };
27912 if (config.defaultWindow?.enabled && config.fromPortal && !config.fromPortalIntent && !hasSession && isNativeDefault) {
27913 const nativeId = defaultUrlEarly.slice("native:".length);
27914 queueMicrotask(() => {
27915 if (nativeId === OS_SETTINGS_WINDOW_ID) {
27916 openOsSettings();
27917 return;
27918 }
27919 void nativeWindows.openById(nativeId);
27920 });
27921 }
27922 const placeSystemTile = (item) => {
27923 layoutDispatcher?.appendSystemTile(item);
27924 };
27925 const syncServerWidgets = createWidgetRegistrySync({
27926 layer: widgetLayer
27927 });
27928 void syncServerWidgets(
27929 Array.isArray(config.serverWidgets) ? config.serverWidgets : []
27930 );
27931 const syncServerWallpapers = createWallpaperRegistrySync({
27932 osSettings
27933 });
27934 void syncServerWallpapers(
27935 Array.isArray(config.serverWallpapers) ? config.serverWallpapers : []
27936 );
27937 const syncServerCommands = createCommandRegistrySync();
27938 void syncServerCommands(
27939 Array.isArray(config.serverCommandScripts) ? config.serverCommandScripts : [],
27940 Array.isArray(config.serverCommands) ? config.serverCommands : []
27941 );
27942 const syncServerSettingsTabs = createSettingsTabRegistrySync();
27943 void syncServerSettingsTabs(
27944 Array.isArray(config.serverSettingsTabScripts) ? config.serverSettingsTabScripts : [],
27945 Array.isArray(config.serverSettingsTabs) ? config.serverSettingsTabs : []
27946 );
27947 const syncServerTitleBarButtons = createTitleBarButtonRegistrySync();
27948 void syncServerTitleBarButtons(
27949 Array.isArray(config.serverTitleBarButtonScripts) ? config.serverTitleBarButtonScripts : []
27950 );
27951 const syncServerDockRailRenderers = createDockRailRendererSync();
27952 void syncServerDockRailRenderers(
27953 Array.isArray(config.serverDockRailRendererScripts) ? config.serverDockRailRendererScripts : []
27954 );
27955 const syncServerWindowThemes = createWindowThemeRegistrySync();
27956 void syncServerWindowThemes(
27957 Array.isArray(config.serverWindowThemeScripts) ? config.serverWindowThemeScripts : [],
27958 Array.isArray(config.serverWindowThemes) ? config.serverWindowThemes : []
27959 );
27960 registerBuiltInControls();
27961 const syncServerWindowControls = createWindowControlRegistrySync();
27962 void syncServerWindowControls(
27963 Array.isArray(config.serverWindowControlScripts) ? config.serverWindowControlScripts : [],
27964 Array.isArray(config.serverWindowControls) ? config.serverWindowControls : []
27965 );
27966 const syncServerWindowSlots = createWindowSlotRegistrySync();
27967 void syncServerWindowSlots(
27968 Array.isArray(config.serverWindowSlotScripts) ? config.serverWindowSlotScripts : [],
27969 Array.isArray(config.serverWindowSlots) ? config.serverWindowSlots : []
27970 );
27971 applyServerWindowNotices(
27972 Array.isArray(config.serverWindowNotices) ? config.serverWindowNotices : []
27973 );
27974 const syncServerWindowChromes = createWindowChromeRegistrySync();
27975 void syncServerWindowChromes(
27976 Array.isArray(config.serverWindowChromeScripts) ? config.serverWindowChromeScripts : [],
27977 Array.isArray(config.serverWindowChromes) ? config.serverWindowChromes : []
27978 );
27979 const connectionBridge = createConnectionBridge(manager);
27980 attachBroadcastBus(manager);
27981 scheduleIdleBoot(() => installBroadcastReceiver());
27982 installWindowLoadingTransitions();
27983 addAction(
27984 "desktop-mode.shell.toast",
27985 "desktop-mode/shell-toast",
27986 (payload) => {
27987 if (!payload || typeof payload.message !== "string") {
27988 return;
27989 }
27990 showToast({
27991 message: payload.message,
27992 action: payload.action,
27993 duration: payload.duration
27994 });
27995 }
27996 );
27997 const cfgWithBin = config;
27998 const cfgCountRaw = cfgWithBin.recycleBinCount;
27999 startRecycleBinBadge(
28000 Number(cfgCountRaw) || 0,
28001 typeof cfgWithBin.recycleBinCountUrl === "string" ? cfgWithBin.recycleBinCountUrl : ""
28002 );
28003 registerBuiltInPeekRenderers({
28004 getRecycleBinCount: _currentRecycleBinBadge
28005 });
28006 window.__desktopModeConnectionBridge = connectionBridge;
28007 addAction(HOOKS.WINDOW_CLOSED, "desktop-mode/connection-cleanup", (e) => {
28008 if (e?.windowId) {
28009 connectionBridge.onWindowClosed(e.windowId);
28010 }
28011 });
28012 addAction(HOOKS.IFRAME_READY, "desktop-mode/connection-rearm", (e) => {
28013 if (e?.windowId) {
28014 connectionBridge.onIframeReady(e.windowId);
28015 }
28016 });
28017 const registerWindow = createRegisterWindow(manager);
28018 const renderIcons = (icons) => {
28019 if (layoutDispatcher) {
28020 layoutDispatcher.applyDesktopIcons(icons);
28021 return;
28022 }
28023 renderDesktopIcons(desktopArea, icons, {
28024 openWindow: nativeWindows.openById,
28025 manager,
28026 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
28027 });
28028 };
28029 const refreshMenu = bindMenuRefresh({
28030 layoutDispatcher,
28031 config,
28032 syncNativeWindows,
28033 syncServerWidgets,
28034 syncServerWallpapers,
28035 syncServerCommands,
28036 syncServerSettingsTabs,
28037 syncServerTitleBarButtons,
28038 syncServerDockRailRenderers,
28039 renderIcons
28040 });
28041 osSettings.subscribeOsSettings((snapshot) => {
28042 if (!layoutDispatcher) {
28043 return;
28044 }
28045 const prevLayout = layoutDispatcher.getLayout();
28046 layoutDispatcher.setLayout(snapshot.desktopLayout);
28047 desktopApi.dock = layoutDispatcher.getPrimary();
28048 desktopApi.sideDock = layoutDispatcher.getSide();
28049 desktopApi.desktopLayout = snapshot.desktopLayout;
28050 if (prevLayout === snapshot.desktopLayout) {
28051 layoutDispatcher.refresh();
28052 }
28053 syncShortcutsWithVisibility(
28054 snapshot.itemVisibility,
28055 snapshot.dockPromotedPositions
28056 );
28057 setCurrentLayout(snapshot.desktopLayout);
28058 });
28059 installShortcutsSync(
28060 () => osSettings.getOsSettingsSnapshot().itemVisibility,
28061 () => osSettings.getOsSettingsSnapshot().dockPromotedPositions
28062 );
28063 setCurrentLayout(osSettings.getOsSettingsSnapshot().desktopLayout);
28064 const desktopApi = buildPublicApi({
28065 manager,
28066 dock,
28067 layoutDispatcher,
28068 osSettings,
28069 iconsApi,
28070 filesApi,
28071 saveSession,
28072 widgetLayer,
28073 registerWindow,
28074 openWindowById: nativeWindows.openById,
28075 openNewWindowById: nativeWindows.openNewById,
28076 placeSystemTile,
28077 setDefaultWindow,
28078 refreshMenu,
28079 openOsSettings,
28080 aiAssistant,
28081 dragBridge,
28082 dragManager,
28083 connect: connectionBridge.connect,
28084 getConnection: connectionBridge.getConnection,
28085 config
28086 });
28087 installPublicApi(desktopApi);
28088 scheduleIdleBoot(() => installRecycleBinDropTargets(dragManager));
28089 bootHeartbeatBus();
28090 scheduleIdleBoot(() => bootNonceRefresh());
28091 bootStickyNotes({
28092 host: desktopArea,
28093 config,
28094 getActiveDesktopId: () => manager.getActiveDesktopId(),
28095 openArtifact: (url, title) => {
28096 const id = deriveWindowId(url, config.adminUrl);
28097 void manager.open({
28098 id,
28099 baseId: id,
28100 url,
28101 title,
28102 icon: "dashicons-edit-page"
28103 });
28104 },
28105 onError: (message) => {
28106 showToast({ message });
28107 }
28108 });
28109 installOpenDeps({
28110 openUrl: ({ id, url, title, icon }) => {
28111 if (tryNativeUrlRemap(url)) {
28112 return true;
28113 }
28114 void manager.open({ id, baseId: id, url, title, icon });
28115 return true;
28116 },
28117 openNativeWindow: (id) => nativeWindows.openById(id),
28118 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
28119 });
28120 setUserAssociations(
28121 config.userFileAssociations ?? {}
28122 );
28123 if (typeof config.filesUrl === "string" && config.filesUrl) {
28124 installRestDeps({
28125 baseUrl: config.filesUrl,
28126 nonce: config.restNonce
28127 });
28128 const rootHost = document.getElementById("desktop-mode-area");
28129 if (rootHost) {
28130 const layerHandle = mountFilesLayer(rootHost, 0);
28131 const reveal = () => {
28132 if (!desktopArea.classList.contains("desktop-mode-area--booting")) {
28133 return;
28134 }
28135 requestAnimationFrame(() => {
28136 desktopArea.classList.remove("desktop-mode-area--booting");
28137 });
28138 };
28139 const safetyTimer = setTimeout(reveal, 2e3);
28140 void layerHandle.hydrated.then(() => {
28141 clearTimeout(safetyTimer);
28142 reveal();
28143 });
28144 }
28145 }
28146 scheduleIdleBoot(() => startFilesHeartbeat());
28147 scheduleIdleBoot(() => startFilesRestoreSync());
28148 scheduleIdleBoot(() => bootPresenceProbe());
28149 doAction(HOOKS.COMPONENTS_REGISTERED, { tags: [...WPD_COMPONENT_TAGS] });
28150 registerBuiltInCommands();
28151 bootstrapPwa(config, showToast);
28152 const overlayPreload = () => {
28153 preloadShellOverlays(config.shellOverlaysBundleUrl ?? "");
28154 preloadWindowSystem(config.windowSystemBundleUrl ?? "");
28155 };
28156 if (typeof window.requestIdleCallback === "function") {
28157 window.requestIdleCallback(overlayPreload, { timeout: 1500 });
28158 } else {
28159 window.setTimeout(overlayPreload, 0);
28160 }
28161 doAction(HOOKS.INIT, { config });
28162 _earlyReady = true;
28163 const queued = _earlyReadyQueue.splice(0);
28164 for (const cb of queued) {
28165 try {
28166 cb();
28167 } catch (err) {
28168 doAction(HOOKS.SHELL_ERROR, {
28169 scope: "when-ready-cb",
28170 error: err
28171 });
28172 if (typeof console !== "undefined") {
28173 console.error("[desktop-mode] whenReady cb threw:", err);
28174 }
28175 }
28176 }
28177 osSettings.apply();
28178 widgetLayer?.hydrate();
28179 window.addEventListener("pagehide", () => {
28180 wallpaperLayer?.teardownActive();
28181 widgetLayer?.disposeAll();
28182 });
28183 bindShellLifecycle();
28184 bindTopWindowLinkInterceptor(manager, config);
28185 const relayoutRoot = (transform, persist2 = true) => {
28186 const root = filesApi.store.getState().placementsByFolder.get(0) ?? [];
28187 const ordered = transform(root);
28188 const rowsPerCol = Math.max(
28189 1,
28190 Math.floor((desktopArea.clientHeight - 16) / 110)
28191 );
28192 const occupied = /* @__PURE__ */ new Set();
28193 let i = 0;
28194 for (const p of ordered) {
28195 const cell = snapToEmptyCell(
28196 16 + Math.floor(i / rowsPerCol) * 96,
28197 16 + i % rowsPerCol * 110,
28198 occupied,
28199 desktopArea
28200 );
28201 occupied.add(`${cell.col},${cell.row}`);
28202 i++;
28203 if (p.x === cell.x && p.y === cell.y) {
28204 continue;
28205 }
28206 filesApi.store.upsertPlacement({
28207 ...p,
28208 x: cell.x,
28209 y: cell.y,
28210 sortOrder: i
28211 });
28212 if (!persist2) {
28213 continue;
28214 }
28215 void updatePlacement(p.id, {
28216 x: cell.x,
28217 y: cell.y,
28218 sortOrder: i
28219 }).catch((err) => {
28220 console.error("[desktop-mode] relayout persist failed", err);
28221 });
28222 }
28223 };
28224 const rootSortTransform = (mode) => (arr) => {
28225 const sorted = arr.slice();
28226 switch (mode) {
28227 case "name-asc":
28228 sorted.sort(
28229 (a, b) => a.file.title.localeCompare(b.file.title)
28230 );
28231 break;
28232 case "name-desc":
28233 sorted.sort(
28234 (a, b) => b.file.title.localeCompare(a.file.title)
28235 );
28236 break;
28237 case "date-asc":
28238 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
28239 break;
28240 case "date-desc":
28241 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
28242 break;
28243 }
28244 return sorted;
28245 };
28246 const ROOT_SORT_MODE_KEY = "desktop-mode:root-sort-mode";
28247 const isRootSortMode = (v) => v === "name-asc" || v === "name-desc" || v === "date-asc" || v === "date-desc";
28248 let rootSortMode = (() => {
28249 try {
28250 const raw = window.localStorage.getItem(ROOT_SORT_MODE_KEY);
28251 return isRootSortMode(raw) ? raw : null;
28252 } catch {
28253 return null;
28254 }
28255 })();
28256 const setRootSortMode = (mode) => {
28257 rootSortMode = mode;
28258 try {
28259 if (mode) {
28260 window.localStorage.setItem(ROOT_SORT_MODE_KEY, mode);
28261 } else {
28262 window.localStorage.removeItem(ROOT_SORT_MODE_KEY);
28263 }
28264 } catch {
28265 }
28266 };
28267 addAction(
28268 "desktop-mode.files.tile-manually-placed",
28269 "desktop-mode/root-sort-clear",
28270 (payload) => {
28271 const folderId = payload?.folderId;
28272 if (folderId === 0) {
28273 setRootSortMode(null);
28274 }
28275 }
28276 );
28277 if (typeof ResizeObserver !== "undefined") {
28278 let lastW = desktopArea.clientWidth;
28279 let lastH = desktopArea.clientHeight;
28280 const ro = new ResizeObserver(() => {
28281 if (!rootSortMode) {
28282 return;
28283 }
28284 const w = desktopArea.clientWidth;
28285 const h = desktopArea.clientHeight;
28286 if (w === lastW && h === lastH) {
28287 return;
28288 }
28289 lastW = w;
28290 lastH = h;
28291 relayoutRoot(rootSortTransform(rootSortMode), false);
28292 });
28293 ro.observe(desktopArea);
28294 }
28295 let pointerdownOnWallpaper = false;
28296 desktopArea.addEventListener("pointerdown", (e) => {
28297 if (!e.isPrimary) {
28298 return;
28299 }
28300 pointerdownOnWallpaper = e.target === desktopArea;
28301 });
28302 desktopArea.addEventListener("click", (e) => {
28303 if (!osSettings.state.showDesktopOnWallpaperClick) {
28304 return;
28305 }
28306 if (e.target !== desktopArea) {
28307 return;
28308 }
28309 if (!pointerdownOnWallpaper) {
28310 return;
28311 }
28312 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
28313 return;
28314 }
28315 if (isWallpaperMenuOpen()) {
28316 return;
28317 }
28318 if (dragManager.recentlyEndedDrag()) {
28319 return;
28320 }
28321 manager.toggleShowDesktop();
28322 });
28323 desktopArea.addEventListener("contextmenu", (e) => {
28324 if (e.target !== desktopArea) {
28325 return;
28326 }
28327 e.preventDefault();
28328 const clientX = e.clientX;
28329 const clientY = e.clientY;
28330 (() => {
28331 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
28332 return;
28333 }
28334 if (isWallpaperMenuOpen()) {
28335 closeWallpaperMenu();
28336 return;
28337 }
28338 const dropClient = { x: clientX, y: clientY };
28339 const cellAtClick = () => {
28340 const rect = desktopArea.getBoundingClientRect();
28341 const rawX = Math.max(0, dropClient.x - rect.left);
28342 const rawY = Math.max(0, dropClient.y - rect.top);
28343 const occupied = buildOccupiedSet(
28344 filesApi.store.getState().placementsByFolder.get(0) ?? []
28345 );
28346 return snapToEmptyCell(rawX, rawY, occupied, desktopArea);
28347 };
28348 const createUrlPlacement = (dialogTitle, description) => {
28349 openUrlDialog({
28350 title: dialogTitle,
28351 description,
28352 nameLabel: "Name",
28353 urlLabel: "URL",
28354 submitLabel: "Create",
28355 onSubmit: async ({ name, url }) => {
28356 const cell = cellAtClick();
28357 const placement = await createPlacement({
28358 type: "link",
28359 ref: url,
28360 parentId: 0,
28361 x: cell.x,
28362 y: cell.y,
28363 meta: name ? { name } : void 0
28364 });
28365 filesApi.store.upsertPlacement(placement);
28366 }
28367 });
28368 };
28369 const items = buildMenuItems({
28370 createFolder: () => {
28371 openCreateFolderDialog({
28372 onSubmit: async (name) => {
28373 const folder = await createFolder({ name });
28374 const cell = cellAtClick();
28375 const placement = await createPlacement({
28376 type: "folder",
28377 ref: String(folder.id),
28378 parentId: 0,
28379 x: cell.x,
28380 y: cell.y
28381 });
28382 filesApi.store.upsertFolder(folder);
28383 filesApi.store.upsertPlacement(placement);
28384 }
28385 });
28386 },
28387 createUrl: () => createUrlPlacement(
28388 "New URL",
28389 "Opens the URL in a new browser tab."
28390 ),
28391 toggleShowDesktop: () => manager.toggleShowDesktop(),
28392 openOsSettings: () => openOsSettings(),
28393 sortIcons: (mode) => {
28394 setRootSortMode(mode);
28395 relayoutRoot(rootSortTransform(mode));
28396 },
28397 currentSortMode: rootSortMode,
28398 includeShowDesktop: !osSettings.state.showDesktopOnWallpaperClick,
28399 labels: {
28400 createFolder: "New folder",
28401 showDesktop: "Show desktop",
28402 osSettings: "OS Settings",
28403 sortHeading: "Sort by",
28404 sortNameAsc: "Name (A → Z)",
28405 sortNameDesc: "Name (Z → A)",
28406 sortDateAsc: "Date (oldest first)",
28407 sortDateDesc: "Date (newest first)",
28408 newUrl: "New URL"
28409 },
28410 serverItems: config.serverWallpaperMenuItems ?? []
28411 });
28412 openWallpaperMenu(
28413 document.body,
28414 { x: clientX, y: clientY },
28415 items
28416 );
28417 })();
28418 });
28419 void Promise.resolve().then(() => index).then((mod) => {
28420 mod.bootOsFileDrop({
28421 config: config.dropConfig,
28422 mediaUrl: config.mediaUrl,
28423 restNonce: config.restNonce
28424 });
28425 });
28426 document.dispatchEvent(
28427 new CustomEvent("desktop-mode-init", {
28428 detail: { config, restored: hasSession }
28429 })
28430 );
28431 }
28432 startMissingImportWarner();
28433 if (document.readyState === "loading") {
28434 document.addEventListener("DOMContentLoaded", init);
28435 } else {
28436 init();
28437 }
28438 const _initial = {
28439 tab: null,
28440 requestedAt: 0
28441 };
28442 let _store = null;
28443 function getStore() {
28444 if (_store) {
28445 return _store;
28446 }
28447 const w = window;
28448 const factory = w.wp?.desktop?.createSharedStore;
28449 if (typeof factory !== "function") {
28450 return null;
28451 }
28452 _store = factory(
28453 "desktop-mode/plugins-window/tab-target",
28454 () => ({ ..._initial })
28455 );
28456 return _store;
28457 }
28458 function setPluginsWindowTab(tab) {
28459 const store2 = getStore();
28460 if (store2) {
28461 store2.state.tab = tab;
28462 store2.state.requestedAt = Date.now();
28463 store2.notify();
28464 return;
28465 }
28466 const w = window;
28467 w._wpdPluginsWindowTab = { tab, requestedAt: Date.now() };
28468 }
28469 function consumePluginsWindowTab() {
28470 const store2 = getStore();
28471 if (store2) {
28472 const tab = store2.state.tab;
28473 if (tab !== null) {
28474 store2.state.tab = null;
28475 store2.state.requestedAt = 0;
28476 store2.notify();
28477 }
28478 return tab;
28479 }
28480 const w = window;
28481 const prev = w._wpdPluginsWindowTab;
28482 if (prev) {
28483 w._wpdPluginsWindowTab = { tab: null, requestedAt: 0 };
28484 return prev.tab;
28485 }
28486 return null;
28487 }
28488 function subscribePluginsWindowTab(cb) {
28489 const store2 = getStore();
28490 if (!store2) {
28491 return () => {
28492 };
28493 }
28494 return store2.subscribe((state2) => cb({ ...state2 }));
28495 }
28496 const tabTarget = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
28497 __proto__: null,
28498 consumePluginsWindowTab,
28499 setPluginsWindowTab,
28500 subscribePluginsWindowTab
28501 }, Symbol.toStringTag, { value: "Module" }));
28502 const FILE_DROP_HOOKS = {
28503 /**
28504 * Filter — fires once per drop, after the manager has parsed
28505 * the OS `DataTransfer` into `File[]` and BEFORE the mime /
28506 * size filter runs.
28507 *
28508 * Signature: `(files: File[], ctx: DropContext) => File[]`.
28509 * Return an empty array to abort the drop silently.
28510 */
28511 FILES_DETECTED: "desktop-mode.drop.files-detected",
28512 /**
28513 * Action — fires after the mime / size filter has rejected
28514 * one or more files. Payload: `{ rejections: DropRejection[],
28515 * context: DropContext }`. The shell toasts a default message;
28516 * subscribers can surface a custom UX (a side panel with the
28517 * list, an analytics call).
28518 */
28519 FILES_REJECTED: "desktop-mode.drop.files-rejected",
28520 /**
28521 * Filter — fires per file before the upload dialog renders.
28522 * Receives `DropFileEntry` (the underlying file + the
28523 * manager's default `fields`). Mutate `fields` (or return a
28524 * new object) to change what the user sees in the form.
28525 *
28526 * Signature: `(entry: DropFileEntry, ctx: DropContext)
28527 * => DropFileEntry`.
28528 */
28529 DIALOG_FIELDS: "desktop-mode.drop.dialog-fields",
28530 /**
28531 * Filter — last call before the manager `POST`s to
28532 * `wp/v2/media`. Receives `{ file: File, fields:
28533 * DropDialogFields, mime: string }`. Return `null` to cancel
28534 * the upload entirely (e.g. a plugin handled it via a
28535 * different endpoint).
28536 *
28537 * Signature: `(payload, ctx: DropContext) => payload | null`.
28538 */
28539 BEFORE_UPLOAD: "desktop-mode.drop.before-upload",
28540 /**
28541 * Action — fires once `BEFORE_UPLOAD` has cleared and the XHR
28542 * is `open()`ed, immediately before `send()`. Payload:
28543 * `{ file: File, fields: DropDialogFields, context: DropContext,
28544 * abort: () => void }`. The `abort` handle aborts the in-flight
28545 * request; the manager rejects with `UploadAbortedError` and
28546 * fires `UPLOAD_FAILED` with that error.
28547 *
28548 * Pair with `UPLOAD_PROGRESS` to drive a progress UI; pair with
28549 * `AFTER_UPLOAD` / `UPLOAD_FAILED` to know when the upload ends.
28550 *
28551 * @since 0.31.0
28552 */
28553 UPLOAD_STARTED: "desktop-mode.drop.upload-started",
28554 /**
28555 * Action — fires for every `XMLHttpRequestUpload.progress` event.
28556 * Payload: `{ file: File, fields: DropDialogFields, context:
28557 * DropContext, loaded: number, total: number, indeterminate:
28558 * boolean }`. `total` is `0` and `indeterminate` is `true` when
28559 * the request body length isn't known (rare for multipart, but
28560 * possible on transcoding proxies); subscribers should treat
28561 * that as an indeterminate state.
28562 *
28563 * A synthetic 100%-loaded event is dispatched once the `upload`
28564 * stream emits `load` so a HUD can show a definite "wrapping up"
28565 * state while the server finishes the response.
28566 *
28567 * @since 0.31.0
28568 */
28569 UPLOAD_PROGRESS: "desktop-mode.drop.upload-progress",
28570 /**
28571 * Action — fires after a successful upload. Payload:
28572 * `{ file: File, result: DropUploadResult, fields:
28573 * DropDialogFields, context: DropContext }`.
28574 *
28575 * The `file` field carries the same `File` reference that
28576 * `UPLOAD_STARTED` / `UPLOAD_PROGRESS` exposed (i.e. the
28577 * payload returned by the `BEFORE_UPLOAD` filter, in case a
28578 * plugin swapped the file). Subscribers tracking per-file
28579 * state — progress HUDs, sequence counters — should match on
28580 * this identity rather than the filename: two drops of
28581 * `photo.jpg` from different folders would otherwise route
28582 * each other's success event to the wrong row.
28583 *
28584 * @since 0.31.0 the `file` field was added; pre-0.31.0 code
28585 * that destructured `{ result, fields, context }` keeps working.
28586 */
28587 AFTER_UPLOAD: "desktop-mode.drop.after-upload",
28588 /**
28589 * Action — fires after an upload fails. Payload:
28590 * `{ file: File, error: Error, context: DropContext }`.
28591 * `error` is an `UploadAbortedError` when the failure came
28592 * from the caller invoking the `abort()` handle on
28593 * `UPLOAD_STARTED`.
28594 *
28595 * `file` carries the same identity as `UPLOAD_STARTED` /
28596 * `UPLOAD_PROGRESS` / `AFTER_UPLOAD` — the post-`BEFORE_UPLOAD`
28597 * `File`, in case a plugin swapped it. Match by reference, not
28598 * filename: a HUD that keys its row map on the started-File
28599 * needs the same key here, otherwise the row stays stuck in
28600 * "running" after a failure when a `BEFORE_UPLOAD` filter
28601 * replaced the file.
28602 */
28603 UPLOAD_FAILED: "desktop-mode.drop.upload-failed"
28604 };
28605 const IFRAME_PASSTHROUGH_SELECTORS = [
28606 ".components-drop-zone",
28607 "[data-drop-zone]",
28608 ".uploader-window",
28609 ".media-frame-content"
28610 ];
28611 function dragHasFiles(ev) {
28612 const types = ev.dataTransfer?.types;
28613 if (!types) {
28614 return false;
28615 }
28616 const list2 = types;
28617 if (typeof list2.includes === "function") {
28618 return list2.includes("Files");
28619 }
28620 if (typeof list2.contains === "function") {
28621 return list2.contains("Files");
28622 }
28623 for (let i = 0; i < list2.length; i++) {
28624 if (list2[i] === "Files") {
28625 return true;
28626 }
28627 }
28628 return false;
28629 }
28630 function resolveWindowIdFromSource(source) {
28631 if (!source) {
28632 return void 0;
28633 }
28634 const iframes = document.querySelectorAll("iframe");
28635 for (const f of Array.from(iframes)) {
28636 if (f.contentWindow === source) {
28637 const host = f.closest("[data-window-id]");
28638 return host?.getAttribute("data-window-id") || void 0;
28639 }
28640 }
28641 return void 0;
28642 }
28643 function mountOsFileDropManager(opts) {
28644 const host = window;
28645 if (host.__desktopModeOsFileDropMounted) {
28646 return host.__desktopModeOsFileDropMounted;
28647 }
28648 if (!opts.config.enabled) {
28649 return mountNoOp();
28650 }
28651 const overlayEl = ensureDropOverlay();
28652 let dragDepth = 0;
28653 let dragWatchdog = null;
28654 const resetOverlay = () => {
28655 dragDepth = 0;
28656 overlayEl.classList.remove("is-active");
28657 if (dragWatchdog !== null) {
28658 clearTimeout(dragWatchdog);
28659 dragWatchdog = null;
28660 }
28661 };
28662 const bumpWatchdog = () => {
28663 if (dragWatchdog !== null) {
28664 clearTimeout(dragWatchdog);
28665 }
28666 dragWatchdog = setTimeout(resetOverlay, 250);
28667 };
28668 const onDragEnter = (ev) => {
28669 if (!dragHasFiles(ev)) {
28670 return;
28671 }
28672 ev.preventDefault();
28673 dragDepth++;
28674 overlayEl.classList.add("is-active");
28675 bumpWatchdog();
28676 };
28677 const onDragOver = (ev) => {
28678 if (!dragHasFiles(ev)) {
28679 return;
28680 }
28681 if (ev.defaultPrevented) {
28682 resetOverlay();
28683 return;
28684 }
28685 ev.preventDefault();
28686 if (ev.dataTransfer) {
28687 ev.dataTransfer.dropEffect = "copy";
28688 }
28689 bumpWatchdog();
28690 };
28691 const onDragLeave = () => {
28692 dragDepth = Math.max(0, dragDepth - 1);
28693 if (dragDepth === 0) {
28694 overlayEl.classList.remove("is-active");
28695 }
28696 };
28697 const onDrop = (ev) => {
28698 if (!dragHasFiles(ev)) {
28699 return;
28700 }
28701 if (ev.defaultPrevented) {
28702 resetOverlay();
28703 return;
28704 }
28705 ev.preventDefault();
28706 resetOverlay();
28707 const files = ev.dataTransfer?.files ? Array.from(ev.dataTransfer.files) : [];
28708 if (files.length === 0) {
28709 return;
28710 }
28711 const ctx = classifyDropTarget(ev);
28712 void handleFiles(files, ctx, opts);
28713 };
28714 const onDragEnd2 = () => resetOverlay();
28715 const onVisibilityChange = () => {
28716 if (document.visibilityState === "hidden") {
28717 resetOverlay();
28718 }
28719 };
28720 const onIframeMessage = (ev) => {
28721 if (ev.origin !== window.location.origin) {
28722 return;
28723 }
28724 const data = ev.data;
28725 if (!data || data.type !== "desktop-mode-os-file-drop") {
28726 return;
28727 }
28728 if (!Array.isArray(data.files) || data.files.length === 0) {
28729 return;
28730 }
28731 const files = data.files.filter((f) => f instanceof File);
28732 if (files.length === 0) {
28733 return;
28734 }
28735 const windowId = resolveWindowIdFromSource(ev.source);
28736 if (!windowId) {
28737 return;
28738 }
28739 const ctx = {
28740 surface: "iframe",
28741 windowId,
28742 x: typeof data.x === "number" ? data.x : 0,
28743 y: typeof data.y === "number" ? data.y : 0
28744 };
28745 dragDepth = 0;
28746 overlayEl.classList.remove("is-active");
28747 void handleFiles(files, ctx, opts);
28748 };
28749 window.addEventListener("dragenter", onDragEnter);
28750 window.addEventListener("dragover", onDragOver);
28751 window.addEventListener("dragleave", onDragLeave);
28752 window.addEventListener("drop", onDrop);
28753 window.addEventListener("dragend", onDragEnd2);
28754 document.addEventListener("visibilitychange", onVisibilityChange);
28755 window.addEventListener("blur", onDragEnd2);
28756 window.addEventListener("message", onIframeMessage);
28757 const manager = {
28758 dispose: () => {
28759 window.removeEventListener("dragenter", onDragEnter);
28760 window.removeEventListener("dragover", onDragOver);
28761 window.removeEventListener("dragleave", onDragLeave);
28762 window.removeEventListener("drop", onDrop);
28763 window.removeEventListener("dragend", onDragEnd2);
28764 document.removeEventListener(
28765 "visibilitychange",
28766 onVisibilityChange
28767 );
28768 window.removeEventListener("blur", onDragEnd2);
28769 window.removeEventListener("message", onIframeMessage);
28770 overlayEl.remove();
28771 delete window.__desktopModeOsFileDropMounted;
28772 }
28773 };
28774 host.__desktopModeOsFileDropMounted = manager;
28775 return manager;
28776 }
28777 function ensureDropOverlay() {
28778 const existing = document.querySelector(".desktop-mode-os-drop-overlay");
28779 if (existing) {
28780 return existing;
28781 }
28782 const el = document.createElement("div");
28783 el.className = "desktop-mode-os-drop-overlay";
28784 el.setAttribute("aria-hidden", "true");
28785 el.style.cssText = [
28786 "position:fixed",
28787 "inset:0",
28788 "pointer-events:none",
28789 "z-index:200",
28790 "opacity:0",
28791 "transition:opacity 120ms ease",
28792 "background:radial-gradient(circle at center, rgba(34,113,177,0.18) 0%, rgba(34,113,177,0.06) 60%, transparent 100%)",
28793 "box-shadow:inset 0 0 0 3px rgba(34,113,177,0.55)"
28794 ].join(";");
28795 const label = document.createElement("div");
28796 label.style.cssText = [
28797 "position:absolute",
28798 "top:50%",
28799 "left:50%",
28800 "transform:translate(-50%,-50%)",
28801 "padding:14px 22px",
28802 "border-radius:12px",
28803 "background:rgba(20,20,24,0.78)",
28804 "color:#fff",
28805 "font:600 14px/1.2 -apple-system,BlinkMacSystemFont,sans-serif",
28806 "letter-spacing:0.02em"
28807 ].join(";");
28808 label.textContent = "Drop to upload";
28809 el.appendChild(label);
28810 document.body.appendChild(el);
28811 const style = document.createElement("style");
28812 style.textContent = ".desktop-mode-os-drop-overlay.is-active{opacity:1!important;}";
28813 document.head.appendChild(style);
28814 return el;
28815 }
28816 function mountNoOp() {
28817 const cancel = (ev) => {
28818 if (!dragHasFiles(ev)) {
28819 return;
28820 }
28821 const target2 = ev.target;
28822 if (target2?.closest && IFRAME_PASSTHROUGH_SELECTORS.some((s) => target2.closest(s))) {
28823 return;
28824 }
28825 ev.preventDefault();
28826 };
28827 window.addEventListener("dragover", cancel);
28828 window.addEventListener("drop", cancel);
28829 const host = window;
28830 const manager = {
28831 dispose: () => {
28832 window.removeEventListener("dragover", cancel);
28833 window.removeEventListener("drop", cancel);
28834 delete host.__desktopModeOsFileDropMounted;
28835 }
28836 };
28837 host.__desktopModeOsFileDropMounted = manager;
28838 return manager;
28839 }
28840 function classifyDropTarget(ev) {
28841 const x = ev.clientX;
28842 const y = ev.clientY;
28843 let node = ev.target;
28844 while (node && node !== document.body) {
28845 if (node.tagName === "IFRAME") {
28846 const id = node.closest(
28847 "[data-window-id]"
28848 );
28849 return {
28850 surface: "iframe",
28851 windowId: id?.getAttribute("data-window-id") || void 0,
28852 x,
28853 y
28854 };
28855 }
28856 if (node.hasAttribute("data-window-id")) {
28857 return {
28858 surface: "window",
28859 windowId: node.getAttribute("data-window-id") || void 0,
28860 x,
28861 y
28862 };
28863 }
28864 if (node.classList.contains("desktop-mode-folder-grid")) {
28865 return { surface: "folder", x, y };
28866 }
28867 if (node.id === "desktop-mode-wallpaper" || node.classList.contains("desktop-mode-wallpaper") || node.classList.contains("desktop-mode-desktop")) {
28868 return { surface: "wallpaper", x, y };
28869 }
28870 node = node.parentElement;
28871 }
28872 return { surface: "unknown", x, y };
28873 }
28874 async function handleFiles(rawFiles, ctx, opts) {
28875 const detected = applyFilters(
28876 FILE_DROP_HOOKS.FILES_DETECTED,
28877 rawFiles,
28878 ctx
28879 );
28880 if (!Array.isArray(detected) || detected.length === 0) {
28881 return;
28882 }
28883 const { accepted, rejected } = partitionByPolicy(
28884 detected,
28885 opts.config
28886 );
28887 if (rejected.length > 0) {
28888 doAction(FILE_DROP_HOOKS.FILES_REJECTED, {
28889 rejections: rejected,
28890 context: ctx
28891 });
28892 showToast({
28893 message: rejected.length === 1 ? rejected[0].message : `${rejected.length} files couldn't be uploaded.`
28894 });
28895 }
28896 if (accepted.length === 0) {
28897 return;
28898 }
28899 const entries = accepted.map(({ file, mime }) => {
28900 const base = {
28901 file,
28902 mime,
28903 fields: defaultFields(file, mime)
28904 };
28905 const filtered = applyFilters(
28906 FILE_DROP_HOOKS.DIALOG_FIELDS,
28907 base,
28908 ctx
28909 );
28910 if (!filtered || typeof filtered !== "object" || !("fields" in filtered) || typeof filtered.fields !== "object") {
28911 return base;
28912 }
28913 return filtered;
28914 });
28915 await opts.openDialog(entries, ctx);
28916 }
28917 function partitionByPolicy(files, config) {
28918 const accepted = [];
28919 const rejected = [];
28920 for (const file of files) {
28921 if (file.size === 0) {
28922 rejected.push({
28923 file,
28924 reason: "empty",
28925 message: `“${file.name}” is empty.`
28926 });
28927 continue;
28928 }
28929 if (config.maxSize > 0 && file.size > config.maxSize) {
28930 rejected.push({
28931 file,
28932 reason: "size",
28933 message: `“${file.name}” exceeds the ${formatBytes$1(
28934 config.maxSize
28935 )} upload limit.`
28936 });
28937 continue;
28938 }
28939 const mime = resolveAllowedMime(
28940 file,
28941 config.allowedMimes,
28942 config.extToMime
28943 );
28944 if (!mime) {
28945 rejected.push({
28946 file,
28947 reason: "mime",
28948 message: `“${file.name}” is not an allowed file type.`
28949 });
28950 continue;
28951 }
28952 accepted.push({ file, mime });
28953 }
28954 return { accepted, rejected };
28955 }
28956 function resolveAllowedMime(file, allowedMimes, extToMime) {
28957 if (allowedMimes.length === 0) {
28958 return null;
28959 }
28960 const lower = file.type.toLowerCase();
28961 if (lower && allowedMimes.includes(lower)) {
28962 return lower;
28963 }
28964 const ext = extensionOf(file.name);
28965 if (!ext) {
28966 return null;
28967 }
28968 if (extToMime) {
28969 for (const [key, mime] of Object.entries(extToMime)) {
28970 if (key.split("|").includes(ext) && allowedMimes.includes(mime)) {
28971 return mime;
28972 }
28973 }
28974 return null;
28975 }
28976 const guess = EXTENSION_GUESSES[ext];
28977 if (guess && allowedMimes.includes(guess)) {
28978 return guess;
28979 }
28980 return null;
28981 }
28982 const EXTENSION_GUESSES = {
28983 jpg: "image/jpeg",
28984 jpeg: "image/jpeg",
28985 png: "image/png",
28986 gif: "image/gif",
28987 webp: "image/webp",
28988 avif: "image/avif",
28989 heic: "image/heic",
28990 heif: "image/heif",
28991 svg: "image/svg+xml",
28992 mp4: "video/mp4",
28993 mov: "video/quicktime",
28994 webm: "video/webm",
28995 mp3: "audio/mpeg",
28996 wav: "audio/wav",
28997 pdf: "application/pdf"
28998 };
28999 function extensionOf(name) {
29000 const dot = name.lastIndexOf(".");
29001 if (dot < 0) {
29002 return "";
29003 }
29004 return name.slice(dot + 1).toLowerCase();
29005 }
29006 function defaultFields(file, mime) {
29007 const safeName = sanitizeFilename(file.name);
29008 const ext = extensionOf(safeName);
29009 const stem = ext ? safeName.slice(0, safeName.length - ext.length - 1) : safeName;
29010 const title = humanize(stem);
29011 return {
29012 title,
29013 altText: mime.startsWith("image/") ? title : "",
29014 caption: "",
29015 description: "",
29016 filename: safeName
29017 };
29018 }
29019 function sanitizeFilename(name) {
29020 const cleaned = name.replace(/[\\/]/g, "-").replace(/[\x00-\x1f\x7f]/g, "").replace(/\s+/g, " ").replace(/ *- */g, "-").replace(/-+/g, "-").trim().replace(/^[-.]+|[-.]+$/g, "");
29021 return cleaned || "upload";
29022 }
29023 function humanize(stem) {
29024 const spaced = stem.replace(/[-_]+/g, " ").trim();
29025 if (!spaced) {
29026 return "Upload";
29027 }
29028 return spaced.charAt(0).toUpperCase() + spaced.slice(1);
29029 }
29030 function formatBytes$1(bytes) {
29031 if (bytes >= 1024 * 1024) {
29032 return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
29033 }
29034 if (bytes >= 1024) {
29035 return `${(bytes / 1024).toFixed(0)} KB`;
29036 }
29037 return `${bytes} B`;
29038 }
29039 function formatBytes(bytes) {
29040 if (!Number.isFinite(bytes) || bytes <= 0) {
29041 return "0 B";
29042 }
29043 const units = ["B", "KB", "MB", "GB", "TB"];
29044 let v = bytes;
29045 let i = 0;
29046 while (v >= 1024 && i < units.length - 1) {
29047 v /= 1024;
29048 i++;
29049 }
29050 const decimals = v >= 100 || i === 0 ? 0 : 1;
29051 return `${v.toFixed(decimals)} ${units[i]}`;
29052 }
29053 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}}`;
29054 const _WpdProgressBar = class _WpdProgressBar extends Component {
29055 constructor() {
29056 super(...arguments);
29057 this._ownedAriaLabel = null;
29058 }
29059 render() {
29060 return html`<div class="root" part="root">
29061 <div class="header" part="header" hidden>
29062 <span class="label" part="label"></span>
29063 <span class="percent" part="percent"></span>
29064 </div>
29065 <div class="track" part="track">
29066 <div class="fill" part="fill"></div>
29067 </div>
29068 </div>`;
29069 }
29070 requestUpdate() {
29071 super.requestUpdate();
29072 queueMicrotask(() => this._paint());
29073 }
29074 connectedCallback() {
29075 super.connectedCallback();
29076 queueMicrotask(() => this._paint());
29077 }
29078 _paint() {
29079 const root = this.shadowRoot;
29080 if (!root) {
29081 return;
29082 }
29083 const max = this._readMax();
29084 const indeterminate = this.hasAttribute("indeterminate") || max <= 0;
29085 const value = indeterminate ? 0 : this._readValue(max);
29086 const ratio = indeterminate ? 0 : value / max;
29087 const percent = Math.round(ratio * 100);
29088 const label = this.getAttribute("label") ?? "";
29089 const showPercent = this.hasAttribute("show-percent");
29090 const fill = root.querySelector(".fill");
29091 if (fill && !indeterminate) {
29092 fill.style.width = `${(ratio * 100).toFixed(2)}%`;
29093 } else if (fill && indeterminate) {
29094 fill.style.removeProperty("width");
29095 }
29096 const header = root.querySelector(".header");
29097 const labelEl = root.querySelector(".label");
29098 const percentEl = root.querySelector(".percent");
29099 if (header && labelEl && percentEl) {
29100 const visible = label || showPercent && !indeterminate;
29101 header.hidden = !visible;
29102 labelEl.textContent = label;
29103 percentEl.hidden = !(showPercent && !indeterminate);
29104 percentEl.textContent = `${percent}%`;
29105 }
29106 this._syncAria(max, value, indeterminate, label);
29107 const track = root.querySelector(".track");
29108 if (track) {
29109 track.setAttribute("role", "progressbar");
29110 track.setAttribute("aria-valuemin", "0");
29111 if (indeterminate) {
29112 track.removeAttribute("aria-valuenow");
29113 track.removeAttribute("aria-valuemax");
29114 } else {
29115 track.setAttribute("aria-valuemax", String(max));
29116 track.setAttribute("aria-valuenow", String(value));
29117 }
29118 if (label) {
29119 track.setAttribute("aria-label", label);
29120 } else {
29121 track.removeAttribute("aria-label");
29122 }
29123 }
29124 }
29125 _syncAria(max, value, indeterminate, label) {
29126 this.setAttribute("role", "progressbar");
29127 this.setAttribute("aria-valuemin", "0");
29128 if (indeterminate) {
29129 this.removeAttribute("aria-valuenow");
29130 this.removeAttribute("aria-valuemax");
29131 } else {
29132 this.setAttribute("aria-valuemax", String(max));
29133 this.setAttribute("aria-valuenow", String(value));
29134 }
29135 const existing = this.getAttribute("aria-label");
29136 if (label) {
29137 if (existing === null || existing === this._ownedAriaLabel) {
29138 this.setAttribute("aria-label", label);
29139 this._ownedAriaLabel = label;
29140 }
29141 } else if (existing !== null && existing === this._ownedAriaLabel) {
29142 this.removeAttribute("aria-label");
29143 this._ownedAriaLabel = null;
29144 }
29145 }
29146 _readMax() {
29147 const attr = this.getAttribute("max");
29148 if (attr === null) {
29149 return 100;
29150 }
29151 const raw = parseFloat(attr);
29152 return Number.isFinite(raw) ? raw : 100;
29153 }
29154 _readValue(max) {
29155 const raw = parseFloat(this.getAttribute("value") ?? "0");
29156 if (!Number.isFinite(raw)) {
29157 return 0;
29158 }
29159 if (raw < 0) {
29160 return 0;
29161 }
29162 if (raw > max) {
29163 return max;
29164 }
29165 return raw;
29166 }
29167 };
29168 _WpdProgressBar.props = [
29169 "value",
29170 "max",
29171 "indeterminate",
29172 "tone",
29173 "label",
29174 "showPercent"
29175 ];
29176 _WpdProgressBar.styles = [styles];
29177 _WpdProgressBar.help = {
29178 title: "Progress bar",
29179 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.",
29180 status: "experimental",
29181 since: "0.31.0",
29182 props: [
29183 {
29184 name: "value",
29185 type: "number",
29186 default: "0",
29187 description: "Current progress. Clamped to `[0, max]`."
29188 },
29189 {
29190 name: "max",
29191 type: "number",
29192 default: "100",
29193 description: "Maximum value. Setting `max <= 0` forces indeterminate."
29194 },
29195 {
29196 name: "indeterminate",
29197 type: "boolean",
29198 description: "Show the sweeping indeterminate animation instead of a value-driven fill. The `value` attribute is ignored while this is set."
29199 },
29200 {
29201 name: "tone",
29202 type: '"default" | "success" | "warning" | "danger"',
29203 default: "default",
29204 description: "Tints the fill from the shared status palette."
29205 },
29206 {
29207 name: "label",
29208 type: "string",
29209 description: "Optional inline label rendered above the track. Also wired into `aria-label` when set."
29210 },
29211 {
29212 name: "show-percent",
29213 type: "boolean",
29214 description: "Render a right-aligned percent readout next to the label. Only meaningful in determinate mode."
29215 }
29216 ],
29217 cssProps: [
29218 {
29219 name: "--wpd-progress-track-bg",
29220 default: "var(--desktop-mode-control-bg, rgba(0,0,0,0.08))"
29221 },
29222 {
29223 name: "--wpd-progress-fill",
29224 default: "var(--wp-admin-theme-color, #2271b1)"
29225 },
29226 { name: "--wpd-progress-height", default: "6px" },
29227 { name: "--wpd-progress-radius", default: "999px" },
29228 { name: "--wpd-progress-label-color", default: "inherit" },
29229 { name: "--wpd-progress-label-size", default: "12px" },
29230 { name: "--wpd-progress-label-gap", default: "4px" }
29231 ],
29232 example: html`<wpd-progress-bar
29233 value="42"
29234 label="Uploading hero.jpg"
29235 show-percent
29236 ></wpd-progress-bar>`
29237 };
29238 let WpdProgressBar = _WpdProgressBar;
29239 defineComponent("wpd-progress-bar", WpdProgressBar);
29240 const ROWS = /* @__PURE__ */ new Map();
29241 let panel = null;
29242 function mountUploadProgressHud() {
29243 if (document.body.hasAttribute("data-desktop-mode-suppress-upload-hud")) {
29244 return;
29245 }
29246 if (window.__wpdUploadHud) {
29247 return;
29248 }
29249 window.__wpdUploadHud = true;
29250 const ns = "desktop-mode/os-file-drop-hud";
29251 addAction(
29252 FILE_DROP_HOOKS.UPLOAD_STARTED,
29253 ns,
29254 (payload) => onStarted(payload.file, payload.fields, payload.abort)
29255 );
29256 addAction(
29257 FILE_DROP_HOOKS.UPLOAD_PROGRESS,
29258 ns,
29259 (payload) => onProgress(
29260 payload.file,
29261 payload.loaded,
29262 payload.total,
29263 payload.indeterminate
29264 )
29265 );
29266 addAction(
29267 FILE_DROP_HOOKS.AFTER_UPLOAD,
29268 ns,
29269 (payload) => onComplete(payload.file, payload.fields, payload.result)
29270 );
29271 addAction(
29272 FILE_DROP_HOOKS.UPLOAD_FAILED,
29273 ns,
29274 (payload) => onFailed(payload.file, payload.error)
29275 );
29276 }
29277 function onStarted(file, fields, abort) {
29278 const p = ensurePanel();
29279 const row = document.createElement("div");
29280 row.className = "desktop-mode-upload-hud__row";
29281 const meta = document.createElement("div");
29282 meta.className = "desktop-mode-upload-hud__meta";
29283 const name = document.createElement("div");
29284 name.className = "desktop-mode-upload-hud__name";
29285 name.textContent = fields.filename || file.name;
29286 name.title = fields.filename || file.name;
29287 const statusEl = document.createElement("div");
29288 statusEl.className = "desktop-mode-upload-hud__status";
29289 statusEl.textContent = "Uploading…";
29290 meta.append(name, statusEl);
29291 const bar = document.createElement("wpd-progress-bar");
29292 bar.setAttribute("indeterminate", "");
29293 bar.setAttribute("show-percent", "");
29294 const actions = document.createElement("div");
29295 actions.className = "desktop-mode-upload-hud__actions";
29296 const cancelBtn = document.createElement("wpd-button");
29297 cancelBtn.setAttribute("variant", "tertiary");
29298 cancelBtn.setAttribute("size", "small");
29299 cancelBtn.textContent = "Cancel";
29300 cancelBtn.addEventListener("click", () => {
29301 const r = ROWS.get(file);
29302 if (!r) {
29303 return;
29304 }
29305 if (r.state === "running") {
29306 r.statusEl.textContent = "Cancelling…";
29307 r.cancelBtn.disabled = true;
29308 r.abort();
29309 } else {
29310 dismissRow(r);
29311 }
29312 });
29313 actions.appendChild(cancelBtn);
29314 row.append(meta, bar, actions);
29315 p.querySelector(".desktop-mode-upload-hud__list").appendChild(row);
29316 ROWS.set(file, {
29317 file,
29318 abort,
29319 root: row,
29320 bar,
29321 statusEl,
29322 cancelBtn,
29323 state: "running",
29324 lingerTimer: null
29325 });
29326 updateHeader();
29327 }
29328 function onProgress(file, loaded, total, indeterminate) {
29329 const r = ROWS.get(file);
29330 if (!r || r.state !== "running") {
29331 return;
29332 }
29333 if (indeterminate || total <= 0) {
29334 r.bar.setAttribute("indeterminate", "");
29335 r.statusEl.textContent = `${formatBytes(loaded)} sent`;
29336 } else {
29337 r.bar.removeAttribute("indeterminate");
29338 r.bar.setAttribute("max", String(total));
29339 r.bar.setAttribute("value", String(loaded));
29340 r.statusEl.textContent = `${formatBytes(loaded)} / ${formatBytes(total)}`;
29341 }
29342 }
29343 function onComplete(file, fields, result) {
29344 const r = ROWS.get(file);
29345 if (!r) {
29346 return;
29347 }
29348 r.state = "success";
29349 r.bar.removeAttribute("indeterminate");
29350 r.bar.setAttribute("value", "100");
29351 r.bar.setAttribute("max", "100");
29352 r.bar.setAttribute("tone", "success");
29353 r.statusEl.textContent = "Uploaded";
29354 r.cancelBtn.textContent = "Dismiss";
29355 r.lingerTimer = setTimeout(() => dismissRow(r), 2500);
29356 updateHeader();
29357 activity.publish("desktop-mode/upload-hud-complete", {
29358 filename: fields.filename || result.filename,
29359 attachmentId: result.id
29360 });
29361 }
29362 function onFailed(file, error) {
29363 const r = ROWS.get(file);
29364 if (!r) {
29365 return;
29366 }
29367 r.bar.removeAttribute("indeterminate");
29368 r.bar.setAttribute("tone", "danger");
29369 r.cancelBtn.textContent = "Dismiss";
29370 r.cancelBtn.disabled = false;
29371 if (error.name === "UploadAbortedError") {
29372 r.state = "aborted";
29373 r.statusEl.textContent = "Cancelled";
29374 } else {
29375 r.state = "failed";
29376 r.statusEl.textContent = error.message || "Upload failed";
29377 }
29378 updateHeader();
29379 }
29380 function dismissRow(r) {
29381 if (r.lingerTimer) {
29382 clearTimeout(r.lingerTimer);
29383 }
29384 ROWS.delete(r.file);
29385 r.root.remove();
29386 updateHeader();
29387 if (ROWS.size === 0 && panel) {
29388 panel.hidden = true;
29389 }
29390 }
29391 function ensurePanel() {
29392 if (panel && panel.isConnected) {
29393 panel.hidden = false;
29394 return panel;
29395 }
29396 const p = document.createElement("div");
29397 p.className = "desktop-mode-upload-hud";
29398 p.setAttribute("role", "region");
29399 p.setAttribute("aria-label", "Uploads");
29400 const header = document.createElement("div");
29401 header.className = "desktop-mode-upload-hud__header";
29402 const title = document.createElement("div");
29403 title.className = "desktop-mode-upload-hud__title";
29404 title.textContent = "Uploads";
29405 const closeBtn = document.createElement("button");
29406 closeBtn.type = "button";
29407 closeBtn.className = "desktop-mode-upload-hud__close";
29408 closeBtn.setAttribute("aria-label", "Hide upload panel");
29409 closeBtn.textContent = "×";
29410 closeBtn.addEventListener("click", () => {
29411 for (const r of [...ROWS.values()]) {
29412 if (r.state !== "running") {
29413 dismissRow(r);
29414 }
29415 }
29416 if (ROWS.size === 0) {
29417 p.hidden = true;
29418 }
29419 });
29420 header.append(title, closeBtn);
29421 const list2 = document.createElement("div");
29422 list2.className = "desktop-mode-upload-hud__list";
29423 p.append(header, list2);
29424 document.body.appendChild(p);
29425 panel = p;
29426 return p;
29427 }
29428 function updateHeader() {
29429 if (!panel) {
29430 return;
29431 }
29432 const title = panel.querySelector(
29433 ".desktop-mode-upload-hud__title"
29434 );
29435 if (!title) {
29436 return;
29437 }
29438 const total = ROWS.size;
29439 const running = [...ROWS.values()].filter((r) => r.state === "running").length;
29440 if (running > 0) {
29441 title.textContent = running === total ? `Uploading ${running} file${running === 1 ? "" : "s"}…` : `${running} of ${total} uploading…`;
29442 } else if (total > 0) {
29443 title.textContent = `Uploads (${total})`;
29444 } else {
29445 title.textContent = "Uploads";
29446 }
29447 }
29448 function mountMediaLibraryRefresher() {
29449 if (document.body.hasAttribute(
29450 "data-desktop-mode-suppress-media-library-refresh"
29451 )) {
29452 return;
29453 }
29454 const sentinel = window;
29455 if (sentinel.__wpdMediaLibraryRefresher) {
29456 return;
29457 }
29458 sentinel.__wpdMediaLibraryRefresher = true;
29459 addAction(
29460 FILE_DROP_HOOKS.AFTER_UPLOAD,
29461 "desktop-mode/os-file-drop-library-refresh",
29462 () => refreshOpenLibraries()
29463 );
29464 }
29465 function refreshOpenLibraries() {
29466 const iframes = document.querySelectorAll("iframe");
29467 for (const frame of Array.from(iframes)) {
29468 if (!isMediaLibraryUrl(resolveIframeUrl(frame))) {
29469 continue;
29470 }
29471 try {
29472 frame.contentWindow?.location.reload();
29473 } catch {
29474 const reloadHref = resolveIframeUrl(frame);
29475 if (reloadHref) {
29476 frame.setAttribute("src", reloadHref);
29477 }
29478 }
29479 }
29480 }
29481 function resolveIframeUrl(frame) {
29482 try {
29483 return frame.contentWindow?.location.href ?? frame.src ?? "";
29484 } catch {
29485 return frame.src ?? "";
29486 }
29487 }
29488 function isMediaLibraryUrl(url) {
29489 if (!url) {
29490 return false;
29491 }
29492 return /\/wp-admin\/upload\.php(?:[?#]|$)/.test(url);
29493 }
29494 function bootOsFileDrop(args) {
29495 const config = args.config || {
29496 enabled: false,
29497 allowedMimes: [],
29498 maxSize: 0
29499 };
29500 mountUploadProgressHud();
29501 mountMediaLibraryRefresher();
29502 mountOsFileDropManager({
29503 config,
29504 mediaUrl: args.mediaUrl,
29505 restNonce: args.restNonce,
29506 openDialog: async (entries, ctx) => {
29507 const { openUploadDialog: openUploadDialog2 } = await Promise.resolve().then(() => dialog);
29508 await openUploadDialog2({
29509 entries,
29510 context: ctx,
29511 mediaUrl: args.mediaUrl,
29512 restNonce: args.restNonce
29513 });
29514 }
29515 });
29516 }
29517 const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
29518 __proto__: null,
29519 FILE_DROP_HOOKS,
29520 bootOsFileDrop
29521 }, Symbol.toStringTag, { value: "Module" }));
29522 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}`;
29523 const _WpdTextField = class _WpdTextField extends Component {
29524 constructor() {
29525 super(...arguments);
29526 this._revealed = false;
29527 }
29528 connectedCallback() {
29529 super.connectedCallback();
29530 ensureAutoId(this);
29531 }
29532 render() {
29533 const label = this.label || "";
29534 const value = this.value ?? "";
29535 const placeholder = this.placeholder || "";
29536 const disabled = this.disabled !== null;
29537 const readonly = this.readonly !== null;
29538 const declaredAutocomplete = this.autocomplete;
29539 const declaredType = this.type || "text";
29540 const isPassword = declaredType === "password";
29541 let autocomplete = declaredAutocomplete || "off";
29542 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
29543 autocomplete = "new-password";
29544 }
29545 const maxLength = this.maxlength;
29546 const minLength = this.minlength;
29547 const pattern = this.pattern || "";
29548 const name = this.name || "";
29549 const suffix = this.suffix || "";
29550 const invalid = this.invalid !== null;
29551 const reveal = this.reveal !== null;
29552 const isPasswordIntent = declaredType === "password";
29553 const isMasked = isPasswordIntent && !(reveal && this._revealed);
29554 let effectiveType;
29555 if (isPasswordIntent) {
29556 effectiveType = "text";
29557 } else if (reveal && this._revealed) {
29558 effectiveType = "text";
29559 } else {
29560 effectiveType = declaredType;
29561 }
29562 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
29563 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
29564 const hostId = this.id || "wpd-unnamed";
29565 const inputId = `${hostId}__input`;
29566 return html`
29567 ${label ? html`<label
29568 class="wpd-text-field__label"
29569 for=${inputId}
29570 >${label}</label>` : html``}
29571 <span class=${rowClass}>
29572 <input
29573 id=${inputId}
29574 class=${inputClass}
29575 type=${effectiveType}
29576 .value=${value}
29577 placeholder=${placeholder}
29578 ?disabled=${disabled}
29579 ?readonly=${readonly}
29580 autocomplete=${autocomplete}
29581 maxlength=${maxLength ?? ""}
29582 minlength=${minLength ?? ""}
29583 pattern=${pattern}
29584 name=${name}
29585 aria-invalid=${invalid ? "true" : "false"}
29586 aria-label=${label || ""}
29587 @input=${(e) => this._onInput(e)}
29588 @change=${(e) => this._onChange(e)}
29589 @keydown=${(e) => this._onKeyDown(e)}
29590 />
29591 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
29592 ${reveal ? this._renderRevealButton(disabled) : html``}
29593 </span>
29594 `;
29595 }
29596 _renderRevealButton(disabled) {
29597 const label = this._revealed ? "Hide" : "Show";
29598 return html`
29599 <button
29600 type="button"
29601 class="wpd-text-field__reveal"
29602 aria-label=${label}
29603 aria-pressed=${this._revealed ? "true" : "false"}
29604 ?disabled=${disabled}
29605 tabindex="0"
29606 @click=${() => this._onToggleReveal()}
29607 >
29608 ${this._revealed ? _iconEyeOff() : _iconEye()}
29609 </button>
29610 `;
29611 }
29612 _onToggleReveal() {
29613 this._revealed = !this._revealed;
29614 this.requestUpdate();
29615 }
29616 _onInput(e) {
29617 const input = e.target;
29618 this.value = input.value;
29619 this.emit("wpd-input-change", { value: input.value });
29620 }
29621 _onChange(e) {
29622 const input = e.target;
29623 this.emit("wpd-input-commit", { value: input.value });
29624 }
29625 _onKeyDown(e) {
29626 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
29627 const input = e.target;
29628 this.emit("wpd-submit", { value: input.value });
29629 }
29630 }
29631 };
29632 _WpdTextField.props = [
29633 "label",
29634 "value",
29635 "placeholder",
29636 "disabled",
29637 "readonly",
29638 "autocomplete",
29639 "type",
29640 "maxlength",
29641 "minlength",
29642 "pattern",
29643 "name",
29644 "suffix",
29645 "invalid",
29646 "reveal"
29647 ];
29648 _WpdTextField.styles = [textFieldStyles];
29649 _WpdTextField.help = {
29650 title: "Text field",
29651 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.",
29652 status: "stable",
29653 since: "0.11.0",
29654 props: [
29655 { name: "label", type: "string", description: "Visible label above the input." },
29656 { name: "value", type: "string", description: "Current input value; reflected two-way." },
29657 { name: "placeholder", type: "string", description: "Native placeholder string." },
29658 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
29659 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
29660 {
29661 name: "autocomplete",
29662 type: "string",
29663 default: "off",
29664 description: "Forwarded to the native input autocomplete attribute."
29665 },
29666 {
29667 name: "type",
29668 type: "string",
29669 default: "text",
29670 description: "Native input type (text, password, email, search, tel, url)."
29671 },
29672 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
29673 { name: "minlength", type: "integer (string)", description: "Native minlength." },
29674 { name: "pattern", type: "regex string", description: "Native validation pattern." },
29675 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
29676 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
29677 {
29678 name: "invalid",
29679 type: "boolean attribute",
29680 description: "Marks the field aria-invalid and applies the error style."
29681 },
29682 {
29683 name: "reveal",
29684 type: "boolean attribute",
29685 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
29686 }
29687 ],
29688 events: [
29689 {
29690 name: "wpd-input-change",
29691 description: "Fires on every input keystroke.",
29692 detail: "{ value: string }"
29693 },
29694 {
29695 name: "wpd-input-commit",
29696 description: "Fires on the native change event (blur / Enter).",
29697 detail: "{ value: string }"
29698 },
29699 {
29700 name: "wpd-submit",
29701 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
29702 detail: "{ value: string }"
29703 }
29704 ],
29705 cssProps: [
29706 { name: "--desktop-mode-text", description: "Text colour." },
29707 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
29708 { name: "--desktop-mode-border", description: "Input outline." },
29709 { name: "--desktop-mode-window-bg", description: "Input background." }
29710 ],
29711 example: html`
29712 <wpd-stack gap="8">
29713 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
29714 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
29715 </wpd-stack>
29716 `
29717 };
29718 let WpdTextField = _WpdTextField;
29719 defineComponent("wpd-text-field", WpdTextField);
29720 function _iconEye() {
29721 return html`
29722 <svg
29723 viewBox="0 0 16 16"
29724 width="14"
29725 height="14"
29726 fill="none"
29727 stroke="currentColor"
29728 stroke-width="1.5"
29729 stroke-linecap="round"
29730 stroke-linejoin="round"
29731 aria-hidden="true"
29732 focusable="false"
29733 >
29734 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
29735 <circle cx="8" cy="8" r="2" />
29736 </svg>
29737 `;
29738 }
29739 function _iconEyeOff() {
29740 return html`
29741 <svg
29742 viewBox="0 0 16 16"
29743 width="14"
29744 height="14"
29745 fill="none"
29746 stroke="currentColor"
29747 stroke-width="1.5"
29748 stroke-linecap="round"
29749 stroke-linejoin="round"
29750 aria-hidden="true"
29751 focusable="false"
29752 >
29753 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
29754 <circle cx="8" cy="8" r="2" />
29755 <line x1="2" y1="2" x2="14" y2="14" />
29756 </svg>
29757 `;
29758 }
29759 async function uploadFile(args) {
29760 const initial = {
29761 file: args.file,
29762 mime: args.mime,
29763 fields: args.fields
29764 };
29765 const filtered = applyFilters(
29766 FILE_DROP_HOOKS.BEFORE_UPLOAD,
29767 initial,
29768 args.context
29769 );
29770 if (!filtered) {
29771 throw new UploadCancelledError();
29772 }
29773 const body = new FormData();
29774 const renamed = filtered.fields.filename !== filtered.file.name ? new File([filtered.file], filtered.fields.filename, {
29775 type: filtered.mime || filtered.file.type
29776 }) : filtered.file;
29777 body.append("file", renamed);
29778 body.append("title", filtered.fields.title);
29779 body.append("alt_text", filtered.fields.altText);
29780 body.append("caption", filtered.fields.caption);
29781 body.append("description", filtered.fields.description);
29782 return new Promise((resolve2, reject) => {
29783 const xhr = new XMLHttpRequest();
29784 xhr.open("POST", args.mediaUrl, true);
29785 xhr.withCredentials = true;
29786 xhr.setRequestHeader("X-WP-Nonce", args.restNonce);
29787 xhr.responseType = "text";
29788 let aborted = false;
29789 let bodyFullySent = false;
29790 let cancelRequested = false;
29791 const abort = () => {
29792 cancelRequested = true;
29793 if (bodyFullySent) {
29794 return;
29795 }
29796 aborted = true;
29797 try {
29798 xhr.abort();
29799 } catch {
29800 }
29801 };
29802 doAction(FILE_DROP_HOOKS.UPLOAD_STARTED, {
29803 file: filtered.file,
29804 fields: filtered.fields,
29805 context: args.context,
29806 abort
29807 });
29808 xhr.upload.addEventListener("progress", (e) => {
29809 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
29810 file: filtered.file,
29811 fields: filtered.fields,
29812 context: args.context,
29813 loaded: e.loaded,
29814 total: e.lengthComputable ? e.total : 0,
29815 indeterminate: !e.lengthComputable
29816 });
29817 });
29818 xhr.upload.addEventListener("load", () => {
29819 bodyFullySent = true;
29820 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
29821 file: filtered.file,
29822 fields: filtered.fields,
29823 context: args.context,
29824 loaded: filtered.file.size,
29825 total: filtered.file.size,
29826 indeterminate: false
29827 });
29828 });
29829 xhr.addEventListener("error", () => {
29830 if (aborted) {
29831 return;
29832 }
29833 const error = new Error("Network error during upload.");
29834 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29835 // `filtered.file` — same identity as UPLOAD_STARTED /
29836 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
29837 // that swapped the File would otherwise route this
29838 // failure to a row keyed by the original (pre-swap)
29839 // File, leaving the HUD row stuck in "running".
29840 file: filtered.file,
29841 error,
29842 context: args.context
29843 });
29844 reject(error);
29845 });
29846 xhr.addEventListener("abort", () => {
29847 const error = new UploadAbortedError();
29848 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29849 // `filtered.file` — same identity as UPLOAD_STARTED /
29850 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
29851 // that swapped the File would otherwise route this
29852 // failure to a row keyed by the original (pre-swap)
29853 // File, leaving the HUD row stuck in "running".
29854 file: filtered.file,
29855 error,
29856 context: args.context
29857 });
29858 reject(error);
29859 });
29860 xhr.addEventListener("load", () => {
29861 if (aborted) {
29862 return;
29863 }
29864 if (xhr.status < 200 || xhr.status >= 300) {
29865 const message = extractXhrMessage(xhr);
29866 const error = new Error(message);
29867 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29868 file: filtered.file,
29869 error,
29870 context: args.context
29871 });
29872 reject(error);
29873 return;
29874 }
29875 let data;
29876 try {
29877 data = JSON.parse(xhr.responseText);
29878 } catch (err) {
29879 const error = err instanceof Error ? err : new Error("Could not parse server response.");
29880 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29881 file: filtered.file,
29882 error,
29883 context: args.context
29884 });
29885 reject(error);
29886 return;
29887 }
29888 if (cancelRequested && data.id) {
29889 void deleteAttachment(
29890 args.mediaUrl,
29891 args.restNonce,
29892 data.id
29893 );
29894 const error = new UploadAbortedError();
29895 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
29896 file: filtered.file,
29897 error,
29898 context: args.context
29899 });
29900 reject(error);
29901 return;
29902 }
29903 const result = {
29904 id: data.id,
29905 url: data.source_url,
29906 mime: data.mime_type || filtered.mime,
29907 title: data.title?.rendered || filtered.fields.title,
29908 filename: data.media_details?.file || filtered.fields.filename
29909 };
29910 doAction(FILE_DROP_HOOKS.AFTER_UPLOAD, {
29911 file: filtered.file,
29912 result,
29913 fields: filtered.fields,
29914 context: args.context
29915 });
29916 resolve2(result);
29917 });
29918 xhr.send(body);
29919 });
29920 }
29921 class UploadCancelledError extends Error {
29922 constructor() {
29923 super("Upload cancelled by desktop-mode.drop.before-upload filter.");
29924 this.name = "UploadCancelledError";
29925 }
29926 }
29927 class UploadAbortedError extends Error {
29928 constructor() {
29929 super("Upload aborted by the caller.");
29930 this.name = "UploadAbortedError";
29931 }
29932 }
29933 function deleteAttachment(mediaUrl, restNonce, id) {
29934 const url = `${mediaUrl.replace(/\/$/, "")}/${id}?force=true`;
29935 const cleanup = new XMLHttpRequest();
29936 cleanup.open("DELETE", url, true);
29937 cleanup.withCredentials = true;
29938 cleanup.setRequestHeader("X-WP-Nonce", restNonce);
29939 return new Promise((resolve2) => {
29940 cleanup.addEventListener("loadend", () => {
29941 if (cleanup.status < 200 || cleanup.status >= 300) {
29942 console.warn(
29943 `[os-file-drop] late-cancel cleanup failed for attachment ${id} (HTTP ${cleanup.status}). The attachment remains in the Media Library; delete it manually.`
29944 );
29945 }
29946 resolve2();
29947 });
29948 cleanup.addEventListener("error", () => {
29949 console.warn(
29950 `[os-file-drop] late-cancel cleanup network error for attachment ${id}. The attachment remains in the Media Library; delete it manually.`
29951 );
29952 resolve2();
29953 });
29954 try {
29955 cleanup.send();
29956 } catch (err) {
29957 console.warn(
29958 `[os-file-drop] late-cancel cleanup could not be dispatched for attachment ${id}:`,
29959 err
29960 );
29961 resolve2();
29962 }
29963 });
29964 }
29965 function extractXhrMessage(xhr) {
29966 const fallback = `Upload failed (HTTP ${xhr.status}).`;
29967 const text = xhr.responseText;
29968 if (!text) {
29969 return fallback;
29970 }
29971 try {
29972 const data = JSON.parse(text);
29973 if (data && typeof data.message === "string") {
29974 return data.message;
29975 }
29976 } catch {
29977 }
29978 return fallback;
29979 }
29980 async function openUploadDialog(args) {
29981 if (args.entries.length === 0) {
29982 return;
29983 }
29984 const modal = document.createElement("wpd-modal");
29985 modal.setAttribute("open", "");
29986 modal.setAttribute("size", "md");
29987 modal.setAttribute(
29988 "title",
29989 args.entries.length === 1 ? "Upload to Media Library" : `Upload ${args.entries.length} files to Media Library`
29990 );
29991 document.body.appendChild(modal);
29992 const draft = args.entries.map((entry) => ({
29993 ...entry.fields
29994 }));
29995 const renderBody = () => {
29996 modal.innerHTML = "";
29997 const list2 = document.createElement("div");
29998 list2.style.cssText = "display:flex;flex-direction:column;gap:18px;max-height:60vh;overflow:auto;padding-right:6px;";
29999 args.entries.forEach((entry, i) => {
30000 list2.appendChild(renderEntry(entry, draft[i], i + 1));
30001 });
30002 modal.appendChild(list2);
30003 const footer = document.createElement("div");
30004 footer.setAttribute("slot", "footer");
30005 footer.style.cssText = "display:flex;gap:8px;justify-content:flex-end;";
30006 const cancel = document.createElement("wpd-button");
30007 cancel.setAttribute("variant", "secondary");
30008 cancel.textContent = "Cancel";
30009 cancel.addEventListener("click", () => {
30010 modal.remove();
30011 });
30012 const upload = document.createElement("wpd-button");
30013 upload.setAttribute("variant", "primary");
30014 upload.textContent = args.entries.length === 1 ? "Upload" : `Upload ${args.entries.length} files`;
30015 upload.addEventListener("click", () => {
30016 void runUploads(upload, cancel);
30017 });
30018 footer.appendChild(cancel);
30019 footer.appendChild(upload);
30020 modal.appendChild(footer);
30021 };
30022 const renderEntry = (entry, fields, index2) => {
30023 const wrap = document.createElement("div");
30024 wrap.style.cssText = "display:flex;flex-direction:column;gap:8px;border:1px solid rgba(255,255,255,0.08);border-radius:10px;padding:14px;";
30025 const heading = document.createElement("div");
30026 heading.style.cssText = "display:flex;gap:10px;align-items:center;font-weight:600;";
30027 const tag = document.createElement("span");
30028 tag.textContent = args.entries.length === 1 ? "" : `#${index2} · `;
30029 tag.style.opacity = "0.6";
30030 const fname = document.createElement("span");
30031 fname.textContent = entry.file.name;
30032 fname.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
30033 const size = document.createElement("span");
30034 size.textContent = `${entry.mime || "unknown"} · ${formatBytes(
30035 entry.file.size
30036 )}`;
30037 size.style.cssText = "opacity:0.6;font-size:12px;";
30038 heading.appendChild(tag);
30039 heading.appendChild(fname);
30040 heading.appendChild(size);
30041 wrap.appendChild(heading);
30042 wrap.appendChild(textField("Title", fields.title, (v) => fields.title = v));
30043 wrap.appendChild(textField("Filename", fields.filename, (v) => fields.filename = v));
30044 if (entry.mime.startsWith("image/")) {
30045 wrap.appendChild(
30046 textField("Alt text", fields.altText, (v) => fields.altText = v)
30047 );
30048 }
30049 wrap.appendChild(textField("Caption", fields.caption, (v) => fields.caption = v));
30050 wrap.appendChild(
30051 textareaField("Description", fields.description, (v) => fields.description = v)
30052 );
30053 return wrap;
30054 };
30055 const runUploads = async (uploadBtn, cancelBtn) => {
30056 uploadBtn.disabled = true;
30057 cancelBtn.disabled = true;
30058 uploadBtn.textContent = "Uploading…";
30059 const total = args.entries.length;
30060 let successes = 0;
30061 let failures = 0;
30062 let cancelled = 0;
30063 const failureDetails = [];
30064 for (let i = 0; i < total; i++) {
30065 const entry = args.entries[i];
30066 try {
30067 await uploadFile({
30068 file: entry.file,
30069 mime: entry.mime,
30070 fields: draft[i],
30071 context: args.context,
30072 mediaUrl: args.mediaUrl,
30073 restNonce: args.restNonce
30074 });
30075 successes++;
30076 } catch (err) {
30077 if (err instanceof UploadCancelledError) {
30078 cancelled++;
30079 continue;
30080 }
30081 if (err instanceof UploadAbortedError) {
30082 cancelled++;
30083 continue;
30084 }
30085 failures++;
30086 const message = err instanceof Error ? err.message : "Upload failed.";
30087 failureDetails.push(`“${entry.file.name}” — ${message}`);
30088 }
30089 }
30090 modal.remove();
30091 showBatchSummaryToast({
30092 total,
30093 successes,
30094 failures,
30095 cancelled,
30096 failureDetails
30097 });
30098 };
30099 renderBody();
30100 await new Promise((resolve2) => {
30101 modal.addEventListener("wpd-modal-cancel", () => {
30102 modal.remove();
30103 resolve2();
30104 });
30105 const observer = new MutationObserver(() => {
30106 if (!modal.isConnected) {
30107 observer.disconnect();
30108 resolve2();
30109 }
30110 });
30111 observer.observe(document.body, { childList: true, subtree: true });
30112 });
30113 }
30114 function textField(label, value, onChange) {
30115 const el = document.createElement("wpd-text-field");
30116 el.setAttribute("label", label);
30117 el.setAttribute("value", value);
30118 el.addEventListener("input", () => {
30119 const v = el.value;
30120 if (typeof v === "string") {
30121 onChange(v);
30122 }
30123 });
30124 return el;
30125 }
30126 function textareaField(label, value, onChange) {
30127 const el = document.createElement("wpd-textarea");
30128 el.setAttribute("label", label);
30129 el.setAttribute("value", value);
30130 el.setAttribute("rows", "3");
30131 el.addEventListener("input", () => {
30132 const v = el.value;
30133 if (typeof v === "string") {
30134 onChange(v);
30135 }
30136 });
30137 return el;
30138 }
30139 function showBatchSummaryToast(args) {
30140 const { total, successes, failures, cancelled, failureDetails } = args;
30141 if (total === 0) {
30142 return;
30143 }
30144 if (total === 1) {
30145 if (successes === 1) {
30146 showToast({ message: "Uploaded to Media Library." });
30147 } else if (failures === 1 && failureDetails[0]) {
30148 showToast({ message: failureDetails[0] });
30149 } else if (cancelled === 1) {
30150 showToast({ message: "Upload cancelled." });
30151 }
30152 return;
30153 }
30154 if (successes === total) {
30155 showToast({
30156 message: `Uploaded ${successes} files to Media Library.`
30157 });
30158 return;
30159 }
30160 if (cancelled === total) {
30161 showToast({ message: "All uploads cancelled." });
30162 return;
30163 }
30164 if (failures === total) {
30165 showToast({
30166 message: failures === 1 && failureDetails[0] ? failureDetails[0] : `${failures} uploads failed.`
30167 });
30168 return;
30169 }
30170 const parts = [];
30171 if (successes > 0) {
30172 parts.push(
30173 `Uploaded ${successes} file${successes === 1 ? "" : "s"}.`
30174 );
30175 }
30176 if (cancelled > 0) {
30177 parts.push(`Cancelled ${cancelled}.`);
30178 }
30179 if (failures > 0) {
30180 parts.push(`Failed ${failures}.`);
30181 }
30182 showToast({ message: parts.join(" ") });
30183 }
30184 const dialog = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
30185 __proto__: null,
30186 openUploadDialog
30187 }, Symbol.toStringTag, { value: "Module" }));
30188 exports.clampGeometryToViewport = clampGeometryToViewport;
30189 Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
30190 return exports;
30191 }({});
30192