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

33,311 lines 1.1 MB
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 /** Filter, receives the unfocused-window effect registry array. */
84 UNFOCUS_EFFECTS: "desktop-mode.unfocus-effects",
85 /** Action before a canvas wallpaper mounts. */
86 WALLPAPER_MOUNTING: "desktop-mode.wallpaper.mounting",
87 /** Action after a canvas wallpaper mounts successfully. */
88 WALLPAPER_MOUNTED: "desktop-mode.wallpaper.mounted",
89 /** Action before a canvas wallpaper tears down. */
90 WALLPAPER_UNMOUNTING: "desktop-mode.wallpaper.unmounting",
91 /** Action when a canvas wallpaper's mount throws / rejects. */
92 WALLPAPER_MOUNT_FAILED: "desktop-mode.wallpaper.mount-failed",
93 /** Action mirroring document.visibilitychange for active canvas wallpapers. */
94 WALLPAPER_VISIBILITY: "desktop-mode.wallpaper.visibility",
95 /**
96 * Filter, receives a wallpaper's preview params (seeded from the
97 * def's `previewParams`) before its `renderPreview` runs in the OS
98 * Settings picker. Args: `( params, wallpaperId )`.
99 */
100 WALLPAPER_PREVIEW_PARAMS: "desktop-mode.wallpaper.preview-params",
101 /**
102 * Action, fires after a wallpaper's persisted settings change (the
103 * user edited them through the wallpaper's config dialog in OS
104 * Settings). Payload: `{ id, settings }` — the wallpaper id and the
105 * full post-merge settings object. A mounted wallpaper subscribes to
106 * live-apply changes without a remount.
107 *
108 * @since 0.9.5
109 */
110 WALLPAPER_SETTINGS_CHANGED: "desktop-mode.wallpaper.settings-changed",
111 // ------------------------------------------------------------------
112 // Observability — iframe errors, iframe network, shell-side errors,
113 // monitor entry aggregation. Designed for dashboard / debug widget
114 // plugins that want genuine admin observability (Gutenberg save
115 // failures, admin-ajax 500s, plugin exceptions) rather than just the
116 // shell's own console-error surface.
117 // ------------------------------------------------------------------
118 /**
119 * Action, fires once per iframe when the chromeless bridge
120 * script has finished wiring its message listeners. Payload:
121 * `{ windowId: string }`. Subscribers get a reliable "safe to
122 * talk to this iframe" signal — the browser's native `load`
123 * event fires before our bridge attaches, so messages sent on
124 * `load` can be dropped on the floor. Use this instead when
125 * timing matters (first-focus dispatch, auto-fill handshakes).
126 *
127 * @since 0.5.0
128 */
129 IFRAME_READY: "desktop-mode.iframe.ready",
130 /**
131 * Action, fires when a chromeless iframe's `error` or
132 * `unhandledrejection` handler catches an exception. Payload: `{
133 * windowId: string, kind: 'error' | 'unhandledrejection', message:
134 * string, filename: string | null, lineno: number | null, colno:
135 * number | null, stack: string | null }`. Origin-filtered at the
136 * parent shell; cross-origin iframe errors never reach here.
137 */
138 IFRAME_ERROR: "desktop-mode.iframe.error",
139 /**
140 * Action, fires when a `fetch` or `XMLHttpRequest` inside a
141 * chromeless iframe completes (success OR failure). Payload: `{
142 * windowId: string, method: string, url: string, status: number,
143 * duration: number, failed: boolean }`. Subscribers get a faithful
144 * view of admin-ajax + REST calls that previously never left the
145 * iframe boundary. `status === 0` indicates a network failure with
146 * no response received.
147 */
148 IFRAME_NETWORK_COMPLETED: "desktop-mode.iframe.network-completed",
149 /**
150 * Action, fires when one of the shell's own try/catch barriers
151 * catches an exception. Payload: `{ scope:
152 * 'widget-mount' | 'widget-teardown' | 'window-open' | 'wallpaper-mount' |
153 * 'wallpaper-teardown' | 'session-save' | 'menu-refresh' | string,
154 * id?: string, error: unknown }`. Paired with the existing
155 * `console.error` calls — a monitor widget can surface these as
156 * first-class entries.
157 */
158 SHELL_ERROR: "desktop-mode.shell.error",
159 /**
160 * Action, fires once per `wp.desktop.broadcast()` call with the
161 * fully-resolved `{ topic, payload }` detail. Lets plugins log,
162 * mirror, or augment broadcast traffic without subscribing for
163 * every individual topic.
164 */
165 BROADCAST: "desktop-mode.broadcast",
166 /**
167 * Filter, applies to a `MonitorEntry` before a monitor widget
168 * renders it. Plugins can mutate the entry (rewrite the message,
169 * add `extra` fields) or return `null` to suppress it. Used by
170 * monitor widgets to converge every plugin on the same shape —
171 * see `MonitorEntry` in `src/types.ts`.
172 */
173 MONITOR_ENTRY: "desktop-mode.monitor.entry",
174 /**
175 * Filter, applies to the list of "solid" surfaces wallpapers
176 * should consider for collision / accumulation effects (snow
177 * piling, leaves settling, rain splash). Seeded by the shell
178 * with: every visible (non-minimized) window's top edge; the
179 * desktop-area floor; the dock's outward-facing edge; and every
180 * mounted widget card's top edge.
181 *
182 * Plugins that own their own DOM (e.g. floating pickers,
183 * custom overlays) can push additional surfaces so snow
184 * accumulates on them too.
185 *
186 * Each entry is a `WallpaperSurface` — see
187 * `src/wallpapers/surfaces.ts` for the shape. Rects are in
188 * viewport coordinates (clientX / clientY), matching what a
189 * canvas mounted inside `#desktop-mode-wallpaper` reads.
190 */
191 WALLPAPER_SURFACES: "desktop-mode.wallpaper.surfaces",
192 // ------------------------------------------------------------------
193 // Window lifecycle actions. All payloads share a `windowId: string`
194 // field; additional fields are documented per-hook in the JS
195 // reference. These mirror the existing `desktop-mode-window-*`
196 // CustomEvents but ship under the hook bus so plugins can use one
197 // idiomatic API for everything the shell emits.
198 // ------------------------------------------------------------------
199 /**
200 * Filter, last call before a window's resolved geometry (x, y,
201 * width, height, initialState) is baked into the `WindowConfig`
202 * passed to the `Window` constructor. Lets a plugin override
203 * default placement for windows it owns, snap restored bounds to
204 * a different region, or force a particular initial state.
205 *
206 * Signature:
207 *
208 * ( geometry: ResolvedWindowGeometry, ctx: WindowGeometryContext )
209 * => ResolvedWindowGeometry
210 *
211 * Where `ResolvedWindowGeometry = { x, y, width, height, state? }`
212 * and `ctx = { windowId, baseId, hasSavedGeometry, callerPinned,
213 * desktopRect }`.
214 *
215 * - `hasSavedGeometry` is `true` when the user previously
216 * dragged or resized this window and the resolved geometry
217 * includes those restored values. Plugins that want to
218 * "leave the user's saved layout alone" should bail when
219 * this is true.
220 * - `callerPinned` is `true` when the caller of `manager.open()`
221 * passed at least one of `{ x, y, width, height, initialState }`
222 * explicitly. For NATIVE windows this is usually true (the
223 * framework's native-window opener passes the registry's
224 * declared dimensions); for admin-page iframe windows opened
225 * from the dock this is usually false. The filter is free to
226 * override registry defaults — `callerPinned: true` does NOT
227 * mean "leave it alone."
228 *
229 * The shell re-clamps `width`/`height` to the registered
230 * `minWidth`/`minHeight` after the filter returns — a buggy
231 * filter cannot ship a sub-minimum window. `x` and `y` are
232 * NOT re-clamped to the desktop rect after the filter (plugins
233 * sometimes want to place windows partially off-screen for
234 * deliberate stylistic reasons); the filter is responsible for
235 * its own viewport math when it cares.
236 *
237 * Companion of `desktop_mode_register_window` server-side
238 * defaults — runs every time a window opens, not just at
239 * registration.
240 *
241 * @since 0.8.6
242 */
243 WINDOW_GEOMETRY: "desktop-mode.window.geometry",
244 /** Action, fires when a window is added to the stack. */
245 WINDOW_OPENED: "desktop-mode.window.opened",
246 /**
247 * Action, fires when a window's body enters the loading state — at
248 * construction (every window starts loading) and whenever a plugin
249 * calls {@link NativeRenderContext.window.markLoading} or
250 * `Window.markContentLoading()` mid-life. Payload: `{ windowId }`.
251 *
252 * The shell shows a `<wpd-spinner>` overlay while the window is in
253 * the loading state and fades content in on the loaded transition.
254 * Subscribe to this hook (or to {@link WINDOW_CONTENT_LOADED}) when
255 * you need to react to either edge — analytics, instrumentation,
256 * decorating the spinner with a per-window message.
257 *
258 * Edge-triggered: idempotent calls don't re-fire. The matching
259 * `desktop-mode-window-content-loading` CustomEvent dispatches on
260 * `document` with the same payload.
261 *
262 * @since 0.6.0
263 */
264 WINDOW_CONTENT_LOADING: "desktop-mode.window.content-loading",
265 /**
266 * Action, fires when a window's body content becomes ready — for
267 * iframe windows the moment the chromeless bridge announces
268 * `desktop-mode-ready`, for native windows after the user's
269 * `render( body )` callback (or its returned promise) resolves, and
270 * whenever a plugin calls {@link NativeRenderContext.window.markReady}
271 * or `Window.markContentLoaded()` mid-life. Payload: `{ windowId }`.
272 *
273 * The unified "window content is ready" signal across both render
274 * strategies — use this instead of branching on iframe vs. native.
275 * Iframe-only consumers can still subscribe to {@link IFRAME_READY},
276 * which fires alongside this hook for iframe windows. The shell
277 * removes the loading overlay and fades the content in on this
278 * transition.
279 *
280 * Edge-triggered: only fires on a loading → ready transition.
281 * The matching `desktop-mode-window-content-loaded` CustomEvent
282 * dispatches on `document` with the same payload.
283 *
284 * @since 0.6.0
285 */
286 WINDOW_CONTENT_LOADED: "desktop-mode.window.content-loaded",
287 /**
288 * Filter, applied to the loading-overlay HTMLElement just after
289 * the shell paints its default `<wpd-spinner>` and after any
290 * per-window inline customization (`config.loading.render`)
291 * runs. Receives the overlay element; context: `{ windowId,
292 * config }`. Plugins may mutate the element (e.g.
293 * `host.replaceChildren( myBrandedLoader )` to swap out the
294 * default entirely, or `host.querySelector('wpd-spinner')!.
295 * setAttribute('preset', 'comet')` to retune the spinner) or
296 * return a different element to replace the overlay wholesale.
297 *
298 * Use cases: a brand-skin plugin that overrides every window's
299 * spinner with its own logo; a status-bar plugin that adds
300 * "Loading… 47% — fetching posts" text; an A/B-test framework
301 * that swaps the loader during an experiment.
302 *
303 * Resolution order for the loading overlay:
304 * 1. Default content (`<wpd-spinner>`) is painted.
305 * 2. Per-window `config.loading.render( host, ctx )` runs.
306 * 3. This filter runs.
307 * 4. The result is appended to the window body.
308 *
309 * @since 0.6.0
310 */
311 WINDOW_LOADING_OVERLAY: "desktop-mode.window.loading-overlay",
312 /**
313 * Action, fires when `manager.open(...)` is called for a baseId
314 * whose window already exists on the active desktop. This is the
315 * unambiguous "user requested to open this window again" signal
316 * — distinct from focus changes (which double-fire on alt-tab and
317 * skip when already focused) and from `WINDOW_OPENED` (which only
318 * fires on first creation). Payload:
319 * `{ windowId: string, baseId: string, wasMinimized: boolean }`.
320 *
321 * Plugins that hold per-window state (e.g. the code-editor's
322 * active file) should listen here to re-orient the existing
323 * window's content to whatever the caller wants to show — the
324 * open-window call is synchronous, so any state the caller sets
325 * BEFORE invoking `openWindow` is already in place when this
326 * fires.
327 */
328 WINDOW_REOPENED: "desktop-mode.window.reopened",
329 /**
330 * Action, fires BEFORE the window's element is detached from the
331 * DOM but AFTER the manager has already removed it from the stack.
332 * Payload: `{ windowId: string, element: HTMLElement }`.
333 *
334 * Use this for cleanup that needs a reference to the live
335 * element (removing anchored snow, wallpaper particles pinned to
336 * window tops, measurement caches keyed by element). `WINDOW_CLOSED`
337 * fires immediately after and only carries the id, which means
338 * subscribers would otherwise have to re-query the DOM — by then
339 * the element is gone, so they can't match at all.
340 */
341 WINDOW_CLOSING: "desktop-mode.window.closing",
342 /** Action, fires when a window is removed from the stack. */
343 WINDOW_CLOSED: "desktop-mode.window.closed",
344 /** Action, fires when focus changes to a different window. */
345 WINDOW_FOCUSED: "desktop-mode.window.focused",
346 /**
347 * Action, fires for the window that LOST focus when another
348 * window takes over. Symmetric counterpart to
349 * `WINDOW_FOCUSED`. Payload: `{ windowId: string, focusedTo:
350 * string | null }` — `focusedTo` identifies the new top of
351 * the stack so blur subscribers can ignore alt-tabs to a
352 * sibling they own.
353 *
354 * No-op when there's no previously-focused window (initial
355 * boot, all-windows-closed). Manager fires this BEFORE
356 * `WINDOW_FOCUSED` so subscribers see "blur old, focus new"
357 * in deterministic order.
358 *
359 * @since 0.5.5
360 */
361 WINDOW_BLURRED: "desktop-mode.window.blurred",
362 /**
363 * Action, fires when a window is minimized. Payload:
364 * `{ windowId: string, element: HTMLElement }`.
365 *
366 * The element ride-along matches {@link WINDOW_CLOSING}'s shape so
367 * wallpaper plugins anchored to window tops (snow, leaves, rain
368 * splash) can match stuck particles by element identity and run
369 * their teardown — minimized windows render at `opacity: 0` so
370 * `offsetParent === null` checks miss them.
371 */
372 WINDOW_MINIMIZED: "desktop-mode.window.minimized",
373 /**
374 * Action, fires when a window is restored from minimized. Payload:
375 * `{ windowId: string, element: HTMLElement }`.
376 */
377 WINDOW_RESTORED: "desktop-mode.window.restored",
378 /**
379 * Action, fires when a window is maximized (fills desktop area).
380 * Payload: `{ windowId: string, element: HTMLElement }`.
381 */
382 WINDOW_MAXIMIZED: "desktop-mode.window.maximized",
383 /**
384 * Action, fires when a window exits maximized state. Payload:
385 * `{ windowId: string, element: HTMLElement }`.
386 */
387 WINDOW_UNMAXIMIZED: "desktop-mode.window.unmaximized",
388 /**
389 * Action, fires when a window enters fullscreen / focus mode.
390 * Payload: `{ windowId: string, element: HTMLElement }`.
391 */
392 WINDOW_FULLSCREEN_ENTERED: "desktop-mode.window.fullscreen-entered",
393 /**
394 * Action, fires when a window exits fullscreen / focus mode.
395 * Payload: `{ windowId: string, element: HTMLElement }`.
396 */
397 WINDOW_FULLSCREEN_EXITED: "desktop-mode.window.fullscreen-exited",
398 /**
399 * Filter, decides whether a fullscreen ("focus mode") window
400 * should auto-exit when focus moves to a different window.
401 *
402 * Default is `true` so a newly-focused window is never silently
403 * occluded by a fullscreen one (its `z-index` sits above all
404 * other windows). Plugins whose fullscreen surface is meant to
405 * persist across focus changes — slideshows, video players,
406 * immersive games — can return `false` to keep their window
407 * fullscreen.
408 *
409 * Signature:
410 *
411 * ( shouldExit: boolean, ctx: {
412 * windowId: string, // the fullscreen window
413 * focusedTo: string, // the window gaining focus
414 * } ) => boolean
415 *
416 * @since 0.8.6
417 */
418 WINDOW_AUTO_EXIT_FULLSCREEN: "desktop-mode.window.auto-exit-fullscreen",
419 /**
420 * Filter, decides whether the window under the cursor is raised
421 * (focused) after a short hover dwell during a drag — any drag,
422 * whatever its source: a shell DragManager session, a
423 * cross-iframe bridge drag, an OS file, or an arbitrary native
424 * HTML5 drag.
425 *
426 * Default is `true`: dragging a payload over a background window
427 * and resting there for ~250 ms brings it forward, so the user
428 * can see the drop target they're aiming at (macOS spring-loading
429 * style). Plugins whose windows must never steal z-order during a
430 * drag — pinned reference panels, HUD/palette windows — can
431 * return `false` for their window id.
432 *
433 * Signature:
434 *
435 * ( shouldFocus: boolean, ctx: {
436 * windowId: string, // the hovered window
437 * payloadType: string, // DragManager payload `type`,
438 * // bridge payload `kind`,
439 * // 'os-file', or 'external'
440 * } ) => boolean
441 *
442 * @since 0.9.4
443 */
444 WINDOW_FOCUS_ON_DRAG_HOVER: "desktop-mode.window.focus-on-drag-hover",
445 /**
446 * Action, fires at most once per animation frame during an
447 * active drag or resize with the live geometry. Payload: `{
448 * windowId: string, x: number, y: number, width: number,
449 * height: number, state: WindowState, phase: 'drag' | 'resize' }`.
450 *
451 * Intended for per-frame collision-aware wallpapers (snow piling
452 * on window tops, rain splash on edges) that would otherwise
453 * poll `getBoundingClientRect` every rAF. Coalesced via
454 * `requestAnimationFrame` so a pointermove storm collapses to
455 * one fire per paint — matches the cadence a wallpaper's own
456 * ticker runs at.
457 *
458 * NOT fired at drag/resize end — `WINDOW_DRAG_END` /
459 * `WINDOW_RESIZE_END` handle the settled geometry. Subscribers
460 * that only want the final position should listen to those
461 * instead.
462 */
463 WINDOW_BOUNDS_CHANGED: "desktop-mode.window.bounds-changed",
464 /** Action, fires at drag-end with the final `{ x, y }` position. */
465 WINDOW_MOVED: "desktop-mode.window.moved",
466 /** Action, fires at resize-end with the final `{ width, height }`. */
467 WINDOW_RESIZED: "desktop-mode.window.resized",
468 /** Action, fires when title-bar drag begins. */
469 WINDOW_DRAG_START: "desktop-mode.window.drag-start",
470 /** Action, fires when title-bar drag ends. Payload mirrors WINDOW_MOVED. */
471 WINDOW_DRAG_END: "desktop-mode.window.drag-end",
472 /** Action, fires when the resize handle is first pressed. */
473 WINDOW_RESIZE_START: "desktop-mode.window.resize-start",
474 /** Action, fires when resize completes. Payload mirrors WINDOW_RESIZED. */
475 WINDOW_RESIZE_END: "desktop-mode.window.resize-end",
476 /** Action, fires when the user "detaches" a window to a classic tab. */
477 WINDOW_DETACHED: "desktop-mode.window.detached",
478 /**
479 * Action, fires when the user clicks the title-bar reload button
480 * on an iframe-backed window. Payload: `{ windowId: string, url:
481 * string }` where `url` is the URL being reloaded (the active
482 * primary or external sub-tab). Subscribers can use this to
483 * invalidate their own cache, force a save before navigation,
484 * track usage as a UX signal, or sync state across companion
485 * surfaces. Native windows do not fire this — they own their
486 * DOM directly and the reload button doesn't apply.
487 */
488 WINDOW_RELOADED: "desktop-mode.window.reloaded",
489 /** Action, fires when iframe title updates change the window title. */
490 WINDOW_TITLE_CHANGED: "desktop-mode.window.title-changed",
491 /**
492 * Action, fires when a window's `setHighlight()` mode changes.
493 * Payload: `{ windowId: string, mode: 'preview' | 'persistent' | null,
494 * color?: string }`. Lets onboarding / guidance / drag-bridge
495 * plugins react when another module flagged one of their
496 * windows as the focus of a multi-step interaction without
497 * having to observe DOM mutations.
498 *
499 * @since 0.6.0
500 */
501 WINDOW_HIGHLIGHT_CHANGED: "desktop-mode.window.highlight-changed",
502 /**
503 * Action, fires when a window's body element's dimensions
504 * change — mount, user resize, viewport reflow. Payload: `{
505 * windowId: string, width: number, height: number }`. Body
506 * dimensions exclude the title bar + tab strip, matching what a
507 * canvas or layout engine inside the body would measure.
508 */
509 WINDOW_BODY_RESIZED: "desktop-mode.window.body-resized",
510 // ------------------------------------------------------------------
511 // Native-window lifecycle. These fire ONLY for windows constructed
512 // with `native: true` — iframe windows have no render phase to
513 // intercept. Use them to wrap / instrument / cancel the paint of
514 // plugin-contributed native windows (the Calculator, Jorvy, custom
515 // native launchers).
516 // ------------------------------------------------------------------
517 /**
518 * Filter, applied to the body element a native window will render
519 * into, just BEFORE the user's `render( body )` callback runs.
520 * Payload: the `HTMLElement`; context: `{ windowId, config }`.
521 *
522 * Return the same element (or a wrapper) to intercept. Subscribers
523 * commonly use this to inject a consistent shell (padding,
524 * background, decorative chrome) around every native window
525 * without every plugin re-implementing the pattern.
526 */
527 NATIVE_WINDOW_BEFORE_RENDER: "desktop-mode.native-window.before-render",
528 /**
529 * Action, fires AFTER a native window's `render( body )` callback
530 * returns. Payload: `{ windowId, body, config }`. Observability
531 * hook — analytics / auto-focus / post-render measurement.
532 */
533 NATIVE_WINDOW_AFTER_RENDER: "desktop-mode.native-window.after-render",
534 /**
535 * Filter, applied when a native window is about to start its
536 * close animation. Return `false` to CANCEL the close — the
537 * window stays open. Payload: `true`; context: `{ windowId,
538 * config }`. Any non-`false` return (including `undefined`) lets
539 * the close proceed.
540 *
541 * Intended for "unsaved changes" guards: a calculator with a
542 * pending operation can prompt the user and abort the close
543 * mid-flight. Does NOT apply to iframe windows — their close is
544 * driven by browser navigation patterns the shell doesn't own.
545 */
546 NATIVE_WINDOW_BEFORE_CLOSE: "desktop-mode.native-window.before-close",
547 // ------------------------------------------------------------------
548 // Window-chrome customization framework. Plugins drive per-window
549 // appearance (theme, controls, slots, full chrome render) through
550 // the `wp.desktop.registerWindow*` registries; these hooks expose
551 // every resolution step so plugins can mutate or observe the
552 // chrome pipeline without owning a registration.
553 //
554 // Layers 1-3 (theme, controls, slots) are Stable. Layer 4 (chrome
555 // render) is Experimental — `WINDOW_CHROME_RENDER` may change.
556 // ------------------------------------------------------------------
557 /**
558 * Filter, applied to the resolved CSS-variable map for a window.
559 * Receives `Record< string, string >`; context: `{ windowId,
560 * config }`. Plugins return a mutated map to override or augment
561 * the per-window theme tokens — e.g. tint every Gutenberg
562 * window's title bar to brand colour.
563 *
564 * Stable since 0.6.0.
565 */
566 WINDOW_CHROME_THEME: "desktop-mode.window.chrome.theme",
567 /**
568 * Filter, applied to the resolved control list for a window.
569 * Receives `WindowControlDef[]`; context: `{ windowId, config,
570 * placement: 'left' | 'right' | 'controls' }`. Plugins return a
571 * mutated array to reorder, hide, or inject controls per-window.
572 *
573 * Stable since 0.6.0.
574 */
575 WINDOW_CHROME_CONTROLS: "desktop-mode.window.chrome.controls",
576 /**
577 * Filter, applied per slot when the chrome paints. Receives the
578 * slot host element; context: `{ windowId, slot, config }`.
579 * Plugins can mutate `host` (append decorative children, set
580 * inline styles) without owning a `WindowSlotDef` registration.
581 * The shell never reads the return value — this is an action-
582 * shaped filter so existing `addFilter` plumbing applies.
583 *
584 * Stable since 0.6.0.
585 */
586 WINDOW_CHROME_SLOT: "desktop-mode.window.chrome.slot",
587 /**
588 * Filter, applied to the chrome id selected for a window.
589 * Receives the resolved id (defaults to `'core/standard'`);
590 * context: `{ windowId, config }`. Returning a different id
591 * swaps the chrome registration. **Experimental** — chrome
592 * render contract may change.
593 *
594 * @since 0.6.0
595 */
596 WINDOW_CHROME_RENDER: "desktop-mode.window.chrome.render",
597 /**
598 * Action, fires after a window chrome layer has been mounted /
599 * remounted. Payload: `{ windowId, layer: 'chrome' | 'controls'
600 * | 'slots', chromeId? }` — `chromeId` is present only when
601 * `layer` is `'chrome'`. Subscribers can post-decorate the
602 * chrome (attach observers, anchor pickers).
603 *
604 * @since 0.6.0
605 */
606 WINDOW_CHROME_APPLIED: "desktop-mode.window.chrome.applied",
607 /**
608 * Action, fires after a window's theme tokens are applied to its
609 * outer element. Payload: `{ windowId, themeId, tokens }`. Lets
610 * plugins react to theme changes without diffing CSS variables.
611 *
612 * @since 0.6.0
613 */
614 WINDOW_CHROME_THEME_CHANGED: "desktop-mode.window.chrome.theme-changed",
615 /**
616 * Action, fires when a user clicks a desktop icon (a shortcut
617 * tile registered server-side via `desktop_mode_register_icon()`
618 * and rendered on the wallpaper). Payload: `{ id: string,
619 * target: 'window' | 'url' }`. Fires BEFORE the default open
620 * action — plugins cannot cancel the open from this hook, but
621 * can use it to track click-throughs or augment behaviour (e.g.
622 * play a sound, surface a confirmation toast).
623 *
624 * @since 0.5.0
625 */
626 DESKTOP_ICON_CLICKED: "desktop-mode.desktop-icon.clicked",
627 /**
628 * Action, fires after the wallpaper icon grid is rendered or
629 * re-rendered. Payload:
630 *
631 * {
632 * ids: string[]; // paint order
633 * container: HTMLElement; // <div class="desktop-mode-icons">
634 * tiles: ReadonlyMap<string, HTMLElement>; // id → tile <button>
635 * }
636 *
637 * Plugins that decorate icons with surfaces the framework doesn't
638 * natively expose (drag handles, status dots, cursor adornments)
639 * subscribe here so their decorations survive a live menu refresh
640 * that legitimately rebuilds the grid. The `container` and
641 * `tiles` map mirror the {@link DOCK_AFTER_RENDER}
642 * `tileElements` contract — reach into them directly instead of
643 * re-`querySelector`ing the rendered DOM.
644 *
645 * Notification badges have a first-class API since 0.6.0 —
646 * use `wp.desktop.icons.setBadge( id, count )` (and subscribe
647 * to {@link ICON_BADGE_CHANGED}) instead of decorating from
648 * here. The framework persists badge state across rebuilds, so
649 * a plugin that uses the API doesn't need to re-decorate on
650 * every render.
651 *
652 * Suppressed entirely when the rendered DOM is unchanged from
653 * the previous call (the fingerprint short-circuit upstream
654 * skips both the rebuild and this signal). When the icon list
655 * is empty the hook does not fire at all — the previous
656 * container is removed and no new one is appended.
657 *
658 * @since 0.6.0
659 * @since 0.8.6 — `container` + `tiles` added to the payload
660 * (`ids` retained for back-compat).
661 */
662 DESKTOP_ICONS_RENDERED: "desktop-mode.desktop-icons.rendered",
663 /**
664 * Action, fires whenever the badge count on a desktop icon
665 * changes. Payload: `{ iconId: string, count: number,
666 * previousCount: number }`. Symmetric to {@link DOCK_ITEM_APPENDED}
667 * and the dock/taskbar `wpd-dock-item-badge-changed` CustomEvent
668 * — the icon rail's lifecycle hook for badge transitions.
669 *
670 * Mirrors `desktop-mode/badge-changed` on the activity bus with
671 * `rail: 'icon'`. Subscribe to whichever surface fits — the
672 * activity channel composes across rails for global widgets,
673 * this hook fires only for icon-rail badges with the previous
674 * count carried alongside for delta-aware consumers.
675 *
676 * @since 0.6.0
677 */
678 ICON_BADGE_CHANGED: "desktop-mode.icon.badge-changed",
679 // ------------------------------------------------------------------
680 // Cross-plugin composition.
681 // ------------------------------------------------------------------
682 /**
683 * Action, fires ONCE after every shell-shipped `<wpd-*>` custom
684 * element has registered with `customElements`. Payload: `{
685 * tags: string[] }` — the list of registered tag names. Plugins
686 * that need to defer work until the component registry is
687 * complete (e.g. hydrate user content that uses these tags)
688 * subscribe here instead of polling `customElements.get()`.
689 */
690 COMPONENTS_REGISTERED: "desktop-mode.components.registered",
691 /**
692 * Action, fires after `wp.desktop.registerSystemTile()` inserts
693 * a tile into the unified dock. Payload: `{ id: string }`. Useful
694 * for plugins that want to decorate tiles they didn't register
695 * themselves — analytics, theming, per-tile badges.
696 */
697 DOCK_ITEM_APPENDED: "desktop-mode.dock.item-appended",
698 /**
699 * Action, fires after a system tile is removed from a rail
700 * via `Dock.removeSystemItem()` (typically the server-driven
701 * native-window-sync path on plugin deactivation). Payload:
702 * `{ id: string, placement: 'dock' | 'taskbar' }`. Symmetric
703 * to {@link DOCK_ITEM_APPENDED}; lets analytics / decorators /
704 * cleanup hooks see the full lifecycle without polling the DOM.
705 *
706 * @since 0.6.0
707 */
708 DOCK_ITEM_REMOVED: "desktop-mode.dock.item-removed",
709 // ------------------------------------------------------------------
710 // Dock decoration hooks — render-pipeline filters and actions the
711 // default `Dock` renderer fires while painting tiles. Plugins
712 // compose decoration (animations, classNames, wrappers, tooltips)
713 // without forking the renderer. Custom rail renderers SHOULD fire
714 // the same hooks for ecosystem compatibility — see
715 // `docs/examples/dock-decoration-hooks.md` for the contract.
716 //
717 // Every detail object carries `{ rail, orientation, dockId,
718 // container }` so a single subscriber can disambiguate when two
719 // rails coexist (Classic layout's left side bar + bottom dock).
720 // `dockId` matches the host element's `id` (e.g. `'desktop-mode-dock'`
721 // or `'desktop-mode-side-dock'`) and is the stable
722 // disambiguator — `rail` and `orientation` are convenience
723 // projections of where the renderer is painting.
724 // ------------------------------------------------------------------
725 /**
726 * Action, fires at the start of every dock paint pass — both the
727 * initial mount and every `replaceItems()` that follows on the
728 * live menu-refresh path. Payload `DockRenderContext`. Use this
729 * to invalidate cached per-render decoration state before the
730 * tiles repopulate.
731 *
732 * @since 0.5.2
733 */
734 DOCK_BEFORE_RENDER: "desktop-mode.dock.before-render",
735 /**
736 * Action, fires once every menu and system tile has landed in
737 * the DOM for a paint pass. Payload `DockRenderContext` plus a
738 * frozen `tileElements: ReadonlyMap<string, HTMLElement>` so a
739 * plugin can decorate every tile in one sweep. Symmetric to
740 * {@link DOCK_BEFORE_RENDER}.
741 *
742 * @since 0.5.2
743 */
744 DOCK_AFTER_RENDER: "desktop-mode.dock.after-render",
745 /**
746 * Filter, runs once per tile while the renderer is composing the
747 * className list. Plugins may add, remove, or reorder classes.
748 * Signature: `( classes: string[], detail: DockTileContext ) =>
749 * string[]`. Order is preserved.
750 *
751 * @since 0.5.2
752 */
753 DOCK_TILE_CLASS: "desktop-mode.dock.tile-class",
754 /**
755 * Filter, runs once per tile after the renderer finishes building
756 * the element but before it lands in the DOM. Return the same
757 * element with mutations, or replace with a wrapper — the shell
758 * inserts whatever you return. Signature:
759 * `( el: HTMLElement, detail: DockTileContext ) => HTMLElement`.
760 *
761 * Returning a different node still has to expose a stable
762 * `[data-menu-slug="<id>"]` (or `[data-system-id="<id>"]`)
763 * descendant for active-state / badge updates to find the tile;
764 * wrap, don't replace.
765 *
766 * @since 0.5.2
767 */
768 DOCK_TILE_ELEMENT: "desktop-mode.dock.tile-element",
769 /**
770 * Action, fires once per tile after it has been inserted into
771 * the DOM. Payload `DockTileContext` plus the resolved `el`. Use
772 * for post-insertion decoration where computed layout matters
773 * (measurements, IntersectionObserver bindings, etc.).
774 *
775 * @since 0.5.2
776 */
777 DOCK_TILE_RENDERED: "desktop-mode.dock.tile-rendered",
778 /**
779 * Filter, resolves the tooltip text for a tile. Runs once at
780 * bind time so the dock doesn't re-filter on every pointerenter.
781 * Signature: `( label: string, detail: DockTileContext ) =>
782 * string`. Return an empty string to suppress the tooltip.
783 *
784 * @since 0.5.2
785 */
786 DOCK_TILE_TOOLTIP: "desktop-mode.dock.tile-tooltip",
787 /**
788 * Filter, resolves the body content of a single hover-peek card.
789 * Runs once per card build (i.e., on every show of the peek for
790 * a multi-instance dock tile that has ≥1 open window). Lets a
791 * plugin render a custom thumbnail, status block, or any other
792 * markup inside the card in place of (or alongside) the default
793 * mini-window styling.
794 *
795 * Signature:
796 * ( body: HTMLElement, detail: DockPeekCardContext ) => HTMLElement
797 *
798 * Where `body` is the `<span class="desktop-mode-dock-peek__card-body">`
799 * element that the peek would otherwise populate with ghosted
800 * content lines. The filter may:
801 * - Mutate `body` in place (e.g., append a custom child) and
802 * return it.
803 * - Empty `body` and append plugin-owned children.
804 * - Return an entirely different element to replace `body`.
805 *
806 * `detail.window` is the live `Window` instance the card represents
807 * — plugins can read `window.config`, call `window.getCurrentUrl()`,
808 * subscribe to lifecycle events, etc. `detail.item` is the dock
809 * item descriptor (id / title / icon / url).
810 *
811 * The filter is invoked under the `applyFilters` namespace
812 * `desktop-mode.dock.peek-card-content`.
813 *
814 * @since 0.6.2
815 */
816 DOCK_PEEK_CARD_CONTENT: "desktop-mode.dock.peek-card-content",
817 /**
818 * Filter, runs once per peek card right before it's appended to
819 * the popover. Receives the fully-built default card (with its
820 * mini-window chrome already populated) and can return either
821 * the same node, a mutated version, or an entirely different
822 * element to replace the card outright. Use this when the
823 * `peek-card-content` body filter isn't enough — e.g., when a
824 * plugin wants to swap the whole card chrome (custom titlebar,
825 * different shape) or wrap the card in a third-party component.
826 *
827 * Signature:
828 * ( card: HTMLElement, detail: DockPeekCardContext ) => HTMLElement
829 *
830 * If a plugin returns a brand-new node, it is responsible for
831 * preserving anything the peek relies on:
832 * - The `desktop-mode-dock-peek__card` class (used by the
833 * fan-out animation timing + hover styles).
834 * - A `click` handler if the card should still focus the
835 * window. The default click handler lives on the original
836 * node — replacing the node loses it.
837 *
838 * @since 0.6.2
839 */
840 DOCK_PEEK_CARD_ELEMENT: "desktop-mode.dock.peek-card-element",
841 // ------------------------------------------------------------------
842 // Overview / Arrange lifecycle actions.
843 //
844 // The "Arrange" admin-bar menu drives two layout algorithms —
845 // Cascade (instantly reposition every window in a staggered
846 // stack) and Overview (zoom-out grid view with click-to-focus).
847 // These hooks surface the state transitions so plugins can
848 // instrument analytics, apply custom transitions, override
849 // thumbnail decorations, etc. All actions; a filter for
850 // mutating the overview layout may be added later if plugins
851 // want to reorder or group thumbnails.
852 // ------------------------------------------------------------------
853 /** Action, fires before the overview enter animation starts. */
854 OVERVIEW_ENTERING: "desktop-mode.overview.entering",
855 /** Action, fires once the overview enter animation has completed. */
856 OVERVIEW_ENTERED: "desktop-mode.overview.entered",
857 /**
858 * Action, fires at the start of the overview-exit animation.
859 * Payload: `{ windowId?: string, reason: 'select' | 'cancel' }` —
860 * `windowId` set when the user clicked a thumbnail (reason
861 * 'select'); omitted when the user pressed Escape or clicked
862 * the backdrop (reason 'cancel').
863 */
864 OVERVIEW_EXITING: "desktop-mode.overview.exiting",
865 /** Action, fires once the overview-exit animation has settled. */
866 OVERVIEW_EXITED: "desktop-mode.overview.exited",
867 /** Action, fires when the cursor enters a thumbnail. Payload `{ windowId }`. */
868 OVERVIEW_WINDOW_HOVER: "desktop-mode.overview.window-hover",
869 /** Action, fires when the cursor leaves a thumbnail. Payload `{ windowId }`. */
870 OVERVIEW_WINDOW_UNHOVER: "desktop-mode.overview.window-unhover",
871 /** Action, fires the instant a thumbnail click is registered (before exit + maximize kick in). Payload `{ windowId }`. */
872 OVERVIEW_WINDOW_CLICK: "desktop-mode.overview.window-click",
873 /** Action, fires before cascade computes + applies new positions. Payload `{ windowCount }`. */
874 ARRANGE_CASCADE_STARTING: "desktop-mode.arrange.cascade.starting",
875 /** Action, fires after cascade has positioned every window. Payload `{ windowCount }`. */
876 ARRANGE_CASCADE_APPLIED: "desktop-mode.arrange.cascade.applied",
877 /** Action, fires before tile computes + applies new positions. Payload `{ windowCount, cols, rows }`. */
878 ARRANGE_TILE_STARTING: "desktop-mode.arrange.tile.starting",
879 /** Action, fires after tile has positioned every window. Payload `{ windowCount, cols, rows }`. */
880 ARRANGE_TILE_APPLIED: "desktop-mode.arrange.tile.applied",
881 /**
882 * Filter on the tile-grid dimensions chosen by the built-in
883 * algorithm. Receives `{ cols, rows }` plus a context arg
884 * `{ windowCount, areaWidth, areaHeight }`. Plugins can return
885 * a different `{ cols, rows }` to enforce a custom layout
886 * (fixed-column newsroom, golden-ratio cells, etc.). Returned
887 * values are validated — non-positive integers, or a product
888 * smaller than `windowCount`, fall back to the original.
889 */
890 ARRANGE_TILE_DIMENSIONS: "desktop-mode.arrange.tile.dimensions",
891 /** Action, fires when snap-to-grid is toggled. Payload `{ enabled }`. */
892 ARRANGE_SNAP_CHANGED: "desktop-mode.arrange.snap.changed",
893 /**
894 * Filter on the snap-grid cell size. Receives
895 * `{ cellWidth, cellHeight }` plus a context arg
896 * `{ areaWidth, areaHeight }`. Plugins can return different
897 * dimensions to enforce a Tetris-style fixed grid, a musical
898 * staff aspect, etc. Non-positive returns fall back to the
899 * original.
900 */
901 ARRANGE_SNAP_CELL_SIZE: "desktop-mode.arrange.snap.cell-size",
902 /**
903 * Action, fires when the user clicks a plugin-registered entry in
904 * the Arrange admin-bar submenu (items added via the
905 * `desktop_mode_arrange_menu_items` PHP filter). Payload `{ id }`
906 * where `id` is the item's `id` field as registered. Plugins
907 * subscribe here to run their custom arrangement logic.
908 */
909 ARRANGE_CUSTOM_ACTION: "desktop-mode.arrange.custom-action",
910 // ------------------------------------------------------------------
911 // Snap-zones — Windows-style edge snapping with a split-overview
912 // picker to fill the opposite half after commit.
913 // ------------------------------------------------------------------
914 /**
915 * Action, fires when the drag cursor enters a snap zone and the
916 * shell shows the target-position preview. Payload
917 * `{ windowId, zone: 'left' | 'right' }`.
918 */
919 SNAP_ZONE_PENDING: "desktop-mode.snap.zone-pending",
920 /**
921 * Action, fires when the drag cursor leaves the snap zone without
922 * releasing — the preview disappears. Payload `{ windowId }`.
923 */
924 SNAP_ZONE_CANCELED: "desktop-mode.snap.zone-canceled",
925 /**
926 * Action, fires once the window has animated into its snapped
927 * bounds. Payload `{ windowId, zone: 'left' | 'right' }`.
928 */
929 SNAP_ZONE_COMMITTED: "desktop-mode.snap.zone-committed",
930 /**
931 * Action, fires when a user picks a thumbnail from the split
932 * overview to fill the opposite half. Payload
933 * `{ windowId, zone: 'left' | 'right' }`.
934 */
935 SNAP_SPLIT_FILLED: "desktop-mode.snap.split-filled",
936 // ------------------------------------------------------------------
937 // Widgets — the right-side column. Widgets paint above the
938 // wallpaper but beneath windows. Lifecycle mirrors canvas
939 // wallpapers: register via filter, mount/unmount actions bracket
940 // each paint, mount-failed fires on sync throws / async rejects.
941 // ------------------------------------------------------------------
942 /** Filter, receives the widget registry array. */
943 WIDGETS: "desktop-mode.widgets",
944 /** Action before a widget mounts. Payload `{ id, container, ctx }`. */
945 WIDGET_MOUNTING: "desktop-mode.widget.mounting",
946 /** Action after a widget mounts successfully. Payload `{ id, container, ctx }`. */
947 WIDGET_MOUNTED: "desktop-mode.widget.mounted",
948 /** Action before a widget tears down. Payload `{ id }`. */
949 WIDGET_UNMOUNTING: "desktop-mode.widget.unmounting",
950 /** Action when a widget's mount throws / rejects. Payload `{ id, error }`. */
951 WIDGET_MOUNT_FAILED: "desktop-mode.widget.mount-failed",
952 /** Action when the user adds a widget via the picker. Payload `{ id }`. */
953 WIDGET_ADDED: "desktop-mode.widget.added",
954 /** Action when the user removes a widget via the card's × button. Payload `{ id }`. */
955 WIDGET_REMOVED: "desktop-mode.widget.removed",
956 // ------------------------------------------------------------------
957 // Virtual-desktop ("Spaces") lifecycle actions.
958 //
959 // Spaces let users group windows into separate workspaces and flip
960 // between them from the overview top bar. These hooks expose every
961 // state change so plugins can persist per-space state, sync custom
962 // indicators, or react to the user's workspace context.
963 // ------------------------------------------------------------------
964 /** Action, fires when a new desktop is created. Payload `{ desktopId }`. */
965 DESKTOP_CREATED: "desktop-mode.desktop.created",
966 /** Action, fires when a desktop is closed. Payload `{ desktopId, migratedTo }`. */
967 DESKTOP_CLOSED: "desktop-mode.desktop.closed",
968 /** Action, fires when the active desktop changes. Payload `{ from, to }`. */
969 DESKTOP_SWITCHED: "desktop-mode.desktop.switched",
970 /**
971 * Filter. Returns the id of the "primary" desktop — the one the
972 * shell treats as canonical for batch operations. Receives the
973 * default (first desktop's id) and the full `Desktop[]` list.
974 * @since 0.5.0
975 */
976 PRIMARY_DESKTOP_ID: "desktop-mode.primary-desktop-id",
977 // ------------------------------------------------------------------
978 // Batch window operations.
979 // ------------------------------------------------------------------
980 /**
981 * Action, fires before {@link WindowManager.closeAll} starts
982 * iterating. Payload `{ candidates: Window[] }` — every window the
983 * shell is about to close (after `exceptIds` was applied).
984 * @since 0.5.0
985 */
986 WINDOWS_BEFORE_CLOSE_ALL: "desktop-mode.windows.before-close-all",
987 /**
988 * Filter, runs inside {@link WindowManager.closeAll}. Receives the
989 * candidate `Window[]` list and returns the (possibly trimmed) list
990 * that will actually be closed. Plugins use this to PROTECT specific
991 * windows from a bulk close — e.g. keep the active draft open.
992 * Returning an empty array cancels the close entirely.
993 * @since 0.5.0
994 */
995 WINDOWS_CLOSE_ALL: "desktop-mode.windows.close-all",
996 /**
997 * Action, fires after {@link WindowManager.closeAll} has finished.
998 * Payload `{ closed: number, skipped: Window[] }`.
999 * @since 0.5.0
1000 */
1001 WINDOWS_AFTER_CLOSE_ALL: "desktop-mode.windows.after-close-all",
1002 // ------------------------------------------------------------------
1003 // Slash-command lifecycle.
1004 // ------------------------------------------------------------------
1005 /**
1006 * Filter. Runs immediately before a command's `run()` is invoked.
1007 * Receives `{ proceed: true, slug, args, command }` and may return
1008 * the same shape with `proceed: false` to cancel the run.
1009 * @since 0.5.0
1010 */
1011 COMMAND_BEFORE_RUN: "desktop-mode.command.before-run",
1012 /**
1013 * Action, fires after a command's `run()` resolves successfully.
1014 * Payload `{ slug, args, command, result }`.
1015 * @since 0.5.0
1016 */
1017 COMMAND_AFTER_RUN: "desktop-mode.command.after-run",
1018 /**
1019 * Action, fires when a command's `run()` throws. Payload
1020 * `{ slug, args, command, error }`.
1021 * @since 0.5.0
1022 */
1023 COMMAND_ERROR: "desktop-mode.command.error",
1024 // ------------------------------------------------------------------
1025 // Shell-level lifecycle actions.
1026 // ------------------------------------------------------------------
1027 /**
1028 * Action, fires (debounced) after the browser viewport stops
1029 * resizing. Payload `{ width, height }` describes the shell's
1030 * bounding rect — plugins that render canvas-driven UIs hook here
1031 * to adjust their render surface.
1032 */
1033 SHELL_RESIZED: "desktop-mode.shell.resized",
1034 /**
1035 * Action mirroring `document.visibilitychange` for the shell as a
1036 * whole. Payload `{ state: 'visible' | 'hidden' }`. Different from
1037 * the wallpaper-specific visibility action in that it fires
1038 * regardless of which wallpaper (if any) is active.
1039 */
1040 SHELL_VISIBILITY: "desktop-mode.shell.visibility",
1041 /**
1042 * Action — fires when a `wp.desktop.connect()` connection
1043 * completes its iframe handshake. Payload:
1044 * `{ connectionId, targetWindowId, topics }`.
1045 *
1046 * @since 0.5.2
1047 */
1048 CONNECTION_OPENED: "desktop-mode.connection.opened",
1049 /**
1050 * Action — fires when a connection tears down. Payload:
1051 * `{ connectionId, reason: 'disconnect' | 'window-closed' | 'navigated' }`.
1052 *
1053 * @since 0.5.2
1054 */
1055 CONNECTION_CLOSED: "desktop-mode.connection.closed",
1056 /**
1057 * Action — fires for every message routed through a connection.
1058 * Payload: `{ connectionId, topic, direction: 'in' | 'out' }`.
1059 * Used for debug consoles + traffic auditing; high-volume topics
1060 * fire this many times per second, so subscribers should be
1061 * cheap.
1062 *
1063 * @since 0.5.2
1064 */
1065 CONNECTION_MESSAGE: "desktop-mode.connection.message",
1066 /**
1067 * Filter — fires when an iframe calls
1068 * `wp.desktop.iframe.requestConnection()`. Default value is
1069 * `true` (accept). Return `false` to reject, or an object
1070 * `{ topics: string[] }` to accept while narrowing the topic
1071 * list. `$context` carries `{ windowId, requestId, topics }`.
1072 *
1073 * @since 0.5.2
1074 */
1075 IFRAME_CONNECTION_REQUEST: "desktop-mode.iframe.connection-request",
1076 // ------------------------------------------------------------------
1077 // Window content relations & link renderers (since 0.9.4). A window
1078 // may carry a content identity ("I am comment 45 of post 123");
1079 // windows resolving to the same root form a relation group, and a
1080 // pluggable renderer draws the ties on the desktop. Engine:
1081 // `src/window-links/engine.ts`; registry:
1082 // `src/window-links/renderer-registry.ts`. See
1083 // `docs/examples/window-links.md`.
1084 // ------------------------------------------------------------------
1085 /**
1086 * Action — fires when a window's content identity is set, replaced,
1087 * or cleared. Payload: `{ windowId: string, content:
1088 * WindowContentRef | null, previous: WindowContentRef | null,
1089 * source: 'config' | 'bridge' | 'api' }`. The matching
1090 * `desktop-mode-window-content-changed` CustomEvent dispatches on
1091 * `document` with the same payload.
1092 *
1093 * @since 0.9.4
1094 */
1095 WINDOW_CONTENT_CHANGED: "desktop-mode.window-links.content-changed",
1096 /**
1097 * Action — fires when relation-group MEMBERSHIP changes (a window
1098 * gained/lost an identity, or a member window opened/closed).
1099 * Payload: `{ groups: WindowLinkGroup[] }`. Deliberately NOT fired
1100 * on move/resize (renderers get live geometry through their frame
1101 * subscription) nor on focus-recency reordering. The matching
1102 * `desktop-mode-window-link-groups-changed` CustomEvent dispatches
1103 * on `document` with the same payload.
1104 *
1105 * @since 0.9.4
1106 */
1107 WINDOW_LINK_GROUPS_CHANGED: "desktop-mode.window-links.groups-changed",
1108 /**
1109 * Filter — applied to every content identity as it is set, before
1110 * storage. Signature: `( ref: WindowContentRef | null, ctx: {
1111 * windowId: string, source: 'config' | 'bridge' | 'api' } ) =>
1112 * WindowContentRef | null`. Return `null` to suppress the identity,
1113 * or a rewritten ref to remap it (e.g. point a custom object type
1114 * at your own root scheme).
1115 *
1116 * @since 0.9.4
1117 */
1118 WINDOW_LINKS_CONTENT: "desktop-mode.window-links.content",
1119 /**
1120 * Filter — applied to the computed relation-group list on every
1121 * read (`wp.desktop.relations.groups()`). Signature:
1122 * `( groups: WindowLinkGroup[] ) => WindowLinkGroup[]`. Merge,
1123 * split, or inject groups here.
1124 *
1125 * @since 0.9.4
1126 */
1127 WINDOW_LINK_GROUPS: "desktop-mode.window-links.groups",
1128 /**
1129 * Filter — applied to the derived directed-edge list on every read
1130 * (`wp.desktop.relations.edges()`). Signature: `( edges:
1131 * WindowLinkEdge[] ) => WindowLinkEdge[]` where each edge is
1132 * `{ fromWindowId, toWindowId, kind: 'child-root' | 'reference',
1133 * bidirectional }`. Add, drop, or redirect ties here — this is
1134 * what the render host feeds to the active renderer.
1135 *
1136 * @since 0.9.4
1137 */
1138 WINDOW_LINK_EDGES: "desktop-mode.window-links.edges",
1139 /**
1140 * Filter — applied to the registered window-link renderer list on
1141 * every read (`wp.desktop.listWindowLinkRenderers()`). Signature:
1142 * `( defs: WindowLinkRendererDef[] ) => WindowLinkRendererDef[]`.
1143 *
1144 * @since 0.9.4
1145 */
1146 WINDOW_LINK_RENDERERS: "desktop-mode.window-links.renderers",
1147 /**
1148 * Filter — applied to the resolved ACTIVE renderer id after the OS
1149 * Settings selection is read, before the registry lookup.
1150 * Signature: `( id: string ) => string`. Return a different
1151 * registered id (or `'none'`) to force-swap the renderer without
1152 * touching the user's setting.
1153 *
1154 * @since 0.9.4
1155 */
1156 WINDOW_LINK_RENDERER: "desktop-mode.window-links.renderer",
1157 // ------------------------------------------------------------------
1158 // OS-file drop manager (since 0.30.0). Catches files dragged from
1159 // the user's host OS (Finder / Explorer / Nautilus) onto any
1160 // desktop-mode surface and routes them through a confirmation
1161 // dialog before uploading to the Media Library. Authoritative
1162 // constants live in `src/os-file-drop/hooks.ts`; mirrored here so
1163 // every hook the shell fires is reachable from a single `HOOKS`
1164 // import. See `docs/examples/os-file-drop.md`.
1165 // ------------------------------------------------------------------
1166 /** Filter — `(files: File[], ctx) => File[]`, before mime/size check. */
1167 FILE_DROP_FILES_DETECTED: "desktop-mode.drop.files-detected",
1168 /** Action — `{ rejections, context }` for files that failed policy. */
1169 FILE_DROP_FILES_REJECTED: "desktop-mode.drop.files-rejected",
1170 /** Filter — `(entry, ctx) => entry`, per-file dialog defaults. */
1171 FILE_DROP_DIALOG_FIELDS: "desktop-mode.drop.dialog-fields",
1172 /** Filter — `(payload, ctx) => payload | null`, last call before POST. */
1173 FILE_DROP_BEFORE_UPLOAD: "desktop-mode.drop.before-upload",
1174 /** Action — `{ file, fields, context, abort }` once XHR is open and about to send. @since 0.31.0 */
1175 FILE_DROP_UPLOAD_STARTED: "desktop-mode.drop.upload-started",
1176 /** Action — `{ file, fields, context, loaded, total, indeterminate }` per progress tick. @since 0.31.0 */
1177 FILE_DROP_UPLOAD_PROGRESS: "desktop-mode.drop.upload-progress",
1178 /** Action — `{ file, result, fields, context }` after successful upload. `file` since 0.31.0. */
1179 FILE_DROP_AFTER_UPLOAD: "desktop-mode.drop.after-upload",
1180 /** Action — `{ file, error, context }` on upload failure. */
1181 FILE_DROP_UPLOAD_FAILED: "desktop-mode.drop.upload-failed"
1182 };
1183 let _whenReadySeq = 0;
1184 function whenReady(cb) {
1185 if (didAction(HOOKS.INIT) > 0) {
1186 Promise.resolve().then(cb);
1187 return;
1188 }
1189 const ns = `desktop-mode/when-ready-${++_whenReadySeq}`;
1190 addAction(HOOKS.INIT, ns, cb);
1191 }
1192 function isReady() {
1193 return didAction(HOOKS.INIT) > 0;
1194 }
1195 const IDENTITY_PARAMS = [
1196 "post_type",
1197 "page",
1198 "taxonomy",
1199 // WooCommerce (and other React-app-style plugins) register
1200 // SEPARATE top-level admin menus that all share `?page=wc-admin`
1201 // and only differ by `path` (e.g. `path=/analytics/overview`,
1202 // `path=/marketing`). Without `path` in the identity set, every
1203 // such menu collapses to the same window id — opening any one of
1204 // them lights up the dock indicator for ALL of them. WC's
1205 // /admin/path query is the most prominent example today; future
1206 // plugins that route inside `admin.php?page=` via a custom param
1207 // can either piggyback on `path` or grow this list.
1208 "path",
1209 // The post ID on `post.php?post=X&action=edit`. Without this, every
1210 // individual post edit URL collapses to `post-php`, so clicking a
1211 // second row in the Posts window just refocuses the first post's
1212 // window instead of opening the new one.
1213 "post",
1214 // The comment ID on `comment.php?action=editcomment&c=X` — the exact
1215 // analogue of `post` above. Without it every comment-edit URL
1216 // collapses to `comment-php`, so opening a second comment replaces
1217 // the first comment's window instead of opening its own (and the
1218 // window-links ties can only ever point at one comment at a time).
1219 "c",
1220 // Site-editor entity path: `site-editor.php?p=/wp_template_part/
1221 // twentytwentyfive//footer-columns`. Each template / template
1222 // part / pattern / navigation entity is a distinct "page" from
1223 // the user's perspective — picking "Header" after "Footer column"
1224 // should open a new window, not refocus the existing footer one.
1225 // Without `p` in identity, every site-editor URL collapses to
1226 // `site-editor-php` and the second pick is a no-op.
1227 "p"
1228 ];
1229 function slugify$1(path) {
1230 return path.replace(/\.php/g, "-php").replace(/[?&=]/g, "-").replace(/[^a-zA-Z0-9_-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "") || "index";
1231 }
1232 function deriveWindowId(url, adminUrl) {
1233 let parsed = null;
1234 try {
1235 parsed = new URL(url, adminUrl);
1236 } catch (err) {
1237 parsed = null;
1238 }
1239 if (parsed) {
1240 const basePath = new URL(adminUrl).pathname;
1241 const filename = parsed.pathname.replace(basePath, "").replace(/^\/+/, "");
1242 const significant = new URLSearchParams();
1243 for (const key of IDENTITY_PARAMS) {
1244 const value = parsed.searchParams.get(key);
1245 if (value) {
1246 significant.set(key, value);
1247 }
1248 }
1249 const query = significant.toString();
1250 return slugify$1(query ? `${filename}?${query}` : filename);
1251 }
1252 let path = url.replace(adminUrl, "");
1253 if (path.startsWith("/")) {
1254 path = path.substring(1);
1255 }
1256 return slugify$1(path);
1257 }
1258 function sanitizeClassName(value) {
1259 return value.replace(/[^a-zA-Z0-9_-]/g, "");
1260 }
1261 function applyTileEntryStagger(tile2) {
1262 tile2.style.setProperty(
1263 "--desktop-mode-file-tile-enter-delay",
1264 `${(Math.random() * 0.25).toFixed(3)}s`
1265 );
1266 tile2.style.setProperty(
1267 "--desktop-mode-file-tile-enter-duration",
1268 `${(0.3 + Math.random() * 0.25).toFixed(3)}s`
1269 );
1270 }
1271 function urlMatchKey(url) {
1272 try {
1273 const parsed = new URL(url, window.location.origin);
1274 parsed.searchParams.delete("desktop_mode_chromeless");
1275 parsed.searchParams.delete("desktop_mode_portal");
1276 return parsed.pathname.replace(/\/+$/, "") + "?" + parsed.searchParams.toString();
1277 } catch {
1278 return url;
1279 }
1280 }
1281 function urlReuseKey(url) {
1282 try {
1283 const parsed = new URL(url, window.location.origin);
1284 parsed.searchParams.delete("desktop_mode_chromeless");
1285 parsed.searchParams.delete("desktop_mode_portal");
1286 parsed.searchParams.delete("_wp_http_referer");
1287 parsed.searchParams.sort();
1288 return parsed.pathname.replace(/\/+$/, "") + "?" + parsed.searchParams.toString();
1289 } catch {
1290 return url;
1291 }
1292 }
1293 function sanitizeIconSvg(svg) {
1294 if (typeof svg !== "string" || svg === "") {
1295 return "";
1296 }
1297 if (typeof DOMParser === "undefined") {
1298 return "";
1299 }
1300 let doc;
1301 try {
1302 doc = new DOMParser().parseFromString(svg, "image/svg+xml");
1303 } catch {
1304 return "";
1305 }
1306 const root = doc.documentElement;
1307 if (!root || root.nodeName.toLowerCase() !== "svg") {
1308 return "";
1309 }
1310 if (doc.getElementsByTagName("parsererror").length > 0) {
1311 return "";
1312 }
1313 const BANNED_TAGS = /* @__PURE__ */ new Set(["script", "style", "foreignobject", "iframe", "object", "embed"]);
1314 const walk2 = (el) => {
1315 const children = Array.from(el.children);
1316 for (const child of children) {
1317 if (BANNED_TAGS.has(child.nodeName.toLowerCase())) {
1318 child.remove();
1319 continue;
1320 }
1321 for (const attr of Array.from(child.attributes)) {
1322 const name = attr.name.toLowerCase();
1323 const value = attr.value.trim().toLowerCase();
1324 if (name.startsWith("on")) {
1325 child.removeAttribute(attr.name);
1326 continue;
1327 }
1328 if (value.startsWith("javascript:")) {
1329 child.removeAttribute(attr.name);
1330 }
1331 }
1332 walk2(child);
1333 }
1334 };
1335 walk2(root);
1336 for (const attr of Array.from(root.attributes)) {
1337 const name = attr.name.toLowerCase();
1338 const value = attr.value.trim().toLowerCase();
1339 if (name.startsWith("on") || value.startsWith("javascript:")) {
1340 root.removeAttribute(attr.name);
1341 }
1342 }
1343 return root.outerHTML;
1344 }
1345 let inflight$1 = null;
1346 function isLoaded$1() {
1347 return !!window.desktopModeWindowSystem;
1348 }
1349 function injectScript$1(scriptUrl) {
1350 return new Promise((resolve2, reject) => {
1351 const existing = document.querySelector(
1352 'script[data-desktop-mode-window-system="1"]'
1353 );
1354 const finish = () => {
1355 if (isLoaded$1()) {
1356 resolve2();
1357 return;
1358 }
1359 reject(
1360 new Error(
1361 "[desktop-mode] window-system bundle loaded but did not register `window.desktopModeWindowSystem`."
1362 )
1363 );
1364 };
1365 if (existing) {
1366 if (isLoaded$1()) {
1367 finish();
1368 } else {
1369 existing.addEventListener("load", finish);
1370 existing.addEventListener(
1371 "error",
1372 () => reject(new Error("failed to load window-system bundle"))
1373 );
1374 }
1375 return;
1376 }
1377 const s = document.createElement("script");
1378 s.src = scriptUrl;
1379 s.async = true;
1380 s.dataset.desktopModeWindowSystem = "1";
1381 s.addEventListener("load", finish);
1382 s.addEventListener(
1383 "error",
1384 () => reject(new Error("failed to load window-system bundle"))
1385 );
1386 document.head.appendChild(s);
1387 });
1388 }
1389 function windowSystemBundleUrl() {
1390 const cfg = window.desktopModeConfig;
1391 return cfg?.windowSystemBundleUrl ?? "";
1392 }
1393 function preloadWindowSystem(scriptUrl) {
1394 if (!scriptUrl || isLoaded$1() || inflight$1) {
1395 return;
1396 }
1397 inflight$1 = injectScript$1(scriptUrl).catch((err) => {
1398 inflight$1 = null;
1399 if (typeof console !== "undefined") {
1400 console.warn(
1401 "[desktop-mode] window-system preload failed; will retry on first open():",
1402 err
1403 );
1404 }
1405 });
1406 }
1407 async function ensureWindowSystemLoaded(scriptUrl) {
1408 if (isLoaded$1()) {
1409 return window.desktopModeWindowSystem;
1410 }
1411 if (!scriptUrl) {
1412 const fn = window.desktopModeWindowSystem;
1413 if (fn) {
1414 return fn;
1415 }
1416 throw new Error(
1417 "[desktop-mode] ensureWindowSystemLoaded(): no bundle URL configured and `window.desktopModeWindowSystem` is not pre-registered."
1418 );
1419 }
1420 if (!inflight$1) {
1421 inflight$1 = injectScript$1(scriptUrl);
1422 }
1423 await inflight$1;
1424 return window.desktopModeWindowSystem;
1425 }
1426 const CANARY_TAG = "wpd-confirm-dialog";
1427 let inflight = null;
1428 function isLoaded() {
1429 return typeof window.customElements !== "undefined" && !!window.customElements.get(CANARY_TAG);
1430 }
1431 function injectScript(scriptUrl) {
1432 return new Promise((resolve2, reject) => {
1433 const existing = document.querySelector(
1434 'script[data-desktop-mode-shell-overlays="1"]'
1435 );
1436 const finish = () => {
1437 if (isLoaded()) {
1438 resolve2();
1439 return;
1440 }
1441 reject(
1442 new Error(
1443 "[desktop-mode] shell-overlays bundle loaded but did not register the overlay components."
1444 )
1445 );
1446 };
1447 if (existing) {
1448 if (isLoaded()) {
1449 finish();
1450 } else {
1451 existing.addEventListener("load", finish);
1452 existing.addEventListener(
1453 "error",
1454 () => reject(new Error("failed to load shell-overlays bundle"))
1455 );
1456 }
1457 return;
1458 }
1459 const s = document.createElement("script");
1460 s.src = scriptUrl;
1461 s.async = true;
1462 s.dataset.desktopModeShellOverlays = "1";
1463 s.addEventListener("load", finish);
1464 s.addEventListener(
1465 "error",
1466 () => reject(new Error("failed to load shell-overlays bundle"))
1467 );
1468 document.head.appendChild(s);
1469 });
1470 }
1471 function preloadShellOverlays(scriptUrl) {
1472 if (!scriptUrl || isLoaded() || inflight) {
1473 return;
1474 }
1475 inflight = injectScript(scriptUrl).catch((err) => {
1476 inflight = null;
1477 if (typeof console !== "undefined") {
1478 console.warn(
1479 "[desktop-mode] shell-overlays preload failed; will retry on first overlay use:",
1480 err
1481 );
1482 }
1483 });
1484 }
1485 function ensureShellOverlaysLoaded(scriptUrl) {
1486 if (isLoaded()) {
1487 return Promise.resolve();
1488 }
1489 if (!scriptUrl) {
1490 return Promise.resolve();
1491 }
1492 if (!inflight) {
1493 inflight = injectScript(scriptUrl);
1494 }
1495 return inflight;
1496 }
1497 function shellOverlaysBundleUrl() {
1498 const cfg = window.desktopModeConfig;
1499 return cfg?.shellOverlaysBundleUrl ?? "";
1500 }
1501 function openWithShellOverlays(isStillCurrent, fn) {
1502 const url = shellOverlaysBundleUrl();
1503 if (isLoaded() || !url) {
1504 fn();
1505 return;
1506 }
1507 void ensureShellOverlaysLoaded(url).then(() => {
1508 if (!isStillCurrent()) {
1509 return;
1510 }
1511 fn();
1512 }).catch((err) => {
1513 if (typeof console !== "undefined") {
1514 console.warn(
1515 "[desktop-mode] shell-overlays failed to load; menu/dialog suppressed:",
1516 err
1517 );
1518 }
1519 });
1520 }
1521 const TEXT_DOMAIN = "desktop-mode";
1522 function i18n() {
1523 return window.wp?.i18n;
1524 }
1525 function __(text, domain = TEXT_DOMAIN) {
1526 return i18n()?.__(text, domain) ?? text;
1527 }
1528 function _n(single, plural, number, domain = TEXT_DOMAIN) {
1529 return i18n()?._n(single, plural, number, domain) ?? (number === 1 ? single : plural);
1530 }
1531 function sprintf(format, ...args) {
1532 const impl = i18n()?.sprintf;
1533 if (impl) {
1534 return impl(format, ...args);
1535 }
1536 let i = 0;
1537 return format.replace(/%(?:(\d+)\$)?[sd]/g, (_match, pos) => {
1538 const idx = pos ? Number.parseInt(pos, 10) - 1 : i++;
1539 return String(args[idx] ?? "");
1540 });
1541 }
1542 function isValidGrid(candidate, windowCount) {
1543 if (!candidate || typeof candidate !== "object") {
1544 return false;
1545 }
1546 const c = candidate.cols;
1547 const r = candidate.rows;
1548 if (typeof c !== "number" || typeof r !== "number") {
1549 return false;
1550 }
1551 if (!Number.isFinite(c) || !Number.isFinite(r)) {
1552 return false;
1553 }
1554 if (c < 1 || r < 1) {
1555 return false;
1556 }
1557 return Math.floor(c) * Math.floor(r) >= windowCount;
1558 }
1559 function isValidCellSize(candidate) {
1560 if (!candidate || typeof candidate !== "object") {
1561 return false;
1562 }
1563 const w = candidate.cellWidth;
1564 const h = candidate.cellHeight;
1565 if (typeof w !== "number" || typeof h !== "number") {
1566 return false;
1567 }
1568 if (!Number.isFinite(w) || !Number.isFinite(h)) {
1569 return false;
1570 }
1571 return w > 0 && h > 0;
1572 }
1573 function pickGridDimensions(n, width, height) {
1574 if (n <= 1) {
1575 return { cols: 1, rows: 1 };
1576 }
1577 const areaAspect = width / Math.max(1, height);
1578 const max = 6;
1579 let best = { cols: n, rows: 1, score: Infinity };
1580 for (let cols = 1; cols <= Math.min(max, n); cols++) {
1581 const rows = Math.min(max, Math.ceil(n / cols));
1582 if (cols * rows < n) {
1583 continue;
1584 }
1585 const cellAspect = width / cols / Math.max(1, height / rows);
1586 const aspectDelta = Math.abs(cellAspect - areaAspect);
1587 const emptyCells = cols * rows - n;
1588 const score = aspectDelta + emptyCells * 0.05;
1589 if (score < best.score) {
1590 best = { cols, rows, score };
1591 }
1592 }
1593 return { cols: best.cols, rows: best.rows };
1594 }
1595 function computeOverviewLayout(windows, rect, topInset = 0) {
1596 const n = windows.length;
1597 if (n === 0) {
1598 return [];
1599 }
1600 const cols = Math.ceil(Math.sqrt(n));
1601 const rows = Math.ceil(n / cols);
1602 const padding = 40;
1603 const gap = 24;
1604 const labelReserve = 34;
1605 const cellWidth = (rect.width - padding * 2 - gap * (cols - 1)) / cols;
1606 const cellHeight = (rect.height - padding * 2 - topInset - gap * (rows - 1)) / rows;
1607 const thumbCellHeight = Math.max(40, cellHeight - labelReserve);
1608 return windows.map((win, i) => {
1609 const col = i % cols;
1610 const row = Math.floor(i / cols);
1611 const cellX = rect.left + padding + col * (cellWidth + gap);
1612 const cellY = rect.top + topInset + padding + row * (cellHeight + gap) + labelReserve;
1613 const sourceW = win.element.offsetWidth;
1614 const sourceH = win.element.offsetHeight;
1615 const scale = Math.min(
1616 cellWidth / sourceW,
1617 thumbCellHeight / sourceH
1618 );
1619 const scaledW = sourceW * scale;
1620 const scaledH = sourceH * scale;
1621 return {
1622 win,
1623 x: cellX + (cellWidth - scaledW) / 2,
1624 y: cellY + (thumbCellHeight - scaledH) / 2,
1625 scale
1626 };
1627 });
1628 }
1629 const OVERVIEW_TOP_BAR_RESERVE = 120;
1630 const OVERVIEW_INERT_ELEMENTS = [
1631 "adminmenumain",
1632 "adminmenuback",
1633 "desktop-mode-dock",
1634 "desktop-mode-side-dock",
1635 "desktop-mode-widgets"
1636 ];
1637 function inertWpBodyContentChildren(inactive) {
1638 const content = document.getElementById("wpbody-content");
1639 if (!content) {
1640 return;
1641 }
1642 for (const child of Array.from(content.children)) {
1643 child.inert = inactive;
1644 }
1645 }
1646 function enterOverview(mgr) {
1647 if (mgr._overviewActive) {
1648 return;
1649 }
1650 const onActive = mgr._stack.filter(
1651 (w) => w.config.desktopId === mgr._activeDesktopId
1652 );
1653 if (onActive.length > 0 && onActive.every((w) => w.state === "minimized")) {
1654 for (const w of onActive) {
1655 try {
1656 w.restore();
1657 } catch (err) {
1658 if (typeof console !== "undefined") {
1659 console.error(
1660 "[desktop-mode] enterOverview: window.restore() threw for",
1661 w.id,
1662 err
1663 );
1664 }
1665 }
1666 }
1667 }
1668 const eligible = mgr._stack.filter(
1669 (w) => w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
1670 );
1671 mgr._overviewActive = true;
1672 doAction(HOOKS.OVERVIEW_ENTERING, {});
1673 for (const id of OVERVIEW_INERT_ELEMENTS) {
1674 const el = document.getElementById(id);
1675 if (el) {
1676 el.inert = true;
1677 }
1678 }
1679 inertWpBodyContentChildren(true);
1680 for (const w of mgr._stack) {
1681 w.element.inert = true;
1682 }
1683 mgr._overviewSnapshot.clear();
1684 for (const w of eligible) {
1685 mgr._overviewSnapshot.set(w.id, {
1686 transform: w.element.style.transform || "",
1687 transition: w.element.style.transition || ""
1688 });
1689 }
1690 for (const w of eligible) {
1691 if (w.state === "fullscreen") {
1692 w.toggleFullscreen();
1693 }
1694 }
1695 const currentRect = mgr._desktop.getBoundingClientRect();
1696 const docks = Array.from(
1697 document.querySelectorAll(".desktop-mode-dock")
1698 );
1699 let reclaimedWidth = 0;
1700 for (const d of docks) {
1701 const r = d.getBoundingClientRect();
1702 const verticallyOverlaps = r.bottom > currentRect.top && r.top < currentRect.bottom;
1703 const isHorizontalRail = r.height > r.width;
1704 if (verticallyOverlaps && isHorizontalRail) {
1705 reclaimedWidth += r.width;
1706 }
1707 }
1708 const targetRect = new DOMRect(
1709 0,
1710 0,
1711 currentRect.width + reclaimedWidth,
1712 currentRect.height
1713 );
1714 mgr._desktop.classList.add("desktop-mode-area--overview");
1715 const shell = document.getElementById("desktop-mode-shell");
1716 shell?.classList.add("desktop-mode-shell--overview");
1717 mgr._overviewTopBar = buildOverviewTopBar(mgr);
1718 mgr._desktop.appendChild(mgr._overviewTopBar);
1719 const layout = computeOverviewLayout(
1720 eligible,
1721 targetRect,
1722 OVERVIEW_TOP_BAR_RESERVE
1723 );
1724 mgr._overviewLabels.clear();
1725 for (const item of layout) {
1726 const el = item.win.element;
1727 el.classList.add("desktop-mode-window--overview");
1728 const dx = item.x - el.offsetLeft;
1729 const dy = item.y - el.offsetTop;
1730 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
1731 const label = createOverviewLabel(item);
1732 el.insertAdjacentElement("afterend", label);
1733 mgr._overviewLabels.set(item.win.id, label);
1734 }
1735 const pressTargetForEvent = (e) => {
1736 const target2 = e.target;
1737 const winEl = target2?.closest(
1738 ".desktop-mode-window--overview"
1739 );
1740 if (winEl) {
1741 return {
1742 id: winEl.id.replace(/^wp-window-/, ""),
1743 element: winEl
1744 };
1745 }
1746 if (target2 === mgr._desktop) {
1747 return { id: "backdrop", element: mgr._desktop };
1748 }
1749 return null;
1750 };
1751 mgr._overviewPointerDownHandler = (e) => {
1752 if (e.button !== 0) {
1753 mgr._overviewPressTarget = null;
1754 return;
1755 }
1756 mgr._overviewPressTarget = pressTargetForEvent(e);
1757 if (mgr._overviewPressTarget) {
1758 e.preventDefault();
1759 e.stopPropagation();
1760 }
1761 };
1762 mgr._overviewPointerUpHandler = (e) => {
1763 if (e.button !== 0) {
1764 return;
1765 }
1766 const pressed = mgr._overviewPressTarget;
1767 mgr._overviewPressTarget = null;
1768 if (!pressed) {
1769 return;
1770 }
1771 const rect = pressed.element.getBoundingClientRect();
1772 const inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
1773 if (!inside) {
1774 return;
1775 }
1776 e.preventDefault();
1777 e.stopPropagation();
1778 if (pressed.id === "backdrop") {
1779 exitOverview(mgr);
1780 return;
1781 }
1782 const selected = mgr.getById(pressed.id);
1783 doAction(HOOKS.OVERVIEW_WINDOW_CLICK, { windowId: pressed.id });
1784 exitOverview(mgr, selected, true);
1785 };
1786 mgr._overviewKeyHandler = (e) => {
1787 if (e.key === "Escape") {
1788 exitOverview(mgr);
1789 return;
1790 }
1791 if (e.key === "Enter") {
1792 const target2 = e.target;
1793 const doc = target2?.ownerDocument || document;
1794 if (doc.activeElement && doc.activeElement.tagName === "BUTTON") {
1795 return;
1796 }
1797 e.preventDefault();
1798 if (mgr._overviewAddTileFocused) {
1799 commitAddTile(mgr);
1800 return;
1801 }
1802 exitOverview(mgr);
1803 }
1804 };
1805 mgr._desktop.addEventListener(
1806 "pointerdown",
1807 mgr._overviewPointerDownHandler,
1808 true
1809 );
1810 mgr._desktop.addEventListener(
1811 "pointerup",
1812 mgr._overviewPointerUpHandler,
1813 true
1814 );
1815 mgr._overviewClickBlocker = (e) => {
1816 const target2 = e.target;
1817 if (target2?.closest(".desktop-mode-overview-top-bar")) {
1818 return;
1819 }
1820 e.stopPropagation();
1821 e.preventDefault();
1822 };
1823 mgr._desktop.addEventListener(
1824 "click",
1825 mgr._overviewClickBlocker,
1826 true
1827 );
1828 document.addEventListener("keydown", mgr._overviewKeyHandler);
1829 mgr._lastOverviewHoverId = null;
1830 mgr._overviewMouseHandler = (e) => {
1831 const target2 = e.target;
1832 const winEl = target2?.closest(
1833 ".desktop-mode-window--overview"
1834 );
1835 const newId = winEl ? winEl.id.replace(/^wp-window-/, "") : null;
1836 if (newId === mgr._lastOverviewHoverId) {
1837 return;
1838 }
1839 if (mgr._lastOverviewHoverId) {
1840 doAction(HOOKS.OVERVIEW_WINDOW_UNHOVER, {
1841 windowId: mgr._lastOverviewHoverId
1842 });
1843 }
1844 if (newId) {
1845 doAction(HOOKS.OVERVIEW_WINDOW_HOVER, { windowId: newId });
1846 }
1847 mgr._lastOverviewHoverId = newId;
1848 };
1849 mgr._desktop.addEventListener("mouseover", mgr._overviewMouseHandler);
1850 mgr._overviewEnterTimeoutId = window.setTimeout(() => {
1851 mgr._overviewEnterTimeoutId = null;
1852 if (mgr._overviewActive) {
1853 doAction(HOOKS.OVERVIEW_ENTERED, {});
1854 }
1855 }, 300);
1856 }
1857 function cancelOverviewTimers(mgr) {
1858 if (mgr._overviewEnterTimeoutId !== null) {
1859 window.clearTimeout(mgr._overviewEnterTimeoutId);
1860 mgr._overviewEnterTimeoutId = null;
1861 }
1862 if (mgr._overviewExitTimeoutId !== null) {
1863 window.clearTimeout(mgr._overviewExitTimeoutId);
1864 mgr._overviewExitTimeoutId = null;
1865 }
1866 }
1867 function buildOverviewTopBar(mgr) {
1868 const bar = document.createElement("div");
1869 bar.className = "desktop-mode-overview-top-bar";
1870 const list2 = document.createElement("div");
1871 list2.className = "desktop-mode-overview-top-bar__list";
1872 bar.appendChild(list2);
1873 for (const d of mgr._desktops) {
1874 list2.appendChild(buildDesktopTile(mgr, d));
1875 }
1876 const addTile = document.createElement("button");
1877 addTile.type = "button";
1878 addTile.className = "desktop-mode-overview-top-bar__tile desktop-mode-overview-top-bar__tile--add";
1879 if (mgr._overviewAddTileFocused) {
1880 addTile.classList.add(
1881 "desktop-mode-overview-top-bar__tile--cursor"
1882 );
1883 }
1884 addTile.setAttribute("aria-label", __("Add new desktop"));
1885 addTile.innerHTML = '<span class="desktop-mode-overview-top-bar__tile-plus" aria-hidden="true">+</span>';
1886 addTile.addEventListener("click", (e) => {
1887 e.preventDefault();
1888 e.stopPropagation();
1889 commitAddTile(mgr);
1890 });
1891 list2.appendChild(addTile);
1892 return bar;
1893 }
1894 function commitAddTile(mgr) {
1895 const created = createDesktop(mgr);
1896 mgr._overviewAddTileFocused = false;
1897 exitOverviewToDesktop(mgr, created.id);
1898 }
1899 function buildDesktopTile(mgr, d) {
1900 const wrapper = document.createElement("div");
1901 wrapper.className = "desktop-mode-overview-top-bar__tile-wrapper";
1902 const tile2 = document.createElement("button");
1903 tile2.type = "button";
1904 tile2.className = "desktop-mode-overview-top-bar__tile";
1905 tile2.dataset.desktopId = d.id;
1906 if (d.id === mgr._activeDesktopId && !mgr._overviewAddTileFocused) {
1907 tile2.classList.add("desktop-mode-overview-top-bar__tile--active");
1908 }
1909 tile2.setAttribute("aria-label", sprintf(__("Switch to %s"), d.label));
1910 const preview = document.createElement("span");
1911 preview.className = "desktop-mode-overview-top-bar__tile-preview";
1912 const count = mgr._stack.filter(
1913 (w) => w.config.desktopId === d.id
1914 ).length;
1915 if (count > 0) {
1916 const badge = document.createElement("span");
1917 badge.className = "desktop-mode-overview-top-bar__tile-count";
1918 badge.textContent = String(count);
1919 preview.appendChild(badge);
1920 }
1921 tile2.appendChild(preview);
1922 const label = document.createElement("span");
1923 label.className = "desktop-mode-overview-top-bar__tile-label";
1924 label.textContent = d.label;
1925 tile2.appendChild(label);
1926 tile2.addEventListener("click", (e) => {
1927 e.preventDefault();
1928 e.stopPropagation();
1929 exitOverviewToDesktop(mgr, d.id);
1930 });
1931 const closeBtn = document.createElement("button");
1932 closeBtn.type = "button";
1933 closeBtn.className = "desktop-mode-overview-top-bar__tile-close";
1934 closeBtn.setAttribute("aria-label", sprintf(__("Close %s"), d.label));
1935 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>';
1936 closeBtn.addEventListener("click", (e) => {
1937 e.preventDefault();
1938 e.stopPropagation();
1939 closeDesktop(mgr, d.id);
1940 refreshOverviewTopBar(mgr);
1941 });
1942 wrapper.appendChild(tile2);
1943 wrapper.appendChild(closeBtn);
1944 return wrapper;
1945 }
1946 function refreshOverviewTopBar(mgr) {
1947 if (!mgr._overviewTopBar) {
1948 return;
1949 }
1950 const fresh = buildOverviewTopBar(mgr);
1951 mgr._overviewTopBar.replaceWith(fresh);
1952 mgr._overviewTopBar = fresh;
1953 }
1954 function exitOverviewToDesktop(mgr, desktopId) {
1955 switchDesktop(mgr, desktopId);
1956 exitOverview(mgr);
1957 }
1958 function createOverviewLabel(item) {
1959 const label = document.createElement("div");
1960 label.className = "desktop-mode-overview-label";
1961 label.dataset.windowId = item.win.id;
1962 const thumbW = item.win.element.offsetWidth * item.scale;
1963 label.style.left = `${item.x}px`;
1964 label.style.top = `${item.y - 34}px`;
1965 label.style.width = `${thumbW}px`;
1966 const iconClass = item.win.config.icon || "dashicons-admin-generic";
1967 const icon = document.createElement("span");
1968 icon.className = `desktop-mode-overview-label__icon dashicons ${iconClass}`;
1969 icon.setAttribute("aria-hidden", "true");
1970 label.appendChild(icon);
1971 const title = document.createElement("span");
1972 title.className = "desktop-mode-overview-label__title";
1973 title.textContent = item.win.config.title;
1974 label.appendChild(title);
1975 const tabCount = item.win.getExternalTabCount();
1976 if (tabCount > 0) {
1977 const meta = document.createElement("span");
1978 meta.className = "desktop-mode-overview-label__meta";
1979 meta.textContent = sprintf(
1980 // translators: %d is the number of external sub-tabs open on this window.
1981 _n("· %d open tab", "· %d open tabs", tabCount),
1982 tabCount
1983 );
1984 label.appendChild(meta);
1985 }
1986 return label;
1987 }
1988 function exitOverview(mgr, selected, maximize = false) {
1989 if (!mgr._overviewActive) {
1990 return;
1991 }
1992 mgr._overviewActive = false;
1993 mgr._overviewAddTileFocused = false;
1994 doAction(HOOKS.OVERVIEW_EXITING, {
1995 windowId: selected && maximize ? selected.id : void 0,
1996 reason: selected && maximize ? "select" : "cancel"
1997 });
1998 mgr._desktop.classList.remove("desktop-mode-area--overview");
1999 const shell = document.getElementById("desktop-mode-shell");
2000 shell?.classList.remove("desktop-mode-shell--overview");
2001 for (const id of OVERVIEW_INERT_ELEMENTS) {
2002 const el = document.getElementById(id);
2003 if (el) {
2004 el.inert = false;
2005 }
2006 }
2007 inertWpBodyContentChildren(false);
2008 for (const w of mgr._stack) {
2009 w.element.inert = false;
2010 }
2011 for (const [id, snap] of mgr._overviewSnapshot) {
2012 const w = mgr.getById(id);
2013 if (!w) {
2014 continue;
2015 }
2016 w.element.style.transform = snap.transform;
2017 }
2018 if (selected && maximize) {
2019 mgr.focus(selected);
2020 selected.maximize();
2021 }
2022 for (const label of mgr._overviewLabels.values()) {
2023 label.classList.add("desktop-mode-overview-label--out");
2024 }
2025 if (mgr._overviewTopBar) {
2026 mgr._overviewTopBar.classList.add(
2027 "desktop-mode-overview-top-bar--out"
2028 );
2029 }
2030 const ANIMATION_MS = 280;
2031 mgr._overviewExitTimeoutId = window.setTimeout(() => {
2032 mgr._overviewExitTimeoutId = null;
2033 for (const w of mgr._stack) {
2034 w.element.classList.remove("desktop-mode-window--overview");
2035 }
2036 for (const label of mgr._overviewLabels.values()) {
2037 label.remove();
2038 }
2039 mgr._overviewLabels.clear();
2040 mgr._overviewSnapshot.clear();
2041 if (mgr._overviewTopBar) {
2042 mgr._overviewTopBar.remove();
2043 mgr._overviewTopBar = null;
2044 }
2045 if (mgr._overviewClickBlocker) {
2046 mgr._desktop.removeEventListener(
2047 "click",
2048 mgr._overviewClickBlocker,
2049 true
2050 );
2051 mgr._overviewClickBlocker = null;
2052 }
2053 doAction(HOOKS.OVERVIEW_EXITED, {
2054 windowId: selected && maximize ? selected.id : void 0,
2055 reason: selected && maximize ? "select" : "cancel"
2056 });
2057 }, ANIMATION_MS);
2058 if (mgr._overviewPointerDownHandler) {
2059 mgr._desktop.removeEventListener(
2060 "pointerdown",
2061 mgr._overviewPointerDownHandler,
2062 true
2063 );
2064 mgr._overviewPointerDownHandler = null;
2065 }
2066 if (mgr._overviewPointerUpHandler) {
2067 mgr._desktop.removeEventListener(
2068 "pointerup",
2069 mgr._overviewPointerUpHandler,
2070 true
2071 );
2072 mgr._overviewPointerUpHandler = null;
2073 }
2074 mgr._overviewPressTarget = null;
2075 if (mgr._overviewKeyHandler) {
2076 document.removeEventListener("keydown", mgr._overviewKeyHandler);
2077 mgr._overviewKeyHandler = null;
2078 }
2079 if (mgr._overviewMouseHandler) {
2080 mgr._desktop.removeEventListener(
2081 "mouseover",
2082 mgr._overviewMouseHandler
2083 );
2084 mgr._overviewMouseHandler = null;
2085 }
2086 if (mgr._lastOverviewHoverId) {
2087 doAction(HOOKS.OVERVIEW_WINDOW_UNHOVER, {
2088 windowId: mgr._lastOverviewHoverId
2089 });
2090 mgr._lastOverviewHoverId = null;
2091 }
2092 }
2093 function getDesktops(mgr) {
2094 return [...mgr._desktops];
2095 }
2096 function getActiveDesktop(mgr) {
2097 const found = mgr._desktops.find((d) => d.id === mgr._activeDesktopId);
2098 return found ?? mgr._desktops[0];
2099 }
2100 function getActiveDesktopId(mgr) {
2101 return getActiveDesktop(mgr).id;
2102 }
2103 function applyDesktopVisibility(mgr, win) {
2104 const visible = win.config.desktopId === mgr._activeDesktopId;
2105 win.element.style.display = visible ? "" : "none";
2106 }
2107 function refreshDesktopVisibility(mgr) {
2108 for (const w of mgr._stack) {
2109 applyDesktopVisibility(mgr, w);
2110 }
2111 }
2112 function createDesktop(mgr) {
2113 mgr._desktopSeq++;
2114 const desktop = {
2115 id: `desktop-${mgr._desktopSeq}`,
2116 // translators: %d is the desktop number (e.g., "Desktop 2")
2117 label: sprintf(__("Desktop %d"), mgr._desktopSeq)
2118 };
2119 mgr._desktops.push(desktop);
2120 doAction(HOOKS.DESKTOP_CREATED, { desktopId: desktop.id });
2121 return desktop;
2122 }
2123 function switchDesktop(mgr, id, opts) {
2124 if (id === mgr._activeDesktopId) {
2125 return;
2126 }
2127 if (!mgr._desktops.some((d) => d.id === id)) {
2128 return;
2129 }
2130 const previousId = mgr._activeDesktopId;
2131 mgr._activeDesktopId = id;
2132 if (mgr._overviewActive) {
2133 relayoutOverviewForActiveDesktop(mgr);
2134 refreshOverviewTopBar(mgr);
2135 } else {
2136 refreshDesktopVisibility(mgr);
2137 if (opts?.direction) {
2138 animateDesktopSwitch(mgr, opts.direction);
2139 }
2140 const topOnNew = [...mgr._stack].reverse().find(
2141 (w) => w.config.desktopId === id && w.state !== "minimized"
2142 );
2143 if (topOnNew) {
2144 mgr.focus(topOnNew);
2145 }
2146 }
2147 doAction(HOOKS.DESKTOP_SWITCHED, {
2148 from: previousId,
2149 to: id
2150 });
2151 }
2152 function animateDesktopSwitch(mgr, direction) {
2153 const el = mgr._desktop;
2154 const cls = direction === "next" ? "desktop-mode-area--sliding-from-right" : "desktop-mode-area--sliding-from-left";
2155 el.classList.remove(
2156 "desktop-mode-area--sliding-from-right",
2157 "desktop-mode-area--sliding-from-left"
2158 );
2159 void el.offsetWidth;
2160 el.classList.add(cls);
2161 const onEnd = (e) => {
2162 if (!e.animationName.startsWith("desktop-mode-area-slide-from-")) {
2163 return;
2164 }
2165 el.classList.remove(cls);
2166 el.removeEventListener("animationend", onEnd);
2167 };
2168 el.addEventListener("animationend", onEnd);
2169 }
2170 function closeDesktop(mgr, id) {
2171 if (mgr._desktops.length <= 1) {
2172 return;
2173 }
2174 const idx = mgr._desktops.findIndex((d) => d.id === id);
2175 if (idx === -1) {
2176 return;
2177 }
2178 const survivorIdx = idx > 0 ? idx - 1 : 1;
2179 const survivor = mgr._desktops[survivorIdx];
2180 for (const w of mgr._stack) {
2181 if (w.config.desktopId === id) {
2182 w.config.desktopId = survivor.id;
2183 }
2184 }
2185 mgr._desktops.splice(idx, 1);
2186 const wasActive = mgr._activeDesktopId === id;
2187 if (wasActive) {
2188 mgr._activeDesktopId = survivor.id;
2189 }
2190 if (mgr._overviewActive) {
2191 relayoutOverviewForActiveDesktop(mgr);
2192 } else {
2193 refreshDesktopVisibility(mgr);
2194 }
2195 doAction(HOOKS.DESKTOP_CLOSED, {
2196 desktopId: id,
2197 migratedTo: survivor.id
2198 });
2199 }
2200 function relayoutOverviewForActiveDesktop(mgr) {
2201 for (const [winId, snap] of mgr._overviewSnapshot) {
2202 const w = mgr.getById(winId);
2203 if (w) {
2204 w.element.style.transform = snap.transform;
2205 w.element.style.transition = snap.transition;
2206 w.element.classList.remove("desktop-mode-window--overview");
2207 }
2208 }
2209 for (const label of mgr._overviewLabels.values()) {
2210 label.remove();
2211 }
2212 mgr._overviewLabels.clear();
2213 mgr._overviewSnapshot.clear();
2214 refreshDesktopVisibility(mgr);
2215 const eligible = mgr._stack.filter(
2216 (w) => w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
2217 );
2218 if (eligible.length === 0) {
2219 return;
2220 }
2221 for (const w of eligible) {
2222 mgr._overviewSnapshot.set(w.id, {
2223 transform: w.element.style.transform || "",
2224 transition: w.element.style.transition || ""
2225 });
2226 }
2227 const live = mgr._desktop.getBoundingClientRect();
2228 const targetRect = new DOMRect(0, 0, live.width, live.height);
2229 const layout = computeOverviewLayout(
2230 eligible,
2231 targetRect,
2232 OVERVIEW_TOP_BAR_RESERVE
2233 );
2234 for (const item of layout) {
2235 const el = item.win.element;
2236 el.classList.add("desktop-mode-window--overview");
2237 const dx = item.x - el.offsetLeft;
2238 const dy = item.y - el.offsetTop;
2239 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
2240 const label = createOverviewLabel(item);
2241 el.insertAdjacentElement("afterend", label);
2242 mgr._overviewLabels.set(item.win.id, label);
2243 }
2244 }
2245 function seedDesktops(mgr, desktops, activeDesktopId) {
2246 if (desktops.length === 0) {
2247 return;
2248 }
2249 mgr._desktops = desktops.map((d) => ({ ...d }));
2250 mgr._activeDesktopId = desktops.some((d) => d.id === activeDesktopId) ? activeDesktopId : desktops[0].id;
2251 let highest = 0;
2252 for (const d of desktops) {
2253 const match = d.id.match(/^desktop-(\d+)$/);
2254 if (match) {
2255 const n = parseInt(match[1], 10);
2256 if (Number.isFinite(n) && n > highest) {
2257 highest = n;
2258 }
2259 }
2260 }
2261 mgr._desktopSeq = Math.max(mgr._desktopSeq, highest);
2262 }
2263 function cascade(mgr) {
2264 const eligible = mgr._stack.filter(
2265 (w) => w.config.desktopId === mgr._activeDesktopId
2266 );
2267 if (eligible.length === 0) {
2268 return;
2269 }
2270 doAction(HOOKS.ARRANGE_CASCADE_STARTING, {
2271 windowCount: eligible.length
2272 });
2273 for (const w of eligible) {
2274 if (w.state === "minimized") {
2275 w.restore();
2276 }
2277 if (w.state === "fullscreen") {
2278 w.toggleFullscreen();
2279 }
2280 if (w.state === "maximized") {
2281 w.toggleMaximize();
2282 }
2283 }
2284 const rect = mgr._desktop.getBoundingClientRect();
2285 const padding = 30;
2286 const offset = 30;
2287 const targetWidth = Math.min(Math.round(rect.width * 0.7), 1100);
2288 const targetHeight = Math.min(Math.round(rect.height * 0.75), 750);
2289 const maxStepsX = Math.max(
2290 1,
2291 Math.floor((rect.width - targetWidth - padding) / offset)
2292 );
2293 const maxStepsY = Math.max(
2294 1,
2295 Math.floor((rect.height - targetHeight - padding) / offset)
2296 );
2297 const maxSteps = Math.min(maxStepsX, maxStepsY);
2298 eligible.forEach((w, i) => {
2299 const step = i % Math.max(1, maxSteps);
2300 w.element.style.left = `${padding + step * offset}px`;
2301 w.element.style.top = `${padding + step * offset}px`;
2302 w.element.style.width = `${targetWidth}px`;
2303 w.element.style.height = `${targetHeight}px`;
2304 });
2305 const focused = mgr.getFocused();
2306 if (focused) {
2307 mgr.focus(focused);
2308 }
2309 document.dispatchEvent(
2310 new CustomEvent("desktop-mode-window-changed", {
2311 detail: { reason: "cascade" }
2312 })
2313 );
2314 doAction(HOOKS.ARRANGE_CASCADE_APPLIED, {
2315 windowCount: eligible.length
2316 });
2317 }
2318 function tile(mgr) {
2319 const eligible = mgr._stack.filter(
2320 (w) => w.config.desktopId === mgr._activeDesktopId
2321 );
2322 if (eligible.length === 0) {
2323 return;
2324 }
2325 for (const w of eligible) {
2326 if (w.state === "minimized") {
2327 w.restore();
2328 }
2329 if (w.state === "fullscreen") {
2330 w.toggleFullscreen();
2331 }
2332 if (w.state === "maximized") {
2333 w.toggleMaximize();
2334 }
2335 }
2336 const rect = mgr._desktop.getBoundingClientRect();
2337 const auto = pickGridDimensions(
2338 eligible.length,
2339 rect.width,
2340 rect.height
2341 );
2342 const filtered = applyFilters(
2343 HOOKS.ARRANGE_TILE_DIMENSIONS,
2344 auto,
2345 {
2346 windowCount: eligible.length,
2347 areaWidth: rect.width,
2348 areaHeight: rect.height
2349 }
2350 );
2351 const { cols, rows } = isValidGrid(filtered, eligible.length) ? { cols: Math.floor(filtered.cols), rows: Math.floor(filtered.rows) } : auto;
2352 doAction(HOOKS.ARRANGE_TILE_STARTING, {
2353 windowCount: eligible.length,
2354 cols,
2355 rows
2356 });
2357 const padding = 16;
2358 const gap = 12;
2359 const cellWidth = Math.floor(
2360 (rect.width - padding * 2 - gap * (cols - 1)) / cols
2361 );
2362 const cellHeight = Math.floor(
2363 (rect.height - padding * 2 - gap * (rows - 1)) / rows
2364 );
2365 eligible.forEach((w, i) => {
2366 const col = i % cols;
2367 const row = Math.floor(i / cols);
2368 w.element.style.left = `${padding + col * (cellWidth + gap)}px`;
2369 w.element.style.top = `${padding + row * (cellHeight + gap)}px`;
2370 w.element.style.width = `${cellWidth}px`;
2371 w.element.style.height = `${cellHeight}px`;
2372 });
2373 const focused = mgr.getFocused();
2374 if (focused) {
2375 mgr.focus(focused);
2376 }
2377 document.dispatchEvent(
2378 new CustomEvent("desktop-mode-window-changed", {
2379 detail: { reason: "tile" }
2380 })
2381 );
2382 doAction(HOOKS.ARRANGE_TILE_APPLIED, {
2383 windowCount: eligible.length,
2384 cols,
2385 rows
2386 });
2387 }
2388 const SNAP_STORAGE_KEY = "desktop-mode-snap-to-grid";
2389 function loadSnapEnabled() {
2390 try {
2391 return window.localStorage.getItem(SNAP_STORAGE_KEY) === "1";
2392 } catch {
2393 return false;
2394 }
2395 }
2396 function setSnapEnabled(mgr, enabled) {
2397 if (mgr._snapEnabled === enabled) {
2398 return;
2399 }
2400 mgr._snapEnabled = enabled;
2401 try {
2402 window.localStorage.setItem(SNAP_STORAGE_KEY, enabled ? "1" : "0");
2403 } catch {
2404 }
2405 doAction(HOOKS.ARRANGE_SNAP_CHANGED, { enabled });
2406 }
2407 function getSnapConfig(mgr) {
2408 if (!mgr._snapEnabled) {
2409 return { enabled: false, cellWidth: 0, cellHeight: 0 };
2410 }
2411 const rect = mgr._desktop.getBoundingClientRect();
2412 const targetCols = rect.width >= rect.height ? 12 : 8;
2413 const auto = {
2414 cellWidth: Math.max(40, Math.round(rect.width / targetCols)),
2415 cellHeight: Math.max(
2416 40,
2417 Math.round(rect.height / Math.round(targetCols * 0.66))
2418 )
2419 };
2420 const filtered = applyFilters(
2421 HOOKS.ARRANGE_SNAP_CELL_SIZE,
2422 auto,
2423 { areaWidth: rect.width, areaHeight: rect.height }
2424 );
2425 const { cellWidth, cellHeight } = isValidCellSize(filtered) ? filtered : auto;
2426 return { enabled: true, cellWidth, cellHeight };
2427 }
2428 function enterSplitOverview(mgr, anchor, zone) {
2429 if (mgr._splitOverviewActive) {
2430 return;
2431 }
2432 mgr._splitOverviewActive = true;
2433 mgr._splitOverviewAnchor = anchor;
2434 mgr._splitOverviewZone = zone;
2435 const eligible = mgr._stack.filter(
2436 (w) => w !== anchor && w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
2437 );
2438 if (eligible.length === 0) {
2439 cleanupSplitOverviewState(mgr);
2440 return;
2441 }
2442 mgr._splitOverviewSnapshot.clear();
2443 for (const w of eligible) {
2444 mgr._splitOverviewSnapshot.set(w.id, {
2445 transform: w.element.style.transform || "",
2446 transition: w.element.style.transition || ""
2447 });
2448 }
2449 mgr._desktop.classList.add("desktop-mode-area--split-overview");
2450 const rect = oppositeHalfRect(mgr, zone);
2451 const layout = computeOverviewLayout(eligible, rect, 0);
2452 mgr._splitOverviewLabels.clear();
2453 for (const item of layout) {
2454 const el = item.win.element;
2455 el.classList.add("desktop-mode-window--overview");
2456 const dx = item.x - el.offsetLeft;
2457 const dy = item.y - el.offsetTop;
2458 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
2459 const label = createOverviewLabel(item);
2460 el.insertAdjacentElement("afterend", label);
2461 mgr._splitOverviewLabels.set(item.win.id, label);
2462 }
2463 const pressTargetForEvent = (e) => {
2464 const target2 = e.target;
2465 const winEl = target2?.closest(
2466 ".desktop-mode-window--overview"
2467 );
2468 if (winEl) {
2469 return {
2470 id: winEl.id.replace(/^wp-window-/, ""),
2471 element: winEl
2472 };
2473 }
2474 if (target2) {
2475 return { id: "dismiss", element: mgr._desktop };
2476 }
2477 return null;
2478 };
2479 mgr._splitOverviewPointerDown = (e) => {
2480 if (e.button !== 0) {
2481 mgr._splitOverviewPressTarget = null;
2482 return;
2483 }
2484 mgr._splitOverviewPressTarget = pressTargetForEvent(e);
2485 if (mgr._splitOverviewPressTarget) {
2486 e.preventDefault();
2487 e.stopPropagation();
2488 }
2489 };
2490 mgr._splitOverviewPointerUp = (e) => {
2491 if (e.button !== 0) {
2492 return;
2493 }
2494 const pressed = mgr._splitOverviewPressTarget;
2495 mgr._splitOverviewPressTarget = null;
2496 if (!pressed) {
2497 return;
2498 }
2499 const r = pressed.element.getBoundingClientRect();
2500 const inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
2501 if (!inside) {
2502 return;
2503 }
2504 e.preventDefault();
2505 e.stopPropagation();
2506 if (pressed.id === "dismiss") {
2507 exitSplitOverview(mgr);
2508 return;
2509 }
2510 const selected = mgr.getById(pressed.id);
2511 if (!selected) {
2512 exitSplitOverview(mgr);
2513 return;
2514 }
2515 fillOppositeHalfAndExit(mgr, selected);
2516 };
2517 mgr._splitOverviewKey = (e) => {
2518 if (e.key === "Escape") {
2519 exitSplitOverview(mgr);
2520 }
2521 };
2522 mgr._splitOverviewClickBlocker = (e) => {
2523 e.stopPropagation();
2524 e.preventDefault();
2525 };
2526 mgr._desktop.addEventListener(
2527 "pointerdown",
2528 mgr._splitOverviewPointerDown,
2529 true
2530 );
2531 mgr._desktop.addEventListener(
2532 "pointerup",
2533 mgr._splitOverviewPointerUp,
2534 true
2535 );
2536 mgr._desktop.addEventListener(
2537 "click",
2538 mgr._splitOverviewClickBlocker,
2539 true
2540 );
2541 document.addEventListener("keydown", mgr._splitOverviewKey);
2542 }
2543 function fillOppositeHalfAndExit(mgr, selected) {
2544 const anchorZone = mgr._splitOverviewZone;
2545 if (!anchorZone) {
2546 exitSplitOverview(mgr);
2547 return;
2548 }
2549 const partnerZone = anchorZone === "left" ? "right" : "left";
2550 selected.element.style.transform = "";
2551 selected.element.classList.remove("desktop-mode-window--overview");
2552 selected.applySnap(partnerZone);
2553 mgr._splitOverviewSnapshot.delete(selected.id);
2554 mgr.focus(selected);
2555 doAction(HOOKS.SNAP_SPLIT_FILLED, {
2556 windowId: selected.id,
2557 zone: partnerZone
2558 });
2559 exitSplitOverview(mgr);
2560 }
2561 function exitSplitOverview(mgr) {
2562 if (!mgr._splitOverviewActive) {
2563 return;
2564 }
2565 mgr._splitOverviewActive = false;
2566 for (const [id, snap] of mgr._splitOverviewSnapshot) {
2567 const w = mgr.getById(id);
2568 if (!w) {
2569 continue;
2570 }
2571 w.element.style.transform = snap.transform;
2572 }
2573 for (const label of mgr._splitOverviewLabels.values()) {
2574 label.classList.add("desktop-mode-overview-label--out");
2575 }
2576 mgr._desktop.classList.remove("desktop-mode-area--split-overview");
2577 const ANIMATION_MS = 260;
2578 window.setTimeout(() => {
2579 for (const w of mgr._stack) {
2580 if (mgr._splitOverviewSnapshot.has(w.id)) {
2581 w.element.classList.remove("desktop-mode-window--overview");
2582 }
2583 }
2584 for (const label of mgr._splitOverviewLabels.values()) {
2585 label.remove();
2586 }
2587 cleanupSplitOverviewState(mgr);
2588 }, ANIMATION_MS);
2589 if (mgr._splitOverviewPointerDown) {
2590 mgr._desktop.removeEventListener(
2591 "pointerdown",
2592 mgr._splitOverviewPointerDown,
2593 true
2594 );
2595 mgr._splitOverviewPointerDown = null;
2596 }
2597 if (mgr._splitOverviewPointerUp) {
2598 mgr._desktop.removeEventListener(
2599 "pointerup",
2600 mgr._splitOverviewPointerUp,
2601 true
2602 );
2603 mgr._splitOverviewPointerUp = null;
2604 }
2605 if (mgr._splitOverviewClickBlocker) {
2606 mgr._desktop.removeEventListener(
2607 "click",
2608 mgr._splitOverviewClickBlocker,
2609 true
2610 );
2611 mgr._splitOverviewClickBlocker = null;
2612 }
2613 if (mgr._splitOverviewKey) {
2614 document.removeEventListener("keydown", mgr._splitOverviewKey);
2615 mgr._splitOverviewKey = null;
2616 }
2617 mgr._splitOverviewPressTarget = null;
2618 }
2619 function cleanupSplitOverviewState(mgr) {
2620 mgr._splitOverviewSnapshot.clear();
2621 mgr._splitOverviewLabels.clear();
2622 mgr._splitOverviewAnchor = null;
2623 mgr._splitOverviewZone = null;
2624 mgr._splitOverviewActive = false;
2625 }
2626 const SNAP_EDGE_THRESHOLD = 30;
2627 const SNAP_COMMIT_MS = 260;
2628 function detectSnapZone(clientX, desktopRect) {
2629 if (clientX <= desktopRect.left + SNAP_EDGE_THRESHOLD) {
2630 return "left";
2631 }
2632 if (clientX >= desktopRect.right - SNAP_EDGE_THRESHOLD) {
2633 return "right";
2634 }
2635 return null;
2636 }
2637 function snapZoneBounds(mgr, zone) {
2638 const rect = mgr._desktop.getBoundingClientRect();
2639 const halfW = Math.floor(rect.width / 2);
2640 const height = Math.floor(rect.height);
2641 return {
2642 x: zone === "left" ? 0 : rect.width - halfW,
2643 y: 0,
2644 width: halfW,
2645 height
2646 };
2647 }
2648 function oppositeHalfRect(mgr, zone) {
2649 const rect = mgr._desktop.getBoundingClientRect();
2650 const halfW = Math.floor(rect.width / 2);
2651 const height = Math.floor(rect.height);
2652 if (zone === "left") {
2653 return new DOMRect(halfW, 0, halfW, height);
2654 }
2655 return new DOMRect(0, 0, halfW, height);
2656 }
2657 function showSnapPreview(mgr, zone) {
2658 if (mgr._snapPendingZone === zone && mgr._snapPreviewEl) {
2659 return;
2660 }
2661 mgr._snapPendingZone = zone;
2662 if (!mgr._snapPreviewEl) {
2663 const el = document.createElement("div");
2664 el.className = "desktop-mode-snap-preview";
2665 el.setAttribute("aria-hidden", "true");
2666 mgr._desktop.appendChild(el);
2667 mgr._snapPreviewEl = el;
2668 Promise.resolve().then(() => {
2669 el.classList.add("desktop-mode-snap-preview--visible");
2670 });
2671 }
2672 const b = snapZoneBounds(mgr, zone);
2673 mgr._snapPreviewEl.style.left = `${b.x}px`;
2674 mgr._snapPreviewEl.style.top = `${b.y}px`;
2675 mgr._snapPreviewEl.style.width = `${b.width}px`;
2676 mgr._snapPreviewEl.style.height = `${b.height}px`;
2677 mgr._snapPreviewEl.dataset.zone = zone;
2678 }
2679 function hideSnapPreview(mgr) {
2680 if (!mgr._snapPreviewEl) {
2681 mgr._snapPendingZone = null;
2682 return;
2683 }
2684 const el = mgr._snapPreviewEl;
2685 mgr._snapPreviewEl = null;
2686 mgr._snapPendingZone = null;
2687 el.classList.remove("desktop-mode-snap-preview--visible");
2688 window.setTimeout(() => {
2689 el.remove();
2690 }, SNAP_COMMIT_MS);
2691 }
2692 function updateSnapZoneForDrag(mgr, win, clientX) {
2693 if (mgr._splitOverviewActive) {
2694 return;
2695 }
2696 const rect = mgr._desktop.getBoundingClientRect();
2697 const zone = detectSnapZone(clientX, rect);
2698 const previous = mgr._snapPendingZone;
2699 if (zone) {
2700 showSnapPreview(mgr, zone);
2701 if (previous !== zone) {
2702 doAction(HOOKS.SNAP_ZONE_PENDING, {
2703 windowId: win.id,
2704 zone
2705 });
2706 }
2707 } else if (previous) {
2708 hideSnapPreview(mgr);
2709 doAction(HOOKS.SNAP_ZONE_CANCELED, { windowId: win.id });
2710 }
2711 }
2712 function commitSnapIfPending(mgr, win) {
2713 const zone = mgr._snapPendingZone;
2714 if (!zone) {
2715 return false;
2716 }
2717 hideSnapPreview(mgr);
2718 if (win.state === "normal") {
2719 win._savedGeometry = {
2720 x: win.element.offsetLeft,
2721 y: win.element.offsetTop,
2722 width: win.element.offsetWidth,
2723 height: win.element.offsetHeight
2724 };
2725 }
2726 win.applySnap(zone);
2727 doAction(HOOKS.SNAP_ZONE_COMMITTED, {
2728 windowId: win.id,
2729 zone
2730 });
2731 window.requestAnimationFrame(() => {
2732 enterSplitOverview(mgr, win, zone);
2733 });
2734 return true;
2735 }
2736 function abortSnapIfPending(mgr) {
2737 if (mgr._snapPendingZone) {
2738 hideSnapPreview(mgr);
2739 }
2740 }
2741 const NATIVE_GEOMETRY_STORAGE_KEY = "desktop-mode-native-window-geometry";
2742 const MAX_ENTRIES = 64;
2743 const MAX_DIMENSION = 8192;
2744 function readMap$1() {
2745 try {
2746 const raw = window.localStorage.getItem(NATIVE_GEOMETRY_STORAGE_KEY);
2747 if (!raw) {
2748 return {};
2749 }
2750 const parsed = JSON.parse(raw);
2751 if (!parsed || typeof parsed !== "object") {
2752 return {};
2753 }
2754 return parsed;
2755 } catch {
2756 return {};
2757 }
2758 }
2759 function writeMap$1(map) {
2760 try {
2761 window.localStorage.setItem(
2762 NATIVE_GEOMETRY_STORAGE_KEY,
2763 JSON.stringify(map)
2764 );
2765 } catch {
2766 }
2767 }
2768 function loadNativeWindowGeometry(baseId) {
2769 if (!baseId) {
2770 return null;
2771 }
2772 const map = readMap$1();
2773 const entry = map[baseId];
2774 if (!entry) {
2775 return null;
2776 }
2777 const width = Number(entry.width);
2778 const height = Number(entry.height);
2779 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2780 return null;
2781 }
2782 const state2 = entry.state === "maximized" ? "maximized" : void 0;
2783 const x = Number(entry.x);
2784 const y = Number(entry.y);
2785 const hasPosition = Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0 && x <= MAX_DIMENSION && y <= MAX_DIMENSION;
2786 return {
2787 width: Math.round(width),
2788 height: Math.round(height),
2789 ...hasPosition ? { x: Math.round(x), y: Math.round(y) } : {},
2790 ...state2 ? { state: state2 } : {}
2791 };
2792 }
2793 function saveNativeWindowGeometry(baseId, geometry) {
2794 if (!baseId) {
2795 return;
2796 }
2797 const width = Math.round(Number(geometry.width));
2798 const height = Math.round(Number(geometry.height));
2799 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2800 return;
2801 }
2802 const map = readMap$1();
2803 const prev = map[baseId];
2804 const state2 = prev && prev.state === "maximized" ? "maximized" : void 0;
2805 const carriedX = typeof prev?.x === "number" ? prev.x : void 0;
2806 const carriedY = typeof prev?.y === "number" ? prev.y : void 0;
2807 if (prev && prev.width === width && prev.height === height && prev.state === state2 && prev.x === carriedX && prev.y === carriedY) {
2808 return;
2809 }
2810 upsertEntry(map, baseId, {
2811 width,
2812 height,
2813 ...typeof carriedX === "number" && typeof carriedY === "number" ? { x: carriedX, y: carriedY } : {},
2814 ...state2 ? { state: state2 } : {}
2815 });
2816 writeMapTrimmed(map);
2817 }
2818 function saveNativeWindowPosition(baseId, position) {
2819 if (!baseId) {
2820 return;
2821 }
2822 const x = Math.round(Number(position.x));
2823 const y = Math.round(Number(position.y));
2824 if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || y < 0 || x > MAX_DIMENSION || y > MAX_DIMENSION) {
2825 return;
2826 }
2827 const map = readMap$1();
2828 const prev = map[baseId];
2829 if (!prev) {
2830 return;
2831 }
2832 if (prev.x === x && prev.y === y) {
2833 return;
2834 }
2835 upsertEntry(map, baseId, {
2836 ...prev,
2837 x,
2838 y
2839 });
2840 writeMapTrimmed(map);
2841 }
2842 function setNativeWindowSavedState(baseId, state2, defaults) {
2843 if (!baseId) {
2844 return;
2845 }
2846 const map = readMap$1();
2847 const prev = map[baseId];
2848 if (!prev) {
2849 if (state2 === null || !defaults) {
2850 return;
2851 }
2852 const width = Math.round(Number(defaults.width));
2853 const height = Math.round(Number(defaults.height));
2854 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2855 return;
2856 }
2857 upsertEntry(map, baseId, { width, height, state: state2 });
2858 writeMapTrimmed(map);
2859 return;
2860 }
2861 if (state2 === null) {
2862 if (!prev.state) {
2863 return;
2864 }
2865 const { state: _state2, ...rest } = prev;
2866 upsertEntry(map, baseId, rest);
2867 writeMapTrimmed(map);
2868 return;
2869 }
2870 if (prev.state === state2) {
2871 return;
2872 }
2873 upsertEntry(map, baseId, {
2874 ...prev,
2875 state: state2
2876 });
2877 writeMapTrimmed(map);
2878 }
2879 function upsertEntry(map, baseId, entry) {
2880 delete map[baseId];
2881 map[baseId] = entry;
2882 }
2883 function writeMapTrimmed(map) {
2884 const keys = Object.keys(map);
2885 if (keys.length > MAX_ENTRIES) {
2886 const trimmed = {};
2887 for (const key of keys.slice(-MAX_ENTRIES)) {
2888 trimmed[key] = map[key];
2889 }
2890 writeMap$1(trimmed);
2891 return;
2892 }
2893 writeMap$1(map);
2894 }
2895 const BASE_Z_INDEX = 100;
2896 const CASCADE_OFFSET = 30;
2897 class WindowManager {
2898 constructor(desktop) {
2899 this._stack = [];
2900 this.cascadeIndex = 0;
2901 this._desktops = [
2902 // translators: default desktop name — "Desktop 1"
2903 { id: "desktop-1", label: "Desktop 1" }
2904 ];
2905 this._activeDesktopId = "desktop-1";
2906 this._desktopSeq = 1;
2907 this.onToggleStartupRequested = null;
2908 this.desktopResizeObserver = null;
2909 this._reflowRestoreTimer = null;
2910 this._snapEnabled = loadSnapEnabled();
2911 this._overviewActive = false;
2912 this._overviewSnapshot = /* @__PURE__ */ new Map();
2913 this._overviewLabels = /* @__PURE__ */ new Map();
2914 this._overviewPointerDownHandler = null;
2915 this._overviewPointerUpHandler = null;
2916 this._overviewKeyHandler = null;
2917 this._overviewPressTarget = null;
2918 this._overviewClickBlocker = null;
2919 this._overviewTopBar = null;
2920 this._overviewMouseHandler = null;
2921 this._lastOverviewHoverId = null;
2922 this._overviewAddTileFocused = false;
2923 this._overviewEnterTimeoutId = null;
2924 this._overviewExitTimeoutId = null;
2925 this._snapPendingZone = null;
2926 this._snapPreviewEl = null;
2927 this._splitOverviewActive = false;
2928 this._splitOverviewAnchor = null;
2929 this._splitOverviewZone = null;
2930 this._splitOverviewSnapshot = /* @__PURE__ */ new Map();
2931 this._splitOverviewLabels = /* @__PURE__ */ new Map();
2932 this._splitOverviewPointerDown = null;
2933 this._splitOverviewPointerUp = null;
2934 this._splitOverviewPressTarget = null;
2935 this._splitOverviewClickBlocker = null;
2936 this._splitOverviewKey = null;
2937 this._desktop = desktop;
2938 if (typeof ResizeObserver !== "undefined") {
2939 this.desktopResizeObserver = new ResizeObserver(
2940 () => this.reflowStatefulWindows()
2941 );
2942 this.desktopResizeObserver.observe(desktop);
2943 }
2944 this.installIframeFocusBridge();
2945 }
2946 /**
2947 * Clicks inside an iframe don't cross the browsing-context
2948 * boundary — pointerdown / focusin in the iframe's document never
2949 * reach the parent. BUT the parent `window` does lose focus,
2950 * because focus moves to the iframe's content window.
2951 *
2952 * We use that signal: listen for `window.blur` on the parent,
2953 * check `document.activeElement` — if it's an iframe, walk up to
2954 * its owning `.desktop-mode-window`, find the matching Window in
2955 * our stack, and focus it. Covers clicks on the primary iframe
2956 * AND any external-tab sub-iframes mounted as descendants of the
2957 * window element.
2958 */
2959 installIframeFocusBridge() {
2960 window.addEventListener("blur", () => {
2961 window.setTimeout(() => {
2962 const active2 = this._desktop.ownerDocument?.activeElement ?? null;
2963 if (!active2 || active2.tagName !== "IFRAME") {
2964 return;
2965 }
2966 const winEl = active2.closest(
2967 ".desktop-mode-window"
2968 );
2969 if (!winEl) {
2970 return;
2971 }
2972 const id = winEl.id.replace(/^wp-window-/, "");
2973 const win = this.getById(id);
2974 if (!win) {
2975 return;
2976 }
2977 if (this._overviewActive) {
2978 return;
2979 }
2980 if (this.getFocused() === win) {
2981 return;
2982 }
2983 this.focus(win);
2984 }, 0);
2985 });
2986 }
2987 /**
2988 * Re-apply state-driven bounds to any window whose geometry is
2989 * derived from the desktop area's dimensions: maximized (full
2990 * area) and snapped-left / snapped-right (half area). Called from
2991 * the desktop-area ResizeObserver so shrinking the browser window
2992 * drags the stateful windows along with it.
2993 *
2994 * Inlines the geometry writes instead of calling `applySnap` —
2995 * that method emits `_emitChange('state')` which would spam the
2996 * session saver on every resize tick. Viewport resize is an
2997 * INCOMING shape change (the shell reshaped us), not an outgoing
2998 * user action worth persisting.
2999 *
3000 * Also toggles `desktop-mode-window--reflowing` so the base
3001 * left/top/width/height transition doesn't interpolate between
3002 * every ResizeObserver tick — without that, the windows would
3003 * always lag ~250 ms behind a browser edge-drag.
3004 *
3005 * Skipped while overview is active — windows are mid-transform
3006 * and touching their inline geometry would desync the live
3007 * transform math; overview exit re-applies state correctly via
3008 * its own path.
3009 */
3010 reflowStatefulWindows() {
3011 if (this._overviewActive) {
3012 return;
3013 }
3014 for (const w of this._stack) {
3015 const parent = w.element.parentElement;
3016 if (!parent) {
3017 continue;
3018 }
3019 if (w.state === "maximized") {
3020 w.element.classList.add("desktop-mode-window--reflowing");
3021 w.element.style.width = `${parent.clientWidth}px`;
3022 w.element.style.height = `${parent.clientHeight}px`;
3023 } else if (w.state === "snapped-left" || w.state === "snapped-right") {
3024 w.element.classList.add("desktop-mode-window--reflowing");
3025 const halfW = Math.floor(parent.clientWidth / 2);
3026 const height = parent.clientHeight;
3027 const left = w.state === "snapped-left" ? 0 : halfW;
3028 w.element.style.left = `${left}px`;
3029 w.element.style.top = "0px";
3030 w.element.style.width = `${halfW}px`;
3031 w.element.style.height = `${height}px`;
3032 }
3033 }
3034 if (this._reflowRestoreTimer !== null) {
3035 window.clearTimeout(this._reflowRestoreTimer);
3036 }
3037 this._reflowRestoreTimer = window.setTimeout(() => {
3038 this._reflowRestoreTimer = null;
3039 for (const w of this._stack) {
3040 w.element.classList.remove("desktop-mode-window--reflowing");
3041 }
3042 }, 140);
3043 }
3044 /**
3045 * Open a new window — or focus an existing one — for the given
3046 * page.
3047 *
3048 * Matches any existing window sharing the same `baseId`
3049 * (defaulting to the config's `id`). For singleton pages
3050 * (Settings, Dashboard, …) `baseId === id`, so this behaves
3051 * exactly like strict id matching. For multi pages, clicking the
3052 * dock icon while a window is already open focuses the
3053 * most-recent instance rather than creating a twin.
3054 *
3055 * URL-aware reuse: when the matched window is NOT already showing
3056 * the requested URL (and the request isn't for the window's home
3057 * / dock landing URL), the existing iframe navigates to it in
3058 * place — an action URL like
3059 * `plugins.php?action=activate&…&_wpnonce=…` actually runs
3060 * instead of being dropped by a bare focus. The
3061 * `desktop-mode-window-reopened` event reports which path was
3062 * taken via its `navigated` flag.
3063 *
3064 * To force a brand-new instance alongside an existing one, use
3065 * {@link openNew}.
3066 */
3067 async open(config) {
3068 if (!config || typeof config !== "object") {
3069 throw new TypeError(
3070 "windowManager.open() requires a config object with at least { id, url, title }; received " + (config === null ? "null" : typeof config)
3071 );
3072 }
3073 if (typeof config.id !== "string" || config.id === "") {
3074 throw new TypeError(
3075 "windowManager.open(): config.id must be a non-empty string."
3076 );
3077 }
3078 if (typeof config.url !== "string" || config.url === "") {
3079 throw new TypeError(
3080 '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.'
3081 );
3082 }
3083 if (typeof config.title !== "string") {
3084 throw new TypeError(
3085 "windowManager.open(): config.title must be a string."
3086 );
3087 }
3088 const baseId = config.baseId || config.id;
3089 const existing = this.getByBaseIdOnActiveDesktop(baseId);
3090 if (existing) {
3091 const wasMinimized = existing.state === "minimized";
3092 this.focus(existing);
3093 if (wasMinimized) {
3094 existing.restore();
3095 }
3096 let navigated = false;
3097 if (!existing.config.native) {
3098 const requestedKey = urlReuseKey(config.url);
3099 const alreadyThere = requestedKey === urlReuseKey(existing.getCurrentUrl()) || requestedKey === urlReuseKey(existing.config.url || "") || requestedKey === urlReuseKey(
3100 existing.config.parentUrl ?? existing.config.url ?? ""
3101 );
3102 if (!alreadyThere) {
3103 navigated = existing.navigateTo(config.url);
3104 }
3105 }
3106 const reopenedDetail = {
3107 windowId: existing.id,
3108 baseId,
3109 wasMinimized,
3110 navigated
3111 };
3112 document.dispatchEvent(
3113 new CustomEvent("desktop-mode-window-reopened", { detail: reopenedDetail })
3114 );
3115 doAction(HOOKS.WINDOW_REOPENED, reopenedDetail);
3116 return existing;
3117 }
3118 const id = this.getByBaseId(baseId) ? this.nextInstanceId(baseId) : config.id;
3119 return this.createWindow({ ...config, id, baseId });
3120 }
3121 /**
3122 * Open a brand-new window even if one is already open for this
3123 * page. Only makes sense for pages flagged `multi`.
3124 *
3125 * Duplicates always open in the floating ('normal') state and at
3126 * a fresh cascade slot — the per-baseId saved size / state /
3127 * position preferences apply to the primary instance only.
3128 * Spawning a maximized twin alongside the maximized primary
3129 * would hide the primary; landing a twin on top of the primary's
3130 * remembered position would hide it too. Callers can override
3131 * either default by passing `initialState` / `x` / `y` explicitly.
3132 */
3133 async openNew(config) {
3134 const baseId = config.baseId || config.id;
3135 const nextId2 = this.nextInstanceId(baseId);
3136 const cascadeX = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
3137 const cascadeY = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
3138 return this.createWindow({
3139 initialState: "normal",
3140 x: cascadeX,
3141 y: cascadeY,
3142 ...config,
3143 id: nextId2,
3144 baseId
3145 });
3146 }
3147 /**
3148 * Build and mount a window element. Common tail shared by
3149 * `open()` and `openNew()`.
3150 */
3151 async createWindow(config) {
3152 const desktopRect = this._desktop.getBoundingClientRect();
3153 const defaultWidth = Math.min(Math.round(desktopRect.width * 0.8), 1200);
3154 const defaultHeight = Math.min(Math.round(desktopRect.height * 0.8), 800);
3155 const cascadeX = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
3156 const cascadeY = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
3157 const resolvedBaseId = config.baseId || config.id;
3158 const minWidth = config.minWidth ?? 320;
3159 const minHeight = config.minHeight ?? 200;
3160 const hasExplicitWidth = typeof config.width === "number";
3161 const hasExplicitHeight = typeof config.height === "number";
3162 const hasExplicitX = typeof config.x === "number";
3163 const hasExplicitY = typeof config.y === "number";
3164 const hasExplicitState = typeof config.initialState === "string";
3165 const saved = !hasExplicitWidth || !hasExplicitHeight || !hasExplicitState || !hasExplicitX || !hasExplicitY ? loadNativeWindowGeometry(resolvedBaseId) : null;
3166 const resolvedWidth = config.width ?? (saved ? Math.max(saved.width, minWidth) : defaultWidth);
3167 const resolvedHeight = config.height ?? (saved ? Math.max(saved.height, minHeight) : defaultHeight);
3168 const resolvedState = config.initialState ?? (saved?.state === "maximized" ? "maximized" : void 0);
3169 let clampedSavedX;
3170 let clampedSavedY;
3171 if (saved && typeof saved.x === "number" && typeof saved.y === "number") {
3172 const margin = 12;
3173 const maxX = Math.max(
3174 0,
3175 desktopRect.width - resolvedWidth - margin
3176 );
3177 const maxY = Math.max(
3178 0,
3179 desktopRect.height - resolvedHeight - margin
3180 );
3181 clampedSavedX = Math.max(margin, Math.min(saved.x, maxX));
3182 clampedSavedY = Math.max(margin, Math.min(saved.y, maxY));
3183 }
3184 const resolvedX = config.x ?? clampedSavedX ?? cascadeX;
3185 const resolvedY = config.y ?? clampedSavedY ?? cascadeY;
3186 const callerPinned = hasExplicitWidth || hasExplicitHeight || hasExplicitX || hasExplicitY || hasExplicitState;
3187 const hasSavedGeometry = !!saved;
3188 const preFilterGeometry = {
3189 x: resolvedX,
3190 y: resolvedY,
3191 width: resolvedWidth,
3192 height: resolvedHeight,
3193 state: resolvedState
3194 };
3195 let filtered;
3196 try {
3197 filtered = applyFilters(
3198 HOOKS.WINDOW_GEOMETRY,
3199 preFilterGeometry,
3200 {
3201 windowId: config.id,
3202 baseId: resolvedBaseId,
3203 hasSavedGeometry,
3204 callerPinned,
3205 desktopRect: {
3206 width: desktopRect.width,
3207 height: desktopRect.height
3208 }
3209 }
3210 );
3211 } catch (err) {
3212 doAction(HOOKS.SHELL_ERROR, {
3213 scope: "window-geometry-filter",
3214 windowId: config.id,
3215 error: err
3216 });
3217 if (typeof console !== "undefined") {
3218 console.error(
3219 `[desktop-mode] WINDOW_GEOMETRY filter threw for "${config.id}":`,
3220 err
3221 );
3222 }
3223 filtered = preFilterGeometry;
3224 }
3225 const coalesce = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback;
3226 const safeFiltered = filtered && typeof filtered === "object" ? filtered : preFilterGeometry;
3227 const finalWidth = Math.max(
3228 coalesce(safeFiltered.width, resolvedWidth),
3229 minWidth
3230 );
3231 const finalHeight = Math.max(
3232 coalesce(safeFiltered.height, resolvedHeight),
3233 minHeight
3234 );
3235 const finalX = coalesce(safeFiltered.x, resolvedX);
3236 const finalY = coalesce(safeFiltered.y, resolvedY);
3237 const finalState = safeFiltered.state ?? resolvedState;
3238 const fullConfig = {
3239 icon: config.icon || "dashicons-admin-generic",
3240 ...config,
3241 // Spread `config` first so callers can pass through any
3242 // extras (render, ownerHandle, parentUrl, …), then pin the
3243 // dimensions + state we resolved above. The pin has to
3244 // follow the spread because an explicit `width: undefined`
3245 // from the caller would otherwise blow away the default.
3246 x: finalX,
3247 y: finalY,
3248 width: finalWidth,
3249 height: finalHeight,
3250 minWidth,
3251 minHeight,
3252 ...finalState ? { initialState: finalState } : {},
3253 baseId: resolvedBaseId,
3254 // New windows always join the active desktop. A caller can
3255 // pre-seed `desktopId` (e.g. session restore) by passing it
3256 // in `config`, which the spread above preserves.
3257 desktopId: config.desktopId || this._activeDesktopId
3258 };
3259 this.cascadeIndex++;
3260 const [system] = await Promise.all([
3261 ensureWindowSystemLoaded(windowSystemBundleUrl()),
3262 ensureShellOverlaysLoaded(shellOverlaysBundleUrl())
3263 ]);
3264 const win = system.createWindow(fullConfig);
3265 win.onFocusRequest = (w) => this.focus(w);
3266 win.onClose = (w) => this.remove(w);
3267 win.onMinimize = () => {
3268 const visible = this._stack.filter((w) => w.state !== "minimized");
3269 if (visible.length > 0) {
3270 this.focus(visible[visible.length - 1]);
3271 }
3272 };
3273 win.onOpenAnother = (w) => {
3274 const baseId = w.config.baseId || w.id;
3275 if (w.config.native) {
3276 const api = window.wp?.desktop;
3277 if (api?.openNewWindow?.(baseId, { source: "open-another" })) {
3278 return;
3279 }
3280 }
3281 void this.openNew({
3282 id: baseId,
3283 baseId,
3284 url: w.config.url || "",
3285 title: w.config.title,
3286 icon: w.config.icon,
3287 submenu: w.config.submenu,
3288 multi: true
3289 });
3290 };
3291 win.onOpenInNewWindow = (w) => {
3292 const baseId = w.config.baseId || w.id;
3293 if (w.config.native) {
3294 const api = window.wp?.desktop;
3295 if (api?.openNewWindow?.(baseId, { source: "open-in-new-window" })) {
3296 return;
3297 }
3298 }
3299 const currentUrl = w.getCurrentUrl();
3300 void this.openNew({
3301 id: baseId,
3302 baseId,
3303 url: currentUrl || w.config.url || "",
3304 title: w.config.title,
3305 icon: w.config.icon,
3306 submenu: w.config.submenu,
3307 multi: true
3308 });
3309 };
3310 win.onToggleStartup = (w) => {
3311 this.onToggleStartupRequested?.(w);
3312 };
3313 win.snapConfigProvider = () => this.getSnapConfig();
3314 win.onDragMove = (w, clientX) => {
3315 updateSnapZoneForDrag(this, w, clientX);
3316 };
3317 win.onDragEnd = (w) => {
3318 if (this._snapPendingZone) {
3319 return commitSnapIfPending(this, w);
3320 }
3321 abortSnapIfPending(this);
3322 return false;
3323 };
3324 this._stack.push(win);
3325 this._desktop.appendChild(win.element);
3326 applyDesktopVisibility(this, win);
3327 win.hydrateNative();
3328 this.focus(win);
3329 const openedDetail = {
3330 windowId: win.id,
3331 page: config.url,
3332 title: config.title,
3333 url: config.url
3334 };
3335 document.dispatchEvent(
3336 new CustomEvent("desktop-mode-window-opened", { detail: openedDetail })
3337 );
3338 doAction(HOOKS.WINDOW_OPENED, openedDetail);
3339 return win;
3340 }
3341 /**
3342 * Find the next unused suffixed id for a given baseId. Prefers
3343 * the bare baseId itself if free (user closed the original), then
3344 * walks `-2`, `-3`, … until it lands on one not currently in the
3345 * stack.
3346 */
3347 nextInstanceId(baseId) {
3348 const taken = new Set(this._stack.map((w) => w.id));
3349 if (!taken.has(baseId)) {
3350 return baseId;
3351 }
3352 let n = 2;
3353 while (taken.has(`${baseId}-${n}`)) {
3354 n++;
3355 }
3356 return `${baseId}-${n}`;
3357 }
3358 /** Focus a window: bring it to top of z-stack. */
3359 focus(win) {
3360 const previouslyFocused = this._stack.length > 0 ? this._stack[this._stack.length - 1] : null;
3361 const priorFullscreen = this._stack.find(
3362 (w) => w !== win && w.isFocused() && w.isFullscreen()
3363 );
3364 if (priorFullscreen) {
3365 const shouldExit = applyFilters(
3366 HOOKS.WINDOW_AUTO_EXIT_FULLSCREEN,
3367 true,
3368 { windowId: priorFullscreen.id, focusedTo: win.id }
3369 );
3370 if (shouldExit) {
3371 priorFullscreen.toggleFullscreen();
3372 }
3373 }
3374 const idx = this._stack.indexOf(win);
3375 if (idx > -1) {
3376 this._stack.splice(idx, 1);
3377 }
3378 this._stack.push(win);
3379 this._stack.forEach((w, i) => {
3380 w.setZIndex(BASE_Z_INDEX + i);
3381 w.setFocused(i === this._stack.length - 1);
3382 });
3383 if (previouslyFocused && previouslyFocused !== win && previouslyFocused.id !== win.id) {
3384 const blurredDetail = {
3385 windowId: previouslyFocused.id,
3386 focusedTo: win.id
3387 };
3388 document.dispatchEvent(
3389 new CustomEvent("desktop-mode-window-blurred", { detail: blurredDetail })
3390 );
3391 doAction(HOOKS.WINDOW_BLURRED, blurredDetail);
3392 }
3393 const focusedDetail = { windowId: win.id };
3394 document.dispatchEvent(
3395 new CustomEvent("desktop-mode-window-focused", { detail: focusedDetail })
3396 );
3397 doAction(HOOKS.WINDOW_FOCUSED, focusedDetail);
3398 }
3399 /**
3400 * Raise a window to just below the top of the stack WITHOUT
3401 * changing focus — the focused window stays on top and keeps
3402 * keyboard/visual focus; the raised window surfaces above
3403 * everything else. No focus/blur events fire (this is a silent
3404 * restack, not a focus change).
3405 *
3406 * Used by the window-links feature to bring a relation group
3407 * forward when one of its members is focused; available to
3408 * plugins for any "surface my companion window" affordance.
3409 *
3410 * @since 0.9.4
3411 *
3412 * @param windowId Window to raise. Unknown ids and the focused
3413 * window itself are no-ops.
3414 */
3415 raise(windowId) {
3416 const win = this.getById(windowId);
3417 if (!win || this._stack.length < 2) {
3418 return;
3419 }
3420 const idx = this._stack.indexOf(win);
3421 if (idx === -1 || idx === this._stack.length - 1) {
3422 return;
3423 }
3424 this._stack.splice(idx, 1);
3425 this._stack.splice(this._stack.length - 1, 0, win);
3426 this._stack.forEach((w, i) => {
3427 w.setZIndex(BASE_Z_INDEX + i);
3428 });
3429 }
3430 /** Remove a window from the stack and DOM. */
3431 remove(win) {
3432 const idx = this._stack.indexOf(win);
3433 if (idx > -1) {
3434 this._stack.splice(idx, 1);
3435 }
3436 for (let i = this._stack.length - 1; i >= 0; i--) {
3437 const candidate = this._stack[i];
3438 if (candidate.state === "minimized") {
3439 continue;
3440 }
3441 const candidateDesktop = candidate.config.desktopId || this._activeDesktopId;
3442 if (candidateDesktop !== this._activeDesktopId) {
3443 continue;
3444 }
3445 this.focus(candidate);
3446 break;
3447 }
3448 const closingDetail = { windowId: win.id, element: win.element };
3449 document.dispatchEvent(
3450 new CustomEvent("desktop-mode-window-closing", { detail: closingDetail })
3451 );
3452 doAction(HOOKS.WINDOW_CLOSING, closingDetail);
3453 const closedDetail = { windowId: win.id };
3454 document.dispatchEvent(
3455 new CustomEvent("desktop-mode-window-closed", { detail: closedDetail })
3456 );
3457 doAction(HOOKS.WINDOW_CLOSED, closedDetail);
3458 }
3459 /** Get a window by its ID. */
3460 getById(id) {
3461 return this._stack.find((w) => w.id === id);
3462 }
3463 /**
3464 * Get the most-recently-focused window for a given baseId.
3465 *
3466 * Multi-instance windows share a baseId; the stack is ordered
3467 * bottom to top by focus, so iterating from the end finds the
3468 * best candidate to bring forward when the user re-clicks the
3469 * dock icon.
3470 */
3471 getByBaseId(baseId) {
3472 for (let i = this._stack.length - 1; i >= 0; i--) {
3473 const w = this._stack[i];
3474 if ((w.config.baseId || w.id) === baseId) {
3475 return w;
3476 }
3477 }
3478 return void 0;
3479 }
3480 /**
3481 * Like {@link getByBaseId} but only considers windows on the
3482 * currently-active virtual desktop. The dock's "open or focus"
3483 * path uses this — a Plugins instance that lives on Desktop 2 is
3484 * invisible from Desktop 1's dock click, so clicking Plugins on
3485 * Desktop 1 should open a fresh instance there instead of trying
3486 * to focus the far-off sibling (which would silently do nothing
3487 * because the other desktop's windows are display: none here).
3488 */
3489 getByBaseIdOnActiveDesktop(baseId) {
3490 for (let i = this._stack.length - 1; i >= 0; i--) {
3491 const w = this._stack[i];
3492 if ((w.config.baseId || w.id) !== baseId) {
3493 continue;
3494 }
3495 const winDesktop = w.config.desktopId || this._activeDesktopId;
3496 if (winDesktop === this._activeDesktopId) {
3497 return w;
3498 }
3499 }
3500 return void 0;
3501 }
3502 /**
3503 * Get every open window sharing the given baseId, ordered by
3504 * instance slot (bare baseId first, then `-2`, `-3`, …) rather
3505 * than z-order — so the dock's instance rail keeps a stable
3506 * left-to-right order even as the user focuses between windows.
3507 */
3508 getAllByBaseId(baseId) {
3509 const instanceSlot = (id) => {
3510 if (id === baseId) {
3511 return 1;
3512 }
3513 const prefix = `${baseId}-`;
3514 if (id.startsWith(prefix)) {
3515 const n = parseInt(id.slice(prefix.length), 10);
3516 return Number.isFinite(n) ? n : 999;
3517 }
3518 return 999;
3519 };
3520 return this._stack.filter((w) => (w.config.baseId || w.id) === baseId).sort((a, b) => instanceSlot(a.id) - instanceSlot(b.id));
3521 }
3522 /**
3523 * Get every open window sharing the given baseId on the active desktop,
3524 * ordered by instance slot.
3525 */
3526 getAllByBaseIdOnActiveDesktop(baseId) {
3527 return this.getAllByBaseId(baseId).filter(
3528 (w) => (w.config.desktopId || this._activeDesktopId) === this._activeDesktopId
3529 );
3530 }
3531 /** Get all open windows. */
3532 getAll() {
3533 return [...this._stack];
3534 }
3535 /**
3536 * Find the window whose iframe's contentWindow matches the given
3537 * message source. Used by cross-frame bridges to attribute inbound
3538 * `postMessage` events to the originating window without reaching
3539 * into `_stack`.
3540 */
3541 findByIframeSource(source) {
3542 if (!source) {
3543 return void 0;
3544 }
3545 return this._stack.find(
3546 (w) => w.iframe !== null && w.iframe.contentWindow === source
3547 );
3548 }
3549 /** Get the currently focused (topmost) window. */
3550 getFocused() {
3551 return this._stack.length > 0 ? this._stack[this._stack.length - 1] : void 0;
3552 }
3553 /**
3554 * "Is the window with this id currently in front of the user?"
3555 *
3556 * Returns true when the window exists in the manager AND it
3557 * isn't minimized AND it's the currently focused (topmost)
3558 * window. False otherwise — including for unknown ids, closed
3559 * windows, minimized windows, or windows that exist but aren't
3560 * on top.
3561 *
3562 * The canonical query for plugins implementing the "show
3563 * something *only when the user can't already see my
3564 * window*" pattern (badge counts, attention pulses, sounds,
3565 * toasts). Plugins that previously hand-rolled
3566 * `getById(id) && state !== 'minimized' && focused` can
3567 * collapse to this.
3568 *
3569 * @since 0.5.5
3570 *
3571 * @param id Window id to query.
3572 * @return True when the user is actively looking at this window.
3573 */
3574 isActive(id) {
3575 const win = this.getById(id);
3576 if (!win) {
3577 return false;
3578 }
3579 if (win.state === "minimized") {
3580 return false;
3581 }
3582 const winDesktop = win.config.desktopId || this._activeDesktopId;
3583 if (winDesktop !== this._activeDesktopId) {
3584 return false;
3585 }
3586 const focused = this.getFocused();
3587 return !!focused && focused.id === id;
3588 }
3589 /**
3590 * Like {@link isActive}, but returns true if *any* window with the
3591 * given baseId is currently active.
3592 */
3593 isActiveByBaseId(baseId) {
3594 const focused = this.getFocused();
3595 if (!focused) {
3596 return false;
3597 }
3598 if (focused.state === "minimized") {
3599 return false;
3600 }
3601 const winDesktop = focused.config.desktopId || this._activeDesktopId;
3602 if (winDesktop !== this._activeDesktopId) {
3603 return false;
3604 }
3605 return (focused.config.baseId || focused.id) === baseId;
3606 }
3607 // ---- Virtual desktop delegations ----
3608 getDesktops() {
3609 return getDesktops(this);
3610 }
3611 getActiveDesktop() {
3612 return getActiveDesktop(this);
3613 }
3614 getActiveDesktopId() {
3615 return getActiveDesktopId(this);
3616 }
3617 createDesktop() {
3618 return createDesktop(this);
3619 }
3620 switchDesktop(id, opts) {
3621 switchDesktop(this, id, opts);
3622 }
3623 closeDesktop(id) {
3624 closeDesktop(this, id);
3625 }
3626 /**
3627 * Returns the "primary" desktop id — the one new sessions land on
3628 * and that batch operations like {@link closeAll} treat as the
3629 * survivor when an `onlyOnPrimary` mode is requested.
3630 *
3631 * Default: the first desktop in `getDesktops()`. Filterable via
3632 * `desktop-mode.primary-desktop-id` so downstream code that wants a
3633 * different convention (e.g. a pinned "Inbox" desktop) can override
3634 * without having to fork the manager.
3635 *
3636 * @since 0.5.0
3637 */
3638 getPrimaryDesktopId() {
3639 const all2 = this.getDesktops();
3640 const fallback = all2.length > 0 ? all2[0].id : "desktop-1";
3641 const filtered = applyFilters(
3642 HOOKS.PRIMARY_DESKTOP_ID,
3643 fallback,
3644 all2
3645 );
3646 if (typeof filtered !== "string" || filtered === "") {
3647 return fallback;
3648 }
3649 const exists = all2.some((d) => d.id === filtered);
3650 return exists ? filtered : fallback;
3651 }
3652 /**
3653 * Close every open window in batch.
3654 *
3655 * Hook chain:
3656 *
3657 * 1. `desktop-mode.windows.before-close-all` — action. Subscribers
3658 * can prepare for the wipe (cancel pending saves, dismiss
3659 * menus, etc.). Detail: `{ candidates: Window[] }`.
3660 *
3661 * 2. `desktop-mode.windows.close-all` — filter. Receives the
3662 * candidate Window list and returns the (possibly smaller) list
3663 * that will actually be closed. Plugins use this to PROTECT
3664 * specific windows — e.g. keep a draft post window open during
3665 * a "Close all" operation. Returning an empty array cancels
3666 * the close entirely.
3667 *
3668 * 3. Each surviving window's `close()` is called.
3669 *
3670 * 4. `desktop-mode.windows.after-close-all` — action. Detail:
3671 * `{ closed: number, skipped: Window[] }`.
3672 *
3673 * @since 0.5.0
3674 *
3675 * @param options Close options.
3676 * @param options.exceptIds Window ids to skip even before the filter runs.
3677 * @return Number of windows actually closed.
3678 */
3679 closeAll(options) {
3680 const exceptSet = new Set(options?.exceptIds ?? []);
3681 const initialCandidates = this._stack.filter(
3682 (w) => !exceptSet.has(w.id)
3683 );
3684 doAction(HOOKS.WINDOWS_BEFORE_CLOSE_ALL, { candidates: initialCandidates });
3685 const filtered = applyFilters(
3686 HOOKS.WINDOWS_CLOSE_ALL,
3687 initialCandidates
3688 );
3689 const finalList = Array.isArray(filtered) ? filtered : initialCandidates;
3690 const skipped = initialCandidates.filter((w) => !finalList.includes(w));
3691 let closed = 0;
3692 for (const win of finalList.slice()) {
3693 try {
3694 win.close();
3695 closed++;
3696 } catch (err) {
3697 if (typeof console !== "undefined") {
3698 console.error(
3699 "[desktop-mode] closeAll: window.close() threw for",
3700 win.id,
3701 err
3702 );
3703 }
3704 }
3705 }
3706 doAction(HOOKS.WINDOWS_AFTER_CLOSE_ALL, { closed, skipped });
3707 return closed;
3708 }
3709 /**
3710 * Minimize every currently-non-minimized window. Returns the
3711 * exact set that was minimized — i.e., excludes windows already
3712 * in the `'minimized'` state — so callers can pair the call with
3713 * a later {@link restoreFrom} that touches only the windows
3714 * they minimized.
3715 *
3716 * The "Show Desktop" gesture (clicking the wallpaper) routes
3717 * through this method (and {@link restoreFrom} on the second
3718 * click); plugin authors building expand/collapse UIs that
3719 * mimic the gesture should use these primitives instead of
3720 * rolling the loop themselves.
3721 *
3722 * @public
3723 * @since 0.6.0
3724 */
3725 minimizeAll() {
3726 const minimized = [];
3727 for (const win of this._stack.slice()) {
3728 const winDesktop = win.config.desktopId || this._activeDesktopId;
3729 if (winDesktop !== this._activeDesktopId) {
3730 continue;
3731 }
3732 if (win.state === "minimized") {
3733 continue;
3734 }
3735 try {
3736 win.minimize();
3737 minimized.push(win);
3738 } catch (err) {
3739 if (typeof console !== "undefined") {
3740 console.error(
3741 "[desktop-mode] minimizeAll: window.minimize() threw for",
3742 win.id,
3743 err
3744 );
3745 }
3746 }
3747 }
3748 return minimized;
3749 }
3750 /**
3751 * Restore the given window list — the symmetric counterpart to
3752 * {@link minimizeAll}. Skips windows that have since been
3753 * closed and windows the user manually un-minimized between
3754 * the minimize and the restore.
3755 *
3756 * Pass the array {@link minimizeAll} returned to restore
3757 * exactly what you minimized; pass any subset to restore
3758 * selectively.
3759 *
3760 * @public
3761 * @since 0.6.0
3762 */
3763 restoreFrom(windows) {
3764 if (!Array.isArray(windows)) {
3765 return;
3766 }
3767 const live = new Set(this._stack);
3768 for (const win of windows) {
3769 if (!live.has(win)) {
3770 continue;
3771 }
3772 const winDesktop = win.config.desktopId || this._activeDesktopId;
3773 if (winDesktop !== this._activeDesktopId) {
3774 continue;
3775 }
3776 if (win.state !== "minimized") {
3777 continue;
3778 }
3779 try {
3780 win.restore();
3781 } catch (err) {
3782 if (typeof console !== "undefined") {
3783 console.error(
3784 "[desktop-mode] restoreFrom: window.restore() threw for",
3785 win.id,
3786 err
3787 );
3788 }
3789 }
3790 }
3791 }
3792 /**
3793 * Toggle the "Show Desktop" state — if every live window is
3794 * already minimized, restore them all; otherwise minimize the
3795 * non-minimized cohort. Returns `true` when the new state is
3796 * "showing the desktop" (everything minimized after the call),
3797 * `false` when windows have just been restored.
3798 *
3799 * Mirrors the wallpaper-click gesture exactly, in one call.
3800 *
3801 * @public
3802 * @since 0.6.0
3803 */
3804 toggleShowDesktop() {
3805 const all2 = this._stack.filter(
3806 (w) => (w.config.desktopId || this._activeDesktopId) === this._activeDesktopId
3807 );
3808 if (all2.length === 0) {
3809 return false;
3810 }
3811 const allMinimized = all2.every((w) => w.state === "minimized");
3812 if (allMinimized) {
3813 for (const win of all2) {
3814 try {
3815 win.restore();
3816 } catch {
3817 }
3818 }
3819 return false;
3820 }
3821 this.minimizeAll();
3822 return true;
3823 }
3824 // ---- Arrange + snap delegations ----
3825 cascade() {
3826 cascade(this);
3827 }
3828 tile() {
3829 tile(this);
3830 }
3831 isSnapEnabled() {
3832 return this._snapEnabled;
3833 }
3834 setSnapEnabled(enabled) {
3835 setSnapEnabled(this, enabled);
3836 }
3837 getSnapConfig() {
3838 return getSnapConfig(this);
3839 }
3840 // ---- Overview delegations ----
3841 enterOverview() {
3842 enterOverview(this);
3843 }
3844 exitOverview(selected, maximize = false) {
3845 exitOverview(this, selected, maximize);
3846 }
3847 /**
3848 * Release resources this instance owns outside its own DOM
3849 * subtree: the document-level overview key handler and any
3850 * pending overview transition timers. Removing `desktop` from the
3851 * DOM does not reach either of those — a caller discarding a
3852 * manager instance (tests; a future SPA-style unmount) that skips
3853 * this leaves a real `setTimeout` to fire later and reach for
3854 * globals that may already be gone, plus a `keydown` listener on
3855 * `document` that keeps responding on behalf of a manager nothing
3856 * else references.
3857 *
3858 * Safe to call unconditionally — a no-op when overview was never
3859 * entered or was already cleanly exited.
3860 */
3861 destroy() {
3862 if (this._overviewActive) {
3863 exitOverview(this);
3864 }
3865 cancelOverviewTimers(this);
3866 }
3867 /**
3868 * Snapshot every open window's current geometry + state.
3869 *
3870 * Returns a plain array of `{ windowId, rect, state, element }`
3871 * entries — one per window in the stack, regardless of which
3872 * virtual desktop owns it. Rect coordinates are in desktop-area
3873 * space (the same coordinate space the windows themselves use
3874 * inline-style left/top); `state` is the live `WindowState`, and
3875 * `element` is the window's outer DOM node.
3876 *
3877 * Intended for wallpaper / overlay plugins that used to scrape
3878 * `document.querySelectorAll('.desktop-mode-window')` + read the
3879 * `--minimized` / `--maximized` modifier classes by name. The
3880 * accessor decouples plugin code from the shell's CSS class
3881 * naming, so a future refactor of modifier prefixes is not an
3882 * ecosystem break.
3883 *
3884 * The array contains every window in the stack — callers filter
3885 * on `state` if they want only "actually visible" (typically
3886 * `state !== 'minimized'`). Minimized windows are included so
3887 * plugins that care about the "will be restored to X geometry"
3888 * case still have the data; filtering them out would be a
3889 * subtraction the caller can do but the provider can't reverse.
3890 *
3891 * Order matches the internal z-stack: earliest-opened first,
3892 * focused window last.
3893 */
3894 getVisibleRects() {
3895 return this._stack.map((w) => {
3896 const snap = w.getSnapshot();
3897 return {
3898 windowId: w.id,
3899 rect: {
3900 x: snap.x,
3901 y: snap.y,
3902 width: snap.width,
3903 height: snap.height
3904 },
3905 state: snap.state,
3906 element: w.element
3907 };
3908 });
3909 }
3910 /**
3911 * Serialize the current window stack for session persistence.
3912 *
3913 * Order in the returned `windows` array mirrors z-order (earliest
3914 * opened / lowest-z first, focused last) so restoring preserves
3915 * the stacking the user left behind.
3916 */
3917 snapshot() {
3918 const focused = this.getFocused();
3919 const persistable = this._stack.filter((w) => !w.config.native);
3920 const windows = persistable.map((w) => {
3921 const snap = w.getSnapshot();
3922 const externalTabs = w.getExternalTabsSnapshot();
3923 return {
3924 id: w.id,
3925 baseId: w.config.baseId || w.id,
3926 desktopId: w.config.desktopId || this._activeDesktopId,
3927 url: w.getCurrentUrl(),
3928 title: w.config.title,
3929 icon: w.config.icon,
3930 state: snap.state,
3931 x: snap.x,
3932 y: snap.y,
3933 width: snap.width,
3934 height: snap.height,
3935 ...externalTabs.length > 0 ? { externalTabs } : {}
3936 };
3937 });
3938 const focusedId = focused && !focused.config.native ? focused.id : "";
3939 return {
3940 windows,
3941 desktops: this.getDesktops(),
3942 activeDesktop: this._activeDesktopId,
3943 focused: focusedId,
3944 updated: Math.floor(Date.now() / 1e3)
3945 };
3946 }
3947 seedDesktops(desktops, activeDesktopId) {
3948 seedDesktops(this, desktops, activeDesktopId);
3949 }
3950 }
3951 function cycleableWindows(mgr) {
3952 const activeDesktopId = mgr.getActiveDesktopId();
3953 const domOrder = Array.from(mgr._desktop.children);
3954 return mgr.getAll().filter((w) => {
3955 const winDesktop = w.config.desktopId || activeDesktopId;
3956 return winDesktop === activeDesktopId;
3957 }).sort(
3958 (a, b) => domOrder.indexOf(a.element) - domOrder.indexOf(b.element)
3959 );
3960 }
3961 function cycleFocus(mgr, direction) {
3962 if (mgr._overviewActive) {
3963 return;
3964 }
3965 const list2 = cycleableWindows(mgr);
3966 if (list2.length < 2) {
3967 return;
3968 }
3969 const focused = mgr.getFocused();
3970 const currentIdx = focused ? list2.indexOf(focused) : -1;
3971 const step = direction === "next" ? 1 : -1;
3972 const nextIdx = (currentIdx + step + list2.length) % list2.length;
3973 const target2 = list2[nextIdx];
3974 if (target2.state === "minimized") {
3975 target2.restore();
3976 } else {
3977 mgr.focus(target2);
3978 }
3979 }
3980 let installed$3 = false;
3981 function isTextEntryFocus(doc) {
3982 let el = doc.activeElement;
3983 while (el && el.shadowRoot && el.shadowRoot.activeElement) {
3984 el = el.shadowRoot.activeElement;
3985 }
3986 if (!el) {
3987 return false;
3988 }
3989 if (el instanceof HTMLIFrameElement) {
3990 return true;
3991 }
3992 if (el instanceof HTMLTextAreaElement) {
3993 return true;
3994 }
3995 if (el instanceof HTMLInputElement) {
3996 const textTypes = /* @__PURE__ */ new Set([
3997 "text",
3998 "search",
3999 "url",
4000 "email",
4001 "password",
4002 "tel",
4003 "number",
4004 "date",
4005 "datetime-local",
4006 "month",
4007 "week",
4008 "time"
4009 ]);
4010 return textTypes.has(el.type);
4011 }
4012 if (el instanceof HTMLElement && el.isContentEditable === true) {
4013 return true;
4014 }
4015 const ce = el.getAttribute("contenteditable");
4016 return ce !== null && ce !== "false";
4017 }
4018 function installWindowSwitcherShortcut(mgr) {
4019 if (installed$3) {
4020 return;
4021 }
4022 installed$3 = true;
4023 document.addEventListener(
4024 "keydown",
4025 (e) => {
4026 if (e.ctrlKey || e.metaKey || e.altKey) {
4027 return;
4028 }
4029 if (e.code !== "Backquote") {
4030 return;
4031 }
4032 if (isTextEntryFocus(document)) {
4033 return;
4034 }
4035 e.preventDefault();
4036 cycleFocus(mgr, e.shiftKey ? "prev" : "next");
4037 },
4038 true
4039 );
4040 const origin = window.location.origin;
4041 window.addEventListener("message", (e) => {
4042 if (e.origin !== origin) {
4043 return;
4044 }
4045 const data = e.data;
4046 if (!data || data.type !== "desktop-mode-window-switch") {
4047 return;
4048 }
4049 cycleFocus(mgr, data.direction === "prev" ? "prev" : "next");
4050 });
4051 }
4052 function switchToAdjacentDesktop(mgr, direction) {
4053 const desktops = mgr.getDesktops();
4054 if (desktops.length < 2) {
4055 return false;
4056 }
4057 const activeId = mgr.getActiveDesktopId();
4058 const idx = desktops.findIndex((d) => d.id === activeId);
4059 if (idx === -1) {
4060 return false;
4061 }
4062 const step = direction === "next" ? 1 : -1;
4063 const targetIdx = (idx + step + desktops.length) % desktops.length;
4064 if (targetIdx === idx) {
4065 return false;
4066 }
4067 mgr.switchDesktop(desktops[targetIdx].id, { direction });
4068 return true;
4069 }
4070 function cycleOverviewCursor(mgr, direction) {
4071 if (!mgr._overviewActive) {
4072 return false;
4073 }
4074 const desktops = mgr.getDesktops();
4075 const cycleLength = desktops.length + 1;
4076 const ADD_INDEX = desktops.length;
4077 const currentIdx = mgr._overviewAddTileFocused ? ADD_INDEX : desktops.findIndex((d) => d.id === mgr.getActiveDesktopId());
4078 if (currentIdx === -1) {
4079 return false;
4080 }
4081 const step = direction === "next" ? 1 : -1;
4082 const targetIdx = (currentIdx + step + cycleLength) % cycleLength;
4083 if (targetIdx === currentIdx) {
4084 return false;
4085 }
4086 if (targetIdx === ADD_INDEX) {
4087 mgr._overviewAddTileFocused = true;
4088 refreshOverviewTopBar(mgr);
4089 return true;
4090 }
4091 mgr._overviewAddTileFocused = false;
4092 mgr.switchDesktop(desktops[targetIdx].id, { direction });
4093 return true;
4094 }
4095 function toggleOverview(mgr) {
4096 if (mgr._overviewActive) {
4097 mgr.exitOverview();
4098 } else {
4099 mgr.enterOverview();
4100 }
4101 return true;
4102 }
4103 function toggleShowDesktop(mgr) {
4104 if (mgr._overviewActive) {
4105 return false;
4106 }
4107 if (mgr.getAll().length === 0) {
4108 return false;
4109 }
4110 mgr.toggleShowDesktop();
4111 return true;
4112 }
4113 function exitOverviewIfActive(mgr) {
4114 if (!mgr._overviewActive) {
4115 return false;
4116 }
4117 mgr.exitOverview();
4118 return true;
4119 }
4120 function isShowDesktopActive(mgr) {
4121 const all2 = mgr.getAll();
4122 if (all2.length === 0) {
4123 return false;
4124 }
4125 return all2.every((w) => w.state === "minimized");
4126 }
4127 function exitShowDesktopIfActive(mgr) {
4128 if (!isShowDesktopActive(mgr)) {
4129 return false;
4130 }
4131 mgr.toggleShowDesktop();
4132 return true;
4133 }
4134 let installed$2 = false;
4135 function installDesktopArrowShortcuts(mgr) {
4136 if (installed$2) {
4137 return;
4138 }
4139 installed$2 = true;
4140 document.addEventListener(
4141 "keydown",
4142 (e) => {
4143 if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) {
4144 return;
4145 }
4146 if (e.code !== "ArrowLeft" && e.code !== "ArrowRight" && e.code !== "ArrowUp" && e.code !== "ArrowDown") {
4147 return;
4148 }
4149 if (isTextEntryFocus(document)) {
4150 return;
4151 }
4152 let handled = false;
4153 switch (e.code) {
4154 case "ArrowLeft":
4155 handled = mgr._overviewActive ? cycleOverviewCursor(mgr, "prev") : switchToAdjacentDesktop(mgr, "prev");
4156 break;
4157 case "ArrowRight":
4158 handled = mgr._overviewActive ? cycleOverviewCursor(mgr, "next") : switchToAdjacentDesktop(mgr, "next");
4159 break;
4160 case "ArrowUp":
4161 handled = exitOverviewIfActive(mgr) || exitShowDesktopIfActive(mgr) || toggleOverview(mgr);
4162 break;
4163 case "ArrowDown":
4164 handled = exitOverviewIfActive(mgr) || toggleShowDesktop(mgr);
4165 break;
4166 }
4167 if (handled) {
4168 e.preventDefault();
4169 }
4170 },
4171 true
4172 );
4173 }
4174 const _parentSubs = /* @__PURE__ */ new Map();
4175 const _nativeSubs = /* @__PURE__ */ new Map();
4176 function bucket(root, windowId, channel, create) {
4177 let perWindow = root.get(windowId);
4178 if (!perWindow) {
4179 if (!create) {
4180 return void 0;
4181 }
4182 perWindow = /* @__PURE__ */ new Map();
4183 root.set(windowId, perWindow);
4184 }
4185 let bucketSet = perWindow.get(channel);
4186 if (!bucketSet) {
4187 if (!create) {
4188 return void 0;
4189 }
4190 bucketSet = /* @__PURE__ */ new Set();
4191 perWindow.set(channel, bucketSet);
4192 }
4193 return bucketSet;
4194 }
4195 function dispatch(root, windowId, channel, payload) {
4196 const meta = { channel, windowId };
4197 const exact = bucket(root, windowId, channel, false);
4198 if (exact) {
4199 for (const cb of Array.from(exact)) {
4200 try {
4201 cb(payload, meta);
4202 } catch (err) {
4203 if (typeof console !== "undefined") {
4204 console.error(
4205 `[desktop-mode] window-channel subscriber for "${channel}" threw:`,
4206 err
4207 );
4208 }
4209 }
4210 }
4211 }
4212 const wildcard = bucket(root, windowId, "*", false);
4213 if (wildcard) {
4214 for (const cb of Array.from(wildcard)) {
4215 try {
4216 cb(payload, meta);
4217 } catch (err) {
4218 if (typeof console !== "undefined") {
4219 console.error(
4220 `[desktop-mode] window-channel wildcard subscriber for "${windowId}" threw:`,
4221 err
4222 );
4223 }
4224 }
4225 }
4226 }
4227 }
4228 function addParentSubscriber(windowId, channel, cb) {
4229 const set = bucket(_parentSubs, windowId, channel, true);
4230 set.add(cb);
4231 let removed = false;
4232 return () => {
4233 if (removed) {
4234 return;
4235 }
4236 removed = true;
4237 set.delete(cb);
4238 };
4239 }
4240 function dispatchFromWindow(windowId, channel, payload) {
4241 dispatch(_parentSubs, windowId, channel, payload);
4242 }
4243 function dispatchToNative(windowId, channel, payload) {
4244 dispatch(_nativeSubs, windowId, channel, payload);
4245 }
4246 const _readyWindows = /* @__PURE__ */ new Set();
4247 const _loadingWindows = /* @__PURE__ */ new Set();
4248 const _pendingSends = /* @__PURE__ */ new Map();
4249 function markWindowContentReady(windowId) {
4250 if (!_readyWindows.has(windowId)) {
4251 _readyWindows.add(windowId);
4252 const queued = _pendingSends.get(windowId);
4253 if (queued) {
4254 _pendingSends.delete(windowId);
4255 for (const m of queued) {
4256 try {
4257 m.flush();
4258 } catch (err) {
4259 if (typeof console !== "undefined") {
4260 console.error(
4261 `[desktop-mode] flushing queued window-send for "${m.channel}" threw:`,
4262 err
4263 );
4264 }
4265 }
4266 }
4267 }
4268 }
4269 if (_loadingWindows.delete(windowId)) {
4270 doAction(HOOKS.WINDOW_CONTENT_LOADED, { windowId });
4271 if (typeof document !== "undefined") {
4272 document.dispatchEvent(
4273 new CustomEvent("desktop-mode-window-content-loaded", {
4274 detail: { windowId }
4275 })
4276 );
4277 }
4278 }
4279 }
4280 const WINDOW_CONFIG_KEY = Symbol.for("desktop-mode/window-config");
4281 function getWindowConfigFromElement(el) {
4282 return el[WINDOW_CONFIG_KEY];
4283 }
4284 function buildDefaultLoadingOverlay() {
4285 const overlay = document.createElement("div");
4286 overlay.className = "desktop-mode-window__loading";
4287 overlay.setAttribute("aria-hidden", "true");
4288 const spinner = document.createElement("wpd-spinner");
4289 spinner.setAttribute("preset", "classic");
4290 spinner.setAttribute("size", "clamp(96px, 14vw, 192px)");
4291 spinner.setAttribute("label", __("Loading window content"));
4292 overlay.appendChild(spinner);
4293 return overlay;
4294 }
4295 function createLoadingOverlay(config) {
4296 let overlay = buildDefaultLoadingOverlay();
4297 const ctx = { windowId: config.id, config };
4298 if (typeof config.loading?.render === "function") {
4299 try {
4300 config.loading.render(overlay, ctx);
4301 } catch (err) {
4302 if (typeof console !== "undefined") {
4303 console.error(
4304 `[desktop-mode] loading.render threw for "${config.id}":`,
4305 err
4306 );
4307 }
4308 }
4309 }
4310 try {
4311 const filtered = applyFilters(
4312 HOOKS.WINDOW_LOADING_OVERLAY,
4313 overlay,
4314 ctx
4315 );
4316 if (filtered instanceof HTMLElement) {
4317 overlay = filtered;
4318 }
4319 } catch (err) {
4320 if (typeof console !== "undefined") {
4321 console.error(
4322 `[desktop-mode] WINDOW_LOADING_OVERLAY filter threw for "${config.id}":`,
4323 err
4324 );
4325 }
4326 }
4327 if (overlay && !overlay.classList.contains("desktop-mode-window__loading")) {
4328 overlay.classList.add("desktop-mode-window__loading");
4329 }
4330 return overlay;
4331 }
4332 function removeLoadingOverlay(windowEl) {
4333 const overlay = windowEl.querySelector(":scope .desktop-mode-window__loading");
4334 overlay?.remove();
4335 }
4336 function ensureLoadingOverlay(windowEl) {
4337 const body = windowEl.querySelector(
4338 ":scope .desktop-mode-window__body"
4339 );
4340 if (!body) {
4341 return;
4342 }
4343 const existing = body.querySelector(":scope .desktop-mode-window__loading");
4344 if (existing) {
4345 return;
4346 }
4347 const config = getWindowConfigFromElement(windowEl);
4348 body.appendChild(config ? createLoadingOverlay(config) : buildDefaultLoadingOverlay());
4349 }
4350 const FADE_OUT_MS$1 = 250;
4351 let _installed$4 = false;
4352 function findWindowElement(windowId) {
4353 if (!windowId) {
4354 return null;
4355 }
4356 return document.getElementById(`wp-window-${windowId}`);
4357 }
4358 function installWindowLoadingTransitions() {
4359 if (_installed$4) {
4360 return;
4361 }
4362 _installed$4 = true;
4363 _installSubscriptions();
4364 }
4365 function _installSubscriptions() {
4366 addAction(
4367 HOOKS.WINDOW_CONTENT_LOADING,
4368 "desktop-mode/window-loading-enter",
4369 (e) => {
4370 const el = findWindowElement(e?.windowId ?? "");
4371 if (!el) {
4372 return;
4373 }
4374 const body = el.querySelector(
4375 ":scope .desktop-mode-window__body"
4376 );
4377 if (!body) {
4378 return;
4379 }
4380 body.classList.add("desktop-mode-window__body--loading");
4381 ensureLoadingOverlay(el);
4382 }
4383 );
4384 addAction(
4385 HOOKS.WINDOW_CONTENT_LOADED,
4386 "desktop-mode/window-loading-exit",
4387 (e) => {
4388 const el = findWindowElement(e?.windowId ?? "");
4389 if (!el) {
4390 return;
4391 }
4392 const body = el.querySelector(
4393 ":scope .desktop-mode-window__body"
4394 );
4395 if (!body) {
4396 return;
4397 }
4398 body.classList.remove("desktop-mode-window__body--loading");
4399 window.setTimeout(() => {
4400 if (!body.classList.contains("desktop-mode-window__body--loading")) {
4401 removeLoadingOverlay(el);
4402 }
4403 }, FADE_OUT_MS$1);
4404 }
4405 );
4406 addAction(
4407 HOOKS.INIT,
4408 "desktop-mode/loading-overlay-init-sweep",
4409 () => {
4410 queueMicrotask(() => repaintLoadingOverlays());
4411 }
4412 );
4413 }
4414 function repaintLoadingOverlays() {
4415 const bodies = document.querySelectorAll(
4416 ".desktop-mode-window__body--loading"
4417 );
4418 bodies.forEach((body) => {
4419 const windowEl = body.closest(".desktop-mode-window");
4420 if (!windowEl) {
4421 return;
4422 }
4423 body.querySelector(":scope .desktop-mode-window__loading")?.remove();
4424 ensureLoadingOverlay(windowEl);
4425 });
4426 }
4427 const SHARED_STORES_SLOT = "__desktopModeSharedStores";
4428 function resolveSlot() {
4429 const w = window;
4430 let slot = w[SHARED_STORES_SLOT];
4431 if (!slot) {
4432 slot = /* @__PURE__ */ new Map();
4433 w[SHARED_STORES_SLOT] = slot;
4434 }
4435 return slot;
4436 }
4437 function createSharedStore(key, initialState) {
4438 const slot = resolveSlot();
4439 let record = slot.get(key);
4440 if (!record) {
4441 record = {
4442 state: initialState(),
4443 listeners: /* @__PURE__ */ new Set(),
4444 rebuild: initialState
4445 };
4446 slot.set(key, record);
4447 }
4448 const handle = {
4449 // `record.state` is the live reference. The getter on the
4450 // `state` field reads the latest value even if `reset()`
4451 // reassigned it to a fresh object.
4452 get state() {
4453 return record.state;
4454 },
4455 set state(next) {
4456 record.state = next;
4457 },
4458 getState() {
4459 return record.state;
4460 },
4461 notify() {
4462 for (const cb of Array.from(record.listeners)) {
4463 try {
4464 cb(record.state);
4465 } catch (err) {
4466 console.error(
4467 `[desktop-mode/shared-store:${key}] subscriber threw:`,
4468 err
4469 );
4470 }
4471 }
4472 },
4473 subscribe(cb) {
4474 record.listeners.add(cb);
4475 return () => {
4476 record.listeners.delete(cb);
4477 };
4478 },
4479 setState(patch) {
4480 const cur = record.state;
4481 if (typeof cur !== "object" || cur === null) {
4482 console.warn(
4483 `[desktop-mode/shared-store:${key}] setState called on a primitive store; use the state setter instead.`
4484 );
4485 return;
4486 }
4487 Object.assign(cur, patch);
4488 handle.notify();
4489 },
4490 reset() {
4491 const fresh = record.rebuild();
4492 const cur = record.state;
4493 if (typeof cur === "object" && cur !== null && typeof fresh === "object" && fresh !== null) {
4494 const target2 = cur;
4495 for (const k of Object.keys(target2)) {
4496 delete target2[k];
4497 }
4498 Object.assign(target2, fresh);
4499 } else {
4500 record.state = fresh;
4501 }
4502 record.listeners.clear();
4503 }
4504 };
4505 return handle;
4506 }
4507 const remapStore = createSharedStore(
4508 "desktop-mode/native-url-remap",
4509 () => ({ remaps: [], deps: null })
4510 );
4511 function bindNativeUrlRemap(bound) {
4512 remapStore.state.deps = bound;
4513 }
4514 function registerNativeUrlRemap(entry) {
4515 if (!entry || typeof entry.id !== "string" || entry.id.trim() === "") {
4516 return () => {
4517 };
4518 }
4519 if (typeof entry.nativeWindowId !== "string" || entry.nativeWindowId === "") {
4520 return () => {
4521 };
4522 }
4523 if (typeof entry.matches !== "function") {
4524 return () => {
4525 };
4526 }
4527 const remaps = remapStore.state.remaps;
4528 const existingIdx = remaps.findIndex((r) => r.id === entry.id);
4529 if (existingIdx >= 0) {
4530 remaps.splice(existingIdx, 1);
4531 }
4532 remaps.push(entry);
4533 return () => unregisterNativeUrlRemap(entry.id);
4534 }
4535 function unregisterNativeUrlRemap(id) {
4536 const remaps = remapStore.state.remaps;
4537 const i = remaps.findIndex((r) => r.id === id);
4538 if (i >= 0) {
4539 remaps.splice(i, 1);
4540 }
4541 }
4542 function resolveNativeUrlRemap(url) {
4543 const { deps: deps2, remaps } = remapStore.state;
4544 if (!deps2 || !url) {
4545 return null;
4546 }
4547 let parsed;
4548 try {
4549 parsed = new URL(url, deps2.adminUrl);
4550 } catch {
4551 return null;
4552 }
4553 const snapshot = deps2.getSnapshot();
4554 for (const entry of remaps) {
4555 if (!entry.matches(url, parsed)) {
4556 continue;
4557 }
4558 if (entry.enabled && !entry.enabled(snapshot)) {
4559 continue;
4560 }
4561 return entry.nativeWindowId;
4562 }
4563 return null;
4564 }
4565 function tryNativeUrlRemap(url) {
4566 const { deps: deps2, remaps } = remapStore.state;
4567 if (!deps2 || !url) {
4568 return false;
4569 }
4570 let parsed;
4571 try {
4572 parsed = new URL(url, deps2.adminUrl);
4573 } catch {
4574 return false;
4575 }
4576 const snapshot = deps2.getSnapshot();
4577 for (const entry of remaps) {
4578 if (!entry.matches(url, parsed)) {
4579 continue;
4580 }
4581 if (entry.enabled && !entry.enabled(snapshot)) {
4582 continue;
4583 }
4584 if (entry.onMatch) {
4585 try {
4586 entry.onMatch(url, parsed);
4587 } catch (err) {
4588 console.warn(
4589 `[desktop-mode] URL remap onMatch hook threw for "${entry.id}":`,
4590 err
4591 );
4592 }
4593 }
4594 if (deps2.openById(entry.nativeWindowId)) {
4595 return true;
4596 }
4597 }
4598 return false;
4599 }
4600 const HOOK_PREFIX = "desktop-mode.activity.";
4601 function hookName(channel) {
4602 return `${HOOK_PREFIX}${String(channel)}`;
4603 }
4604 let subscribeSeq = 0;
4605 const activity = {
4606 publish(channel, payload) {
4607 doAction(hookName(channel), payload);
4608 },
4609 subscribe(channel, cb) {
4610 const ns = `desktop-mode/activity-sub/${++subscribeSeq}`;
4611 const hook = hookName(channel);
4612 addAction(
4613 hook,
4614 ns,
4615 (payload) => cb(payload)
4616 );
4617 let removed = false;
4618 return () => {
4619 if (removed) {
4620 return;
4621 }
4622 removed = true;
4623 removeAction(hook, ns);
4624 };
4625 },
4626 filter(channel, value, ...args) {
4627 return applyFilters(hookName(channel), value, ...args);
4628 }
4629 };
4630 const DEFAULT_DURATION_MS = 4e3;
4631 const FADE_OUT_MS = 200;
4632 function showToast(options) {
4633 const intent = activity.filter(
4634 "desktop-mode/toast-requested",
4635 { ...options }
4636 );
4637 if (!intent || intent.cancel === true) {
4638 return () => void 0;
4639 }
4640 let dismissRequested = false;
4641 let realDismiss = null;
4642 openWithShellOverlays(
4643 () => !dismissRequested,
4644 () => {
4645 realDismiss = renderToast(intent);
4646 }
4647 );
4648 return () => {
4649 dismissRequested = true;
4650 if (realDismiss) {
4651 realDismiss();
4652 }
4653 };
4654 }
4655 function renderToast(intent) {
4656 const container = ensureContainer();
4657 const toast = document.createElement("wpd-toast");
4658 toast.textContent = intent.message;
4659 if (intent.action) {
4660 toast.setAttribute("action", intent.action.label);
4661 toast.addEventListener("wpd-toast-action", () => {
4662 intent.action?.onClick();
4663 dismiss();
4664 });
4665 }
4666 if (intent.dismissible) {
4667 toast.setAttribute("dismissible", "");
4668 toast.addEventListener("wpd-toast-dismiss", () => {
4669 intent.onDismiss?.();
4670 dismiss();
4671 });
4672 }
4673 container.appendChild(toast);
4674 let dismissed = false;
4675 let dismissTimer = null;
4676 const dismiss = () => {
4677 if (dismissed) {
4678 return;
4679 }
4680 dismissed = true;
4681 if (dismissTimer !== null) {
4682 window.clearTimeout(dismissTimer);
4683 dismissTimer = null;
4684 }
4685 toast.setAttribute("state", "out");
4686 window.setTimeout(() => {
4687 toast.remove();
4688 }, FADE_OUT_MS);
4689 };
4690 requestAnimationFrame(() => {
4691 toast.setAttribute("state", "in");
4692 });
4693 if (!intent.persistent) {
4694 dismissTimer = window.setTimeout(
4695 dismiss,
4696 intent.duration ?? DEFAULT_DURATION_MS
4697 );
4698 }
4699 activity.publish("desktop-mode/toast-shown", { ...intent });
4700 return dismiss;
4701 }
4702 function ensureContainer() {
4703 const existing = document.querySelector(
4704 "wpd-toast-container"
4705 );
4706 if (existing) {
4707 return existing;
4708 }
4709 const el = document.createElement("wpd-toast-container");
4710 document.body.appendChild(el);
4711 return el;
4712 }
4713 const store$h = createSharedStore(
4714 "desktop-mode/destructive-admin-actions",
4715 () => ({ entries: [] })
4716 );
4717 function registerDestructiveAdminAction(entry) {
4718 if (!entry || typeof entry.id !== "string" || entry.id.trim() === "") {
4719 return () => {
4720 };
4721 }
4722 if (typeof entry.matches !== "function") {
4723 return () => {
4724 };
4725 }
4726 const entries = store$h.state.entries;
4727 const idx = entries.findIndex((e) => e.id === entry.id);
4728 if (idx >= 0) {
4729 entries.splice(idx, 1);
4730 }
4731 entries.push(entry);
4732 return () => unregisterDestructiveAdminAction(entry.id);
4733 }
4734 function unregisterDestructiveAdminAction(id) {
4735 const entries = store$h.state.entries;
4736 const idx = entries.findIndex((e) => e.id === id);
4737 if (idx >= 0) {
4738 entries.splice(idx, 1);
4739 }
4740 }
4741 function listDestructiveAdminActions() {
4742 return store$h.state.entries.slice();
4743 }
4744 function collectRegistrationErrors(def, checks) {
4745 if (!def || typeof def !== "object") {
4746 return ["def (not an object)"];
4747 }
4748 const d = def;
4749 const errors = [];
4750 for (const check of checks) {
4751 if (!check.valid(d)) {
4752 errors.push(`${check.field} (${check.message})`);
4753 }
4754 }
4755 return errors;
4756 }
4757 class RegistrationError extends Error {
4758 constructor(kind, errors, def) {
4759 super(
4760 `[desktop-mode] ${kind} registration rejected — fields: ` + errors.join(", ") + "."
4761 );
4762 this.name = "RegistrationError";
4763 this.kind = kind;
4764 this.errors = errors;
4765 this.def = def;
4766 }
4767 }
4768 function throwOnRegistrationErrors(kind, errors, def) {
4769 if (errors.length === 0) {
4770 return;
4771 }
4772 throw new RegistrationError(kind, errors, def);
4773 }
4774 function logRegistrationErrors(kind, errors, def) {
4775 if (typeof console === "undefined") {
4776 return;
4777 }
4778 console.warn(
4779 `[desktop-mode] ${kind} registration rejected — fields: ` + errors.join(", ") + ".",
4780 def
4781 );
4782 }
4783 const MAX_LINKS = 32;
4784 const CONTENT_TYPE_ID = /^[a-z0-9_/-]+$/;
4785 const store$g = createSharedStore(
4786 "desktop-mode/window-links",
4787 () => ({
4788 contentByWindow: /* @__PURE__ */ new Map(),
4789 focusSeq: /* @__PURE__ */ new Map(),
4790 seq: 0,
4791 listeners: /* @__PURE__ */ new Set(),
4792 lastGroupsSignature: "",
4793 manager: null,
4794 started: false
4795 })
4796 );
4797 function keyOf(ref) {
4798 return `${ref.type}:${ref.id}`;
4799 }
4800 function rootKeyOf(ref) {
4801 return ref.root ? keyOf(ref.root) : keyOf(ref);
4802 }
4803 function validateRef(ref) {
4804 const isValidId = (v) => typeof v === "number" && Number.isFinite(v) || typeof v === "string" && v.trim() !== "";
4805 const isValidType = (v) => typeof v === "string" && CONTENT_TYPE_ID.test(v.trim().toLowerCase());
4806 return collectRegistrationErrors(ref, [
4807 {
4808 field: "type",
4809 valid: (r) => isValidType(r.type),
4810 message: "must match /^[a-z0-9_/-]+$/ — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-type"
4811 },
4812 {
4813 field: "id",
4814 valid: (r) => isValidId(r.id),
4815 message: "must be a finite number or non-empty string"
4816 },
4817 {
4818 field: "root",
4819 valid: (r) => r.root === void 0 || !!r.root && typeof r.root === "object" && isValidType(r.root.type) && isValidId(r.root.id),
4820 message: "when present, must be { type, id } with the same shapes as the ref itself"
4821 },
4822 {
4823 field: "links",
4824 valid: (r) => r.links === void 0 || Array.isArray(r.links) && r.links.every(
4825 (l) => !!l && typeof l === "object" && isValidType(l.type) && isValidId(l.id) && (l.rel === void 0 || l.rel === "references" || l.rel === "child")
4826 ),
4827 message: "when present, must be an array of { type, id, rel?: 'references'|'child' } entries"
4828 }
4829 ]);
4830 }
4831 function normalizeRef(ref, source) {
4832 const next = {
4833 type: ref.type.trim().toLowerCase(),
4834 id: ref.id,
4835 source
4836 };
4837 if (ref.root) {
4838 next.root = {
4839 type: ref.root.type.trim().toLowerCase(),
4840 id: ref.root.id
4841 };
4842 }
4843 if (Array.isArray(ref.links) && ref.links.length > 0) {
4844 next.links = ref.links.slice(0, MAX_LINKS).map((l) => {
4845 const entry = {
4846 type: l.type.trim().toLowerCase(),
4847 id: l.id
4848 };
4849 if (l.rel === "child") {
4850 entry.rel = "child";
4851 }
4852 return entry;
4853 });
4854 }
4855 if (typeof ref.label === "string" && ref.label !== "") {
4856 next.label = ref.label;
4857 }
4858 return next;
4859 }
4860 function refSignature(ref) {
4861 if (!ref) {
4862 return "";
4863 }
4864 return [
4865 keyOf(ref),
4866 rootKeyOf(ref),
4867 ...(ref.links ?? []).map(
4868 (l) => keyOf(l) + (l.rel === "child" ? "!child" : "")
4869 )
4870 ].join("|");
4871 }
4872 function setWindowContent(windowId, ref, opts = {}) {
4873 const source = opts.source ?? "api";
4874 if (typeof windowId !== "string" || windowId === "") {
4875 throwOnRegistrationErrors(
4876 "WindowContentRef",
4877 ["windowId (must be a non-empty string)"],
4878 ref
4879 );
4880 return;
4881 }
4882 let next = null;
4883 if (ref !== null && ref !== void 0) {
4884 const errors = validateRef(ref);
4885 if (errors.length > 0) {
4886 if (source === "api") {
4887 throwOnRegistrationErrors("WindowContentRef", errors, ref);
4888 }
4889 logRegistrationErrors("WindowContentRef", errors, ref);
4890 return;
4891 }
4892 next = normalizeRef(ref, source);
4893 }
4894 next = applyFilters(
4895 HOOKS.WINDOW_LINKS_CONTENT,
4896 next,
4897 { windowId, source }
4898 );
4899 if (next !== null && (!next || validateRef(next).length > 0)) {
4900 logRegistrationErrors(
4901 "WindowContentRef",
4902 ["filter (desktop-mode.window-links.content returned an invalid ref)"],
4903 next
4904 );
4905 return;
4906 }
4907 const previous = store$g.state.contentByWindow.get(windowId) ?? null;
4908 if (next === null && previous === null) {
4909 return;
4910 }
4911 if (next !== null && previous !== null && refSignature(next) === refSignature(previous) && next.label === previous.label) {
4912 return;
4913 }
4914 if (next === null) {
4915 store$g.state.contentByWindow.delete(windowId);
4916 } else {
4917 store$g.state.contentByWindow.set(windowId, next);
4918 }
4919 const changedDetail = { windowId, content: next, previous, source };
4920 document.dispatchEvent(
4921 new CustomEvent("desktop-mode-window-content-changed", {
4922 detail: changedDetail
4923 })
4924 );
4925 doAction(HOOKS.WINDOW_CONTENT_CHANGED, changedDetail);
4926 broadcastGroupsIfChanged();
4927 notify$g();
4928 }
4929 function getWindowContent(windowId) {
4930 return store$g.state.contentByWindow.get(windowId);
4931 }
4932 function listWindowLinkGroups() {
4933 const byKey = /* @__PURE__ */ new Map();
4934 for (const [windowId, ref] of store$g.state.contentByWindow) {
4935 const groupKey = rootKeyOf(ref);
4936 let group = byKey.get(groupKey);
4937 if (!group) {
4938 group = {
4939 key: groupKey,
4940 root: ref.root ? { ...ref.root } : { type: ref.type, id: ref.id },
4941 rootWindowIds: [],
4942 children: []
4943 };
4944 byKey.set(groupKey, group);
4945 }
4946 if (ref.root) {
4947 group.children.push({ windowId, content: ref });
4948 } else {
4949 group.rootWindowIds.push(windowId);
4950 }
4951 }
4952 const seq = store$g.state.focusSeq;
4953 for (const group of byKey.values()) {
4954 group.rootWindowIds.sort(
4955 (a, b) => (seq.get(b) ?? 0) - (seq.get(a) ?? 0)
4956 );
4957 }
4958 const copy = Array.from(byKey.values());
4959 const filtered = applyFilters(
4960 HOOKS.WINDOW_LINK_GROUPS,
4961 copy
4962 );
4963 if (!Array.isArray(filtered)) {
4964 if (typeof console !== "undefined") {
4965 console.warn(
4966 "[desktop-mode] `desktop-mode.window-links.groups` filter returned a non-array; falling back to computed groups."
4967 );
4968 }
4969 return copy;
4970 }
4971 return filtered;
4972 }
4973 function getWindowLinkGroup(windowId) {
4974 return listWindowLinkGroups().find(
4975 (g) => g.rootWindowIds.includes(windowId) || g.children.some((c) => c.windowId === windowId)
4976 );
4977 }
4978 function getRelatedWindowIds(windowId) {
4979 const related = /* @__PURE__ */ new Set();
4980 const group = getWindowLinkGroup(windowId);
4981 if (group) {
4982 for (const id of [
4983 ...group.rootWindowIds,
4984 ...group.children.map((c) => c.windowId)
4985 ]) {
4986 related.add(id);
4987 }
4988 }
4989 for (const edge of listWindowLinkEdges()) {
4990 if (edge.fromWindowId === windowId) {
4991 related.add(edge.toWindowId);
4992 } else if (edge.toWindowId === windowId) {
4993 related.add(edge.fromWindowId);
4994 }
4995 }
4996 related.delete(windowId);
4997 return Array.from(related);
4998 }
4999 function getDirectlyRelatedWindowIds(windowId) {
5000 const related = /* @__PURE__ */ new Set();
5001 for (const edge of listWindowLinkEdges()) {
5002 if (edge.fromWindowId === windowId) {
5003 related.add(edge.toWindowId);
5004 } else if (edge.toWindowId === windowId) {
5005 related.add(edge.fromWindowId);
5006 }
5007 }
5008 related.delete(windowId);
5009 return Array.from(related);
5010 }
5011 function listWindowLinkEdges() {
5012 const seq = store$g.state.focusSeq;
5013 const windowByKey = /* @__PURE__ */ new Map();
5014 for (const [windowId, ref] of store$g.state.contentByWindow) {
5015 const key = keyOf(ref);
5016 const current = windowByKey.get(key);
5017 if (!current || (seq.get(windowId) ?? 0) > (seq.get(current) ?? 0)) {
5018 windowByKey.set(key, windowId);
5019 }
5020 }
5021 const edges = /* @__PURE__ */ new Map();
5022 const directedKey = (from, to) => `${from}→${to}`;
5023 for (const [windowId, ref] of store$g.state.contentByWindow) {
5024 if (ref.root) {
5025 const target2 = windowByKey.get(keyOf(ref.root));
5026 if (target2 && target2 !== windowId) {
5027 edges.set(directedKey(windowId, target2), {
5028 fromWindowId: windowId,
5029 toWindowId: target2,
5030 kind: "child-root",
5031 bidirectional: false
5032 });
5033 }
5034 }
5035 for (const link of ref.links ?? []) {
5036 const target2 = windowByKey.get(keyOf(link));
5037 if (!target2 || target2 === windowId) {
5038 continue;
5039 }
5040 if (link.rel === "child") {
5041 const key2 = directedKey(target2, windowId);
5042 const existing = edges.get(key2);
5043 if (!existing || existing.kind !== "child-root") {
5044 edges.set(key2, {
5045 fromWindowId: target2,
5046 toWindowId: windowId,
5047 kind: "child-root",
5048 bidirectional: false
5049 });
5050 }
5051 continue;
5052 }
5053 const key = directedKey(windowId, target2);
5054 if (!edges.has(key)) {
5055 edges.set(key, {
5056 fromWindowId: windowId,
5057 toWindowId: target2,
5058 kind: "reference",
5059 bidirectional: false
5060 });
5061 }
5062 }
5063 }
5064 const merged = [];
5065 const dropped = /* @__PURE__ */ new Set();
5066 for (const [key, edge] of edges) {
5067 if (dropped.has(key)) {
5068 continue;
5069 }
5070 const reverseKey = directedKey(edge.toWindowId, edge.fromWindowId);
5071 const reverse = edges.get(reverseKey);
5072 if (reverse && edge.kind === "reference") {
5073 if (reverse.kind === "reference") {
5074 dropped.add(reverseKey);
5075 merged.push({ ...edge, bidirectional: true });
5076 continue;
5077 }
5078 continue;
5079 }
5080 merged.push(edge);
5081 }
5082 const filtered = applyFilters(
5083 HOOKS.WINDOW_LINK_EDGES,
5084 merged
5085 );
5086 if (!Array.isArray(filtered)) {
5087 if (typeof console !== "undefined") {
5088 console.warn(
5089 "[desktop-mode] `desktop-mode.window-links.edges` filter returned a non-array; falling back to derived edges."
5090 );
5091 }
5092 return merged;
5093 }
5094 return filtered;
5095 }
5096 function subscribeWindowLinks(cb) {
5097 store$g.state.listeners.add(cb);
5098 return () => {
5099 store$g.state.listeners.delete(cb);
5100 };
5101 }
5102 function notify$g() {
5103 for (const cb of Array.from(store$g.state.listeners)) {
5104 try {
5105 cb();
5106 } catch (err) {
5107 if (typeof console !== "undefined") {
5108 console.error(
5109 "[desktop-mode] window-links listener threw:",
5110 err
5111 );
5112 }
5113 }
5114 }
5115 }
5116 function relationsSignature() {
5117 return Array.from(store$g.state.contentByWindow).map(([id, ref]) => `${id}=${refSignature(ref)}`).sort().join(";");
5118 }
5119 function broadcastGroupsIfChanged() {
5120 const signature = relationsSignature();
5121 if (signature === store$g.state.lastGroupsSignature) {
5122 return;
5123 }
5124 store$g.state.lastGroupsSignature = signature;
5125 const groups = listWindowLinkGroups();
5126 const detail = { groups };
5127 document.dispatchEvent(
5128 new CustomEvent("desktop-mode-window-link-groups-changed", {
5129 detail
5130 })
5131 );
5132 doAction(HOOKS.WINDOW_LINK_GROUPS_CHANGED, detail);
5133 }
5134 const relationsApi = {
5135 get: getWindowContent,
5136 set: (windowId, ref) => setWindowContent(windowId, ref, { source: "api" }),
5137 groups: listWindowLinkGroups,
5138 edges: listWindowLinkEdges,
5139 groupOf: getWindowLinkGroup,
5140 related: getRelatedWindowIds,
5141 subscribe: subscribeWindowLinks
5142 };
5143 function startWindowLinksEngine({
5144 manager
5145 }) {
5146 store$g.state.manager = manager;
5147 if (store$g.state.started) {
5148 return;
5149 }
5150 store$g.state.started = true;
5151 addAction(
5152 HOOKS.WINDOW_OPENED,
5153 "desktop-mode/window-links-seed",
5154 (e) => {
5155 if (!e?.windowId) {
5156 return;
5157 }
5158 const win = store$g.state.manager?.getById(e.windowId);
5159 const content = win?.config?.content;
5160 if (content) {
5161 setWindowContent(e.windowId, content, { source: "config" });
5162 }
5163 }
5164 );
5165 addAction(
5166 HOOKS.WINDOW_CLOSED,
5167 "desktop-mode/window-links-clear",
5168 (e) => {
5169 if (!e?.windowId) {
5170 return;
5171 }
5172 store$g.state.focusSeq.delete(e.windowId);
5173 setWindowContent(e.windowId, null, { source: "config" });
5174 }
5175 );
5176 addAction(
5177 HOOKS.WINDOW_FOCUSED,
5178 "desktop-mode/window-links-recency",
5179 (e) => {
5180 if (!e?.windowId) {
5181 return;
5182 }
5183 store$g.state.seq += 1;
5184 store$g.state.focusSeq.set(e.windowId, store$g.state.seq);
5185 }
5186 );
5187 window.addEventListener("message", (event) => {
5188 if (event.origin !== window.location.origin) {
5189 return;
5190 }
5191 const data = event.data;
5192 if (!data || data.type !== "desktop-mode-content-identity") {
5193 return;
5194 }
5195 const win = store$g.state.manager?.findByIframeSource?.(
5196 event.source
5197 );
5198 if (!win) {
5199 return;
5200 }
5201 setWindowContent(win.id, data.identity ?? null, {
5202 source: "bridge"
5203 });
5204 });
5205 }
5206 const adminLinkDepsStore = createSharedStore(
5207 "desktop-mode/admin-link-deps",
5208 () => ({ deps: null })
5209 );
5210 function bindAdminLinkDispatch(deps2) {
5211 adminLinkDepsStore.state.deps = deps2;
5212 }
5213 const store$f = createSharedStore(
5214 "desktop-mode/wallpaper-registry",
5215 () => ({
5216 seed: [],
5217 listeners: /* @__PURE__ */ new Set()
5218 })
5219 );
5220 const seed$3 = store$f.state.seed;
5221 const listeners$d = store$f.state.listeners;
5222 function register$2(def) {
5223 throwOnRegistrationErrors(
5224 "Wallpaper",
5225 collectRegistrationErrors(def, WALLPAPER_CHECKS),
5226 def
5227 );
5228 const idx = seed$3.findIndex((w) => w.id === def.id);
5229 if (idx >= 0) {
5230 seed$3[idx] = def;
5231 } else {
5232 seed$3.push(def);
5233 }
5234 notify$f();
5235 }
5236 function unregister$2(id) {
5237 const idx = seed$3.findIndex((w) => w.id === id);
5238 if (idx >= 0) {
5239 seed$3.splice(idx, 1);
5240 notify$f();
5241 }
5242 }
5243 function notify$f() {
5244 const snapshot = Array.from(listeners$d);
5245 for (const cb of snapshot) {
5246 try {
5247 cb();
5248 } catch (err) {
5249 if (typeof console !== "undefined") {
5250 console.error(
5251 "[desktop-mode] wallpaper registry listener threw:",
5252 err
5253 );
5254 }
5255 }
5256 }
5257 }
5258 function all$1() {
5259 const copy = seed$3.slice();
5260 const filtered = applyFilters(HOOKS.WALLPAPERS, copy);
5261 if (!Array.isArray(filtered)) {
5262 if (typeof console !== "undefined") {
5263 console.warn(
5264 "[desktop-mode] `desktop-mode.wallpapers` filter returned a non-array; falling back to seed list."
5265 );
5266 }
5267 return copy;
5268 }
5269 return filtered.filter(isValidDef$1);
5270 }
5271 function get$1(id) {
5272 return all$1().find((w) => w.id === id);
5273 }
5274 const WALLPAPER_CHECKS = [
5275 {
5276 field: "id",
5277 message: "missing or not a non-empty string",
5278 valid: (d) => typeof d.id === "string" && d.id !== ""
5279 },
5280 {
5281 field: "label",
5282 message: "missing or not a non-empty string",
5283 valid: (d) => typeof d.label === "string" && d.label !== ""
5284 },
5285 {
5286 field: "preview",
5287 message: "missing or not a non-empty string",
5288 valid: (d) => typeof d.preview === "string" && d.preview !== ""
5289 },
5290 {
5291 field: "type",
5292 message: 'must be "css" or "canvas"',
5293 valid: (d) => d.type === "css" || d.type === "canvas"
5294 },
5295 {
5296 field: "value/resolveValue/mount",
5297 message: "css types need `value` or `resolveValue`; canvas types need `mount`",
5298 valid: (d) => {
5299 if (d.type === "css") {
5300 return typeof d.value === "string" || typeof d.resolveValue === "function";
5301 }
5302 if (d.type === "canvas") {
5303 return typeof d.mount === "function";
5304 }
5305 return true;
5306 }
5307 }
5308 ];
5309 function isValidDef$1(def) {
5310 return collectRegistrationErrors(def, WALLPAPER_CHECKS).length === 0;
5311 }
5312 const store$e = createSharedStore(
5313 "desktop-mode/wallpaper-settings",
5314 () => ({ values: {} })
5315 );
5316 function getWallpaperSettings(id) {
5317 return { ...store$e.state.values[id] ?? {} };
5318 }
5319 function seedWallpaperSettings(all2) {
5320 const values = store$e.state.values;
5321 for (const key of Object.keys(values)) {
5322 delete values[key];
5323 }
5324 for (const [id, settings] of Object.entries(all2)) {
5325 values[id] = { ...settings };
5326 }
5327 }
5328 const STORAGE_KEY = "desktop-mode-os-settings";
5329 const CUSTOM_GRADIENT_ID = "custom-gradient";
5330 const CUSTOM_IMAGE_ID = "custom-image";
5331 const DEFAULT_WALLPAPER_ID = "dark";
5332 const DEFAULT_ACCENTS = [
5333 { id: "wp-blue", label: "WordPress Blue", value: "#2271b1" },
5334 { id: "indigo", label: "Indigo", value: "#3858e9" },
5335 { id: "teal", label: "Teal", value: "#04a4cc" },
5336 { id: "emerald", label: "Emerald", value: "#059669" },
5337 { id: "amber", label: "Amber", value: "#d97706" },
5338 { id: "rose", label: "Rose", value: "#e11d48" }
5339 ];
5340 function getAccents() {
5341 const config = window.wp?.desktop?.config;
5342 const raw = config?.accentColors;
5343 if (!Array.isArray(raw) || raw.length === 0) {
5344 return DEFAULT_ACCENTS;
5345 }
5346 const clean = [];
5347 for (const entry of raw) {
5348 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)) {
5349 clean.push({ id: entry.id, label: entry.label, value: entry.value });
5350 }
5351 }
5352 return clean.length > 0 ? clean : DEFAULT_ACCENTS;
5353 }
5354 function getDefaultWallpaperId() {
5355 const config = window.wp?.desktop?.config;
5356 const raw = config?.defaultWallpaper;
5357 if (typeof raw === "string" && raw !== "") {
5358 return raw;
5359 }
5360 return DEFAULT_WALLPAPER_ID;
5361 }
5362 const DOCK_SIZES = [
5363 { id: "compact", label: "Compact", width: 48, icon: 18 },
5364 { id: "default", label: "Default", width: 56, icon: 20 },
5365 { id: "large", label: "Large", width: 72, icon: 26 }
5366 ];
5367 const DESKTOP_LAYOUTS = [
5368 { id: "classic", label: "Classic" },
5369 { id: "unified", label: "Unified" },
5370 { id: "spatial", label: "Spatial" }
5371 ];
5372 const DEFAULTS = {
5373 wallpaper: DEFAULT_WALLPAPER_ID,
5374 accent: "wp-blue",
5375 dockSize: "default",
5376 desktopLayout: "classic",
5377 dockRailRenderer: "default",
5378 unfocusEffect: "darken",
5379 windowLinkRenderer: "svg-splines",
5380 windowLinkVisibility: "always",
5381 windowLinksEnabled: true,
5382 windowLinkRaiseOnFocus: true,
5383 windowLinkHighlight: true,
5384 customGradient: {
5385 from: "#2271b1",
5386 to: "#7c3aed",
5387 angle: 135
5388 },
5389 customImage: null,
5390 wallpaperSettings: {},
5391 libraryHdOnly: true,
5392 ai: {
5393 enabled: false
5394 },
5395 // Opt-IN Beta as of 0.9.1. Fresh installs land on the classic
5396 // chromeless `edit.php` iframe; a user opts in via OS Settings →
5397 // Features → Beta features to get the native Posts window. The
5398 // native windows used to default ON (opt-out, 0.8.0) but are now
5399 // opt-in so the redesign is a deliberate choice, not imposed.
5400 heartbeatRate: 60,
5401 nativePostsEnabled: false,
5402 nativePostsHiddenColumns: [],
5403 // Same opt-in Beta posture as Posts — fresh installs keep the
5404 // iframe; users opt in to the native Pages window.
5405 nativePagesEnabled: false,
5406 // Native Users window — same opt-in Beta posture. Capability-gated
5407 // server-side (the window is only registered for users with
5408 // `list_users`), so this toggle only affects the small set of
5409 // users who can see the Users tile in the first place.
5410 nativeUsersEnabled: false,
5411 // Native Plugins window — replaces `plugins.php` and
5412 // `plugin-install.php`. Same opt-in Beta posture; cap-gated on
5413 // `activate_plugins` server-side, so this toggle only affects
5414 // users who could see the Plugins tile anyway.
5415 nativePluginsEnabled: false,
5416 // Native Comments window — replaces `edit-comments.php`. Same
5417 // opt-in Beta posture; cap-gated on `edit_posts` server-side.
5418 nativeCommentsEnabled: false,
5419 showDesktopOnWallpaperClick: false,
5420 showPostStatusRibbons: true,
5421 developerModeEnabled: false,
5422 foldersSharingEnabled: true,
5423 itemVisibility: {},
5424 dockOrder: [],
5425 dockPromotedPositions: {}
5426 };
5427 function isHexColor(value) {
5428 return typeof value === "string" && /^#[0-9a-f]{3,8}$/i.test(value);
5429 }
5430 const NONCE_HEADER = "X-WP-Nonce";
5431 function injectRestNonce(input, init2) {
5432 const nonce = readRestNonce$3();
5433 if (!nonce) {
5434 return init2;
5435 }
5436 const url = resolveUrl(input);
5437 if (!url || !isSameOriginRestUrl(url)) {
5438 return init2;
5439 }
5440 const baseHeaders = init2?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
5441 const headers = new Headers(baseHeaders ?? {});
5442 if (headers.has(NONCE_HEADER)) {
5443 return init2;
5444 }
5445 headers.set(NONCE_HEADER, nonce);
5446 return { ...init2 ?? {}, headers };
5447 }
5448 function readRestNonce$3() {
5449 if (typeof window === "undefined") {
5450 return void 0;
5451 }
5452 const cfg = window.desktopModeConfig;
5453 const value = cfg?.restNonce;
5454 return typeof value === "string" && value.length > 0 ? value : void 0;
5455 }
5456 function resolveUrl(input) {
5457 try {
5458 const base = typeof window !== "undefined" && window.location ? window.location.href : void 0;
5459 if (typeof input === "string") {
5460 return new URL(input, base);
5461 }
5462 if (input instanceof URL) {
5463 return input;
5464 }
5465 if (typeof Request !== "undefined" && input instanceof Request) {
5466 return new URL(input.url, base);
5467 }
5468 return null;
5469 } catch {
5470 return null;
5471 }
5472 }
5473 function isSameOriginRestUrl(url) {
5474 if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) {
5475 return false;
5476 }
5477 if (url.pathname.includes("/wp-json/")) {
5478 return true;
5479 }
5480 if (url.searchParams.has("rest_route")) {
5481 return true;
5482 }
5483 return false;
5484 }
5485 function trackedFetch$1(input, init2, opts = {}) {
5486 const fn = window.wp?.desktop?.fetch;
5487 if (typeof fn === "function") {
5488 return fn(input, init2, opts);
5489 }
5490 const finalInit = injectRestNonce(input, init2);
5491 return fetch(input, finalInit);
5492 }
5493 function loadState() {
5494 const serverRaw = _readServerSettings();
5495 if (serverRaw) {
5496 const state2 = _parseRaw(serverRaw);
5497 _writeLocalStorage(state2);
5498 return state2;
5499 }
5500 try {
5501 const cached = window.localStorage.getItem(STORAGE_KEY);
5502 if (cached) {
5503 return _parseRaw(JSON.parse(cached));
5504 }
5505 } catch {
5506 }
5507 return structuredDefaults();
5508 }
5509 function _readServerSettings() {
5510 const config = window.desktopModeConfig;
5511 const raw = config?.osSettings;
5512 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5513 return null;
5514 }
5515 return raw;
5516 }
5517 function _parseRaw(parsed) {
5518 const accents = getAccents();
5519 return {
5520 wallpaper: typeof parsed.wallpaper === "string" && parsed.wallpaper !== "" ? parsed.wallpaper : getDefaultWallpaperId(),
5521 accent: accents.some((a) => a.id === parsed.accent) ? parsed.accent : DEFAULTS.accent,
5522 dockSize: DOCK_SIZES.some((d) => d.id === parsed.dockSize) ? parsed.dockSize : DEFAULTS.dockSize,
5523 desktopLayout: DESKTOP_LAYOUTS.some(
5524 (l) => l.id === parsed.desktopLayout
5525 ) ? parsed.desktopLayout : DEFAULTS.desktopLayout,
5526 // Dock rail renderer — any sanitize_key()-clean string
5527 // survives; the registry resolves at use time and falls back
5528 // to `'default'` when the picked renderer isn't registered.
5529 dockRailRenderer: typeof parsed.dockRailRenderer === "string" && /^[a-z0-9_-]+$/.test(parsed.dockRailRenderer) ? parsed.dockRailRenderer : DEFAULTS.dockRailRenderer,
5530 // Unfocus effect — any registry id (`vendor/sub-id` allowed) or
5531 // the `'none'` sentinel survives; the engine resolves at use
5532 // time and treats an unknown id as "no effect".
5533 unfocusEffect: typeof parsed.unfocusEffect === "string" && /^[a-z0-9_/-]+$/.test(parsed.unfocusEffect) ? parsed.unfocusEffect : DEFAULTS.unfocusEffect,
5534 // Window-link renderer — same id charset as unfocus effects;
5535 // the render host resolves at use time and falls back to the
5536 // built-in `svg-splines` for unknown ids.
5537 windowLinkRenderer: typeof parsed.windowLinkRenderer === "string" && /^[a-z0-9_/-]+$/.test(parsed.windowLinkRenderer) ? parsed.windowLinkRenderer : DEFAULTS.windowLinkRenderer,
5538 windowLinkVisibility: parsed.windowLinkVisibility === "focus" || parsed.windowLinkVisibility === "always" || parsed.windowLinkVisibility === "off" ? parsed.windowLinkVisibility : DEFAULTS.windowLinkVisibility,
5539 windowLinksEnabled: typeof parsed.windowLinksEnabled === "boolean" ? parsed.windowLinksEnabled : DEFAULTS.windowLinksEnabled,
5540 windowLinkRaiseOnFocus: typeof parsed.windowLinkRaiseOnFocus === "boolean" ? parsed.windowLinkRaiseOnFocus : DEFAULTS.windowLinkRaiseOnFocus,
5541 windowLinkHighlight: typeof parsed.windowLinkHighlight === "boolean" ? parsed.windowLinkHighlight : DEFAULTS.windowLinkHighlight,
5542 customGradient: sanitizeCustomGradient(parsed.customGradient),
5543 customImage: sanitizeCustomImage(parsed.customImage),
5544 wallpaperSettings: sanitizeWallpaperSettings(parsed.wallpaperSettings),
5545 libraryHdOnly: typeof parsed.libraryHdOnly === "boolean" ? parsed.libraryHdOnly : DEFAULTS.libraryHdOnly,
5546 ai: sanitizeAi(parsed.ai),
5547 heartbeatRate: parsed.heartbeatRate === 15 || parsed.heartbeatRate === 30 || parsed.heartbeatRate === 45 || parsed.heartbeatRate === 60 ? parsed.heartbeatRate : DEFAULTS.heartbeatRate,
5548 nativePostsEnabled: typeof parsed.nativePostsEnabled === "boolean" ? parsed.nativePostsEnabled : DEFAULTS.nativePostsEnabled,
5549 nativePostsHiddenColumns: Array.isArray(parsed.nativePostsHiddenColumns) ? parsed.nativePostsHiddenColumns.filter((v) => typeof v === "string" && v !== "").slice(0, 32) : DEFAULTS.nativePostsHiddenColumns.slice(),
5550 nativePagesEnabled: typeof parsed.nativePagesEnabled === "boolean" ? parsed.nativePagesEnabled : DEFAULTS.nativePagesEnabled,
5551 nativeUsersEnabled: typeof parsed.nativeUsersEnabled === "boolean" ? parsed.nativeUsersEnabled : DEFAULTS.nativeUsersEnabled,
5552 nativePluginsEnabled: typeof parsed.nativePluginsEnabled === "boolean" ? parsed.nativePluginsEnabled : DEFAULTS.nativePluginsEnabled,
5553 nativeCommentsEnabled: typeof parsed.nativeCommentsEnabled === "boolean" ? parsed.nativeCommentsEnabled : DEFAULTS.nativeCommentsEnabled,
5554 showDesktopOnWallpaperClick: typeof parsed.showDesktopOnWallpaperClick === "boolean" ? parsed.showDesktopOnWallpaperClick : DEFAULTS.showDesktopOnWallpaperClick,
5555 showPostStatusRibbons: typeof parsed.showPostStatusRibbons === "boolean" ? parsed.showPostStatusRibbons : DEFAULTS.showPostStatusRibbons,
5556 developerModeEnabled: typeof parsed.developerModeEnabled === "boolean" ? parsed.developerModeEnabled : DEFAULTS.developerModeEnabled,
5557 foldersSharingEnabled: typeof parsed.foldersSharingEnabled === "boolean" ? parsed.foldersSharingEnabled : DEFAULTS.foldersSharingEnabled,
5558 itemVisibility: sanitizeItemVisibility(parsed.itemVisibility),
5559 dockOrder: sanitizeDockOrder(parsed.dockOrder),
5560 dockPromotedPositions: sanitizeDockPromotedPositions(
5561 parsed.dockPromotedPositions
5562 )
5563 };
5564 }
5565 function sanitizeWallpaperSettings(raw) {
5566 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5567 return {};
5568 }
5569 const out = {};
5570 let idCount = 0;
5571 for (const [id, bag] of Object.entries(
5572 raw
5573 )) {
5574 if (idCount >= 64) {
5575 break;
5576 }
5577 if (typeof id !== "string" || id === "" || !/^[a-z0-9_/-]+$/.test(id)) {
5578 continue;
5579 }
5580 if (!bag || typeof bag !== "object" || Array.isArray(bag)) {
5581 continue;
5582 }
5583 const clean = {};
5584 let keyCount = 0;
5585 for (const [key, value] of Object.entries(
5586 bag
5587 )) {
5588 if (keyCount >= 32) {
5589 break;
5590 }
5591 if (typeof key !== "string" || key === "" || !/^[a-zA-Z0-9_-]+$/.test(key)) {
5592 continue;
5593 }
5594 if (typeof value === "boolean") {
5595 clean[key] = value;
5596 } else if (typeof value === "number" && Number.isFinite(value)) {
5597 clean[key] = value;
5598 } else if (typeof value === "string") {
5599 clean[key] = value.slice(0, 256);
5600 } else {
5601 continue;
5602 }
5603 keyCount++;
5604 }
5605 if (keyCount === 0) {
5606 continue;
5607 }
5608 out[id] = clean;
5609 idCount++;
5610 }
5611 return out;
5612 }
5613 function sanitizeItemVisibility(raw) {
5614 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5615 return {};
5616 }
5617 const allowed = [
5618 "both",
5619 "dock",
5620 "desktop",
5621 "hidden"
5622 ];
5623 const out = {};
5624 let count = 0;
5625 for (const [k, v] of Object.entries(raw)) {
5626 if (count >= 256) {
5627 break;
5628 }
5629 if (typeof k !== "string" || k === "") {
5630 continue;
5631 }
5632 if (typeof v !== "string") {
5633 continue;
5634 }
5635 const placement = v;
5636 if (!allowed.includes(placement)) {
5637 continue;
5638 }
5639 out[k] = placement;
5640 count++;
5641 }
5642 return out;
5643 }
5644 function sanitizeDockOrder(raw) {
5645 if (!Array.isArray(raw)) {
5646 return [];
5647 }
5648 const out = [];
5649 const seen = /* @__PURE__ */ new Set();
5650 for (const id of raw) {
5651 if (typeof id !== "string" || id === "" || seen.has(id)) {
5652 continue;
5653 }
5654 seen.add(id);
5655 out.push(id);
5656 if (out.length >= 256) {
5657 break;
5658 }
5659 }
5660 return out;
5661 }
5662 function sanitizeDockPromotedPositions(raw) {
5663 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5664 return {};
5665 }
5666 const out = {};
5667 let count = 0;
5668 const MAX_COORD = 1e5;
5669 for (const [k, v] of Object.entries(raw)) {
5670 if (count >= 256) {
5671 break;
5672 }
5673 if (typeof k !== "string" || k === "") {
5674 continue;
5675 }
5676 if (!v || typeof v !== "object" || Array.isArray(v)) {
5677 continue;
5678 }
5679 const pos = v;
5680 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) {
5681 continue;
5682 }
5683 out[k] = { x: pos.x, y: pos.y };
5684 count++;
5685 }
5686 return out;
5687 }
5688 let _syncTimer = null;
5689 const SYNC_DEBOUNCE_MS = 250;
5690 let _lastConfirmedState = null;
5691 function setLastConfirmedState(state2) {
5692 _lastConfirmedState = _cloneState(state2);
5693 }
5694 function _cloneState(state2) {
5695 return {
5696 ...state2,
5697 customGradient: { ...state2.customGradient },
5698 customImage: state2.customImage ? { ...state2.customImage } : null,
5699 wallpaperSettings: Object.fromEntries(
5700 Object.entries(state2.wallpaperSettings).map(([k, v]) => [
5701 k,
5702 { ...v }
5703 ])
5704 ),
5705 ai: { ...state2.ai },
5706 nativePostsHiddenColumns: state2.nativePostsHiddenColumns.slice(),
5707 itemVisibility: { ...state2.itemVisibility },
5708 dockOrder: state2.dockOrder.slice(),
5709 dockPromotedPositions: Object.fromEntries(
5710 Object.entries(state2.dockPromotedPositions).map(([k, v]) => [
5711 k,
5712 { ...v }
5713 ])
5714 )
5715 };
5716 }
5717 function saveState(state2, opts = {}) {
5718 _writeLocalStorage(state2);
5719 _scheduleSyncToServer(state2, opts.windowId);
5720 }
5721 function _writeLocalStorage(state2) {
5722 try {
5723 window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state2));
5724 } catch {
5725 }
5726 }
5727 function _scheduleSyncToServer(state2, windowId) {
5728 if (_syncTimer !== null) {
5729 clearTimeout(_syncTimer);
5730 }
5731 if (windowId) {
5732 _pendingActivityWindowId = windowId;
5733 }
5734 _emitSaveLifecycle("pending");
5735 _syncTimer = setTimeout(() => {
5736 _syncTimer = null;
5737 const id = _pendingActivityWindowId;
5738 _pendingActivityWindowId = null;
5739 _postToServer(state2, id);
5740 }, SYNC_DEBOUNCE_MS);
5741 }
5742 let _pendingActivityWindowId = null;
5743 function _postToServer(state2, windowId) {
5744 const config = window.desktopModeConfig;
5745 const url = config?.osSettingsUrl;
5746 const nonce = config?.restNonce;
5747 if (!url || !nonce) {
5748 _emitSaveLifecycle("saved");
5749 return;
5750 }
5751 _emitSaveLifecycle("saving");
5752 const attributedWindowId = windowId || "desktop-mode-os-settings";
5753 trackedFetch$1(
5754 url,
5755 {
5756 method: "POST",
5757 headers: {
5758 "Content-Type": "application/json",
5759 "X-WP-Nonce": nonce
5760 },
5761 body: JSON.stringify({ settings: state2 })
5762 },
5763 { windowId: attributedWindowId }
5764 ).then((res) => {
5765 if (!res.ok) {
5766 throw new Error(`${res.status} ${res.statusText}`);
5767 }
5768 _lastConfirmedState = _cloneState(state2);
5769 _emitSaveLifecycle("saved");
5770 }).catch((err) => {
5771 if (_lastConfirmedState) {
5772 _writeLocalStorage(_lastConfirmedState);
5773 _emitSaveLifecycle(
5774 "failed",
5775 err instanceof Error ? err.message : String(err),
5776 _cloneState(_lastConfirmedState)
5777 );
5778 } else {
5779 _emitSaveLifecycle(
5780 "failed",
5781 err instanceof Error ? err.message : String(err)
5782 );
5783 }
5784 });
5785 }
5786 function _emitSaveLifecycle(phase, error, rolledBackTo) {
5787 const detail = { phase };
5788 if (error) {
5789 detail.error = error;
5790 }
5791 if (rolledBackTo) {
5792 detail.rolledBackTo = rolledBackTo;
5793 }
5794 document.dispatchEvent(
5795 new CustomEvent("desktop-mode-os-settings-save-lifecycle", { detail })
5796 );
5797 }
5798 function structuredDefaults() {
5799 return {
5800 ...DEFAULTS,
5801 customGradient: { ...DEFAULTS.customGradient },
5802 customImage: null,
5803 wallpaperSettings: { ...DEFAULTS.wallpaperSettings },
5804 ai: { ...DEFAULTS.ai },
5805 // Clone the collection fields too. A shallow `...DEFAULTS`
5806 // aliases these nested objects, so a later in-place mutation
5807 // (e.g. dragging the gradient editor after a Reset, which spreads
5808 // these defaults into live state) would corrupt the module-level
5809 // DEFAULTS singleton for the rest of the session.
5810 //
5811 // These are one-level clones, which is sufficient *because* all
5812 // three defaults are empty (`{}` / `[]`) — there are no inner
5813 // objects to share. If `DEFAULTS.dockPromotedPositions` ever
5814 // ships seeded entries, its `{ x, y }` values would need a
5815 // deeper clone here.
5816 itemVisibility: { ...DEFAULTS.itemVisibility },
5817 dockOrder: [...DEFAULTS.dockOrder],
5818 dockPromotedPositions: { ...DEFAULTS.dockPromotedPositions }
5819 };
5820 }
5821 function sanitizeAi(raw) {
5822 if (!raw || typeof raw !== "object") {
5823 return { ...DEFAULTS.ai };
5824 }
5825 const { enabled } = raw;
5826 return {
5827 enabled: typeof enabled === "boolean" ? enabled : DEFAULTS.ai.enabled
5828 };
5829 }
5830 function sanitizeCustomGradient(raw) {
5831 if (!raw || typeof raw !== "object") {
5832 return { ...DEFAULTS.customGradient };
5833 }
5834 const { from, to, angle } = raw;
5835 return {
5836 from: isHexColor(from) ? from : DEFAULTS.customGradient.from,
5837 to: isHexColor(to) ? to : DEFAULTS.customGradient.to,
5838 angle: typeof angle === "number" && Number.isFinite(angle) && angle >= 0 && angle <= 360 ? angle : DEFAULTS.customGradient.angle
5839 };
5840 }
5841 function sanitizeCustomImage(raw) {
5842 if (!raw || typeof raw !== "object") {
5843 return null;
5844 }
5845 const { id, url } = raw;
5846 if (typeof id !== "number" || !Number.isFinite(id) || id <= 0) {
5847 return null;
5848 }
5849 if (typeof url !== "string" || !/^https?:\/\//i.test(url)) {
5850 return null;
5851 }
5852 return { id, url };
5853 }
5854 const store$d = createSharedStore(
5855 "desktop-mode/dock-rail-registry",
5856 () => ({
5857 registry: /* @__PURE__ */ new Map(),
5858 listeners: /* @__PURE__ */ new Set(),
5859 activeId: "default"
5860 })
5861 );
5862 const registry$a = store$d.state.registry;
5863 const listeners$c = store$d.state.listeners;
5864 const ID_RE = /^[a-z0-9_-]+$/;
5865 function register$1(renderer) {
5866 if (!renderer || typeof renderer !== "object") {
5867 throw new TypeError(
5868 "[desktop-mode] registerDockRailRenderer: renderer must be an object."
5869 );
5870 }
5871 if (typeof renderer.id !== "string" || !ID_RE.test(renderer.id)) {
5872 throw new TypeError(
5873 `[desktop-mode] registerDockRailRenderer: id must match /^[a-z0-9_-]+$/, got: ${String(renderer.id)}`
5874 );
5875 }
5876 if (typeof renderer.label !== "string" || renderer.label === "") {
5877 throw new TypeError(
5878 "[desktop-mode] registerDockRailRenderer: label must be a non-empty string."
5879 );
5880 }
5881 if (typeof renderer.mount !== "function") {
5882 throw new TypeError(
5883 "[desktop-mode] registerDockRailRenderer: mount must be a function."
5884 );
5885 }
5886 if (renderer.apiVersion !== void 0 && renderer.apiVersion !== 1) {
5887 throw new TypeError(
5888 `[desktop-mode] registerDockRailRenderer: unsupported apiVersion ${renderer.apiVersion} (this shell speaks v1).`
5889 );
5890 }
5891 registry$a.set(renderer.id, renderer);
5892 notify$e();
5893 }
5894 function unregister$1(id) {
5895 if (registry$a.delete(id)) {
5896 notify$e();
5897 }
5898 }
5899 function unregisterByOwner$1(owner) {
5900 if (!owner) {
5901 return 0;
5902 }
5903 let removed = 0;
5904 for (const [id, renderer] of Array.from(registry$a.entries())) {
5905 if (renderer.owner === owner) {
5906 registry$a.delete(id);
5907 removed++;
5908 }
5909 }
5910 if (removed > 0) {
5911 notify$e();
5912 }
5913 return removed;
5914 }
5915 function list() {
5916 return Array.from(registry$a.values());
5917 }
5918 function subscribe$3(cb) {
5919 listeners$c.add(cb);
5920 return () => {
5921 listeners$c.delete(cb);
5922 };
5923 }
5924 function setActiveRenderer(id) {
5925 if (store$d.state.activeId === id) {
5926 return;
5927 }
5928 store$d.state.activeId = id;
5929 notify$e();
5930 }
5931 function resolveActive() {
5932 return registry$a.get(store$d.state.activeId) ?? registry$a.get("default") ?? registry$a.values().next().value;
5933 }
5934 function notify$e() {
5935 const snapshot = Array.from(listeners$c);
5936 for (const cb of snapshot) {
5937 try {
5938 cb();
5939 } catch (err) {
5940 if (typeof console !== "undefined") {
5941 console.error(
5942 "[desktop-mode] dock-rail-renderer listener threw:",
5943 err
5944 );
5945 }
5946 }
5947 }
5948 }
5949 function hashTitleToHue(input) {
5950 if (!input) {
5951 return 214;
5952 }
5953 let hash2 = 5381;
5954 for (let i = 0; i < input.length; i++) {
5955 hash2 = Math.imul(hash2, 33) + input.charCodeAt(i);
5956 }
5957 return (hash2 % 360 + 360) % 360;
5958 }
5959 const SHOW_DELAY_MS = 180;
5960 const HIDE_DELAY_MS = 220;
5961 const STAGGER_MS = 32;
5962 function attachDockPeek(deps2) {
5963 const { tile: tile2 } = deps2;
5964 let popover = null;
5965 let showTimer = null;
5966 let hideTimer = null;
5967 let inside = false;
5968 const cancelShow = () => {
5969 if (showTimer !== null) {
5970 window.clearTimeout(showTimer);
5971 showTimer = null;
5972 }
5973 };
5974 const cancelHide = () => {
5975 if (hideTimer !== null) {
5976 window.clearTimeout(hideTimer);
5977 hideTimer = null;
5978 }
5979 };
5980 const tearDown = () => {
5981 cancelShow();
5982 cancelHide();
5983 if (popover) {
5984 popover.remove();
5985 popover = null;
5986 }
5987 deps2.suppressTooltip(false);
5988 };
5989 const onPointerEnterTile = (e) => {
5990 if (e.pointerType !== "mouse") {
5991 return;
5992 }
5993 if (!shouldShowPeek(deps2)) {
5994 return;
5995 }
5996 inside = true;
5997 cancelHide();
5998 if (popover) {
5999 return;
6000 }
6001 showTimer = window.setTimeout(() => {
6002 showTimer = null;
6003 if (!inside) {
6004 return;
6005 }
6006 showPeek();
6007 }, SHOW_DELAY_MS);
6008 };
6009 const onPointerLeaveTile = (e) => {
6010 if (popover && e.relatedTarget instanceof Node && popover.contains(e.relatedTarget)) {
6011 return;
6012 }
6013 inside = false;
6014 cancelShow();
6015 scheduleHide();
6016 };
6017 const scheduleHide = () => {
6018 cancelHide();
6019 hideTimer = window.setTimeout(() => {
6020 hideTimer = null;
6021 if (inside) {
6022 return;
6023 }
6024 tearDown();
6025 }, HIDE_DELAY_MS);
6026 };
6027 const showPeek = () => {
6028 deps2.suppressTooltip(true);
6029 popover = buildPopover(deps2, () => tearDown());
6030 document.body.appendChild(popover);
6031 inheritShellSchemeVars(popover);
6032 positionPopover(popover, tile2, deps2.getOrientation());
6033 requestAnimationFrame(() => {
6034 popover?.classList.add("desktop-mode-dock-peek--open");
6035 });
6036 popover.addEventListener("pointerenter", () => {
6037 inside = true;
6038 cancelHide();
6039 });
6040 popover.addEventListener("pointerleave", (e) => {
6041 if (e.relatedTarget instanceof Node && tile2.contains(e.relatedTarget)) {
6042 return;
6043 }
6044 inside = false;
6045 scheduleHide();
6046 });
6047 };
6048 tile2.addEventListener("pointerenter", onPointerEnterTile);
6049 tile2.addEventListener("pointerleave", onPointerLeaveTile);
6050 return () => {
6051 tile2.removeEventListener("pointerenter", onPointerEnterTile);
6052 tile2.removeEventListener("pointerleave", onPointerLeaveTile);
6053 tearDown();
6054 };
6055 }
6056 function shouldShowPeek(deps2) {
6057 return deps2.getInstances().length >= 1;
6058 }
6059 function buildPopover(deps2, dismiss) {
6060 const root = document.createElement("div");
6061 root.className = "desktop-mode-dock-peek";
6062 root.setAttribute("role", "menu");
6063 root.setAttribute("aria-label", sprintf(
6064 // translators: %s is the dock item's admin-page title (e.g., "Posts")
6065 __("%s — open windows"),
6066 deps2.item.title
6067 ));
6068 const cards = document.createElement("div");
6069 cards.className = "desktop-mode-dock-peek__cards";
6070 root.appendChild(cards);
6071 const instances = deps2.getInstances();
6072 let cardIndex = 0;
6073 for (const win of instances) {
6074 const card = buildInstanceCard(win, deps2, cardIndex++, dismiss);
6075 cards.appendChild(card);
6076 }
6077 if (deps2.enableGhost !== false) {
6078 const ghost = buildGhostCard(deps2, cardIndex, dismiss);
6079 cards.appendChild(ghost);
6080 }
6081 return root;
6082 }
6083 function buildInstanceCard(win, deps2, index2, dismiss) {
6084 const card = document.createElement("button");
6085 card.type = "button";
6086 card.setAttribute("role", "menuitem");
6087 card.className = "desktop-mode-dock-peek__card desktop-mode-dock-peek__card--instance";
6088 card.style.setProperty("--peek-card-index", String(index2));
6089 card.style.setProperty(
6090 "--peek-card-delay",
6091 `${index2 * STAGGER_MS}ms`
6092 );
6093 const title = win.config.title || deps2.item.title;
6094 card.style.setProperty(
6095 "--peek-card-hue",
6096 `${hashTitleToHue(win.id || title)}`
6097 );
6098 card.style.setProperty(
6099 "--peek-card-vt-name",
6100 `desktop-mode-peek-card-${win.id}`
6101 );
6102 const titlebar = document.createElement("span");
6103 titlebar.className = "desktop-mode-dock-peek__card-titlebar";
6104 const dots = document.createElement("span");
6105 dots.className = "desktop-mode-dock-peek__card-dots";
6106 dots.setAttribute("aria-hidden", "true");
6107 for (let i = 0; i < 3; i++) {
6108 dots.appendChild(document.createElement("i"));
6109 }
6110 titlebar.appendChild(dots);
6111 const iconHost = document.createElement("span");
6112 iconHost.className = "desktop-mode-dock-peek__card-icon";
6113 iconHost.setAttribute("aria-hidden", "true");
6114 const iconCls = win.config.icon || deps2.item.icon;
6115 if (iconCls.startsWith("dashicons-")) {
6116 iconHost.classList.add("dashicons", sanitizeClassName(iconCls));
6117 } else {
6118 iconHost.classList.add("dashicons", "dashicons-admin-generic");
6119 }
6120 titlebar.appendChild(iconHost);
6121 const label = document.createElement("span");
6122 label.className = "desktop-mode-dock-peek__card-label";
6123 label.textContent = title;
6124 titlebar.appendChild(label);
6125 card.appendChild(titlebar);
6126 const defaultBody = document.createElement("span");
6127 defaultBody.className = "desktop-mode-dock-peek__card-body";
6128 defaultBody.setAttribute("aria-hidden", "true");
6129 for (let i = 0; i < 3; i++) {
6130 const line = document.createElement("span");
6131 line.className = "desktop-mode-dock-peek__card-line";
6132 defaultBody.appendChild(line);
6133 }
6134 const ctx = { window: win, item: deps2.item };
6135 const body = applyFilters(
6136 HOOKS.DOCK_PEEK_CARD_CONTENT,
6137 defaultBody,
6138 ctx
6139 );
6140 if (body !== defaultBody) {
6141 body.classList.add("desktop-mode-dock-peek__card-body--custom");
6142 }
6143 card.appendChild(body);
6144 card.addEventListener("click", () => {
6145 spawnFocusViewTransition(deps2, win, card, dismiss);
6146 });
6147 card.addEventListener("pointerenter", () => {
6148 if (deps2.windowManager.getFocused() === win) {
6149 return;
6150 }
6151 deps2.windowManager.focus(win);
6152 });
6153 const finalCard = applyFilters(
6154 HOOKS.DOCK_PEEK_CARD_ELEMENT,
6155 card,
6156 ctx
6157 );
6158 return finalCard;
6159 }
6160 function spawnFocusViewTransition(deps2, win, card, dismiss) {
6161 const doc = document;
6162 const vtName = `desktop-mode-peek-card-${win.id}`;
6163 const focus = () => {
6164 dismiss();
6165 deps2.windowManager.focus(win);
6166 };
6167 if (typeof doc.startViewTransition !== "function") {
6168 focus();
6169 return;
6170 }
6171 const targetEl = win.element;
6172 card.style.setProperty("view-transition-name", vtName);
6173 targetEl.style.setProperty("view-transition-name", vtName);
6174 const transition = doc.startViewTransition(focus);
6175 const cleanup = () => {
6176 card.style.removeProperty("view-transition-name");
6177 targetEl.style.removeProperty("view-transition-name");
6178 };
6179 const t = transition;
6180 if (t.finished && typeof t.finished.then === "function") {
6181 t.finished.then(cleanup, cleanup);
6182 } else {
6183 Promise.resolve().then(cleanup);
6184 }
6185 }
6186 function buildGhostCard(deps2, index2, dismiss) {
6187 const card = document.createElement("button");
6188 card.type = "button";
6189 card.setAttribute("role", "menuitem");
6190 card.className = "desktop-mode-dock-peek__card desktop-mode-dock-peek__card--ghost";
6191 card.style.setProperty("--peek-card-index", String(index2));
6192 card.style.setProperty(
6193 "--peek-card-delay",
6194 `${index2 * STAGGER_MS}ms`
6195 );
6196 const plus = document.createElement("span");
6197 plus.className = "desktop-mode-dock-peek__card-plus";
6198 plus.setAttribute("aria-hidden", "true");
6199 plus.textContent = "+";
6200 card.appendChild(plus);
6201 const label = document.createElement("span");
6202 label.className = "desktop-mode-dock-peek__card-label";
6203 label.textContent = sprintf(
6204 // translators: %s is the admin-page title (e.g., "Posts")
6205 __("New %s"),
6206 deps2.item.title
6207 );
6208 card.appendChild(label);
6209 card.addEventListener("click", () => {
6210 spawnWithViewTransition(deps2, dismiss);
6211 });
6212 return card;
6213 }
6214 function spawnWithViewTransition(deps2, dismiss) {
6215 const doc = document;
6216 const spawn = () => {
6217 dismiss();
6218 deps2.openNew();
6219 };
6220 if (typeof doc.startViewTransition === "function") {
6221 doc.startViewTransition(spawn);
6222 return;
6223 }
6224 spawn();
6225 }
6226 const VIEWPORT_MARGIN_PX = 12;
6227 const SHELL_SCHEME_VARS = [
6228 "--wp-admin-theme-color",
6229 "--desktop-mode-titlebar-bg",
6230 "--desktop-mode-titlebar-bg-focused",
6231 "--desktop-mode-titlebar-color",
6232 "--desktop-mode-titlebar-color-focused"
6233 ];
6234 function inheritShellSchemeVars(popover) {
6235 const shell = document.querySelector(".desktop-mode-shell");
6236 if (!shell) {
6237 return;
6238 }
6239 const computed = window.getComputedStyle(shell);
6240 for (const name of SHELL_SCHEME_VARS) {
6241 const value = computed.getPropertyValue(name).trim();
6242 if (value) {
6243 popover.style.setProperty(name, value);
6244 }
6245 }
6246 }
6247 function positionPopover(popover, tile2, orientation) {
6248 const rect = tile2.getBoundingClientRect();
6249 popover.dataset.orientation = orientation;
6250 if (orientation === "bottom") {
6251 popover.style.left = `${rect.left + rect.width / 2}px`;
6252 popover.style.top = `${rect.top - 12}px`;
6253 } else if (orientation === "right") {
6254 popover.style.top = `${rect.top + rect.height / 2}px`;
6255 popover.style.left = `${rect.left - 12}px`;
6256 } else {
6257 popover.style.top = `${rect.top + rect.height / 2}px`;
6258 popover.style.left = `${rect.right + 12}px`;
6259 }
6260 requestAnimationFrame(() => clampToViewport$1(popover));
6261 }
6262 function clampToViewport$1(popover, orientation) {
6263 const rect = popover.getBoundingClientRect();
6264 const vh = window.innerHeight;
6265 const vw = window.innerWidth;
6266 const min = VIEWPORT_MARGIN_PX;
6267 let dy = 0;
6268 let dx = 0;
6269 if (rect.top < min) {
6270 dy = min - rect.top;
6271 } else if (rect.bottom > vh - min) {
6272 dy = vh - min - rect.bottom;
6273 }
6274 if (rect.left < min) {
6275 dx = min - rect.left;
6276 } else if (rect.right > vw - min) {
6277 dx = vw - min - rect.right;
6278 }
6279 if (dx === 0 && dy === 0) {
6280 return;
6281 }
6282 popover.style.setProperty("--peek-clamp-x", `${dx}px`);
6283 popover.style.setProperty("--peek-clamp-y", `${dy}px`);
6284 popover.classList.add("desktop-mode-dock-peek--clamped");
6285 }
6286 function tryOpenExternalUrl(url) {
6287 try {
6288 const parsed = new URL(url, window.location.origin);
6289 if (parsed.origin === window.location.origin) {
6290 return false;
6291 }
6292 window.open(parsed.toString(), "_blank", "noopener,noreferrer");
6293 return true;
6294 } catch {
6295 return false;
6296 }
6297 }
6298 function synthDockId(desktopIconId) {
6299 return `desktop:${desktopIconId}`;
6300 }
6301 function synthIconId(dockItemId) {
6302 return `dock:${dockItemId}`;
6303 }
6304 function canonicalItemId(id) {
6305 if (id.startsWith("dock:")) {
6306 return id.slice(5);
6307 }
6308 if (id.startsWith("desktop:")) {
6309 return id.slice(8);
6310 }
6311 return id;
6312 }
6313 function resolvePlacement(id, nativeRail, visibility) {
6314 const override = visibility[id];
6315 if (override) {
6316 return override;
6317 }
6318 return nativeRail;
6319 }
6320 function shouldShowOnDock(placement) {
6321 return placement === "dock" || placement === "both";
6322 }
6323 function shouldShowOnDesktop(placement) {
6324 return placement === "desktop" || placement === "both";
6325 }
6326 function applyDockPlacement(dockItems, desktopIcons, settings, dockedNativeWindows) {
6327 const visibility = settings.itemVisibility;
6328 const order = settings.dockOrder;
6329 const kept = [];
6330 for (const item of dockItems) {
6331 const placement = resolvePlacement(item.id, "dock", visibility);
6332 if (shouldShowOnDock(placement)) {
6333 kept.push(item);
6334 }
6335 }
6336 for (const icon of desktopIcons) {
6337 const placement = resolvePlacement(icon.id, "desktop", visibility);
6338 if (!shouldShowOnDock(placement)) {
6339 continue;
6340 }
6341 if (icon.window && dockedNativeWindows && dockedNativeWindows.has(icon.window)) {
6342 continue;
6343 }
6344 kept.push({
6345 id: synthIconId(icon.id),
6346 title: icon.title,
6347 icon: icon.icon,
6348 url: icon.url || "",
6349 // Carry the native-window id forward so the dock can light
6350 // the active-dot indicator + show the hover-peek card when
6351 // the target window is open. Without this, window-target
6352 // icons (no `url`) synthesize a tile whose only id-bearing
6353 // field is an empty string — deriveWindowId('') matches
6354 // nothing the window manager has stored.
6355 windowId: icon.window || void 0,
6356 badge: 0,
6357 submenu: [],
6358 isCore: false
6359 });
6360 }
6361 return applyOrder(kept, order);
6362 }
6363 function applyDesktopPlacement(desktopIcons, dockItems, visibility) {
6364 const out = [];
6365 for (const icon of desktopIcons) {
6366 const placement = resolvePlacement(icon.id, "desktop", visibility);
6367 if (shouldShowOnDesktop(placement)) {
6368 out.push(icon);
6369 }
6370 }
6371 let synthIndex = 0;
6372 for (const item of dockItems) {
6373 const placement = resolvePlacement(item.id, "dock", visibility);
6374 if (!shouldShowOnDesktop(placement)) {
6375 continue;
6376 }
6377 out.push({
6378 id: synthDockId(item.id),
6379 title: item.title,
6380 icon: item.icon,
6381 window: "",
6382 url: item.url || "",
6383 // Place synthesized dock-promoted icons after server-registered
6384 // ones. Stable ordering by source-list index inside the bucket.
6385 position: 2e3 + synthIndex++
6386 });
6387 }
6388 return out;
6389 }
6390 function applyOrder(items, order) {
6391 if (order.length === 0 || items.length <= 1) {
6392 return items;
6393 }
6394 const byId = /* @__PURE__ */ new Map();
6395 for (const item of items) {
6396 byId.set(item.id, item);
6397 }
6398 const out = [];
6399 const placed = /* @__PURE__ */ new Set();
6400 for (const id of order) {
6401 const item = byId.get(id);
6402 if (item) {
6403 out.push(item);
6404 placed.add(id);
6405 }
6406 }
6407 for (const item of items) {
6408 if (!placed.has(item.id)) {
6409 out.push(item);
6410 }
6411 }
6412 return out;
6413 }
6414 function html(strings, ...values) {
6415 return { __wpdHtml: true, strings, values };
6416 }
6417 function isTemplateResult(v) {
6418 return !!v && v.__wpdHtml === true;
6419 }
6420 const MARKER_PREFIX = "$$wpd$$";
6421 const MARKER_RE = /\$\$wpd\$\$(\d+)\$\$/g;
6422 function joinWithMarkers(strings) {
6423 let out = strings[0];
6424 for (let i = 1; i < strings.length; i++) {
6425 out += `${MARKER_PREFIX}${i - 1}$$` + strings[i];
6426 }
6427 return out;
6428 }
6429 const compiledCache = /* @__PURE__ */ new WeakMap();
6430 function compile(strings) {
6431 const cached = compiledCache.get(strings);
6432 if (cached) {
6433 return cached;
6434 }
6435 const template = document.createElement("template");
6436 template.innerHTML = joinWithMarkers(strings);
6437 const recipes = [];
6438 const walk2 = (node, path) => {
6439 if (node.nodeType === Node.ELEMENT_NODE) {
6440 const el = node;
6441 for (const attr of Array.from(el.attributes)) {
6442 const rawName = attr.name;
6443 const rawValue = attr.value;
6444 const prefix = rawName[0];
6445 if (MARKER_RE.test(rawValue)) {
6446 MARKER_RE.lastIndex = 0;
6447 if (prefix === "@") {
6448 const match = MARKER_RE.exec(rawValue);
6449 MARKER_RE.lastIndex = 0;
6450 recipes.push({
6451 path,
6452 kind: "event",
6453 name: rawName.slice(1),
6454 valueIndex: match ? Number(match[1]) : 0
6455 });
6456 el.removeAttribute(rawName);
6457 } else if (prefix === ".") {
6458 const match = MARKER_RE.exec(rawValue);
6459 MARKER_RE.lastIndex = 0;
6460 recipes.push({
6461 path,
6462 kind: "prop",
6463 name: rawName.slice(1),
6464 valueIndex: match ? Number(match[1]) : 0
6465 });
6466 el.removeAttribute(rawName);
6467 } else if (prefix === "?") {
6468 const match = MARKER_RE.exec(rawValue);
6469 MARKER_RE.lastIndex = 0;
6470 recipes.push({
6471 path,
6472 kind: "bool",
6473 name: rawName.slice(1),
6474 valueIndex: match ? Number(match[1]) : 0
6475 });
6476 el.removeAttribute(rawName);
6477 } else {
6478 const fragments = [];
6479 const indices = [];
6480 let lastEnd = 0;
6481 let m;
6482 MARKER_RE.lastIndex = 0;
6483 while ((m = MARKER_RE.exec(rawValue)) !== null) {
6484 fragments.push(rawValue.slice(lastEnd, m.index));
6485 indices.push(Number(m[1]));
6486 lastEnd = m.index + m[0].length;
6487 }
6488 fragments.push(rawValue.slice(lastEnd));
6489 recipes.push({
6490 path,
6491 kind: "attr",
6492 name: rawName,
6493 template: fragments,
6494 valueIndices: indices
6495 });
6496 el.setAttribute(rawName, "");
6497 }
6498 }
6499 }
6500 }
6501 const children = Array.from(node.childNodes);
6502 let shift = 0;
6503 for (let i = 0; i < children.length; i++) {
6504 const child = children[i];
6505 const liveIndex = i + shift;
6506 if (child.nodeType === Node.TEXT_NODE) {
6507 const text = child.textContent || "";
6508 if (!MARKER_RE.test(text)) {
6509 MARKER_RE.lastIndex = 0;
6510 continue;
6511 }
6512 MARKER_RE.lastIndex = 0;
6513 const parent = child.parentNode;
6514 let lastEnd = 0;
6515 let m;
6516 const newNodes = [];
6517 const newRecipes = [];
6518 MARKER_RE.lastIndex = 0;
6519 while ((m = MARKER_RE.exec(text)) !== null) {
6520 if (m.index > lastEnd) {
6521 newNodes.push(document.createTextNode(text.slice(lastEnd, m.index)));
6522 }
6523 const placeholder = document.createTextNode("");
6524 newNodes.push(placeholder);
6525 newRecipes.push({
6526 path: [...path, liveIndex + newNodes.length - 1],
6527 kind: "node",
6528 valueIndex: Number(m[1])
6529 });
6530 lastEnd = m.index + m[0].length;
6531 }
6532 if (lastEnd < text.length) {
6533 newNodes.push(document.createTextNode(text.slice(lastEnd)));
6534 }
6535 for (const nn of newNodes) {
6536 parent.insertBefore(nn, child);
6537 }
6538 parent.removeChild(child);
6539 shift += newNodes.length - 1;
6540 recipes.push(...newRecipes);
6541 } else {
6542 walk2(child, [...path, liveIndex]);
6543 }
6544 }
6545 };
6546 walk2(template.content, []);
6547 const buildParts = (fragment) => {
6548 const out = [];
6549 for (const r of recipes) {
6550 let node = fragment;
6551 for (const idx of r.path) {
6552 node = node.childNodes[idx];
6553 }
6554 if (r.kind === "node") {
6555 out.push({
6556 kind: "node",
6557 valueIndex: r.valueIndex,
6558 child: {
6559 anchor: node,
6560 state: null
6561 }
6562 });
6563 } else if (r.kind === "attr") {
6564 out.push({
6565 kind: "attr",
6566 element: node,
6567 name: r.name,
6568 template: r.template,
6569 valueIndices: r.valueIndices
6570 });
6571 } else if (r.kind === "event") {
6572 out.push({
6573 kind: "event",
6574 valueIndex: r.valueIndex,
6575 element: node,
6576 name: r.name
6577 });
6578 } else if (r.kind === "prop") {
6579 out.push({
6580 kind: "prop",
6581 valueIndex: r.valueIndex,
6582 element: node,
6583 name: r.name
6584 });
6585 } else if (r.kind === "bool") {
6586 out.push({
6587 kind: "bool",
6588 valueIndex: r.valueIndex,
6589 element: node,
6590 name: r.name
6591 });
6592 }
6593 }
6594 return out;
6595 };
6596 const entry = { template, buildParts };
6597 compiledCache.set(strings, entry);
6598 return entry;
6599 }
6600 const mountState = /* @__PURE__ */ new WeakMap();
6601 function render$1(result, container) {
6602 const existing = mountState.get(container);
6603 if (existing && existing.strings === result.strings) {
6604 applyValues(existing.parts, result.values);
6605 return;
6606 }
6607 const compiled = compile(result.strings);
6608 const fragment = compiled.template.content.cloneNode(true);
6609 const parts = compiled.buildParts(fragment);
6610 while (container.firstChild) {
6611 container.removeChild(container.firstChild);
6612 }
6613 container.appendChild(fragment);
6614 applyValues(parts, result.values);
6615 mountState.set(container, { strings: result.strings, parts });
6616 }
6617 function applyValues(parts, values) {
6618 for (const part of parts) {
6619 if (part.kind === "node") {
6620 updateChildPart(part.child, values[part.valueIndex]);
6621 } else if (part.kind === "attr") {
6622 let composed = part.template[0];
6623 for (let i = 0; i < part.valueIndices.length; i++) {
6624 composed += formatText(values[part.valueIndices[i]]);
6625 composed += part.template[i + 1];
6626 }
6627 if (composed !== part.last) {
6628 part.last = composed;
6629 if (composed === "") {
6630 part.element.removeAttribute(part.name);
6631 } else {
6632 part.element.setAttribute(part.name, composed);
6633 }
6634 }
6635 } else if (part.kind === "event") {
6636 const next = values[part.valueIndex];
6637 if (next !== part.current) {
6638 if (part.current) {
6639 part.element.removeEventListener(part.name, part.current);
6640 }
6641 if (next) {
6642 part.element.addEventListener(part.name, next);
6643 }
6644 part.current = next;
6645 }
6646 } else if (part.kind === "prop") {
6647 const next = values[part.valueIndex];
6648 if (next !== part.last) {
6649 part.last = next;
6650 part.element[part.name] = next;
6651 }
6652 } else if (part.kind === "bool") {
6653 const next = !!values[part.valueIndex];
6654 if (next !== part.last) {
6655 part.last = next;
6656 if (next) {
6657 part.element.setAttribute(part.name, "");
6658 } else {
6659 part.element.removeAttribute(part.name);
6660 }
6661 }
6662 }
6663 }
6664 }
6665 function updateChildPart(child, value) {
6666 if (value === null || value === void 0 || value === false) {
6667 if (child.state) {
6668 disposeChildState(child.state);
6669 child.state = null;
6670 }
6671 return;
6672 }
6673 if (Array.isArray(value)) {
6674 updateArrayChild(child, value);
6675 return;
6676 }
6677 if (isTemplateResult(value)) {
6678 updateTemplateChild(child, value);
6679 return;
6680 }
6681 if (value instanceof Node) {
6682 updateNodeChild(child, value);
6683 return;
6684 }
6685 updateTextChild(child, formatText(value));
6686 }
6687 function updateNodeChild(child, node) {
6688 const old = child.state;
6689 if (old?.shape === "node" && old.node === node) {
6690 return;
6691 }
6692 if (old) {
6693 disposeChildState(old);
6694 }
6695 insertBeforeAnchor(child, [node]);
6696 child.state = { shape: "node", node };
6697 }
6698 function updateTextChild(child, text) {
6699 const old = child.state;
6700 if (old?.shape === "text") {
6701 if (old.text !== text) {
6702 old.node.textContent = text;
6703 old.text = text;
6704 }
6705 return;
6706 }
6707 if (old) {
6708 disposeChildState(old);
6709 }
6710 const node = document.createTextNode(text);
6711 insertBeforeAnchor(child, [node]);
6712 child.state = { shape: "text", node, text };
6713 }
6714 function updateTemplateChild(child, result) {
6715 const old = child.state;
6716 if (old?.shape === "template" && old.strings === result.strings) {
6717 applyValues(old.parts, result.values);
6718 return;
6719 }
6720 if (old) {
6721 disposeChildState(old);
6722 }
6723 const compiled = compile(result.strings);
6724 const fragment = compiled.template.content.cloneNode(true);
6725 const parts = compiled.buildParts(fragment);
6726 const topNodes = Array.from(fragment.childNodes);
6727 insertBeforeAnchor(child, [fragment]);
6728 applyValues(parts, result.values);
6729 child.state = {
6730 shape: "template",
6731 strings: result.strings,
6732 parts,
6733 nodes: topNodes
6734 };
6735 }
6736 function updateArrayChild(child, arr) {
6737 const old = child.state;
6738 if (old?.shape === "array" && old.entries.length === arr.length) {
6739 for (let i = 0; i < arr.length; i++) {
6740 updateChildPart(old.entries[i], arr[i]);
6741 }
6742 return;
6743 }
6744 if (old) {
6745 disposeChildState(old);
6746 }
6747 const entries = [];
6748 for (const v of arr) {
6749 const entryAnchor = document.createTextNode("");
6750 insertBeforeAnchor(child, [entryAnchor]);
6751 const entry = { anchor: entryAnchor, state: null };
6752 updateChildPart(entry, v);
6753 entries.push(entry);
6754 }
6755 child.state = { shape: "array", entries };
6756 }
6757 function insertBeforeAnchor(child, nodes) {
6758 const parent = child.anchor.parentNode;
6759 if (!parent) {
6760 return;
6761 }
6762 for (const node of nodes) {
6763 parent.insertBefore(node, child.anchor);
6764 }
6765 }
6766 function disposeChildState(state2) {
6767 if (state2.shape === "text") {
6768 state2.node.remove();
6769 return;
6770 }
6771 if (state2.shape === "template") {
6772 for (const node of state2.nodes) {
6773 if (node.parentNode) {
6774 node.parentNode.removeChild(node);
6775 }
6776 }
6777 return;
6778 }
6779 if (state2.shape === "node") {
6780 if (state2.node.parentNode) {
6781 state2.node.parentNode.removeChild(state2.node);
6782 }
6783 return;
6784 }
6785 for (const entry of state2.entries) {
6786 if (entry.state) {
6787 disposeChildState(entry.state);
6788 }
6789 entry.anchor.remove();
6790 }
6791 }
6792 function formatText(v) {
6793 if (v === null || v === void 0 || v === false) {
6794 return "";
6795 }
6796 return String(v);
6797 }
6798 const _Component = class _Component extends HTMLElement {
6799 constructor() {
6800 super();
6801 this._renderScheduled = false;
6802 this._propValues = {};
6803 const ctor = this.constructor;
6804 if (ctor.shadow) {
6805 this.attachShadow({ mode: "open" });
6806 this._renderRoot = this.shadowRoot;
6807 } else {
6808 this._renderRoot = this;
6809 }
6810 this._installPropAccessors();
6811 }
6812 static get observedAttributes() {
6813 return this.props.map(kebab);
6814 }
6815 connectedCallback() {
6816 this._adoptStyles();
6817 this.requestUpdate();
6818 }
6819 attributeChangedCallback(name, oldValue, newValue) {
6820 if (oldValue === newValue) {
6821 return;
6822 }
6823 const prop2 = camel(name);
6824 this._propValues[prop2] = newValue;
6825 this.requestUpdate();
6826 }
6827 /**
6828 * Declarative class-name setter. Assign an array (or a
6829 * space-separated string) and the host's `class` attribute is
6830 * rewritten to match. Intended for programmatic styling — when
6831 * a plugin has enqueued its own stylesheet and wants to apply
6832 * one of those classes to a shell component:
6833 *
6834 * ```js
6835 * element.classNames = [ 'my-plugin-brand', 'is-active' ];
6836 * // → <wpd-select class="my-plugin-brand is-active">
6837 * ```
6838 *
6839 * The plain HTML `class="…"` attribute works just the same and
6840 * is always preferred when writing markup by hand — this setter
6841 * exists for the JS-API case where the caller has an array of
6842 * conditional classes in hand.
6843 *
6844 * Getter returns the current `classList` as a plain array for
6845 * symmetric read/write.
6846 *
6847 * @since 0.5.0
6848 */
6849 get classNames() {
6850 return Array.from(this.classList);
6851 }
6852 set classNames(next) {
6853 if (next === null || next === void 0) {
6854 this.removeAttribute("class");
6855 return;
6856 }
6857 const list2 = Array.isArray(next) ? next : String(next).split(/\s+/);
6858 const cleaned = list2.map((s) => String(s).trim()).filter((s) => s !== "");
6859 this.className = cleaned.join(" ");
6860 }
6861 /**
6862 * Request a re-render explicitly. Components rarely need this —
6863 * declare state via props + attribute observers and the render
6864 * loop picks up changes automatically.
6865 */
6866 requestUpdate() {
6867 this._scheduleRender();
6868 }
6869 /**
6870 * Dispatch a `CustomEvent` with a `detail`. Bubbles + composed
6871 * by default (matches typical WC UX — events cross shadow
6872 * boundaries, parents can listen without knowing about internal
6873 * structure).
6874 */
6875 emit(name, detail) {
6876 return this.dispatchEvent(
6877 new CustomEvent(name, {
6878 detail,
6879 bubbles: true,
6880 composed: true
6881 })
6882 );
6883 }
6884 // ------------------------------------------------------------------
6885 // Internals
6886 // ------------------------------------------------------------------
6887 /**
6888 * Wire every `static props` entry to a matched property getter +
6889 * setter on the element. Setting the property reflects into the
6890 * attribute (so downstream observers + CSS selectors see it);
6891 * reading the property falls back to the attribute.
6892 */
6893 _installPropAccessors() {
6894 const ctor = this.constructor;
6895 for (const prop2 of ctor.props) {
6896 if (Object.getOwnPropertyDescriptor(this, prop2)) {
6897 continue;
6898 }
6899 const attr = kebab(prop2);
6900 Object.defineProperty(this, prop2, {
6901 get: () => {
6902 if (prop2 in this._propValues) {
6903 return this._propValues[prop2];
6904 }
6905 return this.getAttribute(attr);
6906 },
6907 set: (value) => {
6908 let str2;
6909 if (value === null || value === void 0 || value === false) {
6910 str2 = null;
6911 } else if (value === true) {
6912 str2 = "";
6913 } else {
6914 str2 = String(value);
6915 }
6916 this._propValues[prop2] = str2;
6917 if (str2 === null) {
6918 this.removeAttribute(attr);
6919 } else {
6920 this.setAttribute(attr, str2);
6921 }
6922 this.requestUpdate();
6923 },
6924 enumerable: true,
6925 configurable: true
6926 });
6927 }
6928 }
6929 /**
6930 * Schedule a render on the next microtask. Multiple property
6931 * assignments in the same tick collapse into a single render.
6932 */
6933 _scheduleRender() {
6934 if (this._renderScheduled || !this.isConnected) {
6935 return;
6936 }
6937 this._renderScheduled = true;
6938 queueMicrotask(() => {
6939 this._renderScheduled = false;
6940 if (!this.isConnected) {
6941 return;
6942 }
6943 render$1(this.render(), this._renderRoot);
6944 });
6945 }
6946 /**
6947 * Mount adoptable stylesheets onto the shadow root (via
6948 * `adoptedStyleSheets`) or the light DOM (via one `<style>`
6949 * tag per def). No-op if `static styles` is empty.
6950 */
6951 _adoptStyles() {
6952 const ctor = this.constructor;
6953 if (ctor.styles.length === 0) {
6954 return;
6955 }
6956 if (ctor.shadow && this.shadowRoot) {
6957 const sheets = ctor.styles.map((s) => s.sheet).filter((s) => s !== null);
6958 this.shadowRoot.adoptedStyleSheets = sheets;
6959 if (sheets.length !== ctor.styles.length) {
6960 for (const s of ctor.styles) {
6961 if (!s.sheet) {
6962 const tag = document.createElement("style");
6963 tag.textContent = s.cssText;
6964 this.shadowRoot.appendChild(tag);
6965 }
6966 }
6967 }
6968 } else {
6969 this._adoptLightStyles(ctor);
6970 }
6971 }
6972 _adoptLightStyles(ctor) {
6973 if (_Component._lightStylesAdopted.has(ctor)) {
6974 return;
6975 }
6976 _Component._lightStylesAdopted.add(ctor);
6977 for (const s of ctor.styles) {
6978 const tag = document.createElement("style");
6979 tag.dataset.wpdUi = this.tagName.toLowerCase();
6980 tag.textContent = s.cssText;
6981 document.head.appendChild(tag);
6982 }
6983 }
6984 };
6985 _Component.props = [];
6986 _Component.styles = [];
6987 _Component.shadow = true;
6988 _Component._lightStylesAdopted = /* @__PURE__ */ new WeakSet();
6989 let Component = _Component;
6990 function defineComponent(tag, ctor) {
6991 if (customElements.get(tag)) {
6992 return;
6993 }
6994 customElements.define(tag, ctor);
6995 }
6996 function kebab(s) {
6997 return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
6998 }
6999 function camel(s) {
7000 return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
7001 }
7002 const SUPPORTS_CONSTRUCTABLE_SHEETS = (() => {
7003 try {
7004 const s = new CSSStyleSheet();
7005 return typeof s.replaceSync === "function";
7006 } catch {
7007 return false;
7008 }
7009 })();
7010 function css(strings, ...values) {
7011 let text = strings[0];
7012 for (let i = 1; i < strings.length; i++) {
7013 const v = values[i - 1];
7014 if (typeof v === "string" || typeof v === "number") {
7015 text += String(v);
7016 } else if (v && v.__wpdCss) {
7017 text += v.cssText;
7018 } else {
7019 throw new TypeError(
7020 "[wpd-ui] css`` interpolations must be strings, numbers, or other css`` results. Got: " + typeof v
7021 );
7022 }
7023 text += strings[i];
7024 }
7025 if (SUPPORTS_CONSTRUCTABLE_SHEETS) {
7026 const sheet = new CSSStyleSheet();
7027 sheet.replaceSync(text);
7028 return { __wpdCss: true, sheet, cssText: text };
7029 }
7030 return { __wpdCss: true, sheet: null, cssText: text };
7031 }
7032 function computeAutoId(element) {
7033 const parts = [];
7034 const tabs = [];
7035 let windowId = null;
7036 let node = element.parentElement;
7037 while (node) {
7038 if (node === document.body || node === document.documentElement) {
7039 break;
7040 }
7041 const id = node.id || "";
7042 if (id.startsWith("wp-window-")) {
7043 windowId = id.slice("wp-window-".length);
7044 break;
7045 }
7046 if (node.tagName.toLowerCase() === "wpd-tabpanel") {
7047 const forValue = node.getAttribute("for");
7048 if (forValue) {
7049 tabs.unshift(forValue);
7050 }
7051 }
7052 node = node.parentElement;
7053 }
7054 if (windowId) {
7055 parts.push(slugify(windowId));
7056 }
7057 for (const tab of tabs) {
7058 parts.push("tab-" + slugify(tab));
7059 }
7060 const label = element.getAttribute("label");
7061 if (label) {
7062 parts.push(slugify(label));
7063 }
7064 if (parts.length === 0) {
7065 return "wpd-unnamed";
7066 }
7067 return "wpd-" + parts.filter((p) => p !== "").join("-");
7068 }
7069 function slugify(s) {
7070 return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7071 }
7072 function ensureAutoId(element) {
7073 if (element.id) {
7074 return element.id;
7075 }
7076 const id = computeAutoId(element);
7077 element.id = id;
7078 return id;
7079 }
7080 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 )}`;
7081 const _WpdConfirmDialog = class _WpdConfirmDialog extends Component {
7082 constructor() {
7083 super(...arguments);
7084 this._onKey = (e) => {
7085 if (e.key === "Escape") {
7086 e.preventDefault();
7087 this._cancel();
7088 }
7089 if (e.key === "Enter" && !e.isComposing) {
7090 e.preventDefault();
7091 this._confirm();
7092 }
7093 };
7094 this._onBackdrop = (e) => {
7095 const path = e.composedPath();
7096 const original = path.length > 0 ? path[0] : e.target;
7097 if (original === this) {
7098 this._cancel();
7099 }
7100 };
7101 this._confirm = () => {
7102 this.emit("wpd-confirm", { confirmed: true });
7103 this.removeAttribute("open");
7104 };
7105 this._cancel = () => {
7106 this.emit("wpd-cancel", { confirmed: false });
7107 this.removeAttribute("open");
7108 };
7109 }
7110 connectedCallback() {
7111 super.connectedCallback();
7112 this.setAttribute("role", "dialog");
7113 this.setAttribute("aria-modal", "true");
7114 this.addEventListener("keydown", this._onKey);
7115 this.addEventListener("click", this._onBackdrop);
7116 }
7117 disconnectedCallback() {
7118 this.removeEventListener("keydown", this._onKey);
7119 this.removeEventListener("click", this._onBackdrop);
7120 }
7121 render() {
7122 const title = this.title ?? "";
7123 const message = this.message ?? "";
7124 const confirmLabel = this["confirm-label"] || "Confirm";
7125 const cancelLabel = this["cancel-label"] || "Cancel";
7126 const isDanger = this.hasAttribute("danger");
7127 const hideCancel = this.hasAttribute("hide-cancel");
7128 const isDismissable = this.hasAttribute("dismissable");
7129 return html`
7130 <div class="dialog" tabindex="-1">
7131 ${isDismissable ? html`<button
7132 type="button"
7133 class="close"
7134 aria-label="Close"
7135 @click=${() => this._cancel()}
7136 >&times;</button>` : html``}
7137 ${title ? html`<h2 class="title">${title}</h2>` : html``}
7138 ${message ? html`<p class="message">${message}</p>` : html``}
7139 <div class="actions">
7140 ${hideCancel ? html`` : html`<button
7141 type="button"
7142 class="btn btn--secondary"
7143 @click=${() => this._cancel()}
7144 >
7145 ${cancelLabel}
7146 </button>`}
7147 <button
7148 type="button"
7149 class="btn ${isDanger ? "btn--danger" : "btn--primary"}"
7150 @click=${() => this._confirm()}
7151 >
7152 ${confirmLabel}
7153 </button>
7154 </div>
7155 </div>
7156 `;
7157 }
7158 };
7159 _WpdConfirmDialog.props = [
7160 "open",
7161 "title",
7162 "message",
7163 "confirm-label",
7164 "cancel-label",
7165 "danger",
7166 "hide-cancel",
7167 "dismissable"
7168 ];
7169 _WpdConfirmDialog.styles = [dialogStyles];
7170 _WpdConfirmDialog.help = {
7171 title: "Confirm dialog",
7172 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.",
7173 status: "experimental",
7174 since: "0.9.0",
7175 props: [
7176 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
7177 { name: "title", type: "string", description: "Heading shown at the top." },
7178 { name: "message", type: "string", description: "Body copy. Newlines preserved." },
7179 { name: "confirm-label", type: "string", default: "Confirm", description: "Confirm-button label." },
7180 { name: "cancel-label", type: "string", default: "Cancel", description: "Cancel-button label." },
7181 { name: "danger", type: "boolean attribute", description: "Renders the confirm button red." },
7182 { 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." },
7183 { name: "dismissable", type: "boolean attribute", description: "Renders an X close button in the top-right corner. Click emits `wpd-cancel`." }
7184 ],
7185 events: [
7186 {
7187 name: "wpd-confirm",
7188 description: "Fires on confirm. Detail: `{ confirmed: true }`."
7189 },
7190 {
7191 name: "wpd-cancel",
7192 description: "Fires on cancel (Cancel button, Escape, backdrop click). Detail: `{ confirmed: false }`."
7193 }
7194 ]
7195 };
7196 let WpdConfirmDialog = _WpdConfirmDialog;
7197 defineComponent("wpd-confirm-dialog", WpdConfirmDialog);
7198 function wpdConfirm$1(options) {
7199 return new Promise((resolve2) => {
7200 const dialog2 = document.createElement("wpd-confirm-dialog");
7201 dialog2.setAttribute("open", "");
7202 if (options.title) {
7203 dialog2.setAttribute("title", options.title);
7204 }
7205 dialog2.setAttribute("message", options.message);
7206 if (options.confirmLabel) {
7207 dialog2.setAttribute("confirm-label", options.confirmLabel);
7208 }
7209 if (options.cancelLabel) {
7210 dialog2.setAttribute("cancel-label", options.cancelLabel);
7211 }
7212 if (options.danger) {
7213 dialog2.setAttribute("danger", "");
7214 }
7215 if (options.hideCancel) {
7216 dialog2.setAttribute("hide-cancel", "");
7217 }
7218 if (options.dismissable) {
7219 dialog2.setAttribute("dismissable", "");
7220 }
7221 const cleanup = (ok) => {
7222 dialog2.remove();
7223 resolve2(ok);
7224 };
7225 dialog2.addEventListener("wpd-confirm", () => cleanup(true));
7226 dialog2.addEventListener("wpd-cancel", () => cleanup(false));
7227 document.body.appendChild(dialog2);
7228 const inner = dialog2.shadowRoot?.querySelector(".dialog");
7229 (inner ?? dialog2).focus?.();
7230 });
7231 }
7232 const FALLBACK_BASE = "http://localhost/";
7233 function joinRestUrl(restRoot2, path) {
7234 const base = typeof window !== "undefined" && window.location ? window.location.href : FALLBACK_BASE;
7235 const url = new URL(restRoot2, base);
7236 const trimmed = path.replace(/^\/+/, "");
7237 const queryAt = trimmed.indexOf("?");
7238 const route = queryAt === -1 ? trimmed : trimmed.slice(0, queryAt);
7239 const extraQuery = queryAt === -1 ? "" : trimmed.slice(queryAt + 1);
7240 if (url.searchParams.has("rest_route")) {
7241 const existing = url.searchParams.get("rest_route") ?? "/";
7242 const prefix = existing.endsWith("/") ? existing : existing + "/";
7243 url.searchParams.set("rest_route", prefix + route);
7244 } else {
7245 const pathname = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
7246 url.pathname = pathname + route;
7247 }
7248 if (extraQuery) {
7249 const extras = new URLSearchParams(extraQuery);
7250 extras.forEach((value, key) => {
7251 url.searchParams.append(key, value);
7252 });
7253 }
7254 return url.toString();
7255 }
7256 function getApi() {
7257 const w = window;
7258 return w.wp?.desktop ?? null;
7259 }
7260 let activeMenu$3 = null;
7261 function closeMenu$1() {
7262 if (activeMenu$3) {
7263 activeMenu$3.remove();
7264 activeMenu$3 = null;
7265 }
7266 }
7267 function writeVisibility(canonicalId, placement) {
7268 const api = getApi();
7269 if (!api?.getOsSettings || !api?.updateOsSettings) {
7270 return;
7271 }
7272 const snap = api.getOsSettings();
7273 const next = { ...snap.itemVisibility };
7274 next[canonicalId] = placement;
7275 api.updateOsSettings({ itemVisibility: next });
7276 }
7277 function railFromId(id, surface) {
7278 if (id.startsWith("dock:")) {
7279 return "dock";
7280 }
7281 if (id.startsWith("desktop:")) {
7282 return "desktop";
7283 }
7284 return surface;
7285 }
7286 function computeHideTarget(canonicalId, nativeRail, hideSurface, visibility) {
7287 const current = resolvePlacement(canonicalId, nativeRail, visibility);
7288 if (current === "both") {
7289 return hideSurface === "dock" ? "desktop" : "dock";
7290 }
7291 return "hidden";
7292 }
7293 let openGeneration$2 = 0;
7294 function openItemVisibilityMenu(opts) {
7295 closeMenu$1();
7296 const myGen = ++openGeneration$2;
7297 openWithShellOverlays(
7298 () => myGen === openGeneration$2,
7299 () => openItemVisibilityMenuImmediate(opts)
7300 );
7301 }
7302 function openItemVisibilityMenuImmediate(opts) {
7303 closeMenu$1();
7304 const canonical = canonicalItemId(opts.id);
7305 const nativeRail = railFromId(opts.id, opts.surface);
7306 const currentPlacement = resolvePlacement(
7307 canonical,
7308 nativeRail,
7309 getApi()?.getOsSettings?.().itemVisibility ?? {}
7310 );
7311 const options = [];
7312 if (opts.surface === "dock") {
7313 options.push({
7314 id: "hide-from-dock",
7315 label: __("Hide from dock"),
7316 icon: "dashicons-hidden",
7317 onPick: () => writeVisibility(
7318 canonical,
7319 computeHideTarget(
7320 canonical,
7321 nativeRail,
7322 "dock",
7323 getApi()?.getOsSettings?.().itemVisibility ?? {}
7324 )
7325 )
7326 });
7327 if (currentPlacement !== "both") {
7328 options.push({
7329 id: "show-on-desktop-too",
7330 label: __("Also show on desktop"),
7331 icon: "dashicons-desktop",
7332 onPick: () => writeVisibility(canonical, "both")
7333 });
7334 }
7335 } else {
7336 options.push({
7337 id: "hide-from-desktop",
7338 label: __("Hide from desktop"),
7339 icon: "dashicons-hidden",
7340 onPick: () => writeVisibility(
7341 canonical,
7342 computeHideTarget(
7343 canonical,
7344 nativeRail,
7345 "desktop",
7346 getApi()?.getOsSettings?.().itemVisibility ?? {}
7347 )
7348 )
7349 });
7350 if (currentPlacement !== "both") {
7351 options.push({
7352 id: "show-on-dock-too",
7353 label: __("Also show on dock"),
7354 icon: "dashicons-menu",
7355 onPick: () => writeVisibility(canonical, "both")
7356 });
7357 }
7358 }
7359 options.push({
7360 id: "hide-everywhere",
7361 label: __("Hide everywhere"),
7362 icon: "dashicons-no",
7363 danger: true,
7364 onPick: () => writeVisibility(canonical, "hidden")
7365 });
7366 options.push({
7367 id: "open-settings",
7368 label: __("Apps & Icons settings…"),
7369 icon: "dashicons-admin-generic",
7370 onPick: () => {
7371 const api = getApi();
7372 api?.openOsSettings?.({ tabId: "apps-icons" });
7373 }
7374 });
7375 if (opts.pluginFile) {
7376 const pluginFile = opts.pluginFile;
7377 const pluginLabel = opts.pluginName || opts.title;
7378 options.push({ kind: "separator" });
7379 options.push({
7380 id: "deactivate-plugin",
7381 // translators: %s is the owning plugin's display name.
7382 label: sprintf(__("Deactivate %s…"), pluginLabel),
7383 icon: "dashicons-trash",
7384 danger: true,
7385 onPick: () => {
7386 void confirmAndDeactivatePlugin(pluginFile, pluginLabel);
7387 }
7388 });
7389 }
7390 const menu = document.createElement("wpd-context-menu");
7391 menu.setAttribute("open", "");
7392 menu.classList.add("desktop-mode-item-visibility-menu");
7393 menu.dataset.itemId = opts.id;
7394 menu.style.position = "fixed";
7395 menu.style.left = "-9999px";
7396 menu.style.top = "-9999px";
7397 menu.style.visibility = "hidden";
7398 menu.style.zIndex = "1000000";
7399 const byKey = /* @__PURE__ */ new Map();
7400 for (const opt of options) {
7401 if (opt.kind === "separator") {
7402 const hr = document.createElement("hr");
7403 hr.style.cssText = "border: 0; border-top: 1px solid var( --wpd-context-menu-separator-color, rgba(255,255,255,0.12) ); margin: 4px 6px;";
7404 menu.appendChild(hr);
7405 continue;
7406 }
7407 byKey.set(opt.id, opt);
7408 const node = document.createElement("wpd-context-menu-option");
7409 node.dataset.menuItemId = opt.id;
7410 node.setAttribute("value", opt.id);
7411 if (opt.icon) {
7412 node.setAttribute("icon", opt.icon);
7413 }
7414 if (opt.danger) {
7415 node.setAttribute("danger", "");
7416 }
7417 node.textContent = opt.label;
7418 menu.appendChild(node);
7419 }
7420 menu.addEventListener("wpd-context-menu-pick", (e) => {
7421 const detail = e.detail;
7422 const key = detail?.id || detail?.value || "";
7423 const opt = byKey.get(key);
7424 closeMenu$1();
7425 try {
7426 opt?.onPick();
7427 } catch {
7428 }
7429 });
7430 document.body.appendChild(menu);
7431 activeMenu$3 = menu;
7432 const positionMenu = () => {
7433 if (menu !== activeMenu$3) {
7434 return;
7435 }
7436 const rect = menu.getBoundingClientRect();
7437 const margin = 8;
7438 let left = opts.x;
7439 let top;
7440 if (opts.surface === "dock") {
7441 top = Math.max(margin, opts.y - rect.height - margin);
7442 } else {
7443 top = opts.y;
7444 if (top + rect.height + margin > window.innerHeight) {
7445 top = Math.max(margin, opts.y - rect.height);
7446 }
7447 }
7448 if (left + rect.width + margin > window.innerWidth) {
7449 left = Math.max(margin, opts.x - rect.width);
7450 }
7451 menu.style.left = `${left}px`;
7452 menu.style.top = `${top}px`;
7453 menu.style.visibility = "";
7454 };
7455 requestAnimationFrame(positionMenu);
7456 const onOutside = (ev) => {
7457 if (!activeMenu$3) {
7458 return;
7459 }
7460 if (!activeMenu$3.contains(ev.target)) {
7461 closeMenu$1();
7462 document.removeEventListener("mousedown", onOutside, true);
7463 document.removeEventListener("keydown", onKey, true);
7464 }
7465 };
7466 const onKey = (ev) => {
7467 if (ev.key === "Escape") {
7468 closeMenu$1();
7469 document.removeEventListener("mousedown", onOutside, true);
7470 document.removeEventListener("keydown", onKey, true);
7471 }
7472 };
7473 document.addEventListener("mousedown", onOutside, true);
7474 document.addEventListener("keydown", onKey, true);
7475 }
7476 async function confirmAndDeactivatePlugin(pluginFile, title) {
7477 const confirmed = await wpdConfirm$1({
7478 /* translators: %s: plugin title. */
7479 title: sprintf(__("Deactivate %s?"), title),
7480 message: __(
7481 "This plugin will stop running on the site. You can re-activate it later from the Plugins screen."
7482 ),
7483 confirmLabel: __("Deactivate"),
7484 cancelLabel: __("Cancel"),
7485 danger: true
7486 });
7487 if (!confirmed) {
7488 return;
7489 }
7490 const cfg = window.desktopModeConfig ?? {};
7491 const restRoot2 = typeof cfg.restRoot === "string" && cfg.restRoot ? cfg.restRoot : `${window.location.origin}/wp-json/`;
7492 const restNonce = typeof cfg.restNonce === "string" && cfg.restNonce ? cfg.restNonce : "";
7493 const stripped = pluginFile.endsWith(".php") ? pluginFile.slice(0, -4) : pluginFile;
7494 const encoded = stripped.split("/").map(encodeURIComponent).join("/");
7495 const url = joinRestUrl(restRoot2, `wp/v2/plugins/${encoded}`);
7496 try {
7497 const res = await trackedFetch$1(
7498 url,
7499 {
7500 method: "PUT",
7501 headers: {
7502 "Content-Type": "application/json",
7503 "X-WP-Nonce": restNonce
7504 },
7505 body: JSON.stringify({ status: "inactive" }),
7506 credentials: "same-origin"
7507 },
7508 { source: "desktop-mode/dock-deactivate-plugin" }
7509 );
7510 if (!res.ok) {
7511 throw new Error(`HTTP ${res.status}`);
7512 }
7513 } catch (err) {
7514 showToast({
7515 message: sprintf(
7516 /* translators: %s: plugin title. */
7517 __("Could not deactivate %s."),
7518 title
7519 ),
7520 duration: 4e3
7521 });
7522 console.error("[desktop-mode] deactivate plugin failed", err);
7523 return;
7524 }
7525 const closedTitles = closeWindowsForPlugin(pluginFile);
7526 const deactivatedMsg = closedTitles.length > 0 ? sprintf(
7527 /* translators: 1: plugin title. 2: number of windows that were closed. */
7528 __("%1$s deactivated. Closed %2$d window(s)."),
7529 title,
7530 closedTitles.length
7531 ) : sprintf(
7532 /* translators: %s: plugin title. */
7533 __("%s deactivated."),
7534 title
7535 );
7536 showToast({ message: deactivatedMsg, duration: 3e3 });
7537 const w = window;
7538 w.wp?.desktop?.refreshMenu?.();
7539 }
7540 function closeWindowsForPlugin(pluginFile) {
7541 const api = window.wp?.desktop;
7542 if (!api?.windowManager?.getAll) {
7543 return [];
7544 }
7545 const items = api.getMenuItems?.() ?? [];
7546 const owned = items.filter((i) => i.pluginFile === pluginFile);
7547 if (owned.length === 0) {
7548 return [];
7549 }
7550 const ownedKeys = /* @__PURE__ */ new Set();
7551 for (const item of owned) {
7552 ownedKeys.add(item.id);
7553 if (api.deriveWindowId) {
7554 ownedKeys.add(api.deriveWindowId(item.url));
7555 }
7556 }
7557 const toClose = /* @__PURE__ */ new Map();
7558 const windows = api.windowManager.getAll() ?? [];
7559 const derive = api.deriveWindowId;
7560 for (const w of windows) {
7561 if (ownedKeys.has(w.id)) {
7562 toClose.set(w.id, w);
7563 continue;
7564 }
7565 if (w.config?.baseId && ownedKeys.has(w.config.baseId)) {
7566 toClose.set(w.id, w);
7567 continue;
7568 }
7569 if (derive && w.config?.url) {
7570 const derivedFromConfig = derive(w.config.url);
7571 if (ownedKeys.has(derivedFromConfig)) {
7572 toClose.set(w.id, w);
7573 continue;
7574 }
7575 }
7576 if (derive && w.iframe) {
7577 let liveUrl = "";
7578 try {
7579 liveUrl = w.iframe.src || "";
7580 } catch {
7581 }
7582 if (liveUrl) {
7583 const derivedFromLive = derive(liveUrl);
7584 if (ownedKeys.has(derivedFromLive)) {
7585 toClose.set(w.id, w);
7586 }
7587 }
7588 }
7589 }
7590 const titles = [];
7591 for (const w of toClose.values()) {
7592 titles.push(w.config?.title ?? w.id);
7593 try {
7594 w.close();
7595 } catch {
7596 }
7597 }
7598 return titles;
7599 }
7600 const _Dock = class _Dock {
7601 constructor(container, windowManager, items, adminUrl, orientation = "left") {
7602 this.itemElements = /* @__PURE__ */ new Map();
7603 this.systemItems = [];
7604 this.systemItemElements = /* @__PURE__ */ new Map();
7605 this.systemSeparator = null;
7606 this.badgeOverrides = /* @__PURE__ */ new Map();
7607 this.attentionTimers = /* @__PURE__ */ new Map();
7608 this.peekTeardowns = /* @__PURE__ */ new Map();
7609 this.boundRefresh = () => void 0;
7610 this.container = container;
7611 this.windowManager = windowManager;
7612 this.items = items;
7613 this.adminUrl = adminUrl;
7614 this.orientation = orientation;
7615 this.rail = orientation === "bottom" ? "taskbar" : "dock";
7616 this.hooksNamespace = `desktop-mode/dock/${++_Dock.instanceCounter}`;
7617 this.container.setAttribute(
7618 "data-desktop-mode-dock-placement",
7619 orientation
7620 );
7621 const scroll = document.createElement("div");
7622 scroll.className = "desktop-mode-dock__scroll";
7623 const pinned = document.createElement("div");
7624 pinned.className = "desktop-mode-dock__pinned";
7625 container.appendChild(scroll);
7626 container.appendChild(pinned);
7627 this.itemHost = scroll;
7628 this.systemHost = pinned;
7629 this.tooltip = document.createElement("div");
7630 this.tooltip.className = "desktop-mode-dock__tooltip";
7631 this.tooltip.setAttribute("role", "tooltip");
7632 if (orientation === "bottom") {
7633 this.tooltip.classList.add("desktop-mode-dock__tooltip--above");
7634 } else if (orientation === "right") {
7635 this.tooltip.classList.add("desktop-mode-dock__tooltip--before");
7636 } else {
7637 this.tooltip.classList.add("desktop-mode-dock__tooltip--after");
7638 }
7639 document.body.appendChild(this.tooltip);
7640 this.render();
7641 this.bindWindowEvents();
7642 }
7643 /**
7644 * Build the base context object every dock decoration hook
7645 * receives. Read from `this` so a single subscriber can
7646 * disambiguate two coexisting rails by `dockId`.
7647 */
7648 buildHookContextBase() {
7649 return {
7650 rail: this.rail,
7651 orientation: this.orientation,
7652 dockId: this.container.id,
7653 container: this.container
7654 };
7655 }
7656 /**
7657 * Replace the menu-derived tile list with a fresh one, preserving
7658 * any JS-registered system tiles. Used by the live menu-refresh
7659 * path: after a plugin is activated or deactivated, the chromeless
7660 * bridge postMessages a fresh payload built from real admin
7661 * context, and the shell calls this so the dock repaints without
7662 * a tab reload.
7663 *
7664 * Old menu tiles are removed from both the DOM and the lookup
7665 * map; new tiles are inserted before the system separator (or
7666 * appended at the end if none exists yet), so the menu-items →
7667 * hairline → system-items ordering stays intact. Active-state
7668 * classes are re-computed once the new tiles are in place so
7669 * window indicators survive the swap.
7670 *
7671 * @param items New DockItem list. Pass `[]` to clear everything
7672 * menu-derived.
7673 */
7674 /**
7675 * Update the dock's orientation. Writes the new value to the
7676 * dock element's `data-desktop-mode-dock-placement` attribute (CSS
7677 * keys off it for layout) and keeps the tooltip anchor in sync.
7678 *
7679 * In practice, the layout dispatcher in `desktop.ts` rebuilds the
7680 * dock(s) from scratch on a layout change rather than re-orienting
7681 * a live instance — but this stays correct in case any caller
7682 * wants to flip orientation without the rebuild.
7683 */
7684 setOrientation(orientation) {
7685 if (this.orientation === orientation) {
7686 return;
7687 }
7688 this.orientation = orientation;
7689 this.container.setAttribute(
7690 "data-desktop-mode-dock-placement",
7691 orientation
7692 );
7693 this.tooltip.classList.remove(
7694 "desktop-mode-dock__tooltip--above",
7695 "desktop-mode-dock__tooltip--before",
7696 "desktop-mode-dock__tooltip--after"
7697 );
7698 if (orientation === "bottom") {
7699 this.tooltip.classList.add("desktop-mode-dock__tooltip--above");
7700 } else if (orientation === "right") {
7701 this.tooltip.classList.add("desktop-mode-dock__tooltip--before");
7702 } else {
7703 this.tooltip.classList.add("desktop-mode-dock__tooltip--after");
7704 }
7705 }
7706 replaceItems(items) {
7707 for (const itemId of this.itemElements.keys()) {
7708 const teardown = this.peekTeardowns.get(itemId);
7709 if (teardown) {
7710 teardown();
7711 this.peekTeardowns.delete(itemId);
7712 }
7713 }
7714 for (const el of this.itemElements.values()) {
7715 el.remove();
7716 }
7717 this.itemHost.querySelectorAll(
7718 ".desktop-mode-dock__separator--group"
7719 ).forEach((el) => el.remove());
7720 this.itemElements.clear();
7721 this.items = items;
7722 const base = this.buildHookContextBase();
7723 doAction(HOOKS.DOCK_BEFORE_RENDER, {
7724 ...base,
7725 items,
7726 tileElements: this.itemElements
7727 });
7728 let insertedGroupSeparator = false;
7729 let tilesInsertedThisPass = 0;
7730 for (const item of items) {
7731 if (!insertedGroupSeparator && item.isCore === false) {
7732 if (tilesInsertedThisPass > 0) {
7733 const sep = document.createElement("div");
7734 sep.className = "desktop-mode-dock__separator desktop-mode-dock__separator--group";
7735 sep.setAttribute("aria-hidden", "true");
7736 this.itemHost.appendChild(sep);
7737 }
7738 insertedGroupSeparator = true;
7739 }
7740 const btn = this.createItemButton(item);
7741 this.itemElements.set(item.id, btn);
7742 this.itemHost.appendChild(btn);
7743 tilesInsertedThisPass++;
7744 const override = this.badgeOverrides.get(item.id);
7745 if (override !== void 0) {
7746 const primary = btn.querySelector(
7747 ".desktop-mode-dock__item-primary"
7748 );
7749 _applyBadgeNode(primary ?? btn, override);
7750 }
7751 doAction(HOOKS.DOCK_TILE_RENDERED, {
7752 ...base,
7753 item,
7754 isSystem: false,
7755 el: btn
7756 });
7757 }
7758 this.updateActiveStates();
7759 doAction(HOOKS.DOCK_AFTER_RENDER, {
7760 ...base,
7761 items,
7762 tileElements: this.itemElements
7763 });
7764 }
7765 /**
7766 * True when the rail currently has ANY renderable tile —
7767 * either a menu-derived item or a JS-registered system item.
7768 * Lets callers (the shell's live-refresh path) decide whether
7769 * to hide the whole rail without having to peek into two
7770 * internal maps. "System tiles keep the rail alive even when
7771 * menu items are empty" is the user-visible contract we enforce.
7772 */
7773 hasItems() {
7774 return this.itemElements.size > 0 || this.systemItemElements.size > 0;
7775 }
7776 /**
7777 * Remove a previously-registered system item. Used by the
7778 * server-driven native-window sync path — when a plugin is
7779 * deactivated, its native-window entry disappears from the
7780 * server's payload and the shell calls this to pull the tile
7781 * back off the rail without a reload.
7782 *
7783 * Idempotent: an unknown id is a silent no-op. The system
7784 * separator is kept in place as long as at least one system
7785 * item remains; removing the last system item also strips the
7786 * separator so the rail doesn't dangle a divider under nothing.
7787 */
7788 removeSystemItem(id) {
7789 const tile2 = this.systemItemElements.get(id);
7790 if (!tile2) {
7791 return;
7792 }
7793 tile2.remove();
7794 this.systemItemElements.delete(id);
7795 this.systemItems = this.systemItems.filter((s) => s.id !== id);
7796 this.badgeOverrides.delete(id);
7797 if (this.systemItemElements.size === 0 && this.systemSeparator) {
7798 this.systemSeparator.remove();
7799 this.systemSeparator = null;
7800 }
7801 doAction(HOOKS.DOCK_ITEM_REMOVED, { id, placement: this.rail });
7802 }
7803 /**
7804 * Set the badge count on a tile. Live-updates without a full
7805 * dock re-render — the existing tile's badge node is mutated in
7806 * place (or created if missing). Pass `0` to remove the badge.
7807 *
7808 * Resolves the tile in id order: menu items (`data-menu-slug`)
7809 * first, then system items (`data-system-id`), so callers can
7810 * use the same id surface regardless of which rail the tile
7811 * happens to live on.
7812 *
7813 * Idempotent: applying the same count is a no-op (no DOM mutation).
7814 *
7815 * @since 0.6.0
7816 *
7817 * @param itemId Tile id (menu slug for admin pages, system id
7818 * for `appendSystemItem` / `registerSystemTile`).
7819 * @param count Non-negative integer. `>99` renders as `99+`.
7820 */
7821 setBadge(itemId, count) {
7822 const tile2 = this._resolveTileElement(itemId);
7823 if (!tile2) {
7824 return;
7825 }
7826 const safe = Math.max(0, Math.floor(Number(count) || 0));
7827 if (safe === 0) {
7828 this.badgeOverrides.delete(itemId);
7829 } else {
7830 this.badgeOverrides.set(itemId, safe);
7831 }
7832 const primary = tile2.querySelector(
7833 ".desktop-mode-dock__item-primary"
7834 );
7835 _applyBadgeNode(primary ?? tile2, safe);
7836 activity.publish("desktop-mode/badge-changed", {
7837 itemId,
7838 count: safe,
7839 rail: this.rail
7840 });
7841 }
7842 /**
7843 * Clear the badge on a tile. Equivalent to `setBadge( id, 0 )`.
7844 *
7845 * @since 0.6.0
7846 */
7847 clearBadge(itemId) {
7848 this.setBadge(itemId, 0);
7849 }
7850 /**
7851 * Apply or clear an attention animation on a tile.
7852 *
7853 * - `'pulse'` — soft halo + scale, ~1.4 s loop. Default.
7854 * - `'shake'` — short horizontal jiggle.
7855 * - `'bounce'` — vertical bob, attention-grabbing.
7856 * - `null` — clear any active attention.
7857 *
7858 * Animations are gated on `prefers-reduced-motion: no-preference`;
7859 * the reduced-motion fallback shows a static accent ring for the
7860 * same duration so the affordance still works. `durationMs` of
7861 * `0` keeps the attention until the next call clears it.
7862 *
7863 * @since 0.6.0
7864 *
7865 * @param itemId Tile id.
7866 * @param mode Animation mode or `null` to clear.
7867 * @param opts Optional duration / intensity overrides.
7868 */
7869 setAttention(itemId, mode, opts = {}) {
7870 const tile2 = this._resolveTileElement(itemId);
7871 if (!tile2) {
7872 return;
7873 }
7874 const pending2 = this.attentionTimers.get(itemId);
7875 if (pending2 !== void 0) {
7876 window.clearTimeout(pending2);
7877 this.attentionTimers.delete(itemId);
7878 }
7879 tile2.classList.remove(
7880 "desktop-mode-dock__item--attention-pulse",
7881 "desktop-mode-dock__item--attention-shake",
7882 "desktop-mode-dock__item--attention-bounce",
7883 "desktop-mode-dock__item--intensity-subtle",
7884 "desktop-mode-dock__item--intensity-normal",
7885 "desktop-mode-dock__item--intensity-strong"
7886 );
7887 if (mode === null) {
7888 return;
7889 }
7890 tile2.classList.add(`desktop-mode-dock__item--attention-${mode}`);
7891 const intensity = opts.intensity ?? "normal";
7892 tile2.classList.add(`desktop-mode-dock__item--intensity-${intensity}`);
7893 const duration = opts.durationMs ?? 4e3;
7894 if (duration > 0) {
7895 const handle = window.setTimeout(() => {
7896 this.attentionTimers.delete(itemId);
7897 this.setAttention(itemId, null);
7898 }, duration);
7899 this.attentionTimers.set(itemId, handle);
7900 }
7901 }
7902 /**
7903 * Resolve a tile element by id — checks menu items first
7904 * (`data-menu-slug`), then system items (`data-system-id`). Used
7905 * by `setBadge` / `setAttention` so callers can reach either rail
7906 * with one id surface.
7907 */
7908 _resolveTileElement(itemId) {
7909 return this.itemElements.get(itemId) ?? this.systemItemElements.get(itemId) ?? null;
7910 }
7911 /**
7912 * Append a JS-registered system item to the dock.
7913 *
7914 * System items render after the menu-derived items, separated by a
7915 * hairline divider. Use for shell affordances that don't live in
7916 * the admin menu: OS Settings today, Jorvy and desktop widgets
7917 * later. Callers supply their own `onOpen` — the dock doesn't
7918 * assume the item opens a window at all.
7919 */
7920 appendSystemItem(item) {
7921 this.systemItems.push(item);
7922 if (!this.systemSeparator) {
7923 this.systemSeparator = document.createElement("div");
7924 this.systemSeparator.className = "desktop-mode-dock__separator";
7925 this.systemSeparator.setAttribute("aria-hidden", "true");
7926 this.systemHost.appendChild(this.systemSeparator);
7927 }
7928 const tile2 = this.createSystemItemButton(item);
7929 this.systemItemElements.set(item.id, tile2);
7930 this.systemHost.appendChild(tile2);
7931 this.updateActiveStates();
7932 doAction(HOOKS.DOCK_TILE_RENDERED, {
7933 ...this.buildHookContextBase(),
7934 item,
7935 isSystem: true,
7936 el: tile2
7937 });
7938 }
7939 /**
7940 * Render the dock contents.
7941 *
7942 * Items are ordered server-side with core WordPress menus first and
7943 * plugin-contributed menus after. We insert a `--group` separator
7944 * at the first core→plugin transition so the two clusters read as
7945 * distinct groups of tiles — "default apps" and "installed apps"
7946 * in macOS-dock parlance. The separator is skipped when the menu
7947 * contains only one kind (no plugin menus, or a theme's filter
7948 * reordered everything into one class).
7949 */
7950 render() {
7951 if (_Dock.activeDragReset) {
7952 const prev = _Dock.activeDragReset;
7953 _Dock.activeDragReset = null;
7954 prev();
7955 }
7956 for (const teardown of this.peekTeardowns.values()) {
7957 teardown();
7958 }
7959 this.peekTeardowns.clear();
7960 this.itemHost.innerHTML = "";
7961 const base = this.buildHookContextBase();
7962 doAction(HOOKS.DOCK_BEFORE_RENDER, {
7963 ...base,
7964 items: this.items,
7965 tileElements: this.itemElements
7966 });
7967 let insertedGroupSeparator = false;
7968 for (const item of this.items) {
7969 if (!insertedGroupSeparator && item.isCore === false) {
7970 if (this.itemHost.childElementCount > 0) {
7971 const sep = document.createElement("div");
7972 sep.className = "desktop-mode-dock__separator desktop-mode-dock__separator--group";
7973 sep.setAttribute("aria-hidden", "true");
7974 this.itemHost.appendChild(sep);
7975 }
7976 insertedGroupSeparator = true;
7977 }
7978 const btn = this.createItemButton(item);
7979 this.itemElements.set(item.id, btn);
7980 this.itemHost.appendChild(btn);
7981 doAction(HOOKS.DOCK_TILE_RENDERED, {
7982 ...base,
7983 item,
7984 isSystem: false,
7985 el: btn
7986 });
7987 }
7988 doAction(HOOKS.DOCK_AFTER_RENDER, {
7989 ...base,
7990 items: this.items,
7991 tileElements: this.itemElements
7992 });
7993 }
7994 /**
7995 * Create a tile for a JS-registered system item. Structurally simpler
7996 * than a menu tile — no submenu, no multi-instance rail, no badge —
7997 * but uses the same base classes so the hover / focus / active
7998 * styling is shared.
7999 */
8000 createSystemItemButton(item) {
8001 const ctx = {
8002 ...this.buildHookContextBase(),
8003 item,
8004 isSystem: true
8005 };
8006 const tile2 = document.createElement("div");
8007 const baseClasses = [
8008 "desktop-mode-dock__item",
8009 "desktop-mode-dock__item--system"
8010 ];
8011 const filteredClasses = applyFilters(
8012 HOOKS.DOCK_TILE_CLASS,
8013 baseClasses,
8014 ctx
8015 );
8016 tile2.className = filteredClasses.join(" ");
8017 tile2.dataset.systemId = item.id;
8018 const primary = document.createElement("button");
8019 primary.className = "desktop-mode-dock__item-primary";
8020 primary.setAttribute("type", "button");
8021 primary.setAttribute("aria-label", item.title);
8022 primary.appendChild(this.resolveIcon(item.icon, item.title));
8023 primary.addEventListener("click", () => item.onOpen());
8024 tile2.appendChild(primary);
8025 this.bindTooltipFiltered(tile2, item.title, ctx);
8026 const teardown = attachDockPeek({
8027 tile: tile2,
8028 item: {
8029 id: item.id,
8030 title: item.title,
8031 icon: item.icon,
8032 url: ""
8033 },
8034 // System tiles target a single native-window id; that id
8035 // is also the baseId the manager stores duplicates under
8036 // when the user opens additional instances via the Ghost
8037 // Card. `getAllByBaseId` returns `[]` / `[one]` for the
8038 // singleton cases and the full set when a multi-capable
8039 // system tile (`multi: true`) has been duplicated.
8040 getInstances: () => this.windowManager.getAllByBaseIdOnActiveDesktop(item.id),
8041 enableGhost: !!item.multi,
8042 windowManager: this.windowManager,
8043 getOrientation: () => this.orientation,
8044 openNew: () => {
8045 const fn = item.onOpenNew ?? item.onOpen;
8046 fn();
8047 },
8048 suppressTooltip: (on) => {
8049 if (on) {
8050 this.tooltip.classList.remove(
8051 "desktop-mode-dock__tooltip--visible"
8052 );
8053 }
8054 }
8055 });
8056 this.peekTeardowns.set(`system:${item.id}`, teardown);
8057 return applyFilters(
8058 HOOKS.DOCK_TILE_ELEMENT,
8059 tile2,
8060 ctx
8061 );
8062 }
8063 /**
8064 * Create a single dock icon tile.
8065 *
8066 * A tile is a vertical stack: the primary icon button, plus — for
8067 * multi-capable pages — an instance rail rendered below it showing one
8068 * dot per open window and a trailing "+" to open another. The rail is
8069 * hydrated by {@link updateActiveStates}; here we only place the empty
8070 * container so the DOM is stable.
8071 */
8072 createItemButton(item) {
8073 const ctx = {
8074 ...this.buildHookContextBase(),
8075 item,
8076 isSystem: false
8077 };
8078 const tile2 = document.createElement("div");
8079 const baseClasses = ["desktop-mode-dock__item"];
8080 if (item.multi) {
8081 baseClasses.push("desktop-mode-dock__item--multi");
8082 }
8083 const filteredClasses = applyFilters(
8084 HOOKS.DOCK_TILE_CLASS,
8085 baseClasses,
8086 ctx
8087 );
8088 tile2.className = filteredClasses.join(" ");
8089 tile2.dataset.menuSlug = item.id;
8090 const primary = document.createElement("button");
8091 primary.className = "desktop-mode-dock__item-primary";
8092 primary.setAttribute("type", "button");
8093 primary.setAttribute("aria-label", item.title);
8094 const iconEl = this.resolveIcon(item.icon, item.title, item.url);
8095 primary.appendChild(iconEl);
8096 if (item.badge > 0) {
8097 const displayCount = item.badge > 99 ? "99+" : String(item.badge);
8098 const badge = document.createElement("span");
8099 badge.className = "desktop-mode-dock__badge";
8100 badge.textContent = displayCount;
8101 badge.setAttribute(
8102 "aria-label",
8103 sprintf(
8104 // translators: %d is the number of pending updates / items.
8105 _n("%d update", "%d updates", item.badge),
8106 item.badge
8107 )
8108 );
8109 primary.appendChild(badge);
8110 }
8111 primary.addEventListener("click", () => {
8112 this.openPage(item);
8113 });
8114 tile2.addEventListener("contextmenu", (ev) => {
8115 ev.preventDefault();
8116 openItemVisibilityMenu({
8117 x: ev.clientX,
8118 y: ev.clientY,
8119 id: item.id,
8120 title: item.title,
8121 surface: "dock",
8122 pluginFile: item.pluginFile ?? null,
8123 pluginName: item.pluginName ?? null
8124 });
8125 });
8126 tile2.appendChild(primary);
8127 this.bindTooltipFiltered(tile2, item.title, ctx);
8128 const baseId = this.resolveItemBaseId(item);
8129 const teardown = attachDockPeek({
8130 tile: tile2,
8131 item: {
8132 id: item.id,
8133 title: item.title,
8134 icon: item.icon,
8135 url: item.url
8136 },
8137 // Source instances from `getAllByBaseId` regardless of
8138 // `item.multi`. The Ghost Card spawns duplicates on every
8139 // tile (the `enableGhost: true` below), so any tile —
8140 // including ones synthesized from a desktop icon, where
8141 // `multi` is never set — can end up with >1 open instance.
8142 // A `multi`-gated singleton lookup would only return the
8143 // first window and the peek would silently underreport.
8144 // For genuine singletons that never get duplicated, the
8145 // returned array is just `[one]` (or `[]`), same shape the
8146 // old branch produced.
8147 getInstances: () => this.windowManager.getAllByBaseIdOnActiveDesktop(baseId),
8148 // Ghost Card on EVERY tile, regardless of `multi`. The
8149 // affordance reads consistently across the dock — every
8150 // hover-peek surfaces a "+ open another <Page>" card. For
8151 // multi-capable items, clicking it spawns a fresh
8152 // instance. For singletons it falls through to the same
8153 // open-or-focus path the tile click takes — usually a
8154 // no-op (focuses the existing window) but cheap and
8155 // visually consistent.
8156 enableGhost: true,
8157 windowManager: this.windowManager,
8158 getOrientation: () => this.orientation,
8159 openNew: () => this.openNewInstance(item),
8160 suppressTooltip: (on) => {
8161 if (on) {
8162 this.tooltip.classList.remove(
8163 "desktop-mode-dock__tooltip--visible"
8164 );
8165 }
8166 }
8167 });
8168 this.peekTeardowns.set(item.id, teardown);
8169 this.attachDragReorder(tile2, item.id);
8170 return applyFilters(
8171 HOOKS.DOCK_TILE_ELEMENT,
8172 tile2,
8173 ctx
8174 );
8175 }
8176 /**
8177 * Drag-to-reorder for menu tiles. Fixed slots — no interpolated
8178 * positioning. While dragging:
8179 *
8180 * 1. Pointer down on the primary button starts a tentative drag.
8181 * Click handling is preserved by requiring movement past a
8182 * small threshold before we claim the gesture.
8183 * 2. Once claimed, the tile gets a `--dragging` modifier so CSS
8184 * can lift it visually. Every `pointermove` checks which other
8185 * menu tile the cursor is currently over; if it's a different
8186 * tile, we splice the dragged tile in front of it (so adjacent
8187 * tiles slide into the vacated slot).
8188 * 3. On `pointerup` we read the resulting DOM order, persist the
8189 * new id list to `dockOrder` via the public settings writer,
8190 * and the layout-dispatcher subscriber re-applies. Cancellation
8191 * (Escape, pointercancel) reverts to the original order.
8192 *
8193 * @since 0.8.2
8194 */
8195 attachDragReorder(tile2, itemId) {
8196 const THRESHOLD = 5;
8197 const FLIP_MS = 200;
8198 let active2 = false;
8199 let startX = 0;
8200 let startY = 0;
8201 let originalOrder = [];
8202 let originalNext = null;
8203 let pointerId = -1;
8204 let originRect = null;
8205 let justDragged = false;
8206 const hardReset = () => {
8207 active2 = false;
8208 tile2.classList.remove("desktop-mode-dock__item--dragging");
8209 tile2.style.transform = "";
8210 tile2.style.transition = "";
8211 document.removeEventListener("pointermove", onMove);
8212 document.removeEventListener("pointerup", onUp);
8213 document.removeEventListener("pointercancel", onCancel);
8214 document.removeEventListener("keydown", onKey, true);
8215 window.removeEventListener("blur", onBlur);
8216 document.removeEventListener("visibilitychange", onVisibility);
8217 pointerId = -1;
8218 originRect = null;
8219 };
8220 const isMenuTile = (el) => {
8221 return !!el && el instanceof HTMLElement && el.classList.contains("desktop-mode-dock__item") && !el.classList.contains("desktop-mode-dock__item--system") && !!el.dataset.menuSlug;
8222 };
8223 const eachSiblingTile = (fn) => {
8224 for (const child of Array.from(this.itemHost.children)) {
8225 if (child instanceof HTMLElement && child !== tile2 && isMenuTile(child)) {
8226 fn(child);
8227 }
8228 }
8229 };
8230 const snapshotMenuOrder = () => {
8231 const ids = [];
8232 for (const child of Array.from(this.itemHost.children)) {
8233 if (isMenuTile(child)) {
8234 ids.push(child.dataset.menuSlug);
8235 }
8236 }
8237 return ids;
8238 };
8239 const flipSiblings = (prevRects) => {
8240 eachSiblingTile((sib) => {
8241 const prev = prevRects.get(sib);
8242 if (!prev) {
8243 return;
8244 }
8245 const now = sib.getBoundingClientRect();
8246 const dx = prev.left - now.left;
8247 const dy = prev.top - now.top;
8248 if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) {
8249 return;
8250 }
8251 sib.style.transition = "none";
8252 sib.style.transform = `translate(${dx}px, ${dy}px)`;
8253 void sib.offsetHeight;
8254 sib.style.transition = `transform ${FLIP_MS}ms cubic-bezier(0.2, 0.7, 0.3, 1)`;
8255 sib.style.transform = "";
8256 const onEnd = () => {
8257 sib.style.transition = "";
8258 sib.style.transform = "";
8259 sib.removeEventListener("transitionend", onEnd);
8260 };
8261 sib.addEventListener("transitionend", onEnd);
8262 });
8263 };
8264 const onMove = (ev) => {
8265 if (pointerId !== -1 && ev.pointerId !== pointerId) {
8266 return;
8267 }
8268 if (!active2) {
8269 const dx2 = ev.clientX - startX;
8270 const dy2 = ev.clientY - startY;
8271 if (dx2 * dx2 + dy2 * dy2 < THRESHOLD * THRESHOLD) {
8272 return;
8273 }
8274 active2 = true;
8275 originalOrder = snapshotMenuOrder();
8276 originalNext = tile2.nextSibling;
8277 originRect = tile2.getBoundingClientRect();
8278 tile2.classList.add("desktop-mode-dock__item--dragging");
8279 this.tooltip.classList.remove(
8280 "desktop-mode-dock__tooltip--visible"
8281 );
8282 }
8283 if (!originRect) {
8284 return;
8285 }
8286 const dx = ev.clientX - startX;
8287 const dy = ev.clientY - startY;
8288 tile2.style.transform = `translate(${dx}px, ${dy}px)`;
8289 const under = document.elementFromPoint(ev.clientX, ev.clientY);
8290 const targetTile = under?.closest(
8291 ".desktop-mode-dock__item"
8292 );
8293 if (!targetTile || targetTile === tile2) {
8294 return;
8295 }
8296 if (!isMenuTile(targetTile)) {
8297 return;
8298 }
8299 const rect = targetTile.getBoundingClientRect();
8300 let insertBefore;
8301 if (this.orientation === "bottom") {
8302 insertBefore = ev.clientX < rect.left + rect.width / 2;
8303 } else {
8304 insertBefore = ev.clientY < rect.top + rect.height / 2;
8305 }
8306 const prevRects = /* @__PURE__ */ new Map();
8307 eachSiblingTile((sib) => {
8308 prevRects.set(sib, sib.getBoundingClientRect());
8309 });
8310 let reordered = false;
8311 if (insertBefore) {
8312 if (targetTile !== tile2.nextSibling) {
8313 this.itemHost.insertBefore(tile2, targetTile);
8314 reordered = true;
8315 }
8316 } else if (targetTile.nextSibling !== tile2) {
8317 this.itemHost.insertBefore(tile2, targetTile.nextSibling);
8318 reordered = true;
8319 }
8320 if (reordered) {
8321 tile2.style.transform = "";
8322 const fresh = tile2.getBoundingClientRect();
8323 startX = fresh.left + fresh.width / 2;
8324 startY = fresh.top + fresh.height / 2;
8325 tile2.style.transform = `translate(${ev.clientX - startX}px, ${ev.clientY - startY}px)`;
8326 flipSiblings(prevRects);
8327 }
8328 };
8329 const cleanup = () => {
8330 tile2.classList.remove("desktop-mode-dock__item--dragging");
8331 tile2.style.transform = "";
8332 tile2.style.transition = "";
8333 document.removeEventListener("pointermove", onMove);
8334 document.removeEventListener("pointerup", onUp);
8335 document.removeEventListener("pointercancel", onCancel);
8336 document.removeEventListener("keydown", onKey, true);
8337 window.removeEventListener("blur", onBlur);
8338 document.removeEventListener("visibilitychange", onVisibility);
8339 pointerId = -1;
8340 originRect = null;
8341 active2 = false;
8342 if (_Dock.activeDragReset === hardReset) {
8343 _Dock.activeDragReset = null;
8344 }
8345 };
8346 const animateHome = () => {
8347 tile2.style.transition = `transform ${FLIP_MS}ms cubic-bezier(0.2, 0.7, 0.3, 1)`;
8348 tile2.style.transform = "";
8349 const onEnd = () => {
8350 tile2.style.transition = "";
8351 tile2.removeEventListener("transitionend", onEnd);
8352 };
8353 tile2.addEventListener("transitionend", onEnd);
8354 };
8355 const persistDockOrder = (finalOrder) => {
8356 const api = window.wp?.desktop;
8357 if (!api?.getOsSettings || !api?.updateOsSettings) {
8358 return;
8359 }
8360 const existing = api.getOsSettings().dockOrder;
8361 const finalSet = new Set(finalOrder);
8362 const merged = [];
8363 let injected = false;
8364 for (const id of existing) {
8365 if (finalSet.has(id)) {
8366 if (!injected) {
8367 merged.push(...finalOrder);
8368 injected = true;
8369 }
8370 continue;
8371 }
8372 merged.push(id);
8373 }
8374 if (!injected) {
8375 merged.push(...finalOrder);
8376 }
8377 api.updateOsSettings({ dockOrder: merged });
8378 };
8379 const onUp = (ev) => {
8380 if (pointerId !== -1 && ev.pointerId !== pointerId) {
8381 return;
8382 }
8383 if (!active2) {
8384 cleanup();
8385 return;
8386 }
8387 justDragged = true;
8388 const finalOrder = snapshotMenuOrder();
8389 animateHome();
8390 cleanup();
8391 const same = finalOrder.length === originalOrder.length && finalOrder.every((id, i) => id === originalOrder[i]);
8392 if (!same) {
8393 persistDockOrder(finalOrder);
8394 }
8395 setTimeout(() => {
8396 justDragged = false;
8397 }, 200);
8398 };
8399 const onCancel = (ev) => {
8400 if (ev && pointerId !== -1 && ev.pointerId !== pointerId) {
8401 return;
8402 }
8403 if (active2 && originalNext !== void 0) {
8404 const prevRects = /* @__PURE__ */ new Map();
8405 eachSiblingTile((sib) => {
8406 prevRects.set(sib, sib.getBoundingClientRect());
8407 });
8408 this.itemHost.insertBefore(tile2, originalNext);
8409 flipSiblings(prevRects);
8410 }
8411 animateHome();
8412 cleanup();
8413 };
8414 const onKey = (ev) => {
8415 if (ev.key === "Escape") {
8416 onCancel();
8417 }
8418 };
8419 const onBlur = () => onCancel();
8420 const onVisibility = () => {
8421 if (document.visibilityState !== "visible") {
8422 onCancel();
8423 }
8424 };
8425 tile2.addEventListener("pointerdown", (ev) => {
8426 if (ev.button !== 0) {
8427 return;
8428 }
8429 if (_Dock.activeDragReset) {
8430 const prev = _Dock.activeDragReset;
8431 _Dock.activeDragReset = null;
8432 prev();
8433 }
8434 if (active2 || pointerId !== -1) {
8435 hardReset();
8436 }
8437 startX = ev.clientX;
8438 startY = ev.clientY;
8439 pointerId = ev.pointerId;
8440 active2 = false;
8441 _Dock.activeDragReset = hardReset;
8442 document.addEventListener("pointermove", onMove);
8443 document.addEventListener("pointerup", onUp);
8444 document.addEventListener("pointercancel", onCancel);
8445 document.addEventListener("keydown", onKey, true);
8446 window.addEventListener("blur", onBlur);
8447 document.addEventListener("visibilitychange", onVisibility);
8448 });
8449 tile2.addEventListener(
8450 "click",
8451 (ev) => {
8452 if (justDragged) {
8453 ev.preventDefault();
8454 ev.stopImmediatePropagation();
8455 }
8456 },
8457 true
8458 );
8459 }
8460 /**
8461 * Resolve a registered icon value into a DOM element.
8462 *
8463 * Priority: dashicons class → inline SVG data URI → image URL →
8464 * letter badge derived from the item's title. The letter fallback is
8465 * important for plugin tiles: plugin authors routinely register
8466 * top-level menus with `add_menu_page()` and omit the icon argument
8467 * (defaulting to `'div'` or empty), which would otherwise render as
8468 * an indistinguishable wall of generic wrenches. A colored letter
8469 * tile gives each plugin a stable, unique-ish visual identity with
8470 * zero plugin-side effort — the hue derives deterministically from
8471 * the title so the same plugin always gets the same color.
8472 *
8473 * @param icon The icon value from the menu entry.
8474 * @param title Human-readable title, used when falling back to a
8475 * letter badge.
8476 */
8477 resolveIcon(icon, title, url) {
8478 if (icon.startsWith("dashicons-") && icon !== "dashicons-admin-generic") {
8479 const el = document.createElement("span");
8480 el.className = `dashicons ${icon}`;
8481 el.setAttribute("aria-hidden", "true");
8482 return el;
8483 }
8484 if (icon.startsWith("data:image/svg+xml;base64,")) {
8485 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
8486 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
8487 return this._makeSvgIcon(icon);
8488 }
8489 }
8490 if (icon.startsWith("url(")) {
8491 return this._makeSvgIcon(icon);
8492 }
8493 if (icon.startsWith("http://") || icon.startsWith("https://")) {
8494 const img = document.createElement("img");
8495 img.className = "desktop-mode-dock__item-img";
8496 img.src = icon;
8497 img.alt = "";
8498 img.setAttribute("aria-hidden", "true");
8499 return img;
8500 }
8501 if (url) {
8502 const native = this._extractNativeMenuIcon(url);
8503 if (native) {
8504 return native;
8505 }
8506 }
8507 if (icon === "dashicons-admin-generic") {
8508 const el = document.createElement("span");
8509 el.className = "dashicons dashicons-admin-generic";
8510 el.setAttribute("aria-hidden", "true");
8511 return el;
8512 }
8513 return this.createLetterBadge(title);
8514 }
8515 /**
8516 * Build an SVG-background icon tile. Shared between the data-URI
8517 * branch of {@link resolveIcon} and the native-menu extractor.
8518 */
8519 _makeSvgIcon(bgValue) {
8520 const el = document.createElement("span");
8521 el.className = "desktop-mode-dock__item-svg";
8522 el.style.backgroundImage = bgValue.startsWith("url(") ? bgValue : `url("${bgValue}")`;
8523 el.style.backgroundSize = "contain";
8524 el.style.backgroundRepeat = "no-repeat";
8525 el.style.backgroundPosition = "center";
8526 el.setAttribute("aria-hidden", "true");
8527 return el;
8528 }
8529 /**
8530 * Extract a plugin's icon from the hidden `#adminmenu` that still
8531 * exists in the parent shell DOM (display:none'd by desktop.css).
8532 * Handles the three shapes plugins commonly use when the menu-page
8533 * icon_url is 'none' or 'div':
8534 *
8535 * (a) `<img src="...">` nested inside `.wp-menu-image`
8536 * (b) a dashicon class on `.wp-menu-image` itself
8537 * (c) a CSS background-image on `.wp-menu-image::before` (the
8538 * `menu-icon-XYZ` pattern Yoast, WooCommerce, Jetpack, etc. use)
8539 *
8540 * Returns null when the URL doesn't match any admin-menu entry or
8541 * none of the three shapes are detectable.
8542 */
8543 _extractNativeMenuIcon(url) {
8544 const adminMenu = document.getElementById("adminmenu");
8545 if (!adminMenu) {
8546 return null;
8547 }
8548 let target2;
8549 try {
8550 const u = new URL(url, window.location.href);
8551 const filename = u.pathname.split("/").pop() || "";
8552 target2 = filename + u.search;
8553 } catch {
8554 return null;
8555 }
8556 if (!target2) {
8557 return null;
8558 }
8559 const links = adminMenu.querySelectorAll("li.menu-top > a");
8560 let matchLi = null;
8561 for (const link of Array.from(links)) {
8562 if (link.href.endsWith(target2)) {
8563 matchLi = link.closest("li.menu-top");
8564 break;
8565 }
8566 }
8567 if (!matchLi) {
8568 return null;
8569 }
8570 const imgWrap = matchLi.querySelector(".wp-menu-image");
8571 if (!imgWrap) {
8572 return null;
8573 }
8574 const img = imgWrap.querySelector("img");
8575 if (img && img.src) {
8576 const el = document.createElement("img");
8577 el.className = "desktop-mode-dock__item-img";
8578 el.src = img.src;
8579 el.alt = "";
8580 el.setAttribute("aria-hidden", "true");
8581 return el;
8582 }
8583 const dashMatch = imgWrap.className.match(/\bdashicons-[\w-]+\b/);
8584 if (dashMatch && dashMatch[0] !== "dashicons-before") {
8585 const el = document.createElement("span");
8586 el.className = `dashicons ${dashMatch[0]}`;
8587 el.setAttribute("aria-hidden", "true");
8588 return el;
8589 }
8590 const before = window.getComputedStyle(imgWrap, "::before");
8591 const bg = before.backgroundImage;
8592 if (bg && bg !== "none" && !bg.includes('url("")')) {
8593 return this._makeSvgIcon(bg);
8594 }
8595 const bgWrap = window.getComputedStyle(imgWrap).backgroundImage;
8596 if (bgWrap && bgWrap !== "none" && !bgWrap.includes('url("")')) {
8597 return this._makeSvgIcon(bgWrap);
8598 }
8599 return null;
8600 }
8601 /**
8602 * Create a letter-badge icon — a rounded square tinted with a
8603 * deterministic hue derived from the title, displaying the first
8604 * letter of the title. Mirrors the "app icon placeholder" look
8605 * macOS uses when an app ships without artwork.
8606 *
8607 * The title always drives both the letter and the hue — same plugin,
8608 * same color across reloads. An empty title falls through to a `?`
8609 * on a neutral gray tile, but the menu builder upstream guards
8610 * against empty titles, so this is a defensive branch.
8611 */
8612 createLetterBadge(title) {
8613 const el = document.createElement("span");
8614 el.className = "desktop-mode-dock__item-letter";
8615 el.setAttribute("aria-hidden", "true");
8616 const trimmed = title.trim();
8617 const firstCodePoint = trimmed ? Array.from(trimmed)[0] : "?";
8618 el.textContent = firstCodePoint.toUpperCase();
8619 const hue = hashTitleToHue(trimmed);
8620 el.style.background = `linear-gradient(135deg, hsl(${hue} 62% 55%), hsl(${(hue + 24) % 360} 58% 42%))`;
8621 return el;
8622 }
8623 /**
8624 * Bind tooltip show/hide on hover. Tooltip anchor differs per
8625 * orientation: left dock → tile's right side, right dock → tile's
8626 * left side, bottom dock → above the tile. We set the relevant
8627 * coordinate inline each enter; the CSS takes care of the rest.
8628 */
8629 /**
8630 * Resolves the tooltip text through {@link HOOKS.DOCK_TILE_TOOLTIP}
8631 * once at bind time (so the dock doesn't re-filter on every
8632 * pointerenter) and stashes the resolved text on
8633 * `tile.dataset.dockTooltip` so the multi-instance chip can
8634 * restore it on its own pointerleave without going through the
8635 * filter again.
8636 *
8637 * Returning an empty string from the filter suppresses the
8638 * tooltip — the listener short-circuits and never adds the
8639 * `--visible` class.
8640 */
8641 bindTooltipFiltered(tile2, text, ctx) {
8642 const filtered = applyFilters(
8643 HOOKS.DOCK_TILE_TOOLTIP,
8644 text,
8645 ctx
8646 );
8647 tile2.dataset.dockTooltip = filtered;
8648 if (filtered === "") {
8649 return;
8650 }
8651 tile2.addEventListener("pointerenter", () => {
8652 this.positionTooltip(tile2, filtered);
8653 this.tooltip.classList.add("desktop-mode-dock__tooltip--visible");
8654 });
8655 tile2.addEventListener("pointerleave", () => {
8656 this.tooltip.classList.remove("desktop-mode-dock__tooltip--visible");
8657 });
8658 }
8659 /**
8660 * Write the tooltip text + anchor coordinate for `el`. Split out
8661 * because the multi-instance chip's pointerenter handler also
8662 * needs to anchor to a specific element (the chip, not the tile).
8663 */
8664 positionTooltip(el, text) {
8665 const rect = el.getBoundingClientRect();
8666 this.tooltip.textContent = text;
8667 if (this.orientation === "bottom") {
8668 this.tooltip.style.left = `${rect.left + rect.width / 2}px`;
8669 this.tooltip.style.top = `${rect.top - 14}px`;
8670 } else if (this.orientation === "right") {
8671 this.tooltip.style.top = `${rect.top + rect.height / 2 - 14}px`;
8672 this.tooltip.style.left = `${rect.left}px`;
8673 } else {
8674 this.tooltip.style.top = `${rect.top + rect.height / 2 - 14}px`;
8675 this.tooltip.style.left = `${rect.right + 8}px`;
8676 }
8677 }
8678 /**
8679 * Open an admin page in a window (or focus if already open).
8680 *
8681 * Consults the native URL-remap registry first — when an opt-in
8682 * native window has registered itself as the replacement for this
8683 * admin URL (e.g. the native Posts window for `edit.php` when the
8684 * user has flipped `nativePostsEnabled`), the click is rerouted
8685 * to that window and the iframe path is skipped. The dock item
8686 * itself is untouched: same icon, same tooltip, same position —
8687 * only the destination changes.
8688 */
8689 openPage(item) {
8690 if (item.id.startsWith("dock:")) {
8691 const iconId = item.id.slice(5);
8692 const cfg = window.desktopModeConfig;
8693 const icon = cfg?.desktopIcons?.find((i) => i.id === iconId);
8694 if (icon?.window) {
8695 const wp = window.wp?.desktop;
8696 wp?.openWindow?.(icon.window);
8697 return;
8698 }
8699 if (icon?.url) {
8700 if (tryOpenExternalUrl(icon.url)) {
8701 return;
8702 }
8703 const baseId2 = this.deriveWindowId(icon.url);
8704 this.windowManager.open({
8705 id: baseId2,
8706 baseId: baseId2,
8707 url: icon.url,
8708 parentUrl: icon.url,
8709 title: icon.title,
8710 icon: icon.icon.startsWith("dashicons-") ? icon.icon : "dashicons-admin-generic",
8711 submenu: [],
8712 multi: false
8713 });
8714 return;
8715 }
8716 return;
8717 }
8718 if (tryOpenExternalUrl(item.url)) {
8719 return;
8720 }
8721 if (tryNativeUrlRemap(item.url)) {
8722 return;
8723 }
8724 const baseId = this.deriveWindowId(item.url);
8725 this.windowManager.open({
8726 id: baseId,
8727 baseId,
8728 url: item.url,
8729 parentUrl: item.url,
8730 title: item.title,
8731 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
8732 submenu: item.submenu,
8733 multi: !!item.multi
8734 });
8735 }
8736 /**
8737 * Open a brand-new instance of a page, even if one is already
8738 * open. Invoked by the "+" ghost card in the dock peek.
8739 *
8740 * The user explicitly asked for "another window of this thing,"
8741 * so we honour the request even when {@link tryNativeUrlRemap}
8742 * would otherwise route the click into a native-window
8743 * singleton. Result: clicking + while a native Posts window is
8744 * open opens a fresh iframe of `edit.php` alongside it. Two
8745 * windows of Posts is the explicit ask — that's what + is for.
8746 */
8747 openNewInstance(item) {
8748 if (tryOpenExternalUrl(item.url)) {
8749 return;
8750 }
8751 const openNewWindow = window.wp?.desktop?.openNewWindow;
8752 if (item.windowId && !item.url) {
8753 if (openNewWindow?.(item.windowId, { source: "dock-peek" })) {
8754 return;
8755 }
8756 }
8757 const remappedId = resolveNativeUrlRemap(item.url);
8758 if (remappedId) {
8759 if (openNewWindow?.(remappedId, { source: "dock-peek" })) {
8760 return;
8761 }
8762 }
8763 const baseId = this.deriveWindowId(item.url);
8764 void this.windowManager.openNew({
8765 id: baseId,
8766 baseId,
8767 url: item.url,
8768 parentUrl: item.url,
8769 title: item.title,
8770 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
8771 submenu: item.submenu,
8772 multi: true
8773 });
8774 }
8775 /**
8776 * Derive a window ID from an admin page URL.
8777 */
8778 deriveWindowId(url) {
8779 return deriveWindowId(url, this.adminUrl);
8780 }
8781 /**
8782 * Resolve the window-manager key for a dock tile, in this order:
8783 *
8784 * 1. `item.windowId` — set by `applyDockPlacement` when the tile
8785 * is synthesized from a `desktop_mode_register_icon()` entry
8786 * whose target is a native window. Native-window ids never
8787 * pass through the URL → native-window remap layer, so we
8788 * short-circuit before touching it.
8789 * 2. {@link resolveNativeUrlRemap} on `item.url` — captures the
8790 * `nativePostsEnabled` / `nativePagesEnabled` opt-ins that
8791 * repoint a URL-based tile at a native window.
8792 * 3. {@link deriveWindowId} on `item.url` — the URL-based
8793 * fallback for ordinary admin-menu tiles.
8794 *
8795 * Shared by the hover-peek card and the active/focused-dot
8796 * indicator; the two stayed in lockstep before this method existed
8797 * by hand-rolling the same chain at each call site.
8798 */
8799 resolveItemBaseId(item) {
8800 if (item.windowId) {
8801 return item.windowId;
8802 }
8803 const remapped = resolveNativeUrlRemap(item.url);
8804 return remapped ?? this.deriveWindowId(item.url);
8805 }
8806 /**
8807 * Listen to window events to update active/focused/minimized
8808 * indicators on dock items, plus the global Show Desktop body class.
8809 *
8810 * The event detail isn't used — we just need to re-query the
8811 * window manager on every change — so the handlers take no
8812 * argument and the type cast is gone with it.
8813 *
8814 * `WINDOW_MINIMIZED` / `WINDOW_RESTORED` route through the hook bus
8815 * (no DOM CustomEvent equivalent today). Without these, minimizing
8816 * a window via Show Desktop / the title-bar minimize button left
8817 * the dock's active-dot rendering stuck on "visible window" — the
8818 * user had no cue that everything had collapsed to minimized.
8819 */
8820 bindWindowEvents() {
8821 const refresh = () => this.updateActiveStates();
8822 this.boundRefresh = refresh;
8823 document.addEventListener("desktop-mode-window-opened", refresh);
8824 document.addEventListener("desktop-mode-window-closed", refresh);
8825 document.addEventListener("desktop-mode-window-focused", refresh);
8826 window.wp?.hooks?.addAction?.(
8827 "desktop-mode.desktop.switched",
8828 this.hooksNamespace,
8829 refresh
8830 );
8831 window.wp?.hooks?.addAction?.(
8832 "desktop-mode.desktop.closed",
8833 this.hooksNamespace,
8834 refresh
8835 );
8836 window.wp?.hooks?.addAction?.(
8837 HOOKS.WINDOW_MINIMIZED,
8838 this.hooksNamespace,
8839 refresh
8840 );
8841 window.wp?.hooks?.addAction?.(
8842 HOOKS.WINDOW_RESTORED,
8843 this.hooksNamespace,
8844 refresh
8845 );
8846 }
8847 /**
8848 * Tear the dock down: detach window-lifecycle listeners, clear
8849 * pending attention timers, remove the floating tooltip from
8850 * `document.body`, and empty the container's children. Used by
8851 * the layout dispatcher when the user switches `desktopLayout`
8852 * in OS Settings — old dock(s) get destroyed and a fresh set is
8853 * constructed for the new layout.
8854 *
8855 * Idempotent: calling twice is safe.
8856 */
8857 destroy() {
8858 document.removeEventListener(
8859 "desktop-mode-window-opened",
8860 this.boundRefresh
8861 );
8862 document.removeEventListener(
8863 "desktop-mode-window-closed",
8864 this.boundRefresh
8865 );
8866 document.removeEventListener(
8867 "desktop-mode-window-focused",
8868 this.boundRefresh
8869 );
8870 window.wp?.hooks?.removeAction?.(
8871 "desktop-mode.desktop.switched",
8872 this.hooksNamespace
8873 );
8874 window.wp?.hooks?.removeAction?.(
8875 "desktop-mode.desktop.closed",
8876 this.hooksNamespace
8877 );
8878 window.wp?.hooks?.removeAction?.(
8879 HOOKS.WINDOW_MINIMIZED,
8880 this.hooksNamespace
8881 );
8882 window.wp?.hooks?.removeAction?.(
8883 HOOKS.WINDOW_RESTORED,
8884 this.hooksNamespace
8885 );
8886 for (const handle of this.attentionTimers.values()) {
8887 window.clearTimeout(handle);
8888 }
8889 this.attentionTimers.clear();
8890 for (const teardown of this.peekTeardowns.values()) {
8891 teardown();
8892 }
8893 this.peekTeardowns.clear();
8894 this.tooltip.remove();
8895 while (this.container.firstChild) {
8896 this.container.removeChild(this.container.firstChild);
8897 }
8898 this.itemElements.clear();
8899 this.systemItemElements.clear();
8900 this.systemItems = [];
8901 this.systemSeparator = null;
8902 this.container.removeAttribute("data-desktop-mode-dock-placement");
8903 }
8904 /**
8905 * Update the active/focused/minimized classes on every dock item in
8906 * response to a window lifecycle event, and toggle the global Show
8907 * Desktop body class.
8908 *
8909 * For singletons the rail is absent; "active" means "the one window
8910 * is open". For multi-capable items, active means "≥1 instance is
8911 * open" and focused means "the focused window belongs to this item".
8912 *
8913 * `--all-minimized` is layered on top of `--active` and fires only
8914 * when EVERY open instance of the tile is minimized — so a partial
8915 * minimize (one of two windows hidden) keeps the solid dot. CSS
8916 * swaps the dot for a hollow ring on minimized-only tiles so the
8917 * user can tell at a glance "I have something here, it's just
8918 * hidden right now."
8919 */
8920 updateActiveStates() {
8921 const focused = this.windowManager.getFocused();
8922 const focusedBaseId = focused ? focused.config.baseId || focused.id : null;
8923 const activeDesktopId = this.windowManager.getActiveDesktopId();
8924 const onActiveDesktop = (w) => (w.config.desktopId || activeDesktopId) === activeDesktopId;
8925 const isMinimized = (w) => w.state === "minimized";
8926 for (const item of this.items) {
8927 const tile2 = this.itemElements.get(item.id);
8928 if (!tile2) {
8929 continue;
8930 }
8931 const baseId = this.resolveItemBaseId(item);
8932 const instances = this.windowManager.getAllByBaseId(baseId).filter(onActiveDesktop);
8933 const isOpen = instances.length > 0;
8934 const allMinimized = isOpen && instances.every(isMinimized);
8935 const isFocused = focusedBaseId === baseId && !!focused && onActiveDesktop(focused) && !isMinimized(focused);
8936 tile2.classList.toggle("desktop-mode-dock__item--active", isOpen);
8937 tile2.classList.toggle("desktop-mode-dock__item--focused", isFocused);
8938 tile2.classList.toggle(
8939 "desktop-mode-dock__item--all-minimized",
8940 allMinimized
8941 );
8942 }
8943 for (const sys of this.systemItems) {
8944 const tile2 = this.systemItemElements.get(sys.id);
8945 if (!tile2) {
8946 continue;
8947 }
8948 const sysWin = this.windowManager.getById(sys.id);
8949 const isOpen = sys.isOpen ? sys.isOpen() : !!sysWin;
8950 const allMinimized = !!sysWin && isMinimized(sysWin);
8951 const isFocused = !!focused && focused.id === sys.id && !isMinimized(focused);
8952 tile2.classList.toggle("desktop-mode-dock__item--active", isOpen);
8953 tile2.classList.toggle("desktop-mode-dock__item--focused", isFocused);
8954 tile2.classList.toggle(
8955 "desktop-mode-dock__item--all-minimized",
8956 allMinimized
8957 );
8958 }
8959 this.updateShowDesktopBodyClass();
8960 }
8961 /**
8962 * Toggle `body.desktop-mode-show-desktop-active` based on whether
8963 * every live window on the active desktop is minimized. Mirrors
8964 * the heuristic inside {@link WindowManager.toggleShowDesktop} so
8965 * the visual cue tracks the actual state — set by Show Desktop
8966 * gestures, restored when any window is brought back, automatically
8967 * cleared when no windows exist.
8968 *
8969 * @internal
8970 */
8971 updateShowDesktopBodyClass() {
8972 const activeDesktopId = this.windowManager.getActiveDesktopId();
8973 const live = this.windowManager.getAll().filter(
8974 (w) => (w.config.desktopId || activeDesktopId) === activeDesktopId
8975 );
8976 const showDesktop = live.length > 0 && live.every((w) => w.state === "minimized");
8977 document.body.classList.toggle(
8978 "desktop-mode-show-desktop-active",
8979 showDesktop
8980 );
8981 }
8982 };
8983 _Dock.instanceCounter = 0;
8984 _Dock.activeDragReset = null;
8985 let Dock = _Dock;
8986 function _applyBadgeNode(host, count) {
8987 const existing = host.querySelector(
8988 ":scope > .desktop-mode-dock__badge"
8989 );
8990 if (count <= 0) {
8991 existing?.remove();
8992 return;
8993 }
8994 const display = count > 99 ? "99+" : String(count);
8995 if (existing) {
8996 if (existing.textContent !== display) {
8997 existing.textContent = display;
8998 }
8999 existing.setAttribute(
9000 "aria-label",
9001 sprintf(
9002 // translators: %d is the number of pending items in a dock badge.
9003 _n("%d notification", "%d notifications", count),
9004 count
9005 )
9006 );
9007 return;
9008 }
9009 const badge = document.createElement("span");
9010 badge.className = "desktop-mode-dock__badge";
9011 badge.textContent = display;
9012 badge.setAttribute(
9013 "aria-label",
9014 sprintf(
9015 // translators: %d is the number of pending items in a dock badge.
9016 _n("%d notification", "%d notifications", count),
9017 count
9018 )
9019 );
9020 host.appendChild(badge);
9021 }
9022 const DEFAULT_RENDERER_DOCK = Symbol.for(
9023 "desktop-mode/default-dock-rail-renderer/dock"
9024 );
9025 const defaultDockRailRenderer = {
9026 id: "default",
9027 label: "Icon strip",
9028 description: "The shipped baseline — icon tiles with badges, tooltips, multi-instance chips, and attention animations.",
9029 icon: "dashicons-menu-alt",
9030 apiVersion: 1,
9031 mount(deps2) {
9032 const dock = new Dock(
9033 deps2.container,
9034 deps2.windowManager,
9035 deps2.items,
9036 deps2.adminUrl,
9037 deps2.orientation
9038 );
9039 const controller = {
9040 [DEFAULT_RENDERER_DOCK]: dock,
9041 replaceItems: (items) => dock.replaceItems(items),
9042 appendSystemItem: (item) => dock.appendSystemItem(item),
9043 removeSystemItem: (id) => dock.removeSystemItem(id),
9044 setBadge: (itemId, count) => dock.setBadge(itemId, count),
9045 setAttention: (itemId, mode, opts) => dock.setAttention(itemId, mode, opts),
9046 setOrientation: (orientation) => dock.setOrientation(orientation),
9047 destroy: () => dock.destroy()
9048 };
9049 return controller;
9050 }
9051 };
9052 function unwrapDefaultDock(controller) {
9053 if (!controller) {
9054 return null;
9055 }
9056 const probe = controller;
9057 const dock = probe[DEFAULT_RENDERER_DOCK];
9058 return dock instanceof Dock ? dock : null;
9059 }
9060 function installDefaultDockRailRenderer() {
9061 register$1(defaultDockRailRenderer);
9062 }
9063 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;--desktop-mode-text:#f0f0f1;--desktop-mode-text-muted:#bbc1c7;--desktop-mode-muted:#a7aaad;--desktop-mode-muted-fg:#a7aaad;--desktop-mode-border:rgba( 255,255,255,0.25 );--wpd-button-bg-hover:rgba( 255,255,255,0.08 )}: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}`;
9064 const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
9065 const _WpdModal = class _WpdModal extends Component {
9066 constructor() {
9067 super(...arguments);
9068 this._prevFocus = null;
9069 this._onKey = (e) => {
9070 if (e.key === "Escape" && !this.hasAttribute("mandatory")) {
9071 e.preventDefault();
9072 this._cancel();
9073 return;
9074 }
9075 if (e.key === "Tab") {
9076 const f = this._focusables();
9077 if (f.length === 0) {
9078 return;
9079 }
9080 const first = f[0];
9081 const last = f[f.length - 1];
9082 const doc = this.ownerDocument;
9083 const fallback = doc ? doc.activeElement : null;
9084 const active2 = e.composedPath()[0] || fallback;
9085 if (e.shiftKey && active2 === first) {
9086 e.preventDefault();
9087 last.focus();
9088 } else if (!e.shiftKey && active2 === last) {
9089 e.preventDefault();
9090 first.focus();
9091 }
9092 }
9093 };
9094 this._onBackdrop = (e) => {
9095 if (this.hasAttribute("mandatory")) {
9096 return;
9097 }
9098 const path = e.composedPath();
9099 const original = path.length > 0 ? path[0] : e.target;
9100 if (original === this) {
9101 this._cancel();
9102 }
9103 };
9104 }
9105 connectedCallback() {
9106 super.connectedCallback();
9107 this.setAttribute("role", "dialog");
9108 this.setAttribute("aria-modal", "true");
9109 this.addEventListener("keydown", this._onKey);
9110 this.addEventListener("click", this._onBackdrop);
9111 }
9112 disconnectedCallback() {
9113 this.removeEventListener("keydown", this._onKey);
9114 this.removeEventListener("click", this._onBackdrop);
9115 }
9116 attributeChangedCallback(name, oldValue, newValue) {
9117 super.attributeChangedCallback?.(name, oldValue, newValue);
9118 if (name === "open") {
9119 if (newValue !== null) {
9120 const doc = this.ownerDocument;
9121 this._prevFocus = doc ? doc.activeElement : null;
9122 queueMicrotask(() => this._focusFirst());
9123 } else if (this._prevFocus) {
9124 try {
9125 this._prevFocus.focus();
9126 } catch (e) {
9127 }
9128 this._prevFocus = null;
9129 }
9130 }
9131 }
9132 showModal() {
9133 this.setAttribute("open", "");
9134 }
9135 hideModal() {
9136 this.removeAttribute("open");
9137 }
9138 _focusables() {
9139 const root = this.shadowRoot;
9140 if (!root) {
9141 return [];
9142 }
9143 const slotted = Array.from(this.querySelectorAll(FOCUSABLE));
9144 const inShadow = Array.from(root.querySelectorAll(FOCUSABLE));
9145 return [...slotted, ...inShadow].filter((el) => el.offsetParent !== null || el.tagName === "BUTTON");
9146 }
9147 _focusFirst() {
9148 const f = this._focusables();
9149 if (f.length > 0) {
9150 f[0].focus();
9151 } else {
9152 const inner = this.shadowRoot?.querySelector(".dialog");
9153 inner?.focus?.();
9154 }
9155 }
9156 _cancel() {
9157 const ev = new CustomEvent("wpd-modal-cancel", {
9158 bubbles: true,
9159 cancelable: true,
9160 composed: true
9161 });
9162 const allowed = this.dispatchEvent(ev);
9163 if (allowed) {
9164 this.hideModal();
9165 }
9166 }
9167 render() {
9168 const title = this.getAttribute("title") ?? "";
9169 const mandatory = this.hasAttribute("mandatory");
9170 return html`
9171 <div class="dialog" tabindex="-1">
9172 ${title ? html`
9173 <div class="header">
9174 <h2 class="title">${title}</h2>
9175 <div class="header-actions">
9176 <slot name="header-actions"></slot>
9177 ${mandatory ? html`` : html`<button
9178 type="button"
9179 class="close"
9180 aria-label="Close"
9181 @click=${() => this._cancel()}
9182 >×</button>`}
9183 </div>
9184 </div>
9185 ` : html``}
9186 <div class="body">
9187 <slot></slot>
9188 </div>
9189 <div class="footer">
9190 <slot name="footer"></slot>
9191 </div>
9192 </div>
9193 `;
9194 }
9195 };
9196 _WpdModal.props = ["open", "title", "size", "mandatory"];
9197 _WpdModal.styles = [modalStyles];
9198 _WpdModal.help = {
9199 title: "Modal overlay",
9200 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. The dialog surface is dark and re-points the shared surface tokens (--desktop-mode-text/-muted/-border, --wpd-button-bg-hover) so wpd-* controls slotted into it resolve readable dark-surface colors automatically.",
9201 status: "experimental",
9202 since: "0.8.5",
9203 props: [
9204 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
9205 { name: "title", type: "string", description: "Heading shown at the top of the dialog." },
9206 { name: "size", type: "'sm' | 'md' | 'lg'", default: "md", description: "Width preset." },
9207 {
9208 name: "mandatory",
9209 type: "boolean attribute",
9210 description: "Disables ESC, click-outside and the close button."
9211 }
9212 ],
9213 slots: [
9214 { name: "(default)", description: "Body content." },
9215 { name: "footer", description: "Footer button row, right-aligned." },
9216 { name: "header-actions", description: "Extra actions next to the close button." }
9217 ],
9218 events: [
9219 {
9220 name: "wpd-modal-cancel",
9221 description: "Fires when the user dismisses the modal (ESC, click-outside, close button). Cancelable; calling `preventDefault()` keeps the modal open."
9222 }
9223 ]
9224 };
9225 let WpdModal = _WpdModal;
9226 defineComponent("wpd-modal", WpdModal);
9227 const styles$6 = 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:var( --wpd-button-bg-hover,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}`;
9228 const _WpdButton = class _WpdButton extends Component {
9229 render() {
9230 const disabled = this.disabled !== null;
9231 const type = this.type || "button";
9232 return html`
9233 <button part="button" type=${type} ?disabled=${disabled}>
9234 <slot></slot>
9235 </button>
9236 `;
9237 }
9238 };
9239 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
9240 _WpdButton.styles = [styles$6];
9241 _WpdButton.help = {
9242 title: "Button",
9243 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
9244 status: "stable",
9245 since: "0.9.0",
9246 props: [
9247 {
9248 name: "variant",
9249 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
9250 default: "ghost",
9251 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
9252 },
9253 {
9254 name: "disabled",
9255 type: "boolean attribute",
9256 description: "Disable pointer + keyboard interaction and dim the chrome."
9257 },
9258 {
9259 name: "type",
9260 type: "'button' | 'submit' | 'reset'",
9261 default: "button",
9262 description: "Forwarded to the underlying native <button>."
9263 },
9264 {
9265 name: "busy",
9266 type: "boolean attribute",
9267 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
9268 },
9269 {
9270 name: "fill-cell",
9271 type: "boolean attribute",
9272 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
9273 }
9274 ],
9275 slots: [{ name: "(default)", description: "Button label." }],
9276 parts: [{ name: "button", description: "Underlying <button> element." }],
9277 cssProps: [
9278 { name: "--wpd-button-bg", description: "Background color." },
9279 {
9280 name: "--wpd-button-bg-hover",
9281 description: "Hover wash (ghost + secondary variants)."
9282 },
9283 { name: "--wpd-button-fg", description: "Text color." },
9284 { name: "--wpd-button-border", description: "Border shorthand." },
9285 { name: "--wpd-button-border-radius", default: "6px" },
9286 { name: "--wpd-button-padding", default: "6px 12px" },
9287 {
9288 name: "--wpd-button-min-height",
9289 description: "Minimum height when fill-cell is set."
9290 }
9291 ],
9292 example: html`
9293 <wpd-cluster gap="8">
9294 <wpd-button variant="primary">Primary</wpd-button>
9295 <wpd-button variant="secondary">Secondary</wpd-button>
9296 <wpd-button variant="ghost">Ghost</wpd-button>
9297 <wpd-button variant="danger">Danger</wpd-button>
9298 <wpd-button variant="link">Link</wpd-button>
9299 </wpd-cluster>
9300 `
9301 };
9302 let WpdButton = _WpdButton;
9303 defineComponent("wpd-button", WpdButton);
9304 function customGradientCss(state2) {
9305 const { from, to, angle } = state2.customGradient;
9306 return `linear-gradient(${angle}deg, ${from}, ${to})`;
9307 }
9308 const CUSTOM_GRADIENT_DESCRIPTION = () => __("Mix your own two-colour gradient and set the angle — your desk, your palette.");
9309 function registerCustomGradient(ctx) {
9310 register$2({
9311 id: CUSTOM_GRADIENT_ID,
9312 label: __("Custom gradient"),
9313 type: "css",
9314 preview: customGradientCss(ctx.state),
9315 description: CUSTOM_GRADIENT_DESCRIPTION(),
9316 resolveValue: () => customGradientCss(ctx.state)
9317 });
9318 }
9319 function registerCustomImageIfPresent(state2) {
9320 if (!state2.customImage) {
9321 unregister$2(CUSTOM_IMAGE_ID);
9322 return;
9323 }
9324 const safeUrl = encodeURI(state2.customImage.url);
9325 const value = `url("${safeUrl}") center/cover no-repeat, #1d2327`;
9326 register$2({
9327 id: CUSTOM_IMAGE_ID,
9328 label: __("Custom image"),
9329 type: "css",
9330 value,
9331 preview: value,
9332 description: __(
9333 "Any image from your media library or an upload, sized to cover the whole desk."
9334 )
9335 });
9336 }
9337 let _panelLoadPromise = null;
9338 function loadOsSettingsPanelBundle(scriptUrl) {
9339 if (window.desktopModeRenderOsSettingsPanel) {
9340 return Promise.resolve(window.desktopModeRenderOsSettingsPanel);
9341 }
9342 if (_panelLoadPromise) {
9343 return _panelLoadPromise;
9344 }
9345 _panelLoadPromise = new Promise((resolve2, reject) => {
9346 const existing = document.querySelector(
9347 'script[data-desktop-mode-os-settings-panel="1"]'
9348 );
9349 const finish = () => {
9350 const fn = window.desktopModeRenderOsSettingsPanel;
9351 if (!fn) {
9352 reject(
9353 new Error(
9354 "[desktop-mode] os-settings-panel bundle loaded but did not register desktopModeRenderOsSettingsPanel"
9355 )
9356 );
9357 return;
9358 }
9359 resolve2(fn);
9360 };
9361 if (existing) {
9362 if (window.desktopModeRenderOsSettingsPanel) {
9363 finish();
9364 } else {
9365 existing.addEventListener("load", finish);
9366 existing.addEventListener(
9367 "error",
9368 () => reject(new Error("failed to load os-settings-panel bundle"))
9369 );
9370 }
9371 return;
9372 }
9373 const s = document.createElement("script");
9374 s.src = scriptUrl;
9375 s.async = true;
9376 s.dataset.desktopModeOsSettingsPanel = "1";
9377 s.addEventListener("load", finish);
9378 s.addEventListener(
9379 "error",
9380 () => reject(new Error("failed to load os-settings-panel bundle"))
9381 );
9382 document.head.appendChild(s);
9383 });
9384 return _panelLoadPromise;
9385 }
9386 class OsSettings {
9387 constructor(config, layer) {
9388 this.activeEditorTeardown = null;
9389 this.tabRegistryUnsubscribe = null;
9390 this.activeTabId = null;
9391 this.osSettingsListeners = /* @__PURE__ */ new Set();
9392 this._lastRenderedBody = null;
9393 this.config = config;
9394 this.layer = layer;
9395 this.state = loadState();
9396 setLastConfirmedState(this.state);
9397 document.addEventListener(
9398 "desktop-mode-os-settings-save-lifecycle",
9399 (e) => {
9400 const detail = e.detail;
9401 if (!detail || detail.phase !== "failed" || !detail.rolledBackTo) {
9402 return;
9403 }
9404 this.state = detail.rolledBackTo;
9405 this.apply();
9406 if (this._lastRenderedBody?.isConnected) {
9407 this.renderPanel(this._lastRenderedBody);
9408 }
9409 }
9410 );
9411 registerCustomGradient(this);
9412 registerCustomImageIfPresent(this.state);
9413 }
9414 /** Project the private state into the public snapshot shape. */
9415 getOsSettingsSnapshot() {
9416 return {
9417 wallpaper: this.state.wallpaper,
9418 accent: this.state.accent,
9419 dockSize: this.state.dockSize,
9420 desktopLayout: this.state.desktopLayout,
9421 dockRailRenderer: this.state.dockRailRenderer,
9422 unfocusEffect: this.state.unfocusEffect,
9423 windowLinkRenderer: this.state.windowLinkRenderer,
9424 windowLinkVisibility: this.state.windowLinkVisibility,
9425 windowLinksEnabled: this.state.windowLinksEnabled,
9426 windowLinkRaiseOnFocus: this.state.windowLinkRaiseOnFocus,
9427 windowLinkHighlight: this.state.windowLinkHighlight,
9428 ai: { ...this.state.ai },
9429 nativePostsEnabled: this.state.nativePostsEnabled,
9430 nativePostsHiddenColumns: this.state.nativePostsHiddenColumns.slice(),
9431 nativePagesEnabled: this.state.nativePagesEnabled,
9432 nativeUsersEnabled: this.state.nativeUsersEnabled,
9433 nativePluginsEnabled: this.state.nativePluginsEnabled,
9434 nativeCommentsEnabled: this.state.nativeCommentsEnabled,
9435 developerModeEnabled: this.state.developerModeEnabled,
9436 foldersSharingEnabled: this.state.foldersSharingEnabled,
9437 itemVisibility: { ...this.state.itemVisibility },
9438 dockOrder: this.state.dockOrder.slice(),
9439 dockPromotedPositions: Object.fromEntries(
9440 Object.entries(this.state.dockPromotedPositions).map(
9441 ([k, v]) => [k, { ...v }]
9442 )
9443 )
9444 };
9445 }
9446 subscribeOsSettings(cb) {
9447 this.osSettingsListeners.add(cb);
9448 return () => {
9449 this.osSettingsListeners.delete(cb);
9450 };
9451 }
9452 /**
9453 * Apply the current state: wallpaper via the layer, accent + dock
9454 * size as CSS custom properties on the shell.
9455 *
9456 * Safe to call repeatedly — calls into `layer.apply` dedupe via
9457 * generation counter; CSS property writes are idempotent.
9458 */
9459 apply() {
9460 const shell = document.getElementById("desktop-mode-shell");
9461 if (!shell) {
9462 return;
9463 }
9464 seedWallpaperSettings(this.state.wallpaperSettings);
9465 const def = get$1(this.state.wallpaper) || get$1(getDefaultWallpaperId()) || get$1(DEFAULT_WALLPAPER_ID) || all$1()[0];
9466 if (def) {
9467 this.layer.apply(def);
9468 }
9469 const accents = getAccents();
9470 const accent = accents.find((a) => a.id === this.state.accent) ?? accents[0];
9471 const dockSize = DOCK_SIZES.find((d) => d.id === this.state.dockSize) ?? DOCK_SIZES[1];
9472 const root = document.documentElement;
9473 root.style.setProperty("--wp-admin-theme-color", accent.value);
9474 root.style.setProperty("--desktop-mode-dock-width", `${dockSize.width}px`);
9475 root.style.setProperty("--desktop-mode-dock-icon-size", `${dockSize.icon}px`);
9476 shell.setAttribute(
9477 "data-desktop-mode-layout",
9478 this.state.desktopLayout
9479 );
9480 setActiveRenderer(this.state.dockRailRenderer);
9481 }
9482 save(opts = {}) {
9483 saveState(this.state, opts);
9484 if (this.osSettingsListeners.size > 0) {
9485 const snapshot = this.getOsSettingsSnapshot();
9486 const listeners2 = Array.from(this.osSettingsListeners);
9487 for (const cb of listeners2) {
9488 try {
9489 cb(snapshot);
9490 } catch (err) {
9491 if (typeof console !== "undefined") {
9492 console.error(
9493 "[desktop-mode] os-settings listener threw:",
9494 err
9495 );
9496 }
9497 }
9498 }
9499 }
9500 }
9501 /**
9502 * Render the settings panel into the given native-window body.
9503 *
9504 * Builds three sections (wallpaper, accent, dock size) and wires
9505 * each to save/apply on change. The panel is a one-shot build per
9506 * window open — closing and re-opening renders a fresh tree.
9507 */
9508 /**
9509 * Render the settings panel into the given native-window body.
9510 *
9511 * Lazy since 0.8.4 — the actual rendering logic plus every
9512 * `<wpd-*>` component the panel uses lives in
9513 * `src/settings/panel.ts`, compiled into its own Vite target
9514 * `os-settings-panel[.min].js`. The script is injected on the
9515 * first call below and the matching
9516 * `window.desktopModeRenderOsSettingsPanel( ctx, body )` global
9517 * is then invoked. Subsequent calls (registry-driven re-render,
9518 * save-failure rollback) skip the load and forward immediately.
9519 *
9520 * Why this is a `<script>`-injected sibling bundle rather than
9521 * an in-bundle dynamic import: Vite IIFE lib mode inlines
9522 * `import()` calls, so an in-bundle lazy import would give zero
9523 * byte savings. A separate Vite target is the only mechanism
9524 * that actually shrinks `desktop.min.js`. See the Stage 8
9525 * section of `BUNDLE-SIZE-REPORT.md` for the full picture.
9526 */
9527 /**
9528 * Switch the active settings tab. Records the choice on
9529 * {@link activeTabId} (so the next render mounts on it) and, when
9530 * the panel is currently mounted, flips the live `<wpd-tabs>` value
9531 * in place so an already-open OS Settings window jumps to the tab
9532 * without a full re-render. Deep-linking entry points
9533 * (`openOsSettings({ tabId })`) call this after opening the window.
9534 *
9535 * @param tabId Settings tab id, e.g. `'ai'`, `'apps-icons'`.
9536 */
9537 focusTab(tabId) {
9538 this.activeTabId = tabId;
9539 const body = this._lastRenderedBody;
9540 if (!body?.isConnected) {
9541 return;
9542 }
9543 const tabs = body.querySelector("wpd-tabs");
9544 if (tabs) {
9545 tabs.value = tabId;
9546 }
9547 }
9548 renderPanel(body) {
9549 this._lastRenderedBody = body;
9550 const fn = window.desktopModeRenderOsSettingsPanel;
9551 if (fn) {
9552 fn(this, body);
9553 return;
9554 }
9555 void loadOsSettingsPanelBundle(
9556 this.config.osSettingsPanelBundleUrl ?? ""
9557 ).then((render2) => {
9558 if (!body.isConnected) {
9559 return;
9560 }
9561 render2(this, body);
9562 }).catch((err) => {
9563 if (typeof console !== "undefined") {
9564 console.error(
9565 "[desktop-mode] OS Settings panel failed to load:",
9566 err
9567 );
9568 }
9569 });
9570 }
9571 }
9572 const EXIT_DESKTOP_MODE_TILE_ID = "desktop-mode-exit";
9573 function getExitDesktopModeTileDef() {
9574 return {
9575 id: EXIT_DESKTOP_MODE_TILE_ID,
9576 title: __("Exit Desktop Mode"),
9577 // `dashicons-exit` (door with arrow) is the clearest "leave"
9578 // glyph in the WordPress set, distinct from `dashicons-desktop`
9579 // used by OS Settings.
9580 icon: "dashicons-exit",
9581 onOpen: () => {
9582 void exitDesktopMode();
9583 }
9584 };
9585 }
9586 async function exitDesktopMode() {
9587 const cfg = window.desktopModeAdminBar;
9588 const fallback = cfg?.classicUrl || "/wp-admin/";
9589 if (!cfg?.ajaxUrl || !cfg?.nonce) {
9590 navigateTop(fallback);
9591 return;
9592 }
9593 const body = new URLSearchParams();
9594 body.set("action", "save-desktop-mode");
9595 body.set("nonce", cfg.nonce);
9596 body.set("enabled", "");
9597 let target2 = fallback;
9598 try {
9599 const res = await fetch(cfg.ajaxUrl, {
9600 method: "POST",
9601 headers: {
9602 "Content-Type": "application/x-www-form-urlencoded"
9603 },
9604 body: body.toString(),
9605 credentials: "same-origin"
9606 });
9607 if (res.ok) {
9608 const json = await res.json();
9609 if (json?.success && json.data?.redirect) {
9610 target2 = json.data.redirect;
9611 }
9612 }
9613 } catch {
9614 }
9615 navigateTop(target2);
9616 }
9617 function navigateTop(url) {
9618 try {
9619 window.top.location.href = url;
9620 } catch {
9621 window.location.href = url;
9622 }
9623 }
9624 const _initial$1 = {
9625 userId: null,
9626 requestedAt: 0,
9627 tabRequested: false
9628 };
9629 let _store$2 = null;
9630 function getStore$1() {
9631 if (_store$2) {
9632 return _store$2;
9633 }
9634 const w = window;
9635 const factory = w.wp?.desktop?.createSharedStore;
9636 if (typeof factory !== "function") {
9637 return null;
9638 }
9639 _store$2 = factory(
9640 "desktop-mode/user-edit/target",
9641 () => ({ ..._initial$1 })
9642 );
9643 return _store$2;
9644 }
9645 function setUserEditTarget(userId) {
9646 const store2 = getStore$1();
9647 if (store2) {
9648 store2.state.userId = userId;
9649 store2.state.requestedAt = Date.now();
9650 store2.state.tabRequested = true;
9651 store2.notify();
9652 return;
9653 }
9654 const w = window;
9655 w._wpdUserEditTarget = {
9656 userId,
9657 requestedAt: Date.now(),
9658 tabRequested: true
9659 };
9660 }
9661 const pending = /* @__PURE__ */ new Map();
9662 function loadVendorScript(url, extras) {
9663 const existing = pending.get(url);
9664 if (existing) {
9665 return existing;
9666 }
9667 const promise = new Promise((resolve2, reject) => {
9668 const selector = `script[data-desktop-mode-vendor="${cssEscape(url)}"]`;
9669 const preexisting = document.querySelector(selector);
9670 if (preexisting) {
9671 if (preexisting.dataset.loaded === "1") {
9672 resolve2();
9673 return;
9674 }
9675 preexisting.addEventListener("load", () => resolve2(), { once: true });
9676 preexisting.addEventListener(
9677 "error",
9678 () => reject(new Error(`Failed to load ${url}`)),
9679 { once: true }
9680 );
9681 return;
9682 }
9683 if (extras?.translations) {
9684 injectInline(extras.translations);
9685 }
9686 for (const code of extras?.l10n ?? []) {
9687 injectInline(code);
9688 }
9689 for (const code of extras?.before ?? []) {
9690 injectInline(code);
9691 }
9692 const script = document.createElement("script");
9693 script.src = url;
9694 script.async = true;
9695 script.dataset.desktopModeVendor = url;
9696 script.addEventListener(
9697 "load",
9698 () => {
9699 script.dataset.loaded = "1";
9700 for (const code of extras?.after ?? []) {
9701 injectInline(code);
9702 }
9703 resolve2();
9704 },
9705 { once: true }
9706 );
9707 script.addEventListener(
9708 "error",
9709 () => {
9710 pending.delete(url);
9711 script.remove();
9712 reject(new Error(`Failed to load ${url}`));
9713 },
9714 { once: true }
9715 );
9716 document.head.appendChild(script);
9717 });
9718 pending.set(url, promise);
9719 return promise;
9720 }
9721 function injectInline(code) {
9722 if (!code) {
9723 return;
9724 }
9725 const tag = document.createElement("script");
9726 tag.textContent = code;
9727 tag.dataset.desktopModeVendorInline = "1";
9728 document.head.appendChild(tag);
9729 }
9730 function cssEscape(value) {
9731 if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
9732 return CSS.escape(value);
9733 }
9734 return value.replace(/["\\]/g, "\\$&");
9735 }
9736 const registry$9 = /* @__PURE__ */ new Map();
9737 function registerModule(def) {
9738 if (!def || typeof def.id !== "string" || def.id === "") {
9739 if (typeof console !== "undefined") {
9740 console.warn("[desktop-mode] Ignored invalid module registration:", def);
9741 }
9742 return;
9743 }
9744 if (typeof def.url !== "string" || def.url === "") {
9745 if (typeof console !== "undefined") {
9746 console.warn(
9747 `[desktop-mode] Module "${def.id}" has no url; ignored.`
9748 );
9749 }
9750 return;
9751 }
9752 registry$9.set(def.id, def);
9753 }
9754 function moduleIds() {
9755 return Array.from(registry$9.keys());
9756 }
9757 async function loadModules(ids) {
9758 if (!ids || ids.length === 0) {
9759 return;
9760 }
9761 const unknown = ids.filter((id) => !registry$9.has(id));
9762 if (unknown.length > 0) {
9763 throw new Error(
9764 `[desktop-mode] Unknown module(s) in needs: ${unknown.map((id) => `"${id}"`).join(", ")}. Known modules: ${moduleIds().join(", ") || "(none)"}.`
9765 );
9766 }
9767 await Promise.all(
9768 ids.map((id) => {
9769 const def = registry$9.get(id);
9770 if (!def) {
9771 return Promise.resolve();
9772 }
9773 if (def.isReady && def.isReady()) {
9774 return Promise.resolve();
9775 }
9776 return loadVendorScript(def.url);
9777 })
9778 );
9779 }
9780 function createContext(id, pluginUrl) {
9781 return {
9782 id,
9783 pluginUrl,
9784 prefersReducedMotion: prefersReducedMotion(),
9785 visible: !document.hidden,
9786 settings: getWallpaperSettings(id)
9787 };
9788 }
9789 function prefersReducedMotion() {
9790 if (typeof window.matchMedia !== "function") {
9791 return false;
9792 }
9793 return window.matchMedia("( prefers-reduced-motion: reduce )").matches;
9794 }
9795 class WallpaperLayer {
9796 constructor(element, pluginUrl) {
9797 this.generation = 0;
9798 this.active = null;
9799 this.boundVisibilityChange = () => {
9800 if (!this.active) {
9801 return;
9802 }
9803 doAction(HOOKS.WALLPAPER_VISIBILITY, {
9804 id: this.active.id,
9805 state: document.hidden ? "hidden" : "visible"
9806 });
9807 };
9808 this.element = element;
9809 this.pluginUrl = pluginUrl;
9810 document.addEventListener("visibilitychange", this.boundVisibilityChange);
9811 }
9812 /**
9813 * Apply a wallpaper definition. Safe to call from any event
9814 * handler — handles type dispatch, teardown of the prior active
9815 * canvas, and race-safe async mounts.
9816 */
9817 apply(def) {
9818 const gen = ++this.generation;
9819 this.teardownActive();
9820 if (def.type === "css") {
9821 this.applyCss(def);
9822 return;
9823 }
9824 this.applyCanvas(def, gen);
9825 }
9826 /**
9827 * Imperative teardown entry point — called from desktop.ts on
9828 * `pagehide` so a canvas wallpaper's ticker doesn't compete with
9829 * the session-beacon flush at unload.
9830 */
9831 teardownActive() {
9832 if (!this.active) {
9833 return;
9834 }
9835 const { id, teardown } = this.active;
9836 this.active = null;
9837 doAction(HOOKS.WALLPAPER_UNMOUNTING, { id });
9838 try {
9839 teardown();
9840 } catch (err) {
9841 doAction(HOOKS.SHELL_ERROR, { scope: "wallpaper-teardown", id, error: err });
9842 if (typeof console !== "undefined") {
9843 console.error(
9844 `[desktop-mode] Wallpaper "${id}" teardown threw:`,
9845 err
9846 );
9847 }
9848 }
9849 this.element.innerHTML = "";
9850 }
9851 /** Remove listeners. Not called in normal flow — reserved for tests. */
9852 dispose() {
9853 this.teardownActive();
9854 document.removeEventListener("visibilitychange", this.boundVisibilityChange);
9855 }
9856 applyCss(def) {
9857 const value = def.resolveValue ? def.resolveValue(createContext(def.id, this.pluginUrl)) : def.value;
9858 if (typeof value === "string") {
9859 this.element.style.setProperty("--desktop-mode-bg", value);
9860 const shell = document.getElementById("desktop-mode-shell");
9861 shell?.style.setProperty("--desktop-mode-bg", value);
9862 }
9863 }
9864 applyCanvas(def, gen) {
9865 const ctx = createContext(def.id, this.pluginUrl);
9866 doAction(HOOKS.WALLPAPER_MOUNTING, { id: def.id, container: this.element, ctx });
9867 const depsReady = def.needs && def.needs.length > 0 ? loadModules(def.needs) : Promise.resolve();
9868 const onResolve = (teardown) => {
9869 if (gen !== this.generation) {
9870 try {
9871 teardown();
9872 } catch {
9873 }
9874 return;
9875 }
9876 this.active = { id: def.id, teardown };
9877 doAction(HOOKS.WALLPAPER_MOUNTED, { id: def.id, container: this.element, ctx });
9878 };
9879 depsReady.then(
9880 () => {
9881 if (gen !== this.generation) {
9882 return;
9883 }
9884 let result;
9885 try {
9886 result = def.mount(this.element, ctx);
9887 } catch (err) {
9888 this.handleMountFailure(def.id, err);
9889 return;
9890 }
9891 if (isThenable$1(result)) {
9892 result.then(onResolve, (err) => {
9893 if (gen !== this.generation) {
9894 return;
9895 }
9896 this.handleMountFailure(def.id, err);
9897 });
9898 return;
9899 }
9900 onResolve(result);
9901 },
9902 (err) => {
9903 if (gen !== this.generation) {
9904 return;
9905 }
9906 this.handleMountFailure(def.id, err);
9907 }
9908 );
9909 }
9910 handleMountFailure(id, err) {
9911 this.element.innerHTML = "";
9912 doAction(HOOKS.WALLPAPER_MOUNT_FAILED, { id, error: err });
9913 doAction(HOOKS.SHELL_ERROR, { scope: "wallpaper-mount", id, error: err });
9914 if (typeof console !== "undefined") {
9915 console.error(
9916 `[desktop-mode] Wallpaper "${id}" failed to mount:`,
9917 err
9918 );
9919 }
9920 }
9921 }
9922 function isThenable$1(value) {
9923 return !!value && typeof value === "object" && typeof value.then === "function";
9924 }
9925 function createWallpaperRegistrySync(deps2) {
9926 const { osSettings } = deps2;
9927 const registered = /* @__PURE__ */ new Set();
9928 const loadedScripts = /* @__PURE__ */ new Set();
9929 const ensureScript = async (entry) => {
9930 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
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: "wallpaper-script-load",
9943 id: entry.id,
9944 error: err
9945 });
9946 return;
9947 }
9948 loadedScripts.add(entry.scriptUrl);
9949 };
9950 const readDef = (id) => {
9951 const globals = window.desktopModeWallpapers || {};
9952 return globals[id] ?? null;
9953 };
9954 const defFromCssEntry = (entry) => {
9955 if (entry.type !== "css" || entry.value === "") {
9956 return null;
9957 }
9958 return {
9959 id: entry.id,
9960 label: entry.label,
9961 type: "css",
9962 value: entry.value,
9963 preview: entry.preview !== "" ? entry.preview : entry.value,
9964 description: entry.description || void 0
9965 };
9966 };
9967 const registerEntry = async (entry) => {
9968 if (registered.has(entry.id)) {
9969 return;
9970 }
9971 const cssDef = defFromCssEntry(entry);
9972 if (cssDef) {
9973 register$2(cssDef);
9974 registered.add(entry.id);
9975 osSettings.apply();
9976 return;
9977 }
9978 await ensureScript(entry);
9979 let def = readDef(entry.id);
9980 if (def && !def.description && entry.description) {
9981 def = { ...def, description: entry.description };
9982 }
9983 if (!def) {
9984 doAction(HOOKS.SHELL_ERROR, {
9985 scope: "wallpaper-missing-def",
9986 id: entry.id,
9987 error: new Error(
9988 `[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.`
9989 )
9990 });
9991 return;
9992 }
9993 try {
9994 register$2(def);
9995 } catch (err) {
9996 doAction(HOOKS.SHELL_ERROR, {
9997 scope: "wallpaper-register",
9998 id: entry.id,
9999 error: err
10000 });
10001 return;
10002 }
10003 registered.add(entry.id);
10004 osSettings.apply();
10005 };
10006 const unregisterEntry = (id) => {
10007 if (!registered.has(id)) {
10008 return;
10009 }
10010 unregister$2(id);
10011 registered.delete(id);
10012 osSettings.apply();
10013 };
10014 return async (list2) => {
10015 const incoming = /* @__PURE__ */ new Set();
10016 for (const entry of list2) {
10017 incoming.add(entry.id);
10018 }
10019 for (const id of Array.from(registered)) {
10020 if (!incoming.has(id)) {
10021 unregisterEntry(id);
10022 }
10023 }
10024 for (const entry of list2) {
10025 if (!registered.has(entry.id)) {
10026 await registerEntry(entry);
10027 }
10028 }
10029 };
10030 }
10031 const COMMAND_SLUG = /^[a-z0-9_/-]+$/;
10032 const commandRegistryStore = createSharedStore(
10033 "desktop-mode/commands-registry",
10034 () => ({
10035 registry: /* @__PURE__ */ new Map(),
10036 listeners: /* @__PURE__ */ new Set()
10037 })
10038 );
10039 const registry$8 = commandRegistryStore.state.registry;
10040 const listeners$b = commandRegistryStore.state.listeners;
10041 function registerCommand(cmd) {
10042 const errors = [];
10043 const slug = typeof cmd?.slug === "string" ? cmd.slug.trim().toLowerCase() : "";
10044 if (!cmd || typeof cmd !== "object") {
10045 errors.push("def (not an object)");
10046 } else {
10047 if (typeof cmd.slug !== "string" || cmd.slug.trim() === "") {
10048 errors.push("slug (missing)");
10049 } else if (!COMMAND_SLUG.test(slug)) {
10050 errors.push(
10051 `slug (must match ${COMMAND_SLUG} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
10052 );
10053 }
10054 if (typeof cmd.label !== "string" || cmd.label.trim() === "") {
10055 errors.push("label (missing)");
10056 }
10057 if (typeof cmd.run !== "function") {
10058 errors.push("run (must be a function)");
10059 }
10060 }
10061 throwOnRegistrationErrors("Command", errors, cmd);
10062 registry$8.set(slug, { ...cmd, slug });
10063 notify$d();
10064 }
10065 function unregisterCommand(slug) {
10066 if (registry$8.delete(slug.toLowerCase())) {
10067 notify$d();
10068 }
10069 }
10070 function unregisterByOwner(owner) {
10071 if (!owner) {
10072 return 0;
10073 }
10074 let removed = 0;
10075 for (const [slug, cmd] of Array.from(registry$8.entries())) {
10076 if (cmd.owner === owner) {
10077 registry$8.delete(slug);
10078 removed++;
10079 }
10080 }
10081 if (removed > 0) {
10082 notify$d();
10083 }
10084 return removed;
10085 }
10086 function listCommands() {
10087 return Array.from(registry$8.values());
10088 }
10089 function listAiCallableCommands() {
10090 const out = [];
10091 for (const cmd of registry$8.values()) {
10092 if (cmd.aiCallable !== true) {
10093 continue;
10094 }
10095 out.push({
10096 slug: cmd.slug,
10097 label: cmd.label,
10098 description: cmd.description ?? "",
10099 hint: cmd.hint ?? ""
10100 });
10101 }
10102 return out;
10103 }
10104 function findCommand(slug) {
10105 return registry$8.get(slug.toLowerCase()) ?? null;
10106 }
10107 function notify$d() {
10108 const snapshot = Array.from(listeners$b);
10109 for (const cb of snapshot) {
10110 try {
10111 cb();
10112 } catch (err) {
10113 if (typeof console !== "undefined") {
10114 console.error("[desktop-mode] command-registry listener threw:", err);
10115 }
10116 }
10117 }
10118 }
10119 function createCommandRegistrySync() {
10120 const loadedHandles = /* @__PURE__ */ new Set();
10121 const loadedUrls = /* @__PURE__ */ new Set();
10122 let prevSlugsByHandle = /* @__PURE__ */ new Map();
10123 const ensureScript = async (entry) => {
10124 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
10125 loadedHandles.add(entry.handle);
10126 return;
10127 }
10128 try {
10129 await loadVendorScript(entry.scriptUrl, {
10130 translations: entry.scriptTranslations,
10131 l10n: entry.scriptL10n,
10132 before: entry.scriptBefore,
10133 after: entry.scriptAfter
10134 });
10135 } catch (err) {
10136 doAction(HOOKS.SHELL_ERROR, {
10137 scope: "command-script-load",
10138 handle: entry.handle,
10139 url: entry.scriptUrl,
10140 error: err
10141 });
10142 return;
10143 }
10144 loadedUrls.add(entry.scriptUrl);
10145 loadedHandles.add(entry.handle);
10146 };
10147 const slugsByHandleFrom = (commands) => {
10148 const map = /* @__PURE__ */ new Map();
10149 if (!commands) {
10150 return map;
10151 }
10152 for (const entry of commands) {
10153 if (!entry.scriptHandle || !entry.slug) {
10154 continue;
10155 }
10156 let set = map.get(entry.scriptHandle);
10157 if (!set) {
10158 set = /* @__PURE__ */ new Set();
10159 map.set(entry.scriptHandle, set);
10160 }
10161 set.add(entry.slug);
10162 }
10163 return map;
10164 };
10165 const collectSlugsToRemove = (handle) => {
10166 const slugs = /* @__PURE__ */ new Set();
10167 for (const cmd of listCommands()) {
10168 if (cmd.owner === handle) {
10169 slugs.add(cmd.slug);
10170 }
10171 }
10172 const declared = prevSlugsByHandle.get(handle);
10173 if (declared) {
10174 for (const slug of declared) {
10175 slugs.add(slug);
10176 }
10177 }
10178 return slugs;
10179 };
10180 return async (scripts, commands) => {
10181 const incomingHandles = /* @__PURE__ */ new Set();
10182 for (const entry of scripts) {
10183 if (entry.handle) {
10184 incomingHandles.add(entry.handle);
10185 }
10186 }
10187 for (const handle of Array.from(loadedHandles)) {
10188 if (incomingHandles.has(handle)) {
10189 continue;
10190 }
10191 for (const slug of collectSlugsToRemove(handle)) {
10192 unregisterCommand(slug);
10193 }
10194 loadedHandles.delete(handle);
10195 }
10196 for (const entry of scripts) {
10197 if (!entry.handle || loadedHandles.has(entry.handle)) {
10198 continue;
10199 }
10200 await ensureScript(entry);
10201 }
10202 prevSlugsByHandle = slugsByHandleFrom(commands);
10203 };
10204 }
10205 const store$c = createSharedStore(
10206 "desktop-mode/settings-tab-registry",
10207 () => ({
10208 registry: /* @__PURE__ */ new Map(),
10209 listeners: /* @__PURE__ */ new Set()
10210 })
10211 );
10212 const registry$7 = store$c.state.registry;
10213 const listeners$a = store$c.state.listeners;
10214 function registerSettingsTab(tab) {
10215 if (!tab || typeof tab.id !== "string" || tab.id.trim() === "") {
10216 return;
10217 }
10218 if (typeof tab.label !== "string" || tab.label.trim() === "") {
10219 return;
10220 }
10221 if (typeof tab.render !== "function") {
10222 return;
10223 }
10224 const id = tab.id.trim().toLowerCase();
10225 if (!/^[a-z0-9_\-]+$/.test(id)) {
10226 if (typeof console !== "undefined") {
10227 console.warn(
10228 "[desktop-mode] registerSettingsTab: id must be [a-z0-9_-]+, got",
10229 tab.id
10230 );
10231 }
10232 return;
10233 }
10234 registry$7.set(id, { ...tab, id });
10235 notify$c();
10236 }
10237 function unregisterSettingsTab(id) {
10238 if (registry$7.delete(id.toLowerCase())) {
10239 notify$c();
10240 }
10241 }
10242 function unregisterSettingsTabsByOwner(owner) {
10243 if (!owner) {
10244 return 0;
10245 }
10246 let removed = 0;
10247 for (const [id, tab] of Array.from(registry$7.entries())) {
10248 if (tab.owner === owner) {
10249 registry$7.delete(id);
10250 removed++;
10251 }
10252 }
10253 if (removed > 0) {
10254 notify$c();
10255 }
10256 return removed;
10257 }
10258 function listSettingsTabs() {
10259 return Array.from(registry$7.values()).sort(
10260 (a, b) => (a.order ?? 100) - (b.order ?? 100)
10261 );
10262 }
10263 function notify$c() {
10264 const snapshot = Array.from(listeners$a);
10265 for (const cb of snapshot) {
10266 try {
10267 cb();
10268 } catch (err) {
10269 if (typeof console !== "undefined") {
10270 console.error(
10271 "[desktop-mode] settings-tab-registry listener threw:",
10272 err
10273 );
10274 }
10275 }
10276 }
10277 }
10278 function createSettingsTabRegistrySync() {
10279 const loadedHandles = /* @__PURE__ */ new Set();
10280 const loadedUrls = /* @__PURE__ */ new Set();
10281 let prevIdsByHandle = /* @__PURE__ */ new Map();
10282 const ensureScript = async (entry) => {
10283 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
10284 loadedHandles.add(entry.handle);
10285 return;
10286 }
10287 try {
10288 await loadVendorScript(entry.scriptUrl, {
10289 translations: entry.scriptTranslations,
10290 l10n: entry.scriptL10n,
10291 before: entry.scriptBefore,
10292 after: entry.scriptAfter
10293 });
10294 } catch (err) {
10295 doAction(HOOKS.SHELL_ERROR, {
10296 scope: "settings-tab-script-load",
10297 handle: entry.handle,
10298 url: entry.scriptUrl,
10299 error: err
10300 });
10301 return;
10302 }
10303 loadedUrls.add(entry.scriptUrl);
10304 loadedHandles.add(entry.handle);
10305 };
10306 const idsByHandleFrom = (tabs) => {
10307 const map = /* @__PURE__ */ new Map();
10308 if (!tabs) {
10309 return map;
10310 }
10311 for (const entry of tabs) {
10312 if (!entry.scriptHandle || !entry.id) {
10313 continue;
10314 }
10315 let set = map.get(entry.scriptHandle);
10316 if (!set) {
10317 set = /* @__PURE__ */ new Set();
10318 map.set(entry.scriptHandle, set);
10319 }
10320 set.add(entry.id);
10321 }
10322 return map;
10323 };
10324 const removeByHandle = (handle) => {
10325 unregisterSettingsTabsByOwner(handle);
10326 const declared = prevIdsByHandle.get(handle);
10327 if (declared) {
10328 const present = new Set(
10329 listSettingsTabs().map((t) => t.id)
10330 );
10331 for (const id of declared) {
10332 if (present.has(id)) {
10333 unregisterSettingsTab(id);
10334 }
10335 }
10336 }
10337 };
10338 return async (scripts, tabs) => {
10339 const incomingHandles = /* @__PURE__ */ new Set();
10340 for (const entry of scripts) {
10341 if (entry.handle) {
10342 incomingHandles.add(entry.handle);
10343 }
10344 }
10345 for (const handle of Array.from(loadedHandles)) {
10346 if (incomingHandles.has(handle)) {
10347 continue;
10348 }
10349 removeByHandle(handle);
10350 loadedHandles.delete(handle);
10351 }
10352 for (const entry of scripts) {
10353 if (!entry.handle || loadedHandles.has(entry.handle)) {
10354 continue;
10355 }
10356 await ensureScript(entry);
10357 }
10358 prevIdsByHandle = idsByHandleFrom(tabs);
10359 };
10360 }
10361 const store$b = createSharedStore(
10362 "desktop-mode/title-bar-buttons-registry",
10363 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
10364 );
10365 const registry$6 = store$b.state.registry;
10366 const listeners$9 = store$b.state.listeners;
10367 const TITLE_BAR_BUTTON_ID = /^[a-z0-9_/-]+$/;
10368 function registerTitleBarButton(def) {
10369 const errors = [];
10370 if (!def || typeof def !== "object") {
10371 errors.push("def (not an object)");
10372 } else {
10373 if (typeof def.id !== "string" || def.id.trim() === "") {
10374 errors.push("id (missing)");
10375 } else if (!TITLE_BAR_BUTTON_ID.test(def.id.trim().toLowerCase())) {
10376 errors.push(
10377 `id (must match ${TITLE_BAR_BUTTON_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
10378 );
10379 }
10380 if (typeof def.label !== "string" || def.label.trim() === "") {
10381 errors.push("label (missing)");
10382 }
10383 if (typeof def.icon !== "string" || def.icon.trim() === "") {
10384 errors.push("icon (missing)");
10385 }
10386 if (typeof def.match !== "function") {
10387 errors.push("match (must be a function)");
10388 }
10389 if (typeof def.onClick !== "function" && typeof def.render !== "function") {
10390 errors.push("onClick|render (at least one must be a function)");
10391 }
10392 }
10393 throwOnRegistrationErrors("TitleBarButton", errors, def);
10394 const id = def.id.trim().toLowerCase();
10395 registry$6.set(id, { ...def, id });
10396 notify$b();
10397 }
10398 function unregisterTitleBarButton(id) {
10399 if (registry$6.delete(id.toLowerCase())) {
10400 notify$b();
10401 }
10402 }
10403 function unregisterTitleBarButtonsByOwner(owner) {
10404 if (!owner) {
10405 return 0;
10406 }
10407 let removed = 0;
10408 for (const [id, def] of Array.from(registry$6.entries())) {
10409 if (def.owner === owner) {
10410 registry$6.delete(id);
10411 removed++;
10412 }
10413 }
10414 if (removed > 0) {
10415 notify$b();
10416 }
10417 return removed;
10418 }
10419 function listTitleBarButtons() {
10420 return Array.from(registry$6.values()).sort(
10421 (a, b) => (a.order ?? 100) - (b.order ?? 100)
10422 );
10423 }
10424 function notify$b() {
10425 const snapshot = Array.from(listeners$9);
10426 for (const cb of snapshot) {
10427 try {
10428 cb();
10429 } catch (err) {
10430 if (typeof console !== "undefined") {
10431 console.error(
10432 "[desktop-mode] title-bar-button registry listener threw:",
10433 err
10434 );
10435 }
10436 }
10437 }
10438 }
10439 function createTitleBarButtonRegistrySync() {
10440 const loadedHandles = /* @__PURE__ */ new Set();
10441 const loadedUrls = /* @__PURE__ */ new Set();
10442 const ensureScript = async (entry) => {
10443 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
10444 loadedHandles.add(entry.handle);
10445 return;
10446 }
10447 try {
10448 await loadVendorScript(entry.scriptUrl, {
10449 translations: entry.scriptTranslations,
10450 l10n: entry.scriptL10n,
10451 before: entry.scriptBefore,
10452 after: entry.scriptAfter
10453 });
10454 } catch (err) {
10455 doAction(HOOKS.SHELL_ERROR, {
10456 scope: "titlebar-button-script-load",
10457 handle: entry.handle,
10458 url: entry.scriptUrl,
10459 error: err
10460 });
10461 return;
10462 }
10463 loadedUrls.add(entry.scriptUrl);
10464 loadedHandles.add(entry.handle);
10465 };
10466 return async (scripts) => {
10467 const incomingHandles = /* @__PURE__ */ new Set();
10468 for (const entry of scripts) {
10469 if (entry.handle) {
10470 incomingHandles.add(entry.handle);
10471 }
10472 }
10473 for (const handle of Array.from(loadedHandles)) {
10474 if (incomingHandles.has(handle)) {
10475 continue;
10476 }
10477 unregisterTitleBarButtonsByOwner(handle);
10478 loadedHandles.delete(handle);
10479 }
10480 for (const entry of scripts) {
10481 if (!entry.handle || loadedHandles.has(entry.handle)) {
10482 continue;
10483 }
10484 await ensureScript(entry);
10485 }
10486 };
10487 }
10488 const WINDOW_LINK_RENDERER_NONE = "none";
10489 const WINDOW_LINK_RENDERER_DEFAULT = "svg-splines";
10490 const store$a = createSharedStore(
10491 "desktop-mode/window-link-renderer-registry",
10492 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
10493 );
10494 const registry$5 = store$a.state.registry;
10495 const listeners$8 = store$a.state.listeners;
10496 const WINDOW_LINK_RENDERER_ID = /^[a-z0-9_/-]+$/;
10497 function registerWindowLinkRenderer(def) {
10498 const errors = [];
10499 if (!def || typeof def !== "object") {
10500 errors.push("def (not an object)");
10501 } else {
10502 if (typeof def.id !== "string" || def.id.trim() === "") {
10503 errors.push("id (missing)");
10504 } else if (!WINDOW_LINK_RENDERER_ID.test(def.id.trim().toLowerCase())) {
10505 errors.push(
10506 `id (must match ${WINDOW_LINK_RENDERER_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
10507 );
10508 } else if (def.id.trim().toLowerCase() === WINDOW_LINK_RENDERER_NONE) {
10509 errors.push('id ("none" is reserved)');
10510 }
10511 if (typeof def.label !== "string" || def.label.trim() === "") {
10512 errors.push("label (missing)");
10513 }
10514 if (typeof def.mount !== "function") {
10515 errors.push("mount (not a function)");
10516 }
10517 }
10518 throwOnRegistrationErrors("WindowLinkRenderer", errors, def);
10519 const id = def.id.trim().toLowerCase();
10520 registry$5.set(id, { ...def, id });
10521 notify$a();
10522 }
10523 function unregisterWindowLinkRenderer(id) {
10524 if (registry$5.delete(id.toLowerCase())) {
10525 notify$a();
10526 }
10527 }
10528 function unregisterWindowLinkRenderersByOwner(owner) {
10529 if (!owner) {
10530 return 0;
10531 }
10532 let removed = 0;
10533 for (const [id, def] of Array.from(registry$5.entries())) {
10534 if (def.owner === owner) {
10535 registry$5.delete(id);
10536 removed++;
10537 }
10538 }
10539 if (removed > 0) {
10540 notify$a();
10541 }
10542 return removed;
10543 }
10544 function listWindowLinkRenderers() {
10545 const copy = Array.from(registry$5.values());
10546 const filtered = applyFilters(
10547 HOOKS.WINDOW_LINK_RENDERERS,
10548 copy
10549 );
10550 if (!Array.isArray(filtered)) {
10551 if (typeof console !== "undefined") {
10552 console.warn(
10553 "[desktop-mode] `desktop-mode.window-links.renderers` filter returned a non-array; falling back to registry list."
10554 );
10555 }
10556 return copy;
10557 }
10558 return filtered;
10559 }
10560 function getWindowLinkRenderer(id) {
10561 return listWindowLinkRenderers().find((r) => r.id === id);
10562 }
10563 function subscribeWindowLinkRenderers(cb) {
10564 listeners$8.add(cb);
10565 return () => {
10566 listeners$8.delete(cb);
10567 };
10568 }
10569 function notify$a() {
10570 const snapshot = Array.from(listeners$8);
10571 for (const cb of snapshot) {
10572 try {
10573 cb();
10574 } catch (err) {
10575 if (typeof console !== "undefined") {
10576 console.error(
10577 "[desktop-mode] window-link-renderer registry listener threw:",
10578 err
10579 );
10580 }
10581 }
10582 }
10583 }
10584 const MIN_VISIBLE_SEGMENT = 16;
10585 function subtractIntervals(base, holes) {
10586 const sorted = holes.map((h) => ({
10587 start: Math.max(base.start, h.start),
10588 end: Math.min(base.end, h.end)
10589 })).filter((h) => h.end > h.start).sort((a, b) => a.start - b.start);
10590 const out = [];
10591 let cursor = base.start;
10592 for (const hole of sorted) {
10593 if (hole.start > cursor) {
10594 out.push({ start: cursor, end: hole.start });
10595 }
10596 cursor = Math.max(cursor, hole.end);
10597 }
10598 if (cursor < base.end) {
10599 out.push({ start: cursor, end: base.end });
10600 }
10601 return out;
10602 }
10603 function anchorOnBorder(rect, toward) {
10604 const cx = rect.x + rect.width / 2;
10605 const cy = rect.y + rect.height / 2;
10606 const dx = toward.x - cx;
10607 const dy = toward.y - cy;
10608 if (dx === 0 && dy === 0) {
10609 return { x: cx, y: cy, side: "right" };
10610 }
10611 const sx = dx !== 0 ? rect.width / 2 / Math.abs(dx) : Infinity;
10612 const sy = dy !== 0 ? rect.height / 2 / Math.abs(dy) : Infinity;
10613 const s = Math.min(sx, sy);
10614 const x = cx + dx * s;
10615 const y = cy + dy * s;
10616 let side;
10617 if (sx <= sy) {
10618 side = dx > 0 ? "right" : "left";
10619 } else {
10620 side = dy > 0 ? "bottom" : "top";
10621 }
10622 return { x, y, side };
10623 }
10624 function isPointVisible(point, zIndex, obstacles, selfId) {
10625 for (const o of obstacles) {
10626 if (o.windowId === selfId || o.zIndex <= zIndex) {
10627 continue;
10628 }
10629 if (point.x >= o.rect.x && point.x <= o.rect.x + o.rect.width && point.y >= o.rect.y && point.y <= o.rect.y + o.rect.height) {
10630 return false;
10631 }
10632 }
10633 return true;
10634 }
10635 function visibleBorderAnchor(rect, zIndex, obstacles, selfId, toward) {
10636 const occluders = obstacles.filter(
10637 (o) => o.windowId !== selfId && o.zIndex > zIndex
10638 );
10639 const sides = [
10640 {
10641 side: "top",
10642 base: { start: rect.x, end: rect.x + rect.width },
10643 at: rect.y,
10644 horizontal: true
10645 },
10646 {
10647 side: "bottom",
10648 base: { start: rect.x, end: rect.x + rect.width },
10649 at: rect.y + rect.height,
10650 horizontal: true
10651 },
10652 {
10653 side: "left",
10654 base: { start: rect.y, end: rect.y + rect.height },
10655 at: rect.x,
10656 horizontal: false
10657 },
10658 {
10659 side: "right",
10660 base: { start: rect.y, end: rect.y + rect.height },
10661 at: rect.x + rect.width,
10662 horizontal: false
10663 }
10664 ];
10665 let best = null;
10666 let bestDistance = Infinity;
10667 for (const { side, base, at, horizontal } of sides) {
10668 const holes = [];
10669 for (const { rect: o } of occluders) {
10670 const coversLine = horizontal ? o.y <= at && at <= o.y + o.height : o.x <= at && at <= o.x + o.width;
10671 if (!coversLine) {
10672 continue;
10673 }
10674 holes.push(
10675 horizontal ? { start: o.x, end: o.x + o.width } : { start: o.y, end: o.y + o.height }
10676 );
10677 }
10678 for (const segment of subtractIntervals(base, holes)) {
10679 if (segment.end - segment.start < MIN_VISIBLE_SEGMENT) {
10680 continue;
10681 }
10682 const mid = (segment.start + segment.end) / 2;
10683 const x = horizontal ? mid : at;
10684 const y = horizontal ? at : mid;
10685 const distance2 = Math.hypot(toward.x - x, toward.y - y);
10686 if (distance2 < bestDistance) {
10687 bestDistance = distance2;
10688 best = { x, y, side };
10689 }
10690 }
10691 }
10692 return best;
10693 }
10694 function closestBorderAnchors(a, b) {
10695 const gapX = Math.max(b.x - (a.x + a.width), a.x - (b.x + b.width));
10696 const gapY = Math.max(
10697 b.y - (a.y + a.height),
10698 a.y - (b.y + b.height)
10699 );
10700 if (gapX < 0 && gapY < 0) {
10701 return null;
10702 }
10703 const overlapX1 = Math.max(a.x, b.x);
10704 const overlapX2 = Math.min(a.x + a.width, b.x + b.width);
10705 const overlapY1 = Math.max(a.y, b.y);
10706 const overlapY2 = Math.min(a.y + a.height, b.y + b.height);
10707 let ax;
10708 let bx;
10709 if (overlapX2 >= overlapX1) {
10710 ax = bx = (overlapX1 + overlapX2) / 2;
10711 } else if (b.x > a.x) {
10712 ax = a.x + a.width;
10713 bx = b.x;
10714 } else {
10715 ax = a.x;
10716 bx = b.x + b.width;
10717 }
10718 let ay;
10719 let by;
10720 if (overlapY2 >= overlapY1) {
10721 ay = by = (overlapY1 + overlapY2) / 2;
10722 } else if (b.y > a.y) {
10723 ay = a.y + a.height;
10724 by = b.y;
10725 } else {
10726 ay = a.y;
10727 by = b.y + b.height;
10728 }
10729 const horizontal = gapX >= gapY;
10730 const sideOf = (rect, x, y) => {
10731 if (horizontal) {
10732 return x <= rect.x ? "left" : "right";
10733 }
10734 return y <= rect.y ? "top" : "bottom";
10735 };
10736 return {
10737 from: { x: ax, y: ay, side: sideOf(a, ax, ay) },
10738 to: { x: bx, y: by, side: sideOf(b, bx, by) }
10739 };
10740 }
10741 function controlPoint(anchor, distance2) {
10742 const k = Math.min(160, Math.max(24, 0.4 * distance2));
10743 switch (anchor.side) {
10744 case "left":
10745 return { x: anchor.x - k, y: anchor.y };
10746 case "right":
10747 return { x: anchor.x + k, y: anchor.y };
10748 case "top":
10749 return { x: anchor.x, y: anchor.y - k };
10750 default:
10751 return { x: anchor.x, y: anchor.y + k };
10752 }
10753 }
10754 function centerOf(rect) {
10755 return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
10756 }
10757 const SVG_NS = "http://www.w3.org/2000/svg";
10758 let _mountSeq = 0;
10759 function endpointAnchor(rect, zIndex, windowId, obstacles, toward) {
10760 const classic = anchorOnBorder(rect, toward);
10761 if (zIndex === null || isPointVisible(classic, zIndex, obstacles, windowId)) {
10762 return classic;
10763 }
10764 return visibleBorderAnchor(rect, zIndex, obstacles, windowId, toward) ?? classic;
10765 }
10766 function buildMarkers(svg, idBase) {
10767 const defs = document.createElementNS(SVG_NS, "defs");
10768 const make = (suffix, className, size) => {
10769 const id = `${idBase}-${suffix}`;
10770 const marker = document.createElementNS(SVG_NS, "marker");
10771 marker.setAttribute("id", id);
10772 marker.setAttribute("viewBox", "0 0 10 10");
10773 marker.setAttribute("refX", "5");
10774 marker.setAttribute("refY", "5");
10775 marker.setAttribute("markerWidth", size);
10776 marker.setAttribute("markerHeight", size);
10777 marker.setAttribute("markerUnits", "strokeWidth");
10778 const tip = document.createElementNS(SVG_NS, "circle");
10779 tip.setAttribute("cx", "5");
10780 tip.setAttribute("cy", "5");
10781 tip.setAttribute("r", "4");
10782 tip.classList.add(className);
10783 marker.appendChild(tip);
10784 defs.appendChild(marker);
10785 return id;
10786 };
10787 const endpoint = "desktop-mode-window-link__endpoint";
10788 const active2 = `${endpoint}--active`;
10789 const markers = {
10790 dot: {
10791 normal: make("dot", endpoint, "7"),
10792 active: make("dot-active", active2, "7")
10793 },
10794 port: {
10795 normal: make("port", endpoint, "4.5"),
10796 active: make("port-active", active2, "4.5")
10797 }
10798 };
10799 svg.appendChild(defs);
10800 return markers;
10801 }
10802 registerWindowLinkRenderer({
10803 id: "svg-splines",
10804 label: __("Splines"),
10805 description: __(
10806 "Curved connectors between related windows, ending in circular dots — the larger dot sits on the window the content belongs to; windows that reference each other get large dots on both ends."
10807 ),
10808 mount: (ctx) => {
10809 const seq = ++_mountSeq;
10810 const buildSurface = (container, suffix) => {
10811 const svg = document.createElementNS(SVG_NS, "svg");
10812 svg.classList.add("desktop-mode-window-links__svg");
10813 container.appendChild(svg);
10814 return {
10815 svg,
10816 markers: buildMarkers(
10817 svg,
10818 `desktop-mode-window-link-${seq}${suffix}`
10819 )
10820 };
10821 };
10822 const surfaces = {
10823 base: buildSurface(ctx.container, ""),
10824 elevated: buildSurface(ctx.elevatedContainer, "-elevated")
10825 };
10826 const edges = /* @__PURE__ */ new Map();
10827 const draw = (frame) => {
10828 for (const { svg } of [surfaces.base, surfaces.elevated]) {
10829 svg.setAttribute("width", String(frame.container.width));
10830 svg.setAttribute(
10831 "height",
10832 String(frame.container.height)
10833 );
10834 svg.setAttribute(
10835 "viewBox",
10836 `0 0 ${frame.container.width} ${frame.container.height}`
10837 );
10838 }
10839 const seen = /* @__PURE__ */ new Set();
10840 for (const edge of frame.edges) {
10841 if (!edge.from || !edge.to) {
10842 continue;
10843 }
10844 const key = `${edge.fromWindowId}→${edge.toWindowId}:${edge.kind}`;
10845 seen.add(key);
10846 const surfaceName = edge.elevated ? "elevated" : "base";
10847 let el = edges.get(key);
10848 if (el && el.surface !== surfaceName) {
10849 el.group.remove();
10850 edges.delete(key);
10851 el = void 0;
10852 }
10853 if (!el) {
10854 const group = document.createElementNS(SVG_NS, "g");
10855 group.classList.add("desktop-mode-window-link");
10856 const path = document.createElementNS(SVG_NS, "path");
10857 path.classList.add("desktop-mode-window-link__path");
10858 group.appendChild(path);
10859 surfaces[surfaceName].svg.appendChild(group);
10860 el = { group, path, surface: surfaceName };
10861 edges.set(key, el);
10862 }
10863 const obstacles = frame.obstacles ?? [];
10864 const shortest = closestBorderAnchors(edge.from, edge.to);
10865 const visibleAt = (anchor, zIndex, windowId) => zIndex === null || isPointVisible(anchor, zIndex, obstacles, windowId);
10866 let start = null;
10867 if (shortest && visibleAt(
10868 shortest.from,
10869 edge.fromZIndex,
10870 edge.fromWindowId
10871 )) {
10872 start = shortest.from;
10873 }
10874 if (!start) {
10875 start = endpointAnchor(
10876 edge.from,
10877 edge.fromZIndex,
10878 edge.fromWindowId,
10879 obstacles,
10880 shortest ? { x: shortest.to.x, y: shortest.to.y } : centerOf(edge.to)
10881 );
10882 }
10883 let end = null;
10884 if (shortest && visibleAt(shortest.to, edge.toZIndex, edge.toWindowId)) {
10885 end = shortest.to;
10886 }
10887 if (!end) {
10888 end = endpointAnchor(
10889 edge.to,
10890 edge.toZIndex,
10891 edge.toWindowId,
10892 obstacles,
10893 // Aim the target anchor at the resolved source
10894 // anchor so the curve's two ends agree when
10895 // either moved off the shortest pair.
10896 { x: start.x, y: start.y }
10897 );
10898 }
10899 const distance2 = Math.hypot(
10900 end.x - start.x,
10901 end.y - start.y
10902 );
10903 const c1 = controlPoint(start, distance2);
10904 const c2 = controlPoint(end, distance2);
10905 el.path.setAttribute(
10906 "d",
10907 `M ${start.x} ${start.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${end.x} ${end.y}`
10908 );
10909 const markers = surfaces[el.surface].markers;
10910 const variant = edge.focused ? "active" : "normal";
10911 el.path.setAttribute(
10912 "marker-end",
10913 `url(#${markers.dot[variant]})`
10914 );
10915 el.path.setAttribute(
10916 "marker-start",
10917 `url(#${edge.bidirectional ? markers.dot[variant] : markers.port[variant]})`
10918 );
10919 el.group.classList.toggle(
10920 "desktop-mode-window-link--active",
10921 edge.focused
10922 );
10923 }
10924 for (const [key, el] of Array.from(edges)) {
10925 if (!seen.has(key)) {
10926 el.group.remove();
10927 edges.delete(key);
10928 }
10929 }
10930 };
10931 const unsubscribe = ctx.onFrame(draw);
10932 draw(ctx.getFrame());
10933 return () => {
10934 unsubscribe();
10935 edges.clear();
10936 surfaces.base.svg.remove();
10937 surfaces.elevated.svg.remove();
10938 };
10939 }
10940 });
10941 const LAYER_ID = "desktop-mode-window-links";
10942 const LINKED_CLASS = "desktop-mode-window--linked";
10943 const VISIBLE_CLASS = "desktop-mode-window-links--visible";
10944 let _started$2 = false;
10945 function startWindowLinkRenderHost({
10946 manager,
10947 osSettings
10948 }) {
10949 if (_started$2) {
10950 return;
10951 }
10952 _started$2 = true;
10953 let snapshot = osSettings.getOsSettingsSnapshot();
10954 let layer = null;
10955 let elevatedLayer = null;
10956 let mountedId = null;
10957 let teardown = null;
10958 let mountToken = 0;
10959 const frameSubscribers = /* @__PURE__ */ new Set();
10960 let framePending = false;
10961 const linkedWindows = /* @__PURE__ */ new Set();
10962 let overviewActive = false;
10963 const rectOf = (win) => {
10964 const el = win.element;
10965 if (!el || !el.isConnected || win.state === "minimized" || // Hidden desktops / display-suppressed windows measure 0×0
10966 // and have no offsetParent — skip their edges entirely.
10967 el.offsetParent === null) {
10968 return null;
10969 }
10970 return {
10971 x: el.offsetLeft,
10972 y: el.offsetTop,
10973 width: el.offsetWidth,
10974 height: el.offsetHeight
10975 };
10976 };
10977 const drawableRectOf = (win) => {
10978 if (win.state === "snapped-left" || win.state === "snapped-right") {
10979 return null;
10980 }
10981 return rectOf(win);
10982 };
10983 const buildFrame2 = () => {
10984 const groups = [];
10985 for (const group of listWindowLinkGroups()) {
10986 if (group.rootWindowIds.length === 0 || group.children.length === 0) {
10987 continue;
10988 }
10989 const members = [];
10990 const push = (windowId, role, content) => {
10991 const win = manager.getById(windowId);
10992 if (!win || !content) {
10993 return;
10994 }
10995 members.push({
10996 windowId,
10997 role,
10998 content,
10999 rect: drawableRectOf(win),
11000 focused: win.isFocused(),
11001 state: win.state
11002 });
11003 };
11004 for (const id of group.rootWindowIds) {
11005 push(id, "root", getWindowContent(id));
11006 }
11007 for (const child of group.children) {
11008 push(child.windowId, "child", child.content);
11009 }
11010 if (members.length > 0) {
11011 groups.push({ key: group.key, root: group.root, members });
11012 }
11013 }
11014 const zOf = (win) => {
11015 const z = Number.parseInt(
11016 win.element?.style.zIndex || "",
11017 10
11018 );
11019 return Number.isFinite(z) ? z : null;
11020 };
11021 const focusedId = manager.getFocused()?.id ?? null;
11022 const edges = [];
11023 for (const edge of listWindowLinkEdges()) {
11024 const fromWin = manager.getById(edge.fromWindowId);
11025 const toWin = manager.getById(edge.toWindowId);
11026 if (!fromWin || !toWin) {
11027 continue;
11028 }
11029 const focused = fromWin.isFocused() || toWin.isFocused();
11030 edges.push({
11031 fromWindowId: edge.fromWindowId,
11032 toWindowId: edge.toWindowId,
11033 kind: edge.kind,
11034 bidirectional: edge.bidirectional,
11035 focused,
11036 from: drawableRectOf(fromWin),
11037 to: drawableRectOf(toWin),
11038 fromZIndex: zOf(fromWin),
11039 toZIndex: zOf(toWin),
11040 // Only ties TOUCHING the focused window ride the
11041 // elevated layer — an edge between two unfocused
11042 // windows must never draw over a window that happens
11043 // to share a group with the focused one.
11044 elevated: focusedId !== null && (edge.fromWindowId === focusedId || edge.toWindowId === focusedId)
11045 });
11046 }
11047 const obstacles = [];
11048 for (const win of manager.getAll()) {
11049 const rect = rectOf(win);
11050 if (!rect) {
11051 continue;
11052 }
11053 obstacles.push({
11054 windowId: win.id,
11055 rect,
11056 zIndex: zOf(win) ?? 0
11057 });
11058 }
11059 return {
11060 groups,
11061 edges,
11062 obstacles,
11063 container: {
11064 width: layer?.offsetWidth ?? 0,
11065 height: layer?.offsetHeight ?? 0
11066 }
11067 };
11068 };
11069 const emitFrame = () => {
11070 if (framePending || frameSubscribers.size === 0) {
11071 return;
11072 }
11073 framePending = true;
11074 requestAnimationFrame(() => {
11075 framePending = false;
11076 if (!mountedId) {
11077 return;
11078 }
11079 const frame = buildFrame2();
11080 for (const cb of Array.from(frameSubscribers)) {
11081 try {
11082 cb(frame);
11083 } catch (err) {
11084 if (typeof console !== "undefined") {
11085 console.error(
11086 "[desktop-mode] window-link frame subscriber threw:",
11087 err
11088 );
11089 }
11090 }
11091 }
11092 });
11093 };
11094 const ensureLayer = () => {
11095 if (layer && layer.isConnected && elevatedLayer?.isConnected) {
11096 return layer;
11097 }
11098 const area = document.getElementById("desktop-mode-area");
11099 if (!area) {
11100 return null;
11101 }
11102 layer = document.createElement("div");
11103 layer.id = LAYER_ID;
11104 layer.className = "desktop-mode-window-links";
11105 layer.setAttribute("aria-hidden", "true");
11106 elevatedLayer = document.createElement("div");
11107 elevatedLayer.id = `${LAYER_ID}-elevated`;
11108 elevatedLayer.className = "desktop-mode-window-links desktop-mode-window-links--elevated";
11109 elevatedLayer.setAttribute("aria-hidden", "true");
11110 const widgets = document.getElementById("desktop-mode-widgets");
11111 if (widgets && widgets.parentElement === area) {
11112 widgets.insertAdjacentElement("afterend", elevatedLayer);
11113 widgets.insertAdjacentElement("afterend", layer);
11114 } else {
11115 area.prepend(layer, elevatedLayer);
11116 }
11117 return layer;
11118 };
11119 const isRenderable = () => listWindowLinkEdges().length > 0;
11120 const resolveRendererId = () => {
11121 let id = snapshot.windowLinkRenderer || WINDOW_LINK_RENDERER_DEFAULT;
11122 id = applyFilters(HOOKS.WINDOW_LINK_RENDERER, id);
11123 if (id === WINDOW_LINK_RENDERER_NONE) {
11124 return WINDOW_LINK_RENDERER_NONE;
11125 }
11126 if (getWindowLinkRenderer(id)) {
11127 return id;
11128 }
11129 return getWindowLinkRenderer(WINDOW_LINK_RENDERER_DEFAULT) ? WINDOW_LINK_RENDERER_DEFAULT : WINDOW_LINK_RENDERER_NONE;
11130 };
11131 const unmountRenderer = () => {
11132 mountToken++;
11133 frameSubscribers.clear();
11134 framePending = false;
11135 if (teardown) {
11136 try {
11137 teardown();
11138 } catch (err) {
11139 doAction(HOOKS.SHELL_ERROR, {
11140 scope: "window-link-renderer-teardown",
11141 error: err
11142 });
11143 }
11144 teardown = null;
11145 }
11146 mountedId = null;
11147 layer?.replaceChildren();
11148 elevatedLayer?.replaceChildren();
11149 };
11150 const mountRenderer = (id) => {
11151 const def = getWindowLinkRenderer(id);
11152 const host = ensureLayer();
11153 if (!def || !host || !elevatedLayer) {
11154 return;
11155 }
11156 mountedId = id;
11157 const token = ++mountToken;
11158 const ctx = {
11159 container: host,
11160 elevatedContainer: elevatedLayer,
11161 getFrame: buildFrame2,
11162 onFrame: (cb) => {
11163 frameSubscribers.add(cb);
11164 return () => {
11165 frameSubscribers.delete(cb);
11166 };
11167 }
11168 };
11169 try {
11170 const result = def.mount(ctx);
11171 if (result instanceof Promise) {
11172 result.then((cleanup) => {
11173 if (token !== mountToken) {
11174 if (typeof cleanup === "function") {
11175 cleanup();
11176 }
11177 return;
11178 }
11179 if (typeof cleanup === "function") {
11180 teardown = cleanup;
11181 }
11182 }).catch((err) => {
11183 doAction(HOOKS.SHELL_ERROR, {
11184 scope: "window-link-renderer-mount",
11185 error: err
11186 });
11187 if (token === mountToken) {
11188 mountedId = null;
11189 }
11190 });
11191 } else if (typeof result === "function") {
11192 teardown = result;
11193 }
11194 } catch (err) {
11195 doAction(HOOKS.SHELL_ERROR, {
11196 scope: "window-link-renderer-mount",
11197 error: err
11198 });
11199 mountedId = null;
11200 }
11201 emitFrame();
11202 };
11203 const focusedNeighbors = () => {
11204 const focused = manager.getFocused();
11205 if (!focused) {
11206 return /* @__PURE__ */ new Set();
11207 }
11208 return new Set(getRelatedWindowIds(focused.id));
11209 };
11210 const isEnabled = () => snapshot.windowLinksEnabled !== false;
11211 const applyVisibility = () => {
11212 if (!layer) {
11213 return;
11214 }
11215 const visible = !overviewActive && isEnabled() && (snapshot.windowLinkVisibility === "always" || snapshot.windowLinkVisibility === "focus" && focusedNeighbors().size > 0);
11216 layer.classList.toggle(VISIBLE_CLASS, visible);
11217 elevatedLayer?.classList.toggle(VISIBLE_CLASS, visible);
11218 };
11219 const raiseRelated = () => {
11220 if (!isEnabled() || snapshot.windowLinkRaiseOnFocus === false || snapshot.windowLinkVisibility === "off") {
11221 return;
11222 }
11223 const focused = manager.getFocused();
11224 if (!focused) {
11225 return;
11226 }
11227 for (const id of getDirectlyRelatedWindowIds(focused.id)) {
11228 const win = manager.getById(id);
11229 if (win && win.state !== "minimized") {
11230 manager.raise(id);
11231 }
11232 }
11233 };
11234 const applyLayerElevation = () => {
11235 if (!elevatedLayer) {
11236 return;
11237 }
11238 const focused = manager.getFocused();
11239 const related = focusedNeighbors();
11240 if (!focused || related.size === 0 || !isEnabled() || snapshot.windowLinkVisibility === "off") {
11241 elevatedLayer.style.zIndex = "";
11242 return;
11243 }
11244 let maxZ = -Infinity;
11245 for (const id of [focused.id, ...related]) {
11246 const win = manager.getById(id);
11247 const el = win?.element;
11248 if (!el || win.state === "minimized") {
11249 continue;
11250 }
11251 const z = Number.parseInt(el.style.zIndex || "", 10);
11252 if (Number.isFinite(z)) {
11253 maxZ = Math.max(maxZ, z);
11254 }
11255 }
11256 elevatedLayer.style.zIndex = Number.isFinite(maxZ) ? String(maxZ) : "";
11257 };
11258 const applyLinkedHighlight = () => {
11259 const next = isEnabled() && snapshot.windowLinkHighlight !== false && snapshot.windowLinkVisibility !== "off" ? focusedNeighbors() : /* @__PURE__ */ new Set();
11260 for (const id of linkedWindows) {
11261 if (!next.has(id)) {
11262 manager.getById(id)?.element?.classList.remove(LINKED_CLASS);
11263 }
11264 }
11265 for (const id of next) {
11266 manager.getById(id)?.element?.classList.add(LINKED_CLASS);
11267 }
11268 linkedWindows.clear();
11269 for (const id of next) {
11270 linkedWindows.add(id);
11271 }
11272 };
11273 const recompute = () => {
11274 const wantedId = isEnabled() && snapshot.windowLinkVisibility !== "off" && isRenderable() ? resolveRendererId() : WINDOW_LINK_RENDERER_NONE;
11275 if (wantedId === WINDOW_LINK_RENDERER_NONE) {
11276 if (mountedId) {
11277 unmountRenderer();
11278 }
11279 } else if (wantedId !== mountedId) {
11280 unmountRenderer();
11281 mountRenderer(wantedId);
11282 }
11283 applyVisibility();
11284 applyLinkedHighlight();
11285 applyLayerElevation();
11286 emitFrame();
11287 };
11288 addAction(
11289 HOOKS.WINDOW_BOUNDS_CHANGED,
11290 "desktop-mode/window-links-frame",
11291 () => emitFrame()
11292 );
11293 for (const hook of [
11294 HOOKS.WINDOW_MOVED,
11295 HOOKS.WINDOW_RESIZED,
11296 HOOKS.WINDOW_MINIMIZED,
11297 HOOKS.WINDOW_RESTORED,
11298 HOOKS.WINDOW_MAXIMIZED,
11299 HOOKS.WINDOW_UNMAXIMIZED,
11300 HOOKS.WINDOW_FULLSCREEN_ENTERED,
11301 HOOKS.WINDOW_FULLSCREEN_EXITED,
11302 HOOKS.SNAP_ZONE_COMMITTED,
11303 HOOKS.SNAP_SPLIT_FILLED,
11304 HOOKS.DESKTOP_SWITCHED,
11305 HOOKS.SHELL_RESIZED
11306 ]) {
11307 addAction(
11308 hook,
11309 "desktop-mode/window-links-frame",
11310 () => emitFrame()
11311 );
11312 }
11313 addAction(
11314 HOOKS.WINDOW_FOCUSED,
11315 "desktop-mode/window-links-focus",
11316 () => {
11317 raiseRelated();
11318 applyVisibility();
11319 applyLinkedHighlight();
11320 applyLayerElevation();
11321 emitFrame();
11322 }
11323 );
11324 addAction(
11325 HOOKS.WINDOW_BLURRED,
11326 "desktop-mode/window-links-blur",
11327 () => {
11328 applyVisibility();
11329 applyLinkedHighlight();
11330 applyLayerElevation();
11331 emitFrame();
11332 }
11333 );
11334 addAction(
11335 HOOKS.OVERVIEW_ENTERING,
11336 "desktop-mode/window-links-overview",
11337 () => {
11338 overviewActive = true;
11339 applyVisibility();
11340 }
11341 );
11342 addAction(
11343 HOOKS.OVERVIEW_EXITED,
11344 "desktop-mode/window-links-overview",
11345 () => {
11346 overviewActive = false;
11347 applyVisibility();
11348 emitFrame();
11349 }
11350 );
11351 subscribeWindowLinks(recompute);
11352 subscribeWindowLinkRenderers(recompute);
11353 osSettings.subscribeOsSettings((next) => {
11354 const rendererChanged = next.windowLinkRenderer !== snapshot.windowLinkRenderer;
11355 const anyChanged = rendererChanged || next.windowLinkVisibility !== snapshot.windowLinkVisibility || next.windowLinksEnabled !== snapshot.windowLinksEnabled || next.windowLinkRaiseOnFocus !== snapshot.windowLinkRaiseOnFocus || next.windowLinkHighlight !== snapshot.windowLinkHighlight;
11356 snapshot = next;
11357 if (anyChanged) {
11358 if (rendererChanged && mountedId) {
11359 unmountRenderer();
11360 }
11361 recompute();
11362 }
11363 });
11364 recompute();
11365 }
11366 const UNFOCUS_EFFECT_NONE = "none";
11367 const store$9 = createSharedStore(
11368 "desktop-mode/unfocus-effect-registry",
11369 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
11370 );
11371 const registry$4 = store$9.state.registry;
11372 const listeners$7 = store$9.state.listeners;
11373 const UNFOCUS_EFFECT_ID = /^[a-z0-9_/-]+$/;
11374 function registerUnfocusEffect(def) {
11375 const errors = [];
11376 if (!def || typeof def !== "object") {
11377 errors.push("def (not an object)");
11378 } else {
11379 if (typeof def.id !== "string" || def.id.trim() === "") {
11380 errors.push("id (missing)");
11381 } else if (!UNFOCUS_EFFECT_ID.test(def.id.trim().toLowerCase())) {
11382 errors.push(
11383 `id (must match ${UNFOCUS_EFFECT_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
11384 );
11385 } else if (def.id.trim().toLowerCase() === UNFOCUS_EFFECT_NONE) {
11386 errors.push('id ("none" is reserved)');
11387 }
11388 if (typeof def.label !== "string" || def.label.trim() === "") {
11389 errors.push("label (missing)");
11390 }
11391 if (typeof def.className !== "string" && typeof def.apply !== "function") {
11392 errors.push(
11393 "className|apply (at least one must be provided — a CSS class to toggle or an apply callback)"
11394 );
11395 }
11396 }
11397 throwOnRegistrationErrors("UnfocusEffect", errors, def);
11398 const id = def.id.trim().toLowerCase();
11399 registry$4.set(id, { ...def, id });
11400 notify$9();
11401 }
11402 function unregisterUnfocusEffect(id) {
11403 if (registry$4.delete(id.toLowerCase())) {
11404 notify$9();
11405 }
11406 }
11407 function unregisterUnfocusEffectsByOwner(owner) {
11408 if (!owner) {
11409 return 0;
11410 }
11411 let removed = 0;
11412 for (const [id, def] of Array.from(registry$4.entries())) {
11413 if (def.owner === owner) {
11414 registry$4.delete(id);
11415 removed++;
11416 }
11417 }
11418 if (removed > 0) {
11419 notify$9();
11420 }
11421 return removed;
11422 }
11423 function listUnfocusEffects() {
11424 const copy = Array.from(registry$4.values());
11425 const filtered = applyFilters(
11426 HOOKS.UNFOCUS_EFFECTS,
11427 copy
11428 );
11429 if (!Array.isArray(filtered)) {
11430 if (typeof console !== "undefined") {
11431 console.warn(
11432 "[desktop-mode] `desktop-mode.unfocus-effects` filter returned a non-array; falling back to registry list."
11433 );
11434 }
11435 return copy;
11436 }
11437 return filtered;
11438 }
11439 function getUnfocusEffect(id) {
11440 return listUnfocusEffects().find((e) => e.id === id);
11441 }
11442 function subscribeUnfocusEffects(cb) {
11443 listeners$7.add(cb);
11444 return () => {
11445 listeners$7.delete(cb);
11446 };
11447 }
11448 function notify$9() {
11449 const snapshot = Array.from(listeners$7);
11450 for (const cb of snapshot) {
11451 try {
11452 cb();
11453 } catch (err) {
11454 if (typeof console !== "undefined") {
11455 console.error(
11456 "[desktop-mode] unfocus-effect registry listener threw:",
11457 err
11458 );
11459 }
11460 }
11461 }
11462 }
11463 registerUnfocusEffect({
11464 id: "darken",
11465 label: __("Darken"),
11466 description: __("Dim unfocused windows so the focused one stands out."),
11467 className: "desktop-mode-window--fx-darken"
11468 });
11469 registerUnfocusEffect({
11470 id: "frost",
11471 label: __("Frost"),
11472 description: __(
11473 "Throw unfocused windows out of focus — a soft, frosted-glass blur, as if you were looking at them through an iced-over pane."
11474 ),
11475 className: "desktop-mode-window--fx-frost"
11476 });
11477 registerUnfocusEffect({
11478 id: "grayscale",
11479 label: __("Grayscale"),
11480 description: __(
11481 "Drain the colour from unfocused windows so the focused one is the only thing still in colour — your eye snaps right to it."
11482 ),
11483 className: "desktop-mode-window--fx-grayscale"
11484 });
11485 function createUnfocusEffectRegistrySync() {
11486 const loadedHandles = /* @__PURE__ */ new Set();
11487 const loadedUrls = /* @__PURE__ */ new Set();
11488 const ensureScript = async (entry) => {
11489 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
11490 loadedHandles.add(entry.handle);
11491 return;
11492 }
11493 try {
11494 await loadVendorScript(entry.scriptUrl, {
11495 translations: entry.scriptTranslations,
11496 l10n: entry.scriptL10n,
11497 before: entry.scriptBefore,
11498 after: entry.scriptAfter
11499 });
11500 } catch (err) {
11501 doAction(HOOKS.SHELL_ERROR, {
11502 scope: "unfocus-effect-script-load",
11503 handle: entry.handle,
11504 url: entry.scriptUrl,
11505 error: err
11506 });
11507 return;
11508 }
11509 loadedUrls.add(entry.scriptUrl);
11510 loadedHandles.add(entry.handle);
11511 };
11512 return async (scripts) => {
11513 const incomingHandles = /* @__PURE__ */ new Set();
11514 for (const entry of scripts) {
11515 if (entry.handle) {
11516 incomingHandles.add(entry.handle);
11517 }
11518 }
11519 for (const handle of Array.from(loadedHandles)) {
11520 if (incomingHandles.has(handle)) {
11521 continue;
11522 }
11523 unregisterUnfocusEffectsByOwner(handle);
11524 loadedHandles.delete(handle);
11525 }
11526 for (const entry of scripts) {
11527 if (!entry.handle || loadedHandles.has(entry.handle)) {
11528 continue;
11529 }
11530 await ensureScript(entry);
11531 }
11532 };
11533 }
11534 function createWindowLinkRendererRegistrySync() {
11535 const loadedHandles = /* @__PURE__ */ new Set();
11536 const loadedUrls = /* @__PURE__ */ new Set();
11537 const ensureScript = async (entry) => {
11538 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
11539 loadedHandles.add(entry.handle);
11540 return;
11541 }
11542 try {
11543 await loadVendorScript(entry.scriptUrl, {
11544 translations: entry.scriptTranslations,
11545 l10n: entry.scriptL10n,
11546 before: entry.scriptBefore,
11547 after: entry.scriptAfter
11548 });
11549 } catch (err) {
11550 doAction(HOOKS.SHELL_ERROR, {
11551 scope: "window-link-renderer-script-load",
11552 handle: entry.handle,
11553 url: entry.scriptUrl,
11554 error: err
11555 });
11556 return;
11557 }
11558 loadedUrls.add(entry.scriptUrl);
11559 loadedHandles.add(entry.handle);
11560 };
11561 return async (scripts) => {
11562 const incomingHandles = /* @__PURE__ */ new Set();
11563 for (const entry of scripts) {
11564 if (entry.handle) {
11565 incomingHandles.add(entry.handle);
11566 }
11567 }
11568 for (const handle of Array.from(loadedHandles)) {
11569 if (incomingHandles.has(handle)) {
11570 continue;
11571 }
11572 unregisterWindowLinkRenderersByOwner(handle);
11573 loadedHandles.delete(handle);
11574 }
11575 for (const entry of scripts) {
11576 if (!entry.handle || loadedHandles.has(entry.handle)) {
11577 continue;
11578 }
11579 await ensureScript(entry);
11580 }
11581 };
11582 }
11583 const EFFECT_ATTR = "data-desktop-unfocus-effect";
11584 const EFFECT_CLASS_ATTR = "data-desktop-unfocus-effect-class";
11585 let _started$1 = false;
11586 function hostsCanvas(el) {
11587 return el.querySelector("canvas") !== null;
11588 }
11589 function startUnfocusEngine({ manager, osSettings }) {
11590 if (_started$1) {
11591 return;
11592 }
11593 _started$1 = true;
11594 let currentId = osSettings.getOsSettingsSnapshot().unfocusEffect;
11595 const clear = (el, allEffects) => {
11596 const storedClass = el.getAttribute(EFFECT_CLASS_ATTR);
11597 if (storedClass) {
11598 el.classList.remove(storedClass);
11599 el.removeAttribute(EFFECT_CLASS_ATTR);
11600 }
11601 const priorId = el.getAttribute(EFFECT_ATTR);
11602 if (priorId) {
11603 getUnfocusEffect(priorId)?.clear?.(el);
11604 }
11605 for (const def of allEffects) {
11606 if (def.className) {
11607 el.classList.remove(def.className);
11608 }
11609 }
11610 el.removeAttribute(EFFECT_ATTR);
11611 };
11612 const apply = (el, def) => {
11613 if (def.className) {
11614 el.classList.add(def.className);
11615 el.setAttribute(EFFECT_CLASS_ATTR, def.className);
11616 }
11617 el.setAttribute(EFFECT_ATTR, def.id);
11618 def.apply?.(el);
11619 };
11620 const recompute = () => {
11621 const def = currentId === UNFOCUS_EFFECT_NONE ? void 0 : getUnfocusEffect(currentId);
11622 const allEffects = listUnfocusEffects();
11623 for (const win of manager.getAll()) {
11624 const el = win.element;
11625 if (!el) {
11626 continue;
11627 }
11628 clear(el, allEffects);
11629 if (!def || win.isFocused() || win.state === "minimized") {
11630 continue;
11631 }
11632 if (hostsCanvas(el)) {
11633 continue;
11634 }
11635 apply(el, def);
11636 }
11637 };
11638 for (const name of [
11639 "desktop-mode-window-opened",
11640 "desktop-mode-window-reopened",
11641 "desktop-mode-window-closed",
11642 "desktop-mode-window-focused",
11643 "desktop-mode-window-blurred"
11644 ]) {
11645 document.addEventListener(name, () => recompute());
11646 }
11647 osSettings.subscribeOsSettings((snapshot) => {
11648 currentId = snapshot.unfocusEffect;
11649 recompute();
11650 });
11651 subscribeUnfocusEffects(() => recompute());
11652 recompute();
11653 }
11654 function createDockRailRendererSync() {
11655 const loadedHandles = /* @__PURE__ */ new Set();
11656 const loadedUrls = /* @__PURE__ */ new Set();
11657 const ensureScript = async (entry) => {
11658 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
11659 loadedHandles.add(entry.handle);
11660 return;
11661 }
11662 try {
11663 await loadVendorScript(entry.scriptUrl, {
11664 translations: entry.scriptTranslations,
11665 l10n: entry.scriptL10n,
11666 before: entry.scriptBefore,
11667 after: entry.scriptAfter
11668 });
11669 } catch (err) {
11670 doAction(HOOKS.SHELL_ERROR, {
11671 scope: "dock-rail-renderer-script-load",
11672 handle: entry.handle,
11673 url: entry.scriptUrl,
11674 error: err
11675 });
11676 return;
11677 }
11678 loadedUrls.add(entry.scriptUrl);
11679 loadedHandles.add(entry.handle);
11680 };
11681 return async (scripts) => {
11682 const incomingHandles = /* @__PURE__ */ new Set();
11683 for (const entry of scripts) {
11684 if (entry.handle) {
11685 incomingHandles.add(entry.handle);
11686 }
11687 }
11688 for (const handle of Array.from(loadedHandles)) {
11689 if (incomingHandles.has(handle)) {
11690 continue;
11691 }
11692 unregisterByOwner$1(handle);
11693 loadedHandles.delete(handle);
11694 }
11695 for (const entry of scripts) {
11696 if (!entry.handle || loadedHandles.has(entry.handle)) {
11697 continue;
11698 }
11699 await ensureScript(entry);
11700 }
11701 };
11702 }
11703 const store$8 = createSharedStore(
11704 "desktop-mode/window-themes-registry",
11705 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
11706 );
11707 const registry$3 = store$8.state.registry;
11708 const listeners$6 = store$8.state.listeners;
11709 const WINDOW_THEME_ID = /^[a-z0-9_/-]+$/;
11710 function registerWindowTheme(def) {
11711 const errors = [];
11712 if (!def || typeof def !== "object") {
11713 errors.push("def (not an object)");
11714 } else {
11715 if (typeof def.id !== "string" || def.id.trim() === "") {
11716 errors.push("id (missing)");
11717 } else if (!WINDOW_THEME_ID.test(def.id.trim().toLowerCase())) {
11718 errors.push(
11719 `id (must match ${WINDOW_THEME_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
11720 );
11721 }
11722 if (!def.tokens || typeof def.tokens !== "object") {
11723 errors.push("tokens (must be an object of CSS custom-property → value)");
11724 } else {
11725 for (const key of Object.keys(def.tokens)) {
11726 if (!key.startsWith("--")) {
11727 errors.push(
11728 `tokens.${key} (CSS custom-property keys must start with "--")`
11729 );
11730 break;
11731 }
11732 }
11733 }
11734 if (typeof def.match !== "function") {
11735 errors.push("match (must be a function)");
11736 }
11737 }
11738 throwOnRegistrationErrors("WindowTheme", errors, def);
11739 const id = def.id.trim().toLowerCase();
11740 registry$3.set(id, { ...def, id });
11741 notify$8();
11742 }
11743 function unregisterWindowTheme(id) {
11744 if (registry$3.delete(id.toLowerCase())) {
11745 notify$8();
11746 }
11747 }
11748 function unregisterWindowThemesByOwner(owner) {
11749 if (!owner) {
11750 return 0;
11751 }
11752 let removed = 0;
11753 for (const [id, def] of Array.from(registry$3.entries())) {
11754 if (def.owner === owner) {
11755 registry$3.delete(id);
11756 removed++;
11757 }
11758 }
11759 if (removed > 0) {
11760 notify$8();
11761 }
11762 return removed;
11763 }
11764 function listWindowThemes() {
11765 return Array.from(registry$3.values()).sort(
11766 (a, b) => (a.priority ?? 100) - (b.priority ?? 100)
11767 );
11768 }
11769 function notify$8() {
11770 const snapshot = Array.from(listeners$6);
11771 for (const cb of snapshot) {
11772 try {
11773 cb();
11774 } catch (err) {
11775 if (typeof console !== "undefined") {
11776 console.error(
11777 "[desktop-mode] window-theme registry listener threw:",
11778 err
11779 );
11780 }
11781 }
11782 }
11783 }
11784 function createWindowThemeRegistrySync() {
11785 const loadedHandles = /* @__PURE__ */ new Set();
11786 const loadedUrls = /* @__PURE__ */ new Set();
11787 let prevIdsByHandle = /* @__PURE__ */ new Map();
11788 const shellRegistered = /* @__PURE__ */ new Set();
11789 const ensureScript = async (entry) => {
11790 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
11791 loadedHandles.add(entry.handle);
11792 return;
11793 }
11794 try {
11795 await loadVendorScript(entry.scriptUrl, {
11796 translations: entry.scriptTranslations,
11797 l10n: entry.scriptL10n,
11798 before: entry.scriptBefore,
11799 after: entry.scriptAfter
11800 });
11801 } catch (err) {
11802 doAction(HOOKS.SHELL_ERROR, {
11803 scope: "window-theme-script-load",
11804 handle: entry.handle,
11805 url: entry.scriptUrl,
11806 error: err
11807 });
11808 return;
11809 }
11810 loadedUrls.add(entry.scriptUrl);
11811 loadedHandles.add(entry.handle);
11812 };
11813 const idsByHandleFrom = (themes) => {
11814 const map = /* @__PURE__ */ new Map();
11815 if (!themes) {
11816 return map;
11817 }
11818 for (const entry of themes) {
11819 if (!entry.scriptHandle || !entry.id) {
11820 continue;
11821 }
11822 let set = map.get(entry.scriptHandle);
11823 if (!set) {
11824 set = /* @__PURE__ */ new Set();
11825 map.set(entry.scriptHandle, set);
11826 }
11827 set.add(entry.id);
11828 }
11829 return map;
11830 };
11831 const collectIdsToRemove = (handle) => {
11832 const ids = /* @__PURE__ */ new Set();
11833 for (const def of listWindowThemes()) {
11834 if (def.owner === handle) {
11835 ids.add(def.id);
11836 }
11837 }
11838 const declared = prevIdsByHandle.get(handle);
11839 if (declared) {
11840 for (const id of declared) {
11841 ids.add(id);
11842 }
11843 }
11844 return ids;
11845 };
11846 const applyMetadata = (themes) => {
11847 if (!themes) {
11848 return;
11849 }
11850 for (const entry of themes) {
11851 if (!entry.id || !entry.tokens) {
11852 continue;
11853 }
11854 try {
11855 registerWindowTheme({
11856 id: entry.id,
11857 label: entry.label,
11858 tokens: entry.tokens,
11859 priority: entry.priority,
11860 match: () => true,
11861 owner: entry.scriptHandle || void 0
11862 });
11863 shellRegistered.add(entry.id);
11864 } catch (err) {
11865 doAction(HOOKS.SHELL_ERROR, {
11866 scope: "window-theme-shell-register",
11867 id: entry.id,
11868 error: err
11869 });
11870 }
11871 }
11872 };
11873 return async (scripts, themes) => {
11874 const incomingHandles = /* @__PURE__ */ new Set();
11875 for (const entry of scripts) {
11876 if (entry.handle) {
11877 incomingHandles.add(entry.handle);
11878 }
11879 }
11880 for (const handle of Array.from(loadedHandles)) {
11881 if (incomingHandles.has(handle)) {
11882 continue;
11883 }
11884 const ids = collectIdsToRemove(handle);
11885 for (const id of ids) {
11886 unregisterWindowTheme(id);
11887 shellRegistered.delete(id);
11888 }
11889 unregisterWindowThemesByOwner(handle);
11890 loadedHandles.delete(handle);
11891 }
11892 applyMetadata(themes);
11893 for (const entry of scripts) {
11894 if (!entry.handle || loadedHandles.has(entry.handle)) {
11895 continue;
11896 }
11897 await ensureScript(entry);
11898 }
11899 prevIdsByHandle = idsByHandleFrom(themes);
11900 };
11901 }
11902 const store$7 = createSharedStore(
11903 "desktop-mode/window-controls-registry",
11904 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
11905 );
11906 const registry$2 = store$7.state.registry;
11907 const listeners$5 = store$7.state.listeners;
11908 const WINDOW_CONTROL_ID = /^[a-z0-9_/-]+$/;
11909 function registerWindowControl(def) {
11910 const errors = [];
11911 if (!def || typeof def !== "object") {
11912 errors.push("def (not an object)");
11913 } else {
11914 if (typeof def.id !== "string" || def.id.trim() === "") {
11915 errors.push("id (missing)");
11916 } else if (!WINDOW_CONTROL_ID.test(def.id.trim().toLowerCase())) {
11917 errors.push(
11918 `id (must match ${WINDOW_CONTROL_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
11919 );
11920 }
11921 if (typeof def.label !== "string" || def.label.trim() === "") {
11922 errors.push("label (missing)");
11923 }
11924 if (typeof def.onClick !== "function" && typeof def.render !== "function") {
11925 errors.push("onClick|render (at least one must be a function)");
11926 }
11927 if (typeof def.render !== "function") {
11928 if (typeof def.icon !== "string" || def.icon.trim() === "") {
11929 errors.push("icon (required when render is omitted)");
11930 }
11931 }
11932 if (typeof def.match !== "function") {
11933 errors.push("match (must be a function)");
11934 }
11935 if (def.placement !== void 0 && def.placement !== "left" && def.placement !== "right" && def.placement !== "controls") {
11936 errors.push('placement (must be "left", "right", or "controls")');
11937 }
11938 }
11939 throwOnRegistrationErrors("WindowControl", errors, def);
11940 const id = def.id.trim().toLowerCase();
11941 registry$2.set(id, { ...def, id });
11942 notify$7();
11943 }
11944 function unregisterWindowControl(id) {
11945 if (registry$2.delete(id.toLowerCase())) {
11946 notify$7();
11947 }
11948 }
11949 function unregisterWindowControlsByOwner(owner) {
11950 if (!owner) {
11951 return 0;
11952 }
11953 let removed = 0;
11954 for (const [id, def] of Array.from(registry$2.entries())) {
11955 if (def.owner === owner) {
11956 registry$2.delete(id);
11957 removed++;
11958 }
11959 }
11960 if (removed > 0) {
11961 notify$7();
11962 }
11963 return removed;
11964 }
11965 function listWindowControls() {
11966 return Array.from(registry$2.values()).sort((a, b) => {
11967 const oa = a.order ?? 100;
11968 const ob = b.order ?? 100;
11969 if (oa !== ob) {
11970 return oa - ob;
11971 }
11972 return a.id.localeCompare(b.id);
11973 });
11974 }
11975 function notify$7() {
11976 const snapshot = Array.from(listeners$5);
11977 for (const cb of snapshot) {
11978 try {
11979 cb();
11980 } catch (err) {
11981 if (typeof console !== "undefined") {
11982 console.error(
11983 "[desktop-mode] window-control registry listener threw:",
11984 err
11985 );
11986 }
11987 }
11988 }
11989 }
11990 function registerBuiltInControls() {
11991 registerWindowControl({
11992 id: "core/minimize",
11993 label: __("Minimize"),
11994 icon: "minimize",
11995 placement: "controls",
11996 order: 10,
11997 core: true,
11998 match: () => true,
11999 onClick: (win) => {
12000 win.minimize();
12001 }
12002 });
12003 registerWindowControl({
12004 id: "core/maximize",
12005 label: __("Maximize"),
12006 icon: "maximize",
12007 placement: "controls",
12008 order: 20,
12009 core: true,
12010 match: () => true,
12011 onClick: (win) => {
12012 win.toggleMaximize();
12013 }
12014 });
12015 registerWindowControl({
12016 id: "core/focus-tab",
12017 label: __("Enter fullscreen"),
12018 icon: "fullscreen",
12019 placement: "controls",
12020 order: 30,
12021 core: true,
12022 match: () => true,
12023 onClick: (win) => {
12024 win.toggleFullscreen();
12025 }
12026 });
12027 registerWindowControl({
12028 id: "core/close",
12029 label: __("Close"),
12030 icon: "close",
12031 placement: "controls",
12032 order: 50,
12033 core: true,
12034 match: () => true,
12035 onClick: (win) => {
12036 win.close();
12037 }
12038 });
12039 }
12040 function createWindowControlRegistrySync() {
12041 const loadedHandles = /* @__PURE__ */ new Set();
12042 const loadedUrls = /* @__PURE__ */ new Set();
12043 let prevIdsByHandle = /* @__PURE__ */ new Map();
12044 const ensureScript = async (entry) => {
12045 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
12046 loadedHandles.add(entry.handle);
12047 return;
12048 }
12049 try {
12050 await loadVendorScript(entry.scriptUrl, {
12051 translations: entry.scriptTranslations,
12052 l10n: entry.scriptL10n,
12053 before: entry.scriptBefore,
12054 after: entry.scriptAfter
12055 });
12056 } catch (err) {
12057 doAction(HOOKS.SHELL_ERROR, {
12058 scope: "window-control-script-load",
12059 handle: entry.handle,
12060 url: entry.scriptUrl,
12061 error: err
12062 });
12063 return;
12064 }
12065 loadedUrls.add(entry.scriptUrl);
12066 loadedHandles.add(entry.handle);
12067 };
12068 const idsByHandleFrom = (controls) => {
12069 const map = /* @__PURE__ */ new Map();
12070 if (!controls) {
12071 return map;
12072 }
12073 for (const entry of controls) {
12074 if (!entry.scriptHandle || !entry.id) {
12075 continue;
12076 }
12077 let set = map.get(entry.scriptHandle);
12078 if (!set) {
12079 set = /* @__PURE__ */ new Set();
12080 map.set(entry.scriptHandle, set);
12081 }
12082 set.add(entry.id);
12083 }
12084 return map;
12085 };
12086 const collectIdsToRemove = (handle) => {
12087 const ids = /* @__PURE__ */ new Set();
12088 for (const def of listWindowControls()) {
12089 if (def.owner === handle) {
12090 ids.add(def.id);
12091 }
12092 }
12093 const declared = prevIdsByHandle.get(handle);
12094 if (declared) {
12095 for (const id of declared) {
12096 ids.add(id);
12097 }
12098 }
12099 return ids;
12100 };
12101 return async (scripts, controls) => {
12102 const incomingHandles = /* @__PURE__ */ new Set();
12103 for (const entry of scripts) {
12104 if (entry.handle) {
12105 incomingHandles.add(entry.handle);
12106 }
12107 }
12108 for (const handle of Array.from(loadedHandles)) {
12109 if (incomingHandles.has(handle)) {
12110 continue;
12111 }
12112 for (const id of collectIdsToRemove(handle)) {
12113 unregisterWindowControl(id);
12114 }
12115 unregisterWindowControlsByOwner(handle);
12116 loadedHandles.delete(handle);
12117 }
12118 for (const entry of scripts) {
12119 if (!entry.handle || loadedHandles.has(entry.handle)) {
12120 continue;
12121 }
12122 await ensureScript(entry);
12123 }
12124 prevIdsByHandle = idsByHandleFrom(controls);
12125 };
12126 }
12127 const store$6 = createSharedStore(
12128 "desktop-mode/window-slots-registry",
12129 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
12130 );
12131 const registry$1 = store$6.state.registry;
12132 const listeners$4 = store$6.state.listeners;
12133 const WINDOW_SLOT_ID = /^[a-z0-9_/-]+$/;
12134 const KNOWN_SLOTS = /* @__PURE__ */ new Set([
12135 "before-titlebar",
12136 "before-icon",
12137 "icon",
12138 "title",
12139 "after-title",
12140 "before-controls",
12141 "controls",
12142 "after-controls",
12143 "after-titlebar"
12144 ]);
12145 function registerWindowSlot(def) {
12146 const errors = [];
12147 if (!def || typeof def !== "object") {
12148 errors.push("def (not an object)");
12149 } else {
12150 if (typeof def.id !== "string" || def.id.trim() === "") {
12151 errors.push("id (missing)");
12152 } else if (!WINDOW_SLOT_ID.test(def.id.trim().toLowerCase())) {
12153 errors.push(
12154 `id (must match ${WINDOW_SLOT_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
12155 );
12156 }
12157 if (typeof def.slot !== "string" || def.slot.trim() === "") {
12158 errors.push("slot (missing)");
12159 } else if (!KNOWN_SLOTS.has(def.slot)) {
12160 errors.push(
12161 `slot (must be one of ${Array.from(KNOWN_SLOTS).join(", ")})`
12162 );
12163 }
12164 if (typeof def.match !== "function") {
12165 errors.push("match (must be a function)");
12166 }
12167 if (typeof def.render !== "function") {
12168 errors.push("render (must be a function)");
12169 }
12170 }
12171 throwOnRegistrationErrors("WindowSlot", errors, def);
12172 const id = def.id.trim().toLowerCase();
12173 registry$1.set(id, { ...def, id });
12174 notify$6();
12175 }
12176 function unregisterWindowSlot(id) {
12177 if (registry$1.delete(id.toLowerCase())) {
12178 notify$6();
12179 }
12180 }
12181 function unregisterWindowSlotsByOwner(owner) {
12182 if (!owner) {
12183 return 0;
12184 }
12185 let removed = 0;
12186 for (const [id, def] of Array.from(registry$1.entries())) {
12187 if (def.owner === owner) {
12188 registry$1.delete(id);
12189 removed++;
12190 }
12191 }
12192 if (removed > 0) {
12193 notify$6();
12194 }
12195 return removed;
12196 }
12197 function listWindowSlots() {
12198 return Array.from(registry$1.values()).sort((a, b) => {
12199 const oa = a.order ?? 100;
12200 const ob = b.order ?? 100;
12201 if (oa !== ob) {
12202 return oa - ob;
12203 }
12204 return a.id.localeCompare(b.id);
12205 });
12206 }
12207 function notify$6() {
12208 const snapshot = Array.from(listeners$4);
12209 for (const cb of snapshot) {
12210 try {
12211 cb();
12212 } catch (err) {
12213 if (typeof console !== "undefined") {
12214 console.error(
12215 "[desktop-mode] window-slot registry listener threw:",
12216 err
12217 );
12218 }
12219 }
12220 }
12221 }
12222 function createWindowSlotRegistrySync() {
12223 const loadedHandles = /* @__PURE__ */ new Set();
12224 const loadedUrls = /* @__PURE__ */ new Set();
12225 let prevIdsByHandle = /* @__PURE__ */ new Map();
12226 const ensureScript = async (entry) => {
12227 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
12228 loadedHandles.add(entry.handle);
12229 return;
12230 }
12231 try {
12232 await loadVendorScript(entry.scriptUrl, {
12233 translations: entry.scriptTranslations,
12234 l10n: entry.scriptL10n,
12235 before: entry.scriptBefore,
12236 after: entry.scriptAfter
12237 });
12238 } catch (err) {
12239 doAction(HOOKS.SHELL_ERROR, {
12240 scope: "window-slot-script-load",
12241 handle: entry.handle,
12242 url: entry.scriptUrl,
12243 error: err
12244 });
12245 return;
12246 }
12247 loadedUrls.add(entry.scriptUrl);
12248 loadedHandles.add(entry.handle);
12249 };
12250 const idsByHandleFrom = (slots) => {
12251 const map = /* @__PURE__ */ new Map();
12252 if (!slots) {
12253 return map;
12254 }
12255 for (const entry of slots) {
12256 if (!entry.scriptHandle || !entry.id) {
12257 continue;
12258 }
12259 let set = map.get(entry.scriptHandle);
12260 if (!set) {
12261 set = /* @__PURE__ */ new Set();
12262 map.set(entry.scriptHandle, set);
12263 }
12264 set.add(entry.id);
12265 }
12266 return map;
12267 };
12268 const collectIdsToRemove = (handle) => {
12269 const ids = /* @__PURE__ */ new Set();
12270 for (const def of listWindowSlots()) {
12271 if (def.owner === handle) {
12272 ids.add(def.id);
12273 }
12274 }
12275 const declared = prevIdsByHandle.get(handle);
12276 if (declared) {
12277 for (const id of declared) {
12278 ids.add(id);
12279 }
12280 }
12281 return ids;
12282 };
12283 return async (scripts, slots) => {
12284 const incomingHandles = /* @__PURE__ */ new Set();
12285 for (const entry of scripts) {
12286 if (entry.handle) {
12287 incomingHandles.add(entry.handle);
12288 }
12289 }
12290 for (const handle of Array.from(loadedHandles)) {
12291 if (incomingHandles.has(handle)) {
12292 continue;
12293 }
12294 for (const id of collectIdsToRemove(handle)) {
12295 unregisterWindowSlot(id);
12296 }
12297 unregisterWindowSlotsByOwner(handle);
12298 loadedHandles.delete(handle);
12299 }
12300 for (const entry of scripts) {
12301 if (!entry.handle || loadedHandles.has(entry.handle)) {
12302 continue;
12303 }
12304 await ensureScript(entry);
12305 }
12306 prevIdsByHandle = idsByHandleFrom(slots);
12307 };
12308 }
12309 const KEY_PREFIX = "desktop-mode-notice-dismissed";
12310 function currentUserSuffix() {
12311 const w = window.wp;
12312 const uid = w?.desktop?.config?.currentUserId;
12313 if (typeof uid === "number" && uid > 0) {
12314 return String(uid);
12315 }
12316 return "anon";
12317 }
12318 function storageKey() {
12319 return `${KEY_PREFIX}:${currentUserSuffix()}`;
12320 }
12321 function readMap() {
12322 try {
12323 const raw = window.localStorage.getItem(storageKey());
12324 if (!raw) {
12325 return {};
12326 }
12327 const parsed = JSON.parse(raw);
12328 if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
12329 return parsed;
12330 }
12331 } catch {
12332 }
12333 return {};
12334 }
12335 function writeMap(map) {
12336 try {
12337 window.localStorage.setItem(storageKey(), JSON.stringify(map));
12338 } catch {
12339 }
12340 }
12341 function isNoticeDismissed(id) {
12342 if (!id) {
12343 return false;
12344 }
12345 return readMap()[id] === true;
12346 }
12347 function markNoticeDismissed(id) {
12348 if (!id) {
12349 return;
12350 }
12351 const map = readMap();
12352 map[id] = true;
12353 writeMap(map);
12354 }
12355 function clearNoticeDismissed(id) {
12356 if (!id) {
12357 return;
12358 }
12359 const map = readMap();
12360 if (map[id]) {
12361 delete map[id];
12362 writeMap(map);
12363 }
12364 }
12365 const styles$5 = 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 ) )}`;
12366 const _WpdNotice = class _WpdNotice extends Component {
12367 connectedCallback() {
12368 super.connectedCallback();
12369 if (!this.hasAttribute("role")) {
12370 this.setAttribute("role", "status");
12371 }
12372 if (!this.hasAttribute("tone")) {
12373 this.setAttribute("tone", "info");
12374 }
12375 const id = this.getAttribute("notice-id");
12376 if (id && isNoticeDismissed(id)) {
12377 this.hidden = true;
12378 }
12379 }
12380 /**
12381 * Imperatively dismiss the notice — hides the host and records
12382 * the dismissal in localStorage when `notice-id` is set.
12383 */
12384 dismiss() {
12385 this.hidden = true;
12386 const id = this.getAttribute("notice-id");
12387 if (id) {
12388 markNoticeDismissed(id);
12389 }
12390 this.emit("wpd-notice-dismiss", { noticeId: id ?? void 0 });
12391 }
12392 /**
12393 * Clear a previously recorded dismissal and re-show the notice.
12394 * Useful in tests and for "Show again" affordances.
12395 */
12396 undismiss() {
12397 const id = this.getAttribute("notice-id");
12398 if (id) {
12399 clearNoticeDismissed(id);
12400 }
12401 this.hidden = false;
12402 }
12403 render() {
12404 const icon = this.getAttribute("icon");
12405 const dismissible = !this.hasAttribute("not-dismissible");
12406 return html`
12407 <span
12408 class="wpd-notice__icon dashicons ${icon ?? ""}"
12409 ?hidden=${!icon}
12410 aria-hidden="true"
12411 ></span>
12412 <span class="wpd-notice__label"><slot></slot></span>
12413 <button
12414 type="button"
12415 class="wpd-notice__close"
12416 ?hidden=${!dismissible}
12417 aria-label=${__("Dismiss notice")}
12418 @click=${(e) => this._onDismiss(e)}
12419 >
12420 <svg viewBox="0 0 14 14" aria-hidden="true">
12421 <path
12422 d="M3 3 L11 11 M11 3 L3 11"
12423 stroke="currentColor"
12424 stroke-width="1.6"
12425 stroke-linecap="round"
12426 fill="none"
12427 ></path>
12428 </svg>
12429 </button>
12430 `;
12431 }
12432 _onDismiss(e) {
12433 e.preventDefault();
12434 e.stopPropagation();
12435 this.dismiss();
12436 }
12437 };
12438 _WpdNotice.props = ["tone", "notDismissible", "icon", "noticeId"];
12439 _WpdNotice.styles = [styles$5];
12440 _WpdNotice.help = {
12441 title: "Notice",
12442 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.",
12443 status: "experimental",
12444 since: "0.8.6",
12445 props: [
12446 {
12447 name: "tone",
12448 type: '"info" | "success" | "warning" | "error" | "danger" | "neutral"',
12449 description: "Color palette. Defaults to `info`. `error` and `danger` are aliases."
12450 },
12451 {
12452 name: "not-dismissible",
12453 type: "boolean",
12454 description: "Suppress the trailing close button. Defaults to dismissible."
12455 },
12456 {
12457 name: "icon",
12458 type: "string",
12459 description: "Optional Dashicons class for a leading glyph (e.g. `dashicons-info`)."
12460 },
12461 {
12462 name: "notice-id",
12463 type: "string",
12464 description: "Persistence key. When set, the notice records its dismissed state in localStorage so it stays closed across reloads for the same user."
12465 }
12466 ],
12467 slots: [
12468 {
12469 name: "(default)",
12470 description: "Message HTML. Links, `<strong>`, `<em>`, and other inline formatting are allowed."
12471 }
12472 ],
12473 events: [
12474 {
12475 name: "wpd-notice-dismiss",
12476 description: "Fires after the user clicks the close button.",
12477 detail: "{ noticeId?: string }"
12478 }
12479 ],
12480 cssProps: [
12481 { name: "--wpd-notice-bg", description: "Background color." },
12482 { name: "--wpd-notice-accent", description: "Left-edge stripe + icon color." },
12483 { name: "--wpd-notice-color", description: "Text color." },
12484 { name: "--wpd-notice-border", description: "Bottom border color." },
12485 { name: "--wpd-notice-link", description: "Color for slotted <a> elements." }
12486 ],
12487 example: html`
12488 <wpd-notice tone="warning" notice-id="docs/example">
12489 Heads up — this is a demo notice.
12490 <a href="#">Learn more</a>.
12491 </wpd-notice>
12492 `
12493 };
12494 let WpdNotice = _WpdNotice;
12495 defineComponent("wpd-notice", WpdNotice);
12496 const store$5 = createSharedStore(
12497 "desktop-mode/window-notices",
12498 () => ({ entries: /* @__PURE__ */ new Map() })
12499 );
12500 const ID_PATTERN = /^[a-z0-9_/-]+$/;
12501 function slotIdFor(id) {
12502 return `desktop-mode-notice/${id.toLowerCase()}`;
12503 }
12504 function buildNoticeElement(entry) {
12505 const el = document.createElement("wpd-notice");
12506 el.setAttribute("tone", entry.tone ?? "info");
12507 el.setAttribute("notice-id", entry.id);
12508 if (entry.dismissible === false) {
12509 el.setAttribute("not-dismissible", "");
12510 }
12511 if (entry.icon) {
12512 el.setAttribute("icon", entry.icon);
12513 }
12514 el.innerHTML = entry.message;
12515 return el;
12516 }
12517 function registerWindowNotice(entry) {
12518 if (!entry || typeof entry !== "object") {
12519 return () => {
12520 };
12521 }
12522 const id = String(entry.id ?? "").trim().toLowerCase();
12523 if (!id || !ID_PATTERN.test(id)) {
12524 return () => {
12525 };
12526 }
12527 if (typeof entry.message !== "string" || entry.message === "") {
12528 return () => {
12529 };
12530 }
12531 const normalised = { ...entry, id };
12532 store$5.state.entries.set(id, normalised);
12533 const slotId = slotIdFor(id);
12534 registerWindowSlot({
12535 id: slotId,
12536 slot: "after-titlebar",
12537 order: normalised.order ?? 100,
12538 // Append rather than clear — every notice slot entry appends
12539 // its own `<wpd-notice>` so multiple notices stack.
12540 replace: false,
12541 owner: normalised.owner,
12542 match: (win) => {
12543 const def = store$5.state.entries.get(id);
12544 if (!def) {
12545 return false;
12546 }
12547 if (typeof def.match !== "function") {
12548 return true;
12549 }
12550 try {
12551 return def.match(win) === true;
12552 } catch {
12553 return false;
12554 }
12555 },
12556 render: (host) => {
12557 const def = store$5.state.entries.get(id);
12558 if (!def) {
12559 return;
12560 }
12561 host.appendChild(buildNoticeElement(def));
12562 }
12563 });
12564 return () => unregisterWindowNotice(id);
12565 }
12566 function unregisterWindowNotice(id) {
12567 const key = String(id ?? "").trim().toLowerCase();
12568 if (!key) {
12569 return;
12570 }
12571 if (store$5.state.entries.delete(key)) {
12572 unregisterWindowSlot(slotIdFor(key));
12573 }
12574 }
12575 function listWindowNotices() {
12576 return Array.from(store$5.state.entries.values()).sort((a, b) => {
12577 const oa = a.order ?? 100;
12578 const ob = b.order ?? 100;
12579 if (oa !== ob) {
12580 return oa - ob;
12581 }
12582 return a.id.localeCompare(b.id);
12583 });
12584 }
12585 function dismissWindowNotice(id) {
12586 const key = String(id ?? "").trim().toLowerCase();
12587 if (!key) {
12588 return;
12589 }
12590 markNoticeDismissed(key);
12591 }
12592 function undismissWindowNotice(id) {
12593 const key = String(id ?? "").trim().toLowerCase();
12594 if (!key) {
12595 return;
12596 }
12597 clearNoticeDismissed(key);
12598 }
12599 function buildMatcher(match) {
12600 if (!match) {
12601 return void 0;
12602 }
12603 const ids = /* @__PURE__ */ new Set();
12604 if (typeof match.window === "string" && match.window !== "") {
12605 ids.add(match.window);
12606 }
12607 if (Array.isArray(match.windows)) {
12608 for (const id of match.windows) {
12609 if (typeof id === "string" && id !== "") {
12610 ids.add(id);
12611 }
12612 }
12613 }
12614 const needle = typeof match.urlContains === "string" && match.urlContains !== "" ? match.urlContains.toLowerCase() : null;
12615 if (ids.size === 0 && needle === null) {
12616 return void 0;
12617 }
12618 return (w) => {
12619 if (ids.size > 0 && !ids.has(w.id)) {
12620 return false;
12621 }
12622 if (needle !== null) {
12623 const url = typeof w.config.url === "string" ? w.config.url.toLowerCase() : "";
12624 if (!url.includes(needle)) {
12625 return false;
12626 }
12627 }
12628 return true;
12629 };
12630 }
12631 function applyServerWindowNotices(entries) {
12632 const wanted = /* @__PURE__ */ new Set();
12633 for (const entry of entries) {
12634 if (!entry || typeof entry.id !== "string" || !entry.id) {
12635 continue;
12636 }
12637 wanted.add(entry.id.toLowerCase());
12638 registerWindowNotice({
12639 id: entry.id,
12640 message: entry.message,
12641 tone: entry.tone,
12642 dismissible: entry.dismissible !== false,
12643 icon: entry.icon,
12644 match: buildMatcher(entry.match),
12645 order: typeof entry.order === "number" ? entry.order : void 0,
12646 // `owner` tag marks every server-shipped notice so a
12647 // targeted cleanup is trivial if/when we surface a sweep
12648 // helper later. Matches the convention used by the
12649 // command / settings-tab sync modules.
12650 owner: "__server__"
12651 });
12652 }
12653 for (const existing of listWindowNotices()) {
12654 if (existing.owner !== "__server__") {
12655 continue;
12656 }
12657 if (!wanted.has(existing.id)) {
12658 unregisterWindowNotice(existing.id);
12659 }
12660 }
12661 }
12662 const store$4 = createSharedStore(
12663 "desktop-mode/window-chrome-registry",
12664 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
12665 );
12666 const registry = store$4.state.registry;
12667 const listeners$3 = store$4.state.listeners;
12668 const WINDOW_CHROME_ID = /^[a-z0-9_/-]+$/;
12669 function registerWindowChrome(def) {
12670 const errors = [];
12671 if (!def || typeof def !== "object") {
12672 errors.push("def (not an object)");
12673 } else {
12674 if (typeof def.id !== "string" || def.id.trim() === "") {
12675 errors.push("id (missing)");
12676 } else if (!WINDOW_CHROME_ID.test(def.id.trim().toLowerCase())) {
12677 errors.push(
12678 `id (must match ${WINDOW_CHROME_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
12679 );
12680 }
12681 if (typeof def.match !== "function") {
12682 errors.push("match (must be a function)");
12683 }
12684 if (typeof def.render !== "function") {
12685 errors.push("render (must be a function)");
12686 }
12687 }
12688 throwOnRegistrationErrors("WindowChrome", errors, def);
12689 const id = def.id.trim().toLowerCase();
12690 registry.set(id, { ...def, id });
12691 notify$5();
12692 }
12693 function unregisterWindowChrome(id) {
12694 if (registry.delete(id.toLowerCase())) {
12695 notify$5();
12696 }
12697 }
12698 function unregisterWindowChromesByOwner(owner) {
12699 if (!owner) {
12700 return 0;
12701 }
12702 let removed = 0;
12703 for (const [id, def] of Array.from(registry.entries())) {
12704 if (def.owner === owner) {
12705 registry.delete(id);
12706 removed++;
12707 }
12708 }
12709 if (removed > 0) {
12710 notify$5();
12711 }
12712 return removed;
12713 }
12714 function listWindowChromes() {
12715 return Array.from(registry.values()).sort(
12716 (a, b) => a.id.localeCompare(b.id)
12717 );
12718 }
12719 function notify$5() {
12720 const snapshot = Array.from(listeners$3);
12721 for (const cb of snapshot) {
12722 try {
12723 cb();
12724 } catch (err) {
12725 if (typeof console !== "undefined") {
12726 console.error(
12727 "[desktop-mode] window-chrome registry listener threw:",
12728 err
12729 );
12730 }
12731 }
12732 }
12733 }
12734 function createWindowChromeRegistrySync() {
12735 const loadedHandles = /* @__PURE__ */ new Set();
12736 const loadedUrls = /* @__PURE__ */ new Set();
12737 let prevIdsByHandle = /* @__PURE__ */ new Map();
12738 const ensureScript = async (entry) => {
12739 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
12740 loadedHandles.add(entry.handle);
12741 return;
12742 }
12743 try {
12744 await loadVendorScript(entry.scriptUrl, {
12745 translations: entry.scriptTranslations,
12746 l10n: entry.scriptL10n,
12747 before: entry.scriptBefore,
12748 after: entry.scriptAfter
12749 });
12750 } catch (err) {
12751 doAction(HOOKS.SHELL_ERROR, {
12752 scope: "window-chrome-script-load",
12753 handle: entry.handle,
12754 url: entry.scriptUrl,
12755 error: err
12756 });
12757 return;
12758 }
12759 loadedUrls.add(entry.scriptUrl);
12760 loadedHandles.add(entry.handle);
12761 };
12762 const idsByHandleFrom = (chromes) => {
12763 const map = /* @__PURE__ */ new Map();
12764 if (!chromes) {
12765 return map;
12766 }
12767 for (const entry of chromes) {
12768 if (!entry.scriptHandle || !entry.id) {
12769 continue;
12770 }
12771 let set = map.get(entry.scriptHandle);
12772 if (!set) {
12773 set = /* @__PURE__ */ new Set();
12774 map.set(entry.scriptHandle, set);
12775 }
12776 set.add(entry.id);
12777 }
12778 return map;
12779 };
12780 const collectIdsToRemove = (handle) => {
12781 const ids = /* @__PURE__ */ new Set();
12782 for (const def of listWindowChromes()) {
12783 if (def.owner === handle) {
12784 ids.add(def.id);
12785 }
12786 }
12787 const declared = prevIdsByHandle.get(handle);
12788 if (declared) {
12789 for (const id of declared) {
12790 ids.add(id);
12791 }
12792 }
12793 return ids;
12794 };
12795 return async (scripts, chromes) => {
12796 const incomingHandles = /* @__PURE__ */ new Set();
12797 for (const entry of scripts) {
12798 if (entry.handle) {
12799 incomingHandles.add(entry.handle);
12800 }
12801 }
12802 for (const handle of Array.from(loadedHandles)) {
12803 if (incomingHandles.has(handle)) {
12804 continue;
12805 }
12806 for (const id of collectIdsToRemove(handle)) {
12807 unregisterWindowChrome(id);
12808 }
12809 unregisterWindowChromesByOwner(handle);
12810 loadedHandles.delete(handle);
12811 }
12812 for (const entry of scripts) {
12813 if (!entry.handle || loadedHandles.has(entry.handle)) {
12814 continue;
12815 }
12816 await ensureScript(entry);
12817 }
12818 prevIdsByHandle = idsByHandleFrom(chromes);
12819 };
12820 }
12821 const INITIAL_ORIGIN$2 = window.location.origin;
12822 let _connSeq = 0;
12823 const _connections = /* @__PURE__ */ new Map();
12824 const _connectionsByTarget = /* @__PURE__ */ new Map();
12825 const _syntheticIframes = /* @__PURE__ */ new Map();
12826 function registerSyntheticIframe(windowId, iframe) {
12827 _syntheticIframes.set(windowId, iframe);
12828 return () => {
12829 if (_syntheticIframes.get(windowId) === iframe) {
12830 _syntheticIframes.delete(windowId);
12831 }
12832 };
12833 }
12834 function nextId() {
12835 return `desktop-mode-conn-${++_connSeq}`;
12836 }
12837 function createConnectionBridge(manager) {
12838 const sendToIframe = (win, message) => {
12839 try {
12840 win.contentWindow?.postMessage(message, INITIAL_ORIGIN$2);
12841 } catch (err) {
12842 if (typeof console !== "undefined") {
12843 console.error(
12844 "[desktop-mode] connection: postMessage failed",
12845 err
12846 );
12847 }
12848 }
12849 };
12850 const connect = (targetWindowId, opts = {}) => {
12851 const id = nextId();
12852 const topics = Array.isArray(opts.topics) ? [...opts.topics] : [];
12853 const subs = /* @__PURE__ */ new Map();
12854 const queue = [];
12855 let isOpen = false;
12856 let destroyed = false;
12857 const targetIframe = () => {
12858 const synth = _syntheticIframes.get(targetWindowId);
12859 if (synth) {
12860 return synth;
12861 }
12862 const w = manager.getById(targetWindowId);
12863 return w?.iframe ?? null;
12864 };
12865 const isNativeTarget = () => {
12866 if (targetIframe()) {
12867 return false;
12868 }
12869 const w = manager.getById(targetWindowId);
12870 return !!w && w.config?.native === true;
12871 };
12872 const nativeSubUnsubs = [];
12873 const flushQueue = () => {
12874 const iframe2 = targetIframe();
12875 if (!iframe2) {
12876 return;
12877 }
12878 while (queue.length) {
12879 const msg = queue.shift();
12880 sendToIframe(iframe2, {
12881 type: "desktop-mode-bridge-publish",
12882 connectionId: id,
12883 topic: msg.topic,
12884 payload: msg.payload
12885 });
12886 }
12887 };
12888 const conn = {
12889 id,
12890 target: targetWindowId,
12891 isOpen: () => isOpen,
12892 subscribe(topic, cb) {
12893 const wrapped = cb;
12894 if (isNativeTarget()) {
12895 const off = addParentSubscriber(
12896 targetWindowId,
12897 topic,
12898 (payload, meta) => {
12899 doAction(HOOKS.CONNECTION_MESSAGE, {
12900 connectionId: id,
12901 topic: meta.channel,
12902 direction: "in"
12903 });
12904 try {
12905 wrapped(payload, { topic: meta.channel });
12906 } catch (err) {
12907 if (typeof console !== "undefined") {
12908 console.error(
12909 "[desktop-mode] connection subscriber threw:",
12910 err
12911 );
12912 }
12913 }
12914 }
12915 );
12916 nativeSubUnsubs.push(off);
12917 return off;
12918 }
12919 let bucket22 = subs.get(topic);
12920 if (!bucket22) {
12921 bucket22 = /* @__PURE__ */ new Set();
12922 subs.set(topic, bucket22);
12923 }
12924 bucket22.add(wrapped);
12925 return () => {
12926 bucket22?.delete(wrapped);
12927 };
12928 },
12929 send(topic, payload) {
12930 if (destroyed) {
12931 return;
12932 }
12933 doAction(HOOKS.CONNECTION_MESSAGE, {
12934 connectionId: id,
12935 topic,
12936 direction: "out"
12937 });
12938 if (isNativeTarget()) {
12939 dispatchToNative(targetWindowId, topic, payload);
12940 return;
12941 }
12942 if (!isOpen) {
12943 queue.push({ topic, payload });
12944 return;
12945 }
12946 const iframe2 = targetIframe();
12947 if (!iframe2) {
12948 return;
12949 }
12950 sendToIframe(iframe2, {
12951 type: "desktop-mode-bridge-publish",
12952 connectionId: id,
12953 topic,
12954 payload
12955 });
12956 },
12957 disconnect() {
12958 conn._destroy("disconnect");
12959 },
12960 _targetWindow: targetIframe,
12961 _handleIframeMessage(data) {
12962 if (!data || typeof data !== "object") {
12963 return;
12964 }
12965 const msg = data;
12966 if (msg.type === "desktop-mode-bridge-handshake-ack") {
12967 if (isOpen) {
12968 return;
12969 }
12970 isOpen = true;
12971 doAction(HOOKS.CONNECTION_OPENED, {
12972 connectionId: id,
12973 targetWindowId,
12974 topics,
12975 // Ship the live Connection alongside the id so
12976 // iframe-initiated connections can be subscribed
12977 // to directly from the hook handler — without
12978 // `wp.desktop.getConnection(id)` plumbing the
12979 // payload would carry the id but no way to call
12980 // `.subscribe()` against it.
12981 connection: conn
12982 });
12983 try {
12984 opts.onOpen?.();
12985 } catch (err) {
12986 if (typeof console !== "undefined") {
12987 console.error(
12988 "[desktop-mode] connection.onOpen threw:",
12989 err
12990 );
12991 }
12992 }
12993 flushQueue();
12994 return;
12995 }
12996 if (msg.type === "desktop-mode-bridge-publish") {
12997 const m = data;
12998 const topic = typeof m.topic === "string" ? m.topic : "";
12999 if (!topic) {
13000 return;
13001 }
13002 doAction(HOOKS.CONNECTION_MESSAGE, {
13003 connectionId: id,
13004 topic,
13005 direction: "in"
13006 });
13007 const exact = subs.get(topic);
13008 if (exact) {
13009 for (const cb of Array.from(exact)) {
13010 try {
13011 cb(m.payload, { topic });
13012 } catch (err) {
13013 if (typeof console !== "undefined") {
13014 console.error(
13015 "[desktop-mode] connection subscriber threw:",
13016 err
13017 );
13018 }
13019 }
13020 }
13021 }
13022 const wildcard = subs.get("*");
13023 if (wildcard) {
13024 for (const cb of Array.from(wildcard)) {
13025 try {
13026 cb(m.payload, { topic });
13027 } catch (err) {
13028 if (typeof console !== "undefined") {
13029 console.error(
13030 "[desktop-mode] connection wildcard subscriber threw:",
13031 err
13032 );
13033 }
13034 }
13035 }
13036 }
13037 return;
13038 }
13039 if (msg.type === "desktop-mode-bridge-disconnect") {
13040 conn._destroy("disconnect");
13041 }
13042 },
13043 _destroy(reason) {
13044 if (destroyed) {
13045 return;
13046 }
13047 destroyed = true;
13048 const wasOpen = isOpen;
13049 isOpen = false;
13050 _connections.delete(id);
13051 const targetSet = _connectionsByTarget.get(targetWindowId);
13052 if (targetSet) {
13053 targetSet.delete(id);
13054 if (targetSet.size === 0) {
13055 _connectionsByTarget.delete(targetWindowId);
13056 }
13057 }
13058 for (const off of nativeSubUnsubs.splice(0)) {
13059 try {
13060 off();
13061 } catch {
13062 }
13063 }
13064 if (wasOpen) {
13065 const iframe2 = targetIframe();
13066 if (iframe2) {
13067 sendToIframe(iframe2, {
13068 type: "desktop-mode-bridge-disconnect",
13069 connectionId: id
13070 });
13071 }
13072 }
13073 doAction(HOOKS.CONNECTION_CLOSED, {
13074 connectionId: id,
13075 reason
13076 });
13077 try {
13078 opts.onClose?.(reason);
13079 } catch (err) {
13080 if (typeof console !== "undefined") {
13081 console.error(
13082 "[desktop-mode] connection.onClose threw:",
13083 err
13084 );
13085 }
13086 }
13087 }
13088 };
13089 _connections.set(id, conn);
13090 let bucket2 = _connectionsByTarget.get(targetWindowId);
13091 if (!bucket2) {
13092 bucket2 = /* @__PURE__ */ new Set();
13093 _connectionsByTarget.set(targetWindowId, bucket2);
13094 }
13095 bucket2.add(id);
13096 if (isNativeTarget()) {
13097 Promise.resolve().then(() => {
13098 if (destroyed || isOpen) {
13099 return;
13100 }
13101 isOpen = true;
13102 doAction(HOOKS.CONNECTION_OPENED, {
13103 connectionId: id,
13104 targetWindowId,
13105 topics
13106 });
13107 try {
13108 opts.onOpen?.();
13109 } catch (err) {
13110 if (typeof console !== "undefined") {
13111 console.error(
13112 "[desktop-mode] connection.onOpen threw:",
13113 err
13114 );
13115 }
13116 }
13117 });
13118 return conn;
13119 }
13120 const iframe = targetIframe();
13121 if (iframe) {
13122 sendToIframe(iframe, {
13123 type: "desktop-mode-bridge-handshake",
13124 connectionId: id,
13125 targetWindowId,
13126 topics
13127 });
13128 }
13129 return conn;
13130 };
13131 const routeIncomingFromIframe = (data, windowId) => {
13132 if (!data || typeof data !== "object") {
13133 return;
13134 }
13135 const msg = data;
13136 if (typeof msg.type !== "string" || !msg.type.startsWith("desktop-mode-bridge-")) {
13137 return;
13138 }
13139 if (msg.type === "desktop-mode-bridge-connection-request" && typeof msg.requestId === "string" && typeof windowId === "string" && windowId !== "") {
13140 handleConnectionRequest(windowId, msg.requestId, Array.isArray(msg.topics) ? msg.topics : []);
13141 return;
13142 }
13143 if (typeof msg.connectionId !== "string") {
13144 return;
13145 }
13146 const conn = _connections.get(msg.connectionId);
13147 conn?._handleIframeMessage(data);
13148 };
13149 const handleConnectionRequest = (windowId, requestId, topics) => {
13150 const synth = _syntheticIframes.get(windowId);
13151 const iframe = synth ?? manager.getById(windowId)?.iframe ?? null;
13152 if (!iframe) {
13153 return;
13154 }
13155 const decision = applyFilters(
13156 HOOKS.IFRAME_CONNECTION_REQUEST,
13157 true,
13158 { windowId, requestId, topics: topics.slice() }
13159 );
13160 if (decision === false) {
13161 try {
13162 iframe.contentWindow?.postMessage({
13163 type: "desktop-mode-bridge-connection-ack",
13164 requestId,
13165 accepted: false,
13166 reason: "rejected"
13167 }, INITIAL_ORIGIN$2);
13168 } catch {
13169 }
13170 return;
13171 }
13172 const finalTopics = decision && typeof decision === "object" && Array.isArray(decision.topics) ? decision.topics : topics;
13173 const conn = connect(windowId, { topics: finalTopics });
13174 try {
13175 iframe.contentWindow?.postMessage({
13176 type: "desktop-mode-bridge-connection-ack",
13177 requestId,
13178 accepted: true,
13179 connectionId: conn.id
13180 }, INITIAL_ORIGIN$2);
13181 } catch {
13182 }
13183 };
13184 const onIframeReady = (windowId) => {
13185 const bucket2 = _connectionsByTarget.get(windowId);
13186 if (!bucket2) {
13187 return;
13188 }
13189 for (const connId of Array.from(bucket2)) {
13190 const conn = _connections.get(connId);
13191 if (!conn || conn.isOpen()) {
13192 continue;
13193 }
13194 const iframe = conn._targetWindow();
13195 if (!iframe) {
13196 continue;
13197 }
13198 sendToIframe(iframe, {
13199 type: "desktop-mode-bridge-handshake",
13200 connectionId: conn.id,
13201 targetWindowId: conn.target,
13202 topics: []
13203 // already negotiated client-side; iframe re-uses
13204 });
13205 }
13206 };
13207 const onWindowClosed = (windowId) => {
13208 const bucket2 = _connectionsByTarget.get(windowId);
13209 if (!bucket2) {
13210 return;
13211 }
13212 for (const connId of Array.from(bucket2)) {
13213 const conn = _connections.get(connId);
13214 conn?._destroy("window-closed");
13215 }
13216 };
13217 const getConnection = (connectionId) => {
13218 const conn = _connections.get(connectionId);
13219 return conn ?? null;
13220 };
13221 return {
13222 connect,
13223 getConnection,
13224 routeIncomingFromIframe,
13225 onIframeReady,
13226 onWindowClosed
13227 };
13228 }
13229 const __vite_import_meta_env__ = {};
13230 function devLog(...args) {
13231 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;
13232 if (mode !== "production") {
13233 console.log(...args);
13234 }
13235 }
13236 const OWNER_PREFIX = "iframe:";
13237 function ownerFor(windowId) {
13238 return OWNER_PREFIX + windowId;
13239 }
13240 function iconFor(harvested) {
13241 if (harvested.icon && typeof harvested.icon === "string" && harvested.icon.startsWith("dashicons-")) {
13242 return harvested.icon;
13243 }
13244 return harvested.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
13245 }
13246 function slugFor(windowId, name) {
13247 const safeName = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
13248 const safeWin = windowId.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
13249 return `win-${safeWin}-${safeName}`;
13250 }
13251 class IframeCommandBridge {
13252 constructor(opts) {
13253 this.subscribedWindowId = null;
13254 this.manager = opts.manager;
13255 this.adminUrl = opts.adminUrl;
13256 }
13257 /** Wire up the focus / close / message listeners. Idempotent. */
13258 install() {
13259 document.addEventListener("desktop-mode-window-focused", (e) => {
13260 const detail = e.detail;
13261 if (detail && typeof detail.windowId === "string") {
13262 this.onFocused(detail.windowId);
13263 }
13264 });
13265 document.addEventListener("desktop-mode-window-closed", (e) => {
13266 const detail = e.detail;
13267 if (detail && typeof detail.windowId === "string") {
13268 unregisterByOwner(ownerFor(detail.windowId));
13269 if (this.subscribedWindowId === detail.windowId) {
13270 this.subscribedWindowId = null;
13271 }
13272 }
13273 });
13274 document.addEventListener("desktop-mode-window-changed", (e) => {
13275 const detail = e.detail;
13276 if (!detail || typeof detail.windowId !== "string") {
13277 return;
13278 }
13279 if (detail.reason !== "state") {
13280 return;
13281 }
13282 if (detail.state !== "minimized") {
13283 return;
13284 }
13285 if (this.subscribedWindowId === detail.windowId) {
13286 this.subscribedWindowId = null;
13287 }
13288 });
13289 window.addEventListener("message", (e) => {
13290 if (e.origin !== window.location.origin) {
13291 return;
13292 }
13293 const data = e.data;
13294 if (!data || typeof data.type !== "string") {
13295 return;
13296 }
13297 if (data.type === "desktop-mode-bridge-ready") {
13298 const win2 = this.manager.findByIframeSource(e.source);
13299 if (win2 && win2.id === this.subscribedWindowId) {
13300 this.sendSubscribe(win2.id);
13301 }
13302 return;
13303 }
13304 if (data.type !== "desktop-mode-commands-list") {
13305 return;
13306 }
13307 if (!Array.isArray(data.commands)) {
13308 return;
13309 }
13310 const win = this.manager.findByIframeSource(e.source);
13311 if (!win) {
13312 return;
13313 }
13314 if (win.id !== this.subscribedWindowId) {
13315 return;
13316 }
13317 this.applyList(win.id, data.commands);
13318 });
13319 const focused = this.manager.getFocused();
13320 if (focused) {
13321 this.onFocused(focused.id);
13322 }
13323 }
13324 onFocused(windowId) {
13325 if (this.subscribedWindowId === windowId) {
13326 return;
13327 }
13328 if (this.subscribedWindowId) {
13329 const prev = this.manager.getById(this.subscribedWindowId);
13330 if (prev && prev.iframe && prev.iframe.contentWindow) {
13331 try {
13332 prev.iframe.contentWindow.postMessage(
13333 { type: "desktop-mode-commands-unsubscribe" },
13334 window.location.origin
13335 );
13336 } catch {
13337 }
13338 }
13339 unregisterByOwner(ownerFor(this.subscribedWindowId));
13340 }
13341 this.subscribedWindowId = windowId;
13342 this.sendSubscribe(windowId);
13343 }
13344 sendSubscribe(windowId) {
13345 const win = this.manager.getById(windowId);
13346 if (!win) {
13347 return;
13348 }
13349 if (!win.iframe) {
13350 return;
13351 }
13352 if (!win.iframe.contentWindow) {
13353 return;
13354 }
13355 try {
13356 win.iframe.contentWindow.postMessage(
13357 { type: "desktop-mode-commands-subscribe" },
13358 window.location.origin
13359 );
13360 } catch (err) {
13361 devLog("[wpd-cmd:parent] sendSubscribe: postMessage threw", err);
13362 }
13363 }
13364 applyList(windowId, commands) {
13365 const owner = ownerFor(windowId);
13366 unregisterByOwner(owner);
13367 for (const cmd of commands) {
13368 if (!cmd || !cmd.name || !cmd.label) {
13369 continue;
13370 }
13371 const slug = slugFor(windowId, cmd.name);
13372 const safeSvg = typeof cmd.iconSvg === "string" && cmd.iconSvg !== "" ? sanitizeIconSvg(cmd.iconSvg) : "";
13373 const def = {
13374 slug,
13375 label: cmd.label,
13376 icon: iconFor(cmd),
13377 iconSvg: safeSvg !== "" ? safeSvg : void 0,
13378 owner,
13379 // Harvested commands are contextual by construction —
13380 // they come from whichever window has focus. Surface
13381 // them eagerly so the user sees "Duplicate block" /
13382 // "Toggle distraction free" without having to type `/`
13383 // first.
13384 eager: true,
13385 run: cmd.kind === "navigate" && cmd.url ? this.runNavigate(cmd.url, cmd.label, iconFor(cmd)) : this.runProxy(windowId, cmd.name)
13386 };
13387 try {
13388 registerCommand(def);
13389 } catch (err) {
13390 console.error(
13391 "[desktop-mode] iframe-bridge: dropping bad command",
13392 def,
13393 err
13394 );
13395 }
13396 }
13397 }
13398 runNavigate(url, title, icon) {
13399 return (_args, ctx) => {
13400 ctx.close();
13401 if (tryNativeUrlRemap(url)) {
13402 return;
13403 }
13404 const id = deriveWindowId(url, this.adminUrl);
13405 this.manager.open({ id, baseId: id, url, title, icon });
13406 };
13407 }
13408 runProxy(windowId, name) {
13409 return (_args, ctx) => {
13410 ctx.close();
13411 const win = this.manager.getById(windowId);
13412 if (!win || !win.iframe || !win.iframe.contentWindow) {
13413 return;
13414 }
13415 try {
13416 win.iframe.contentWindow.postMessage(
13417 { type: "desktop-mode-commands-invoke", name },
13418 window.location.origin
13419 );
13420 } catch {
13421 }
13422 this.manager.focus(win);
13423 };
13424 }
13425 }
13426 const OWNER = "global";
13427 const NAV_HREF_LITERAL_RE = /(?:document\.location\.href|window\.location\.href|location\.href)\s*=\s*['"]([^'"$]+?)['"]/;
13428 const NAV_ASSIGN_LITERAL_RE = /(?:document\.location|window\.location|location)\s*=\s*['"]([^'"$]+?)['"]/;
13429 const NAV_CALL_LITERAL_RE = /location\.(?:assign|replace)\s*\(\s*['"]([^'"$]+?)['"]\s*\)/;
13430 const NAV_INTENT_RE = /(?:document\.location|window\.location|location)\s*(?:\.href\s*)?=|location\.(?:assign|replace)\s*\(/;
13431 const SITE_EDITOR_INTENT_RE = /getSiteEditorPage\s*\(|site-editor\.php/;
13432 const SITE_EDITOR_NAME_RE = /^(wp_template_part|wp_template|wp_navigation|wp_block)-(.+)$/;
13433 function lookupMenuCommand(name) {
13434 const list2 = window.__desktopModeMenuCommands;
13435 if (!Array.isArray(list2)) {
13436 return null;
13437 }
13438 for (const entry of list2) {
13439 if (entry && typeof entry === "object" && entry.name === name && typeof entry.url === "string" && entry.url !== "") {
13440 return {
13441 label: typeof entry.label === "string" ? entry.label : "",
13442 url: entry.url
13443 };
13444 }
13445 }
13446 return null;
13447 }
13448 class ShellCommandHarvester {
13449 constructor(opts) {
13450 this.mounted = false;
13451 this.host = null;
13452 this.root = null;
13453 this.kindCache = /* @__PURE__ */ Object.create(null);
13454 this.callbackCache = /* @__PURE__ */ Object.create(null);
13455 this.lastFingerprint = "";
13456 this.manager = opts.manager;
13457 this.adminUrl = opts.adminUrl;
13458 }
13459 /** Mount the harvester. Idempotent. Safe to call before `wp.data` loads. */
13460 install() {
13461 this.tryMount(0);
13462 }
13463 tryMount(attempt) {
13464 if (this.mounted) {
13465 return;
13466 }
13467 const wp = window.wp;
13468 if (!wp || !wp.data || !wp.element || typeof wp.data.subscribe !== "function") {
13469 if (attempt < 40) {
13470 window.setTimeout(() => this.tryMount(attempt + 1), 150);
13471 }
13472 return;
13473 }
13474 this.mount();
13475 }
13476 mount() {
13477 const wp = window.wp;
13478 const el = wp.element;
13479 const data = wp.data;
13480 const createEl = el.createElement;
13481 const useEffect = el.useEffect;
13482 const useRef = el.useRef;
13483 const useMemo = el.useMemo;
13484 const useSelect = data.useSelect;
13485 if (typeof createEl !== "function" || typeof useEffect !== "function" || typeof useRef !== "function" || typeof useMemo !== "function" || typeof useSelect !== "function" || typeof el.createRoot !== "function") {
13486 return;
13487 }
13488 this.mounted = true;
13489 const host = document.createElement("div");
13490 host.setAttribute("aria-hidden", "true");
13491 host.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;left:-9999px;top:-9999px;";
13492 (document.body || document.documentElement).appendChild(host);
13493 this.host = host;
13494 const bucket2 = {
13495 perLoader: {},
13496 statics: [],
13497 loadersList: []
13498 };
13499 const fingerprint2 = (cmds) => {
13500 if (!Array.isArray(cmds) || cmds.length === 0) {
13501 return "";
13502 }
13503 const keys = new Array(cmds.length);
13504 for (let i = 0; i < cmds.length; i++) {
13505 const c = cmds[i];
13506 keys[i] = c && c.name ? c.name : "";
13507 }
13508 return keys.join("|");
13509 };
13510 const mergeAndPublish = () => {
13511 let merged = [];
13512 for (const name of bucket2.loadersList) {
13513 const slice = bucket2.perLoader[name];
13514 if (Array.isArray(slice)) {
13515 merged = merged.concat(slice);
13516 }
13517 }
13518 if (Array.isArray(bucket2.statics)) {
13519 merged = merged.concat(bucket2.statics);
13520 }
13521 this.callbackCache = /* @__PURE__ */ Object.create(null);
13522 for (const cc of merged) {
13523 if (cc && cc.name && typeof cc.callback === "function") {
13524 this.callbackCache[cc.name] = cc.callback;
13525 }
13526 }
13527 this.publish(merged);
13528 };
13529 const LoaderSlot = (props) => {
13530 const loader = props.loader;
13531 let result = null;
13532 try {
13533 result = loader.hook({ search: "" });
13534 } catch {
13535 }
13536 const cmds = result && Array.isArray(result.commands) ? result.commands : [];
13537 const key = useMemo(() => fingerprint2(cmds), [cmds]);
13538 useEffect(() => {
13539 bucket2.perLoader[loader.name] = cmds;
13540 mergeAndPublish();
13541 }, [key]);
13542 useEffect(() => {
13543 return () => {
13544 delete bucket2.perLoader[loader.name];
13545 mergeAndPublish();
13546 };
13547 }, []);
13548 return null;
13549 };
13550 const Harvester = () => {
13551 const loaders = useSelect((s) => {
13552 const ss = s("core/commands");
13553 if (!ss || typeof ss.getCommandLoaders !== "function") {
13554 return [];
13555 }
13556 return [
13557 ...ss.getCommandLoaders(false) || [],
13558 ...ss.getCommandLoaders(true) || []
13559 ];
13560 }, []);
13561 const staticCmds = useSelect((s) => {
13562 const ss = s("core/commands");
13563 if (!ss || typeof ss.getCommands !== "function") {
13564 return [];
13565 }
13566 return [
13567 ...ss.getCommands(false) || [],
13568 ...ss.getCommands(true) || []
13569 ];
13570 }, []);
13571 const loadersNames = useMemo(() => {
13572 return Array.isArray(loaders) ? loaders.map((l) => l ? l.name || "" : "") : [];
13573 }, [loaders]);
13574 const loadersKey = loadersNames.join("|");
13575 useEffect(() => {
13576 bucket2.loadersList = loadersNames;
13577 mergeAndPublish();
13578 }, [loadersKey]);
13579 const staticKey = useMemo(
13580 () => fingerprint2(Array.isArray(staticCmds) ? staticCmds : []),
13581 [staticCmds]
13582 );
13583 useEffect(() => {
13584 bucket2.statics = Array.isArray(staticCmds) ? staticCmds : [];
13585 mergeAndPublish();
13586 }, [staticKey]);
13587 if (!Array.isArray(loaders) || loaders.length === 0) {
13588 return null;
13589 }
13590 const children = [];
13591 for (const loader of loaders) {
13592 if (!loader || typeof loader.hook !== "function") {
13593 continue;
13594 }
13595 children.push(
13596 createEl(LoaderSlot, { key: loader.name, loader })
13597 );
13598 }
13599 return createEl(el.Fragment || "div", null, children);
13600 };
13601 try {
13602 this.root = el.createRoot(host);
13603 this.root.render(createEl(Harvester));
13604 } catch {
13605 this.mounted = false;
13606 this.root = null;
13607 if (this.host && this.host.parentNode) {
13608 this.host.parentNode.removeChild(this.host);
13609 }
13610 this.host = null;
13611 }
13612 }
13613 publish(raw) {
13614 const seen = /* @__PURE__ */ Object.create(null);
13615 const classified = [];
13616 for (const cmd of raw) {
13617 if (!cmd || !cmd.name || !cmd.label) {
13618 continue;
13619 }
13620 if (cmd.disabled) {
13621 continue;
13622 }
13623 if (seen[cmd.name]) {
13624 continue;
13625 }
13626 seen[cmd.name] = true;
13627 classified.push(this.classify(cmd));
13628 }
13629 let key = "";
13630 for (const c of classified) {
13631 key += `${c.name}|${c.kind}|${c.url || ""}
13632 `;
13633 }
13634 if (key === this.lastFingerprint) {
13635 return;
13636 }
13637 this.lastFingerprint = key;
13638 unregisterByOwner(OWNER);
13639 for (const c of classified) {
13640 if (c.kind === "skip") {
13641 continue;
13642 }
13643 const slug = `global-${c.name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-")}`;
13644 const icon = this.iconFor(c);
13645 const def = {
13646 slug,
13647 label: c.label,
13648 icon,
13649 iconSvg: c.iconSvg && c.iconSvg !== "" ? sanitizeIconSvg(c.iconSvg) : void 0,
13650 owner: OWNER,
13651 // NOT eager. The palette splits the registry into two
13652 // disjoint surfaces: `eager` commands show on empty
13653 // input (and are excluded from slash search at
13654 // `src/ai-assistant/impl.ts:494`); non-eager commands
13655 // show when the user types `/<query>`. The WP baseline
13656 // is large (~150 entries) and meant to be searched —
13657 // surfacing it eagerly would drown the iframe-harvested
13658 // contextual shortcuts on every open. Slash-search is
13659 // the right surface for it, matching the native WP
13660 // palette UX (open, type, find).
13661 run: c.kind === "navigate" && c.url ? this.runNavigate(c.url, c.windowTitle || c.label, icon) : this.runInvoke(c.name, c.label, icon)
13662 };
13663 try {
13664 registerCommand(def);
13665 } catch (err) {
13666 console.error(
13667 "[desktop-mode] shell-harvester: dropping bad command",
13668 def,
13669 err
13670 );
13671 }
13672 }
13673 }
13674 classify(cmd) {
13675 const out = {
13676 name: String(cmd.name),
13677 label: String(cmd.label),
13678 icon: typeof cmd.icon === "string" ? cmd.icon : void 0,
13679 iconSvg: void 0,
13680 kind: "action",
13681 url: void 0,
13682 callback: typeof cmd.callback === "function" ? cmd.callback : void 0
13683 };
13684 const cached = this.kindCache[out.name];
13685 if (cached) {
13686 out.kind = cached.kind;
13687 out.url = cached.url;
13688 out.iconSvg = cached.iconSvg;
13689 return out;
13690 }
13691 if (cmd.icon && typeof cmd.icon !== "string") {
13692 out.iconSvg = this.renderIcon(cmd.icon);
13693 }
13694 const menuEntry = lookupMenuCommand(out.name);
13695 if (menuEntry) {
13696 try {
13697 out.url = new URL(menuEntry.url, this.adminUrl).toString();
13698 out.kind = "navigate";
13699 if (menuEntry.label !== "") {
13700 out.windowTitle = menuEntry.label;
13701 }
13702 } catch {
13703 out.kind = "skip";
13704 }
13705 this.kindCache[out.name] = {
13706 kind: out.kind,
13707 url: out.url,
13708 iconSvg: out.iconSvg
13709 };
13710 return out;
13711 }
13712 if (typeof cmd.callback === "function") {
13713 let src = "";
13714 try {
13715 src = Function.prototype.toString.call(cmd.callback);
13716 } catch {
13717 src = "";
13718 }
13719 const literal = src.match(NAV_HREF_LITERAL_RE) || src.match(NAV_ASSIGN_LITERAL_RE) || src.match(NAV_CALL_LITERAL_RE);
13720 if (literal && literal[1]) {
13721 try {
13722 out.url = new URL(literal[1], window.location.href).toString();
13723 out.kind = "navigate";
13724 } catch {
13725 out.kind = "action";
13726 }
13727 } else if (NAV_INTENT_RE.test(src)) {
13728 const isSiteEditorIntent = SITE_EDITOR_INTENT_RE.test(src);
13729 const nameMatch = isSiteEditorIntent ? out.name.match(SITE_EDITOR_NAME_RE) : null;
13730 if (nameMatch) {
13731 const entityType = nameMatch[1];
13732 const entityId = nameMatch[2];
13733 const p = `/${entityType}/${entityId}`;
13734 try {
13735 const siteEditor = new URL("site-editor.php", this.adminUrl);
13736 siteEditor.searchParams.set("p", p);
13737 siteEditor.searchParams.set("canvas", "edit");
13738 out.url = siteEditor.toString();
13739 out.kind = "navigate";
13740 } catch {
13741 out.kind = "skip";
13742 }
13743 } else {
13744 out.kind = "skip";
13745 }
13746 }
13747 }
13748 this.kindCache[out.name] = {
13749 kind: out.kind,
13750 url: out.url,
13751 iconSvg: out.iconSvg
13752 };
13753 return out;
13754 }
13755 renderIcon(icon) {
13756 const wp = window.wp;
13757 if (!wp || !wp.element || typeof wp.element.renderToString !== "function") {
13758 return "";
13759 }
13760 try {
13761 const rendered = wp.element.renderToString(icon);
13762 if (typeof rendered === "string" && rendered.toLowerCase().startsWith("<svg")) {
13763 return rendered;
13764 }
13765 } catch {
13766 }
13767 return "";
13768 }
13769 iconFor(c) {
13770 if (c.icon && c.icon.startsWith("dashicons-")) {
13771 return c.icon;
13772 }
13773 return c.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
13774 }
13775 runNavigate(url, title, icon) {
13776 return (_args, ctx) => {
13777 ctx.close();
13778 if (tryNativeUrlRemap(url)) {
13779 return;
13780 }
13781 const id = deriveWindowId(url, this.adminUrl);
13782 this.manager.open({ id, baseId: id, url, title, icon });
13783 };
13784 }
13785 runInvoke(name, title, icon) {
13786 return (_args, ctx) => {
13787 ctx.close();
13788 const cb = this.callbackCache[name];
13789 if (typeof cb !== "function") {
13790 return;
13791 }
13792 const captured = this.runWithNavCapture(cb);
13793 if (captured) {
13794 const id = deriveWindowId(captured, this.adminUrl);
13795 this.manager.open({ id, baseId: id, url: captured, title, icon });
13796 }
13797 };
13798 }
13799 /**
13800 * Invoke `cb` with navigation sinks (`document.location`,
13801 * `window.location`, `location.assign`, `location.replace`)
13802 * shadowed so any assignment is captured instead of navigating
13803 * the shell. Returns the captured URL or `null` if the callback
13804 * was a pure JS action.
13805 *
13806 * The shadow uses `Object.defineProperty` on the document /
13807 * window instance to override the prototype's accessor for the
13808 * duration of the call. `delete` afterwards unshadows so the
13809 * native setter is restored.
13810 */
13811 runWithNavCapture(cb) {
13812 let captured = null;
13813 const setCaptured = (v) => {
13814 if (captured === null && typeof v === "string" && v !== "") {
13815 captured = v;
13816 }
13817 };
13818 const realLocation = window.location;
13819 const locationProxy = new Proxy(realLocation, {
13820 get(target2, prop2) {
13821 const value = target2[prop2];
13822 if (prop2 === "assign" || prop2 === "replace") {
13823 return (url) => setCaptured(url);
13824 }
13825 if (typeof value === "function") {
13826 return value.bind(target2);
13827 }
13828 return value;
13829 },
13830 set(_target, prop2, value) {
13831 if (prop2 === "href") {
13832 setCaptured(value);
13833 return true;
13834 }
13835 return true;
13836 }
13837 });
13838 const shadowed = [];
13839 const installShadow = (obj) => {
13840 try {
13841 Object.defineProperty(obj, "location", {
13842 configurable: true,
13843 get: () => locationProxy,
13844 set: (v) => setCaptured(v)
13845 });
13846 shadowed.push({ obj, key: "location" });
13847 } catch {
13848 }
13849 };
13850 installShadow(document);
13851 installShadow(window);
13852 try {
13853 cb({ close: () => {
13854 } });
13855 } catch {
13856 } finally {
13857 for (const s of shadowed) {
13858 try {
13859 delete s.obj[s.key];
13860 } catch {
13861 }
13862 }
13863 }
13864 return captured;
13865 }
13866 }
13867 const seed$2 = [];
13868 function register(def) {
13869 throwOnRegistrationErrors(
13870 "Widget",
13871 collectRegistrationErrors(def, WIDGET_CHECKS),
13872 def
13873 );
13874 const idx = seed$2.findIndex((w) => w.id === def.id);
13875 if (idx >= 0) {
13876 seed$2[idx] = def;
13877 } else {
13878 seed$2.push(def);
13879 }
13880 }
13881 function unregister(id) {
13882 const idx = seed$2.findIndex((w) => w.id === id);
13883 if (idx >= 0) {
13884 seed$2.splice(idx, 1);
13885 }
13886 }
13887 function all() {
13888 const copy = seed$2.slice();
13889 const filtered = applyFilters(HOOKS.WIDGETS, copy);
13890 if (!Array.isArray(filtered)) {
13891 if (typeof console !== "undefined") {
13892 console.warn(
13893 "[desktop-mode] `desktop-mode.widgets` filter returned a non-array; falling back to seed list."
13894 );
13895 }
13896 return copy;
13897 }
13898 return filtered.filter(isValidDef);
13899 }
13900 function get(id) {
13901 return all().find((w) => w.id === id);
13902 }
13903 const WIDGET_CHECKS = [
13904 {
13905 field: "id",
13906 message: "missing or not a non-empty string",
13907 valid: (d) => typeof d.id === "string" && d.id !== ""
13908 },
13909 {
13910 field: "label",
13911 message: "missing or not a non-empty string",
13912 valid: (d) => typeof d.label === "string" && d.label !== ""
13913 },
13914 {
13915 field: "description",
13916 message: "not a string",
13917 valid: (d) => typeof d.description === "string"
13918 },
13919 {
13920 field: "icon",
13921 message: "missing or not a non-empty string",
13922 valid: (d) => typeof d.icon === "string" && d.icon !== ""
13923 },
13924 {
13925 field: "mount",
13926 message: "not a function",
13927 valid: (d) => typeof d.mount === "function"
13928 }
13929 ];
13930 function isValidDef(def) {
13931 return collectRegistrationErrors(def, WIDGET_CHECKS).length === 0;
13932 }
13933 let active$2 = null;
13934 function openWidgetPicker(options) {
13935 if (active$2) {
13936 return;
13937 }
13938 const panel2 = document.createElement("div");
13939 panel2.className = "desktop-mode-widget-picker";
13940 panel2.setAttribute("role", "menu");
13941 panel2.setAttribute("aria-label", __("Add widget"));
13942 const title = document.createElement("div");
13943 title.className = "desktop-mode-widget-picker__title";
13944 title.textContent = __("Add widget");
13945 panel2.appendChild(title);
13946 const list2 = document.createElement("div");
13947 list2.className = "desktop-mode-widget-picker__list";
13948 panel2.appendChild(list2);
13949 paintList(list2, options);
13950 document.body.appendChild(panel2);
13951 positionPanel(panel2, options.anchor);
13952 const onOutsidePointerDown = (e) => {
13953 const target2 = e.target;
13954 if (!target2) {
13955 return;
13956 }
13957 if (panel2.contains(target2) || options.anchor.contains(target2)) {
13958 return;
13959 }
13960 closeWidgetPicker();
13961 };
13962 window.setTimeout(() => {
13963 document.addEventListener("pointerdown", onOutsidePointerDown, true);
13964 }, 0);
13965 const onKeyDown = (e) => {
13966 if (e.key === "Escape") {
13967 closeWidgetPicker();
13968 }
13969 };
13970 document.addEventListener("keydown", onKeyDown);
13971 active$2 = { panel: panel2, options, onOutsidePointerDown, onKeyDown };
13972 const first = list2.querySelector(
13973 "button:not([disabled])"
13974 );
13975 first?.focus();
13976 }
13977 function refreshWidgetPicker() {
13978 if (!active$2) {
13979 return;
13980 }
13981 const list2 = active$2.panel.querySelector(
13982 ".desktop-mode-widget-picker__list"
13983 );
13984 if (list2) {
13985 paintList(list2, active$2.options);
13986 }
13987 }
13988 function closeWidgetPicker() {
13989 if (!active$2) {
13990 return;
13991 }
13992 document.removeEventListener(
13993 "pointerdown",
13994 active$2.onOutsidePointerDown,
13995 true
13996 );
13997 document.removeEventListener("keydown", active$2.onKeyDown);
13998 active$2.panel.remove();
13999 active$2 = null;
14000 }
14001 function paintList(list2, options) {
14002 list2.innerHTML = "";
14003 const enabled = new Set(options.enabledIds());
14004 const defs = options.registry();
14005 if (defs.length === 0) {
14006 const empty = document.createElement("div");
14007 empty.className = "desktop-mode-widget-picker__empty";
14008 empty.textContent = __(
14009 "No widgets available. Activate a plugin that registers one, or see the docs for the registerWidget API."
14010 );
14011 list2.appendChild(empty);
14012 return;
14013 }
14014 for (const def of defs) {
14015 const entry = document.createElement("button");
14016 entry.type = "button";
14017 entry.className = "desktop-mode-widget-picker__entry";
14018 const isAdded = enabled.has(def.id);
14019 if (isAdded) {
14020 entry.classList.add(
14021 "desktop-mode-widget-picker__entry--added"
14022 );
14023 entry.disabled = true;
14024 entry.setAttribute("aria-disabled", "true");
14025 }
14026 entry.setAttribute("role", "menuitem");
14027 let ariaLabel;
14028 if (isAdded) {
14029 ariaLabel = sprintf(__("%s (already added)"), def.label);
14030 } else {
14031 ariaLabel = sprintf(__("Add %s"), def.label);
14032 }
14033 entry.setAttribute("aria-label", ariaLabel);
14034 const icon = document.createElement("span");
14035 icon.className = `desktop-mode-widget-picker__entry-icon dashicons ${def.icon}`;
14036 icon.setAttribute("aria-hidden", "true");
14037 entry.appendChild(icon);
14038 const textWrap = document.createElement("span");
14039 textWrap.className = "desktop-mode-widget-picker__entry-text";
14040 const label = document.createElement("span");
14041 label.className = "desktop-mode-widget-picker__entry-label";
14042 label.textContent = def.label;
14043 textWrap.appendChild(label);
14044 if (def.description) {
14045 const desc = document.createElement("span");
14046 desc.className = "desktop-mode-widget-picker__entry-description";
14047 desc.textContent = def.description;
14048 textWrap.appendChild(desc);
14049 }
14050 entry.appendChild(textWrap);
14051 if (isAdded) {
14052 const status = document.createElement("span");
14053 status.className = "desktop-mode-widget-picker__entry-status";
14054 status.textContent = __("Added");
14055 entry.appendChild(status);
14056 }
14057 if (!isAdded) {
14058 entry.addEventListener("click", (e) => {
14059 e.preventDefault();
14060 e.stopPropagation();
14061 options.onAdd(def.id);
14062 });
14063 }
14064 list2.appendChild(entry);
14065 }
14066 }
14067 function positionPanel(panel2, anchor) {
14068 const rect = anchor.getBoundingClientRect();
14069 panel2.style.position = "fixed";
14070 panel2.style.left = "0px";
14071 panel2.style.top = "0px";
14072 panel2.style.visibility = "hidden";
14073 const panelRect = panel2.getBoundingClientRect();
14074 const width = panelRect.width || 320;
14075 const height = panelRect.height || 200;
14076 const gap = 6;
14077 let left = rect.right - width;
14078 let top = rect.top - height - gap;
14079 if (left < 8) {
14080 left = 8;
14081 }
14082 if (top < 8) {
14083 top = rect.bottom + gap;
14084 }
14085 panel2.style.left = `${Math.round(left)}px`;
14086 panel2.style.top = `${Math.round(top)}px`;
14087 panel2.style.visibility = "";
14088 }
14089 const FLOATING_CLASS = "desktop-mode-widgets__card--floating";
14090 const MOVABLE_CLASS = "desktop-mode-widgets__card--movable";
14091 const RESIZABLE_CLASS = "desktop-mode-widgets__card--resizable";
14092 const DRAGGING_CLASS = "desktop-mode-widgets__card--dragging";
14093 const RESIZING_CLASS = "desktop-mode-widgets__card--resizing";
14094 const DEFAULT_MIN_WIDTH = 160;
14095 const DEFAULT_MIN_HEIGHT = 80;
14096 const DEFAULT_WIDTH$1 = 280;
14097 const DEFAULT_HEIGHT$1 = 180;
14098 const VIEWPORT_MARGIN = 20;
14099 const DRAG_THRESHOLD_PX$1 = 5;
14100 const DRAG_THRESHOLD_SQUARED = DRAG_THRESHOLD_PX$1 * DRAG_THRESHOLD_PX$1;
14101 const DRAG_EXCLUDED_SELECTORS = 'input, textarea, select, button, a, [contenteditable="true"]';
14102 function buildFrame(def, ctx, handlers) {
14103 const card = document.createElement("div");
14104 card.className = "desktop-mode-widgets__card";
14105 card.dataset.widgetId = def.id;
14106 const movable = def.movable === true;
14107 const resizable = def.resizable === true;
14108 if (movable) {
14109 card.classList.add(MOVABLE_CLASS);
14110 }
14111 if (resizable) {
14112 card.classList.add(RESIZABLE_CLASS);
14113 }
14114 if (movable) {
14115 card.appendChild(buildChrome(def, handlers.onRemove, handlers.onRedock));
14116 } else {
14117 card.appendChild(buildCornerClose(def, handlers.onRemove));
14118 }
14119 const body = document.createElement("div");
14120 body.className = "desktop-mode-widgets__card-body";
14121 card.appendChild(body);
14122 if (ctx.geometry) {
14123 applyGeometry(
14124 card,
14125 clampGeometryToParent(ctx.geometry, ctx.floatingParent)
14126 );
14127 card.classList.add(FLOATING_CLASS);
14128 } else if (resizable && typeof ctx.dockedHeight === "number") {
14129 card.style.height = `${clampDockedHeight(ctx.dockedHeight, def)}px`;
14130 }
14131 const isFloating = () => card.classList.contains(FLOATING_CLASS);
14132 const resizeCleanups = [];
14133 if (resizable) {
14134 for (const dir of allHandleDirs()) {
14135 const handle = document.createElement("div");
14136 handle.className = `desktop-mode-widgets__resize desktop-mode-widgets__resize--${dir}`;
14137 handle.setAttribute("aria-hidden", "true");
14138 handle.dataset.dir = dir;
14139 card.appendChild(handle);
14140 resizeCleanups.push(
14141 attachResize(card, handle, dir, def, ctx, handlers, isFloating)
14142 );
14143 }
14144 }
14145 let dragCleanup = null;
14146 if (movable) {
14147 const chrome = card.querySelector(
14148 ".desktop-mode-widgets__chrome"
14149 );
14150 if (chrome) {
14151 dragCleanup = attachDrag(card, chrome, def, ctx, handlers);
14152 }
14153 }
14154 return {
14155 card,
14156 body,
14157 dispose: () => {
14158 for (const fn of resizeCleanups) {
14159 try {
14160 fn();
14161 } catch {
14162 }
14163 }
14164 if (dragCleanup) {
14165 try {
14166 dragCleanup();
14167 } catch {
14168 }
14169 }
14170 card.remove();
14171 }
14172 };
14173 }
14174 function buildChrome(def, onRemove, onRedock) {
14175 const chrome = document.createElement("header");
14176 chrome.className = "desktop-mode-widgets__chrome";
14177 const grip = document.createElement("span");
14178 grip.className = "desktop-mode-widgets__grip";
14179 grip.setAttribute("aria-hidden", "true");
14180 chrome.appendChild(grip);
14181 const title = document.createElement("span");
14182 title.className = "desktop-mode-widgets__title";
14183 title.textContent = def.label;
14184 chrome.appendChild(title);
14185 chrome.appendChild(buildRedockButton(def, onRedock));
14186 const close = buildCloseButton(def, onRemove);
14187 chrome.appendChild(close);
14188 return chrome;
14189 }
14190 function buildRedockButton(def, onRedock) {
14191 const btn = document.createElement("button");
14192 btn.type = "button";
14193 btn.className = "desktop-mode-widgets__card-redock";
14194 btn.setAttribute(
14195 "aria-label",
14196 // translators: %s is the widget label (e.g., "Clock")
14197 sprintf(__("Dock %s back to widget column"), def.label)
14198 );
14199 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>';
14200 btn.addEventListener("click", (e) => {
14201 e.preventDefault();
14202 e.stopPropagation();
14203 onRedock();
14204 });
14205 btn.dataset.noDrag = "true";
14206 return btn;
14207 }
14208 function buildCornerClose(def, onRemove) {
14209 const close = buildCloseButton(def, onRemove);
14210 close.classList.add("desktop-mode-widgets__card-close--corner");
14211 return close;
14212 }
14213 function buildCloseButton(def, onRemove) {
14214 const close = document.createElement("button");
14215 close.type = "button";
14216 close.className = "desktop-mode-widgets__card-close";
14217 close.setAttribute("aria-label", sprintf(__("Remove %s"), def.label));
14218 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>';
14219 close.addEventListener("click", (e) => {
14220 e.preventDefault();
14221 e.stopPropagation();
14222 onRemove();
14223 });
14224 return close;
14225 }
14226 function attachDrag(card, chrome, def, ctx, handlers) {
14227 let pointerId = null;
14228 let startX = 0;
14229 let startY = 0;
14230 let initialLeft = 0;
14231 let initialTop = 0;
14232 let committed = false;
14233 const onDown = (e) => {
14234 if (e.button !== 0) {
14235 return;
14236 }
14237 const target2 = e.target;
14238 if (target2 && target2.closest(DRAG_EXCLUDED_SELECTORS)) {
14239 return;
14240 }
14241 e.preventDefault();
14242 pointerId = e.pointerId;
14243 startX = e.clientX;
14244 startY = e.clientY;
14245 committed = false;
14246 initialLeft = parseFloat(card.style.left) || 0;
14247 initialTop = parseFloat(card.style.top) || 0;
14248 chrome.setPointerCapture(pointerId);
14249 };
14250 const commitDrag = () => {
14251 if (!card.classList.contains(FLOATING_CLASS)) {
14252 const parentRect = ctx.floatingParent.getBoundingClientRect();
14253 const rect = card.getBoundingClientRect();
14254 const initial = {
14255 x: rect.left - parentRect.left,
14256 y: rect.top - parentRect.top,
14257 width: rect.width || def.defaultWidth || DEFAULT_WIDTH$1,
14258 height: rect.height || def.defaultHeight || DEFAULT_HEIGHT$1
14259 };
14260 applyGeometry(card, initial);
14261 card.classList.add(FLOATING_CLASS);
14262 handlers.onLiberate(initial);
14263 initialLeft = parseFloat(card.style.left) || 0;
14264 initialTop = parseFloat(card.style.top) || 0;
14265 }
14266 card.classList.add(DRAGGING_CLASS);
14267 };
14268 const onMove = (e) => {
14269 if (pointerId === null || e.pointerId !== pointerId) {
14270 return;
14271 }
14272 const dx = e.clientX - startX;
14273 const dy = e.clientY - startY;
14274 if (!committed) {
14275 if (dx * dx + dy * dy < DRAG_THRESHOLD_SQUARED) {
14276 return;
14277 }
14278 committed = true;
14279 commitDrag();
14280 }
14281 const clamped = clampToParent(
14282 initialLeft + dx,
14283 initialTop + dy,
14284 card.offsetWidth,
14285 card.offsetHeight,
14286 ctx.floatingParent
14287 );
14288 card.style.left = `${clamped.x}px`;
14289 card.style.top = `${clamped.y}px`;
14290 };
14291 const onUp = (e) => {
14292 if (pointerId === null || e.pointerId !== pointerId) {
14293 return;
14294 }
14295 try {
14296 chrome.releasePointerCapture(pointerId);
14297 } catch {
14298 }
14299 pointerId = null;
14300 if (!committed) {
14301 return;
14302 }
14303 committed = false;
14304 card.classList.remove(DRAGGING_CLASS);
14305 handlers.onGeometryChanged(currentGeometry(card));
14306 };
14307 chrome.addEventListener("pointerdown", onDown);
14308 chrome.addEventListener("pointermove", onMove);
14309 chrome.addEventListener("pointerup", onUp);
14310 chrome.addEventListener("pointercancel", onUp);
14311 return () => {
14312 chrome.removeEventListener("pointerdown", onDown);
14313 chrome.removeEventListener("pointermove", onMove);
14314 chrome.removeEventListener("pointerup", onUp);
14315 chrome.removeEventListener("pointercancel", onUp);
14316 };
14317 }
14318 function attachResize(card, handle, dir, def, ctx, handlers, isFloating) {
14319 let pointerId = null;
14320 let startX = 0;
14321 let startY = 0;
14322 let startLeft = 0;
14323 let startTop = 0;
14324 let startW = 0;
14325 let startH = 0;
14326 const onDown = (e) => {
14327 if (e.button !== 0) {
14328 return;
14329 }
14330 if (!isFloating() && !isHeightOnlyDir(dir)) {
14331 return;
14332 }
14333 e.preventDefault();
14334 e.stopPropagation();
14335 pointerId = e.pointerId;
14336 startX = e.clientX;
14337 startY = e.clientY;
14338 const rect = card.getBoundingClientRect();
14339 const parentRect = ctx.floatingParent.getBoundingClientRect();
14340 startLeft = rect.left - parentRect.left;
14341 startTop = rect.top - parentRect.top;
14342 startW = rect.width;
14343 startH = rect.height;
14344 handle.setPointerCapture(pointerId);
14345 card.classList.add(RESIZING_CLASS);
14346 };
14347 const onMove = (e) => {
14348 if (pointerId === null || e.pointerId !== pointerId) {
14349 return;
14350 }
14351 const dx = e.clientX - startX;
14352 const dy = e.clientY - startY;
14353 const next = computeResize(
14354 dir,
14355 dx,
14356 dy,
14357 startLeft,
14358 startTop,
14359 startW,
14360 startH,
14361 def,
14362 ctx.floatingParent,
14363 isFloating()
14364 );
14365 if (isFloating()) {
14366 card.style.left = `${next.x}px`;
14367 card.style.top = `${next.y}px`;
14368 card.style.width = `${next.width}px`;
14369 }
14370 card.style.height = `${next.height}px`;
14371 };
14372 const onUp = (e) => {
14373 if (pointerId === null || e.pointerId !== pointerId) {
14374 return;
14375 }
14376 try {
14377 handle.releasePointerCapture(pointerId);
14378 } catch {
14379 }
14380 pointerId = null;
14381 card.classList.remove(RESIZING_CLASS);
14382 if (isFloating()) {
14383 handlers.onGeometryChanged(currentGeometry(card));
14384 } else {
14385 handlers.onDockedHeightChanged(card.offsetHeight);
14386 }
14387 };
14388 handle.addEventListener("pointerdown", onDown);
14389 handle.addEventListener("pointermove", onMove);
14390 handle.addEventListener("pointerup", onUp);
14391 handle.addEventListener("pointercancel", onUp);
14392 return () => {
14393 handle.removeEventListener("pointerdown", onDown);
14394 handle.removeEventListener("pointermove", onMove);
14395 handle.removeEventListener("pointerup", onUp);
14396 handle.removeEventListener("pointercancel", onUp);
14397 };
14398 }
14399 function allHandleDirs() {
14400 return ["n", "e", "s", "w", "ne", "nw", "se", "sw"];
14401 }
14402 function isHeightOnlyDir(dir) {
14403 return dir === "s";
14404 }
14405 function applyGeometry(card, geometry) {
14406 card.style.left = `${geometry.x}px`;
14407 card.style.top = `${geometry.y}px`;
14408 card.style.width = `${geometry.width}px`;
14409 card.style.height = `${geometry.height}px`;
14410 }
14411 function currentGeometry(card) {
14412 return {
14413 x: parseFloat(card.style.left) || 0,
14414 y: parseFloat(card.style.top) || 0,
14415 width: card.offsetWidth,
14416 height: card.offsetHeight
14417 };
14418 }
14419 function clampDockedHeight(height, def) {
14420 return clamp$1(
14421 height,
14422 def.minHeight ?? DEFAULT_MIN_HEIGHT,
14423 def.maxHeight ?? Infinity
14424 );
14425 }
14426 function clampGeometryToParent(geometry, parent) {
14427 if (!parent.clientWidth || !parent.clientHeight) {
14428 return geometry;
14429 }
14430 const clamped = clampToParent(
14431 geometry.x,
14432 geometry.y,
14433 geometry.width,
14434 geometry.height,
14435 parent
14436 );
14437 return { ...geometry, x: clamped.x, y: clamped.y };
14438 }
14439 function clampToParent(x, y, width, height, parent) {
14440 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
14441 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
14442 const maxX = Math.max(0, parentWidth - width - VIEWPORT_MARGIN);
14443 const maxY = Math.max(0, parentHeight - height - VIEWPORT_MARGIN);
14444 return {
14445 x: Math.min(Math.max(VIEWPORT_MARGIN, x), maxX),
14446 y: Math.min(Math.max(VIEWPORT_MARGIN, y), maxY)
14447 };
14448 }
14449 function computeResize(dir, dx, dy, startLeft, startTop, startW, startH, def, parent, floating) {
14450 const minW = def.minWidth ?? DEFAULT_MIN_WIDTH;
14451 const minH = def.minHeight ?? DEFAULT_MIN_HEIGHT;
14452 const maxW = def.maxWidth ?? Infinity;
14453 const maxH = def.maxHeight ?? Infinity;
14454 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
14455 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
14456 let x = startLeft;
14457 let y = startTop;
14458 let width = startW;
14459 let height = startH;
14460 if (dir === "e" || dir === "ne" || dir === "se") {
14461 width = clamp$1(startW + dx, minW, Math.min(maxW, parentWidth - startLeft));
14462 }
14463 if (dir === "w" || dir === "nw" || dir === "sw") {
14464 const nextWidth = clamp$1(startW - dx, minW, Math.min(maxW, startLeft + startW));
14465 x = startLeft + (startW - nextWidth);
14466 width = nextWidth;
14467 }
14468 if (dir === "s" || dir === "se" || dir === "sw") {
14469 height = clamp$1(
14470 startH + dy,
14471 minH,
14472 Math.min(maxH, parentHeight - startTop)
14473 );
14474 }
14475 if (dir === "n" || dir === "ne" || dir === "nw") {
14476 const nextHeight = clamp$1(startH - dy, minH, Math.min(maxH, startTop + startH));
14477 y = startTop + (startH - nextHeight);
14478 height = nextHeight;
14479 }
14480 if (!floating) {
14481 width = startW;
14482 x = startLeft;
14483 }
14484 return { x, y, width, height };
14485 }
14486 function clamp$1(value, min, max) {
14487 if (max < min) {
14488 return min;
14489 }
14490 return Math.min(Math.max(value, min), max);
14491 }
14492 const IDS_KEY = "desktop-mode-widgets";
14493 const GEOMETRY_KEY$1 = "desktop-mode-widgets-geometry";
14494 const DOCKED_HEIGHTS_KEY = "desktop-mode-widgets-docked-heights";
14495 function readRawEnabled() {
14496 try {
14497 return window.localStorage.getItem(IDS_KEY);
14498 } catch {
14499 return null;
14500 }
14501 }
14502 function loadEnabledIds() {
14503 const raw = readRawEnabled();
14504 if (raw === null) {
14505 return [];
14506 }
14507 try {
14508 const parsed = JSON.parse(raw);
14509 if (!Array.isArray(parsed)) {
14510 return [];
14511 }
14512 return parsed.filter((x) => typeof x === "string");
14513 } catch {
14514 return [];
14515 }
14516 }
14517 function saveEnabledIds(ids) {
14518 try {
14519 window.localStorage.setItem(IDS_KEY, JSON.stringify(ids));
14520 } catch {
14521 }
14522 }
14523 function loadGeometry$1() {
14524 try {
14525 const raw = window.localStorage.getItem(GEOMETRY_KEY$1);
14526 if (!raw) {
14527 return {};
14528 }
14529 const parsed = JSON.parse(raw);
14530 if (!parsed || typeof parsed !== "object") {
14531 return {};
14532 }
14533 const out = {};
14534 for (const [id, rawEntry] of Object.entries(parsed)) {
14535 const entry = sanitizeGeometry(rawEntry);
14536 if (entry) {
14537 out[id] = entry;
14538 }
14539 }
14540 return out;
14541 } catch {
14542 return {};
14543 }
14544 }
14545 function saveGeometry$1(geometry) {
14546 try {
14547 window.localStorage.setItem(GEOMETRY_KEY$1, JSON.stringify(geometry));
14548 } catch {
14549 }
14550 }
14551 function loadDockedHeights() {
14552 try {
14553 const raw = window.localStorage.getItem(DOCKED_HEIGHTS_KEY);
14554 if (!raw) {
14555 return {};
14556 }
14557 const parsed = JSON.parse(raw);
14558 if (!parsed || typeof parsed !== "object") {
14559 return {};
14560 }
14561 const out = {};
14562 for (const [id, value] of Object.entries(parsed)) {
14563 if (typeof value === "number" && Number.isFinite(value) && value > 0) {
14564 out[id] = value;
14565 }
14566 }
14567 return out;
14568 } catch {
14569 return {};
14570 }
14571 }
14572 function saveDockedHeights(heights) {
14573 try {
14574 window.localStorage.setItem(
14575 DOCKED_HEIGHTS_KEY,
14576 JSON.stringify(heights)
14577 );
14578 } catch {
14579 }
14580 }
14581 function sanitizeGeometry(raw) {
14582 if (!raw || typeof raw !== "object") {
14583 return null;
14584 }
14585 const { x, y, width, height } = raw;
14586 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) {
14587 return null;
14588 }
14589 return { x, y, width, height };
14590 }
14591 function createWidgetStorage(widgetId) {
14592 const prefix = `desktop-mode.widget.${widgetId}.`;
14593 const safeGet = (key) => {
14594 try {
14595 return localStorage.getItem(prefix + key);
14596 } catch {
14597 return null;
14598 }
14599 };
14600 return {
14601 get(key) {
14602 const raw = safeGet(key);
14603 if (raw === null) {
14604 return null;
14605 }
14606 try {
14607 return JSON.parse(raw);
14608 } catch {
14609 return null;
14610 }
14611 },
14612 set(key, value) {
14613 try {
14614 localStorage.setItem(prefix + key, JSON.stringify(value));
14615 } catch {
14616 }
14617 },
14618 remove(key) {
14619 try {
14620 localStorage.removeItem(prefix + key);
14621 } catch {
14622 }
14623 },
14624 clear() {
14625 try {
14626 for (let i = localStorage.length - 1; i >= 0; i--) {
14627 const key = localStorage.key(i);
14628 if (key && key.startsWith(prefix)) {
14629 localStorage.removeItem(key);
14630 }
14631 }
14632 } catch {
14633 }
14634 }
14635 };
14636 }
14637 const DEFAULT_ENABLED_IDS = ["clock"];
14638 class WidgetLayer {
14639 /**
14640 * @param root The column element (`#desktop-mode-widgets`).
14641 * @param pluginUrl Absolute plugin URL — passed to widget ctx.
14642 * @param floatingHost Parent for liberated (floating) widgets.
14643 * Defaults to the column's parent (the desktop
14644 * area) so floats are bounded by the visible
14645 * desktop, not the 320 px-wide column.
14646 */
14647 constructor(root, pluginUrl, floatingHost) {
14648 this.mounted = /* @__PURE__ */ new Map();
14649 this.generation = 0;
14650 this.root = root;
14651 this.pluginUrl = pluginUrl;
14652 this.enabledIds = loadEnabledIds();
14653 this.geometry = loadGeometry$1();
14654 this.dockedHeights = loadDockedHeights();
14655 this.floatingHost = floatingHost ?? root.parentElement ?? root;
14656 this.listEl = document.createElement("div");
14657 this.listEl.className = "desktop-mode-widgets__list";
14658 this.root.appendChild(this.listEl);
14659 this.addTile = this.buildAddTile();
14660 this.root.appendChild(this.addTile);
14661 this.paintEmptyState();
14662 }
14663 /**
14664 * Mount every widget the user has enabled (per localStorage).
14665 * Called once during shell boot, AFTER the registry seed has run
14666 * so built-ins are available. Safe to call multiple times — the
14667 * `mounted` map dedupes.
14668 */
14669 hydrate() {
14670 if (readRawEnabled() === null) {
14671 this.enabledIds = DEFAULT_ENABLED_IDS.filter(
14672 (id) => !!get(id)
14673 );
14674 saveEnabledIds(this.enabledIds);
14675 }
14676 for (const id of this.enabledIds) {
14677 if (this.mounted.has(id)) {
14678 continue;
14679 }
14680 this.mountById(id);
14681 }
14682 this.paintEmptyState();
14683 }
14684 /**
14685 * Add a widget by id — called by the picker after the user
14686 * selects an available entry. Idempotent.
14687 */
14688 add(id) {
14689 if (this.enabledIds.includes(id)) {
14690 return;
14691 }
14692 if (!get(id)) {
14693 return;
14694 }
14695 this.enabledIds.push(id);
14696 saveEnabledIds(this.enabledIds);
14697 this.mountById(id);
14698 this.paintEmptyState();
14699 doAction(HOOKS.WIDGET_ADDED, { id });
14700 refreshWidgetPicker();
14701 }
14702 /**
14703 * Remove a widget by id — called from the card's × button and
14704 * from the picker. Idempotent.
14705 */
14706 remove(id) {
14707 const before = this.enabledIds.length;
14708 this.enabledIds = this.enabledIds.filter((e) => e !== id);
14709 if (this.enabledIds.length === before) {
14710 return;
14711 }
14712 saveEnabledIds(this.enabledIds);
14713 if (this.geometry[id]) {
14714 delete this.geometry[id];
14715 saveGeometry$1(this.geometry);
14716 }
14717 if (this.dockedHeights[id] !== void 0) {
14718 delete this.dockedHeights[id];
14719 saveDockedHeights(this.dockedHeights);
14720 }
14721 this.unmountById(id);
14722 this.paintEmptyState();
14723 doAction(HOOKS.WIDGET_REMOVED, { id });
14724 refreshWidgetPicker();
14725 }
14726 /** Public read for the picker / external callers. */
14727 getEnabledIds() {
14728 return [...this.enabledIds];
14729 }
14730 /**
14731 * Mount a widget ONLY if it's already in the user's enabled
14732 * list AND not currently mounted. No-op when the widget isn't
14733 * enabled (user never opted in) and no-op when it's already on
14734 * screen. Used by the server-driven sync: when a plugin
14735 * activates mid-session, its widget def registers via the
14736 * sync's path; if the user had previously enabled that widget
14737 * (in a prior session or before the plugin was deactivated),
14738 * we want to bring it back on screen without toggling the
14739 * "enabled" state or firing a `WIDGET_ADDED` action.
14740 *
14741 * The net behaviour is "rehydrate this one widget now that
14742 * its def is finally registered," which is subtly different
14743 * from `ensureMounted` (which OPT-INs the user into enabling
14744 * the widget for the first time).
14745 */
14746 mountIfEnabled(id) {
14747 if (!get(id)) {
14748 return;
14749 }
14750 if (!this.enabledIds.includes(id)) {
14751 return;
14752 }
14753 if (this.mounted.has(id)) {
14754 return;
14755 }
14756 this.mountById(id);
14757 this.paintEmptyState();
14758 }
14759 /**
14760 * Unmount a widget without touching the persisted enablement.
14761 * Used by the server-driven widget-registry sync: when a plugin
14762 * deactivates mid-session, its widget defs disappear from the
14763 * registry and we need to pull any mounted instance off the
14764 * screen — but we deliberately KEEP the id in the user's
14765 * enabled list so re-activating the plugin re-mounts it
14766 * automatically through `hydrate()`.
14767 *
14768 * Idempotent; a no-op when the widget isn't currently mounted.
14769 */
14770 unmount(id) {
14771 if (!this.mounted.has(id)) {
14772 return;
14773 }
14774 this.unmountById(id);
14775 this.paintEmptyState();
14776 }
14777 /**
14778 * Guarantee the widget identified by `id` is currently mounted,
14779 * adding it to the enabled list if it isn't. No-op when the
14780 * widget is already on screen. Intended for companion plugins
14781 * that want to pin their widget programmatically — a monitor
14782 * plugin that auto-pins itself on the first error burst, a
14783 * first-run onboarding flow that ensures the quick-start widget
14784 * is present, etc.
14785 *
14786 * Returns `true` when the widget is mounted (either newly added
14787 * or already present), `false` when the id isn't registered —
14788 * callers can branch on the failure without having to maintain
14789 * their own registry snapshot.
14790 */
14791 ensureMounted(id) {
14792 if (!get(id)) {
14793 return false;
14794 }
14795 if (this.enabledIds.includes(id)) {
14796 return true;
14797 }
14798 this.add(id);
14799 return true;
14800 }
14801 /**
14802 * Tear down every widget. Called on shell unload via `pagehide`
14803 * so intervals / RAF loops stop before the beacon flush.
14804 */
14805 disposeAll() {
14806 for (const id of Array.from(this.mounted.keys())) {
14807 this.unmountById(id);
14808 }
14809 }
14810 // --- Internal ---------------------------------------------------
14811 mountById(id) {
14812 const def = get(id);
14813 if (!def) {
14814 return;
14815 }
14816 const gen = ++this.generation;
14817 const initialGeometry = def.movable === true ? this.geometry[id] : void 0;
14818 const frame = buildFrame(
14819 def,
14820 {
14821 floatingParent: this.floatingHost,
14822 geometry: initialGeometry,
14823 dockedHeight: this.dockedHeights[id]
14824 },
14825 {
14826 onRemove: () => this.remove(id),
14827 onGeometryChanged: (geom) => this.persistGeometry(id, geom),
14828 onDockedHeightChanged: (height) => this.persistDockedHeight(id, height),
14829 onLiberate: (geom) => this.liberate(id, geom),
14830 onRedock: () => this.redock(id)
14831 }
14832 );
14833 const floating = !!initialGeometry;
14834 const record = {
14835 id,
14836 frame,
14837 generation: gen,
14838 teardown: null,
14839 floating
14840 };
14841 this.mounted.set(id, record);
14842 this.placeCard(frame.card, floating);
14843 const ctx = {
14844 id,
14845 pluginUrl: this.pluginUrl,
14846 storage: createWidgetStorage(id)
14847 };
14848 doAction(HOOKS.WIDGET_MOUNTING, { id, container: frame.body, ctx });
14849 const onResolve = (teardown) => {
14850 const current = this.mounted.get(id);
14851 if (!current || current.generation !== gen) {
14852 try {
14853 teardown();
14854 } catch {
14855 }
14856 return;
14857 }
14858 current.teardown = teardown;
14859 doAction(HOOKS.WIDGET_MOUNTED, { id, container: frame.body, ctx });
14860 };
14861 let result;
14862 try {
14863 result = def.mount(frame.body, ctx);
14864 } catch (err) {
14865 this.handleMountFailure(id, err);
14866 return;
14867 }
14868 if (isThenable(result)) {
14869 result.then(onResolve, (err) => {
14870 if (this.mounted.get(id)?.generation === gen) {
14871 this.handleMountFailure(id, err);
14872 }
14873 });
14874 return;
14875 }
14876 onResolve(result);
14877 }
14878 unmountById(id) {
14879 const record = this.mounted.get(id);
14880 if (!record) {
14881 return;
14882 }
14883 doAction(HOOKS.WIDGET_UNMOUNTING, { id });
14884 try {
14885 record.teardown?.();
14886 } catch (err) {
14887 doAction(HOOKS.SHELL_ERROR, { scope: "widget-teardown", id, error: err });
14888 if (typeof console !== "undefined") {
14889 console.error(
14890 `[desktop-mode] Widget "${id}" teardown threw:`,
14891 err
14892 );
14893 }
14894 }
14895 this.generation++;
14896 record.frame.dispose();
14897 this.mounted.delete(id);
14898 }
14899 handleMountFailure(id, err) {
14900 const record = this.mounted.get(id);
14901 if (record) {
14902 record.frame.dispose();
14903 this.mounted.delete(id);
14904 }
14905 doAction(HOOKS.WIDGET_MOUNT_FAILED, { id, error: err });
14906 doAction(HOOKS.SHELL_ERROR, { scope: "widget-mount", id, error: err });
14907 if (typeof console !== "undefined") {
14908 console.error(
14909 `[desktop-mode] Widget "${id}" failed to mount:`,
14910 err
14911 );
14912 }
14913 }
14914 buildAddTile() {
14915 const tile2 = document.createElement("button");
14916 tile2.type = "button";
14917 tile2.className = "desktop-mode-widgets__add";
14918 tile2.setAttribute("aria-label", __("Add widget"));
14919 const plus = document.createElement("span");
14920 plus.className = "desktop-mode-widgets__add-plus";
14921 plus.setAttribute("aria-hidden", "true");
14922 plus.textContent = "+";
14923 const label = document.createElement("span");
14924 label.className = "desktop-mode-widgets__add-label";
14925 label.textContent = __("Add widget");
14926 tile2.appendChild(plus);
14927 tile2.appendChild(label);
14928 tile2.addEventListener("click", (e) => {
14929 e.preventDefault();
14930 e.stopPropagation();
14931 openWidgetPicker({
14932 anchor: tile2,
14933 registry: () => all(),
14934 enabledIds: () => [...this.enabledIds],
14935 onAdd: (id) => this.add(id)
14936 });
14937 });
14938 return tile2;
14939 }
14940 /**
14941 * Drop a card into the right parent based on its floating state.
14942 * Docked cards append to the column list above the `+` tile;
14943 * floating cards append to the desktop-area-level host so they
14944 * sit above the wallpaper and can range across the viewport.
14945 */
14946 placeCard(card, floating) {
14947 if (floating) {
14948 this.floatingHost.appendChild(card);
14949 } else {
14950 this.listEl.appendChild(card);
14951 }
14952 }
14953 /**
14954 * Move a widget from the column into the floating host. Called by
14955 * the frame on the user's first drag of a movable widget.
14956 */
14957 liberate(id, geometry) {
14958 const record = this.mounted.get(id);
14959 if (!record || record.floating) {
14960 return;
14961 }
14962 record.floating = true;
14963 this.floatingHost.appendChild(record.frame.card);
14964 applyGeometry(record.frame.card, geometry);
14965 this.persistGeometry(id, geometry);
14966 this.paintEmptyState();
14967 }
14968 /**
14969 * Inverse of {@link liberate}: move a floating card back into
14970 * the column and drop its persisted geometry so a subsequent
14971 * shell boot brings it up docked. Called when the user clicks
14972 * the re-dock button in the card's chrome header, or
14973 * programmatically by companion plugins via
14974 * `wp.desktop.widgets.redock( id )` /
14975 * `wp.desktop.widgetLayer.redock( id )`.
14976 *
14977 * Idempotent — a docked widget silently no-ops, an unknown id
14978 * silently no-ops. The `--floating` class on the card is
14979 * removed as part of the same write so CSS rules that depend
14980 * on it (re-dock button visibility, absolute positioning) flip
14981 * back in one paint.
14982 *
14983 * @since 0.7.0 (private)
14984 * @since 0.8.6 (public)
14985 */
14986 redock(id) {
14987 const record = this.mounted.get(id);
14988 if (!record || !record.floating) {
14989 return;
14990 }
14991 record.floating = false;
14992 if (this.geometry[id]) {
14993 delete this.geometry[id];
14994 saveGeometry$1(this.geometry);
14995 }
14996 const card = record.frame.card;
14997 card.classList.remove("desktop-mode-widgets__card--floating");
14998 card.style.left = "";
14999 card.style.top = "";
15000 card.style.width = "";
15001 const dockedHeight = this.dockedHeights[id];
15002 card.style.height = dockedHeight !== void 0 ? `${dockedHeight}px` : "";
15003 this.listEl.appendChild(card);
15004 this.paintEmptyState();
15005 }
15006 persistGeometry(id, geometry) {
15007 this.geometry[id] = geometry;
15008 saveGeometry$1(this.geometry);
15009 }
15010 persistDockedHeight(id, height) {
15011 if (!Number.isFinite(height) || height <= 0) {
15012 return;
15013 }
15014 this.dockedHeights[id] = height;
15015 saveDockedHeights(this.dockedHeights);
15016 }
15017 /**
15018 * Toggle a `--has-widgets` modifier so CSS can hide the column's
15019 * decorative backdrop when nothing's mounted (keeps the empty
15020 * state clean — just the `+` tile floating in the corner).
15021 *
15022 * Floating widgets don't count toward "has widgets" in the column
15023 * sense — if every enabled widget is floating, the column itself
15024 * shows only the empty state + add tile.
15025 */
15026 paintEmptyState() {
15027 let docked = 0;
15028 for (const record of this.mounted.values()) {
15029 if (!record.floating) {
15030 docked++;
15031 }
15032 }
15033 this.root.classList.toggle(
15034 "desktop-mode-widgets--has-widgets",
15035 docked > 0
15036 );
15037 }
15038 }
15039 function isThenable(x) {
15040 return !!x && (typeof x === "object" || typeof x === "function") && typeof x.then === "function";
15041 }
15042 const DEFAULT_NATIVE_MIN_WIDTH = 280;
15043 const DEFAULT_NATIVE_MIN_HEIGHT = 220;
15044 const DEFAULT_NATIVE_WIDTH = 520;
15045 const DEFAULT_NATIVE_HEIGHT = 400;
15046 function buildIframeContentRender(cfg, cleanups, windowId) {
15047 return (body) => {
15048 const iframe = document.createElement("iframe");
15049 iframe.style.width = "100%";
15050 iframe.style.height = "100%";
15051 iframe.style.border = "0";
15052 iframe.setAttribute("src", cfg.url);
15053 if (typeof cfg.sandbox === "string" && cfg.sandbox !== "") {
15054 iframe.setAttribute("sandbox", cfg.sandbox);
15055 }
15056 body.style.padding = "0";
15057 body.appendChild(iframe);
15058 const unregisterSynth = registerSyntheticIframe(windowId, iframe);
15059 cleanups.push(unregisterSynth);
15060 let targetOrigin;
15061 try {
15062 targetOrigin = new URL(cfg.url, window.location.origin).origin;
15063 } catch {
15064 targetOrigin = window.location.origin;
15065 }
15066 let resolveReady = null;
15067 const readyPromise = new Promise((resolve2) => {
15068 resolveReady = resolve2;
15069 });
15070 const onLoad = () => {
15071 if (cfg.bridge) {
15072 try {
15073 const doc = iframe.contentDocument;
15074 if (doc && !doc.querySelector("script[data-desktop-mode-iframe-bridge]")) {
15075 const bridgeUrl = window.desktopModeConfig?.iframeBridgeUrl;
15076 if (bridgeUrl) {
15077 const s = doc.createElement("script");
15078 s.src = bridgeUrl;
15079 s.setAttribute("data-desktop-mode-iframe-bridge", "1");
15080 doc.head?.appendChild(s);
15081 }
15082 }
15083 } catch {
15084 }
15085 }
15086 markWindowContentReady(windowId);
15087 resolveReady?.();
15088 };
15089 iframe.addEventListener("load", onLoad);
15090 const onMessage = (e) => {
15091 if (!iframe.contentWindow || e.source !== iframe.contentWindow) {
15092 return;
15093 }
15094 if (e.origin !== targetOrigin && e.origin !== window.location.origin) {
15095 return;
15096 }
15097 const data = e.data;
15098 if (data && typeof data === "object" && typeof data.type === "string" && data.type.startsWith("desktop-mode-bridge-")) {
15099 const bridgeRouter = window.__desktopModeConnectionBridge;
15100 bridgeRouter?.routeIncomingFromIframe(data, windowId);
15101 }
15102 if (data && typeof data === "object" && data.type === "desktop-mode-window-publish" && typeof data.channel === "string" && data.channel !== "") {
15103 dispatchFromWindow(
15104 windowId,
15105 data.channel,
15106 data.payload
15107 );
15108 }
15109 try {
15110 cfg.onMessage?.(e.data);
15111 } catch (err) {
15112 if (typeof console !== "undefined") {
15113 console.error(
15114 "[desktop-mode] iframeContent.onMessage threw:",
15115 err
15116 );
15117 }
15118 }
15119 };
15120 window.addEventListener("message", onMessage);
15121 cleanups.push(() => {
15122 window.removeEventListener("message", onMessage);
15123 iframe.removeEventListener("load", onLoad);
15124 });
15125 return readyPromise;
15126 };
15127 }
15128 function createRegisterWindow(manager) {
15129 return async (def) => {
15130 const userRender = def.render;
15131 let render2 = userRender;
15132 const cleanups = [];
15133 if (def.iframeContent) {
15134 if (userRender && typeof console !== "undefined") {
15135 console.warn(
15136 "[desktop-mode] registerWindow: both `render` and `iframeContent` provided — ignoring `render` and using the iframe shorthand. Drop one."
15137 );
15138 }
15139 render2 = buildIframeContentRender(
15140 def.iframeContent,
15141 cleanups,
15142 def.id
15143 );
15144 }
15145 const userOnClose = def.onClose;
15146 const onClose = cleanups.length ? () => {
15147 for (const fn of cleanups) {
15148 try {
15149 fn();
15150 } catch {
15151 }
15152 }
15153 userOnClose?.();
15154 } : userOnClose;
15155 const win = await manager.open({
15156 id: def.id,
15157 baseId: def.baseId || def.id,
15158 native: true,
15159 url: def.url || `#${def.id}`,
15160 title: def.title,
15161 icon: def.icon,
15162 x: def.x ?? 0,
15163 y: def.y ?? 0,
15164 width: def.width ?? DEFAULT_NATIVE_WIDTH,
15165 height: def.height ?? DEFAULT_NATIVE_HEIGHT,
15166 minWidth: def.minWidth ?? DEFAULT_NATIVE_MIN_WIDTH,
15167 minHeight: def.minHeight ?? DEFAULT_NATIVE_MIN_HEIGHT,
15168 render: render2,
15169 onClose,
15170 onResize: def.onResize,
15171 autofocus: def.autofocus,
15172 initialState: def.initialState,
15173 ownerHandle: def.ownerHandle,
15174 multi: def.multi,
15175 desktopId: def.desktopId
15176 });
15177 return win;
15178 };
15179 }
15180 let onWindowInstanceCounter = 0;
15181 function onWindow(id, handlers, options = {}) {
15182 const namespace = `desktop-mode/on-window/${id}/${++onWindowInstanceCounter}`;
15183 const persistent = options.persistent === true;
15184 const bindings = [
15185 ["opened", HOOKS.WINDOW_OPENED],
15186 ["reopened", HOOKS.WINDOW_REOPENED],
15187 ["focused", HOOKS.WINDOW_FOCUSED],
15188 ["blurred", HOOKS.WINDOW_BLURRED],
15189 ["closing", HOOKS.WINDOW_CLOSING],
15190 ["closed", HOOKS.WINDOW_CLOSED],
15191 ["minimized", HOOKS.WINDOW_MINIMIZED],
15192 ["restored", HOOKS.WINDOW_RESTORED],
15193 ["maximized", HOOKS.WINDOW_MAXIMIZED],
15194 ["unmaximized", HOOKS.WINDOW_UNMAXIMIZED],
15195 ["fullscreenEntered", HOOKS.WINDOW_FULLSCREEN_ENTERED],
15196 ["fullscreenExited", HOOKS.WINDOW_FULLSCREEN_EXITED],
15197 ["resized", HOOKS.WINDOW_RESIZED],
15198 ["bodyResized", HOOKS.WINDOW_BODY_RESIZED],
15199 ["boundsChanged", HOOKS.WINDOW_BOUNDS_CHANGED]
15200 ];
15201 const registered = [];
15202 let disposed = false;
15203 const unsubscribe = () => {
15204 if (disposed) {
15205 return;
15206 }
15207 disposed = true;
15208 for (const hookName2 of registered) {
15209 removeAction(hookName2, namespace);
15210 }
15211 };
15212 for (const [key, hookName2] of bindings) {
15213 const handler = handlers[key];
15214 if (!handler) {
15215 continue;
15216 }
15217 registered.push(hookName2);
15218 addAction(hookName2, namespace, (payload) => {
15219 const p = payload;
15220 if (p.windowId !== id) {
15221 return;
15222 }
15223 const { windowId: _w, ...rest } = p;
15224 handler(rest);
15225 if (key === "closed" && !persistent) {
15226 unsubscribe();
15227 }
15228 });
15229 }
15230 return unsubscribe;
15231 }
15232 function readGlobalRegistry() {
15233 const g = window;
15234 return {
15235 ...g.wpDesktopNativeWindows || {},
15236 ...g.desktopModeNativeWindows || {}
15237 };
15238 }
15239 function createNativeWindowSync(deps2) {
15240 const { manager, appendSystemTile, removeSystemTile } = deps2;
15241 const registered = /* @__PURE__ */ new Set();
15242 const injectedTemplates = /* @__PURE__ */ new Set();
15243 const loadedScripts = /* @__PURE__ */ new Set();
15244 const loadedStyles = /* @__PURE__ */ new Set();
15245 const entriesById = /* @__PURE__ */ new Map();
15246 const resolveSizeForEntry = (entry) => {
15247 const saved = loadNativeWindowGeometry(entry.id);
15248 if (!saved) {
15249 return { width: entry.width, height: entry.height };
15250 }
15251 return {
15252 width: Math.max(saved.width, entry.minWidth),
15253 height: Math.max(saved.height, entry.minHeight)
15254 };
15255 };
15256 const ensureTemplate = (entry) => {
15257 if (injectedTemplates.has(entry.templateId)) {
15258 return;
15259 }
15260 if (document.getElementById(entry.templateId)) {
15261 injectedTemplates.add(entry.templateId);
15262 return;
15263 }
15264 if (!entry.templateHtml) {
15265 return;
15266 }
15267 const tpl = document.createElement("template");
15268 tpl.id = entry.templateId;
15269 tpl.innerHTML = entry.templateHtml;
15270 document.body.appendChild(tpl);
15271 injectedTemplates.add(entry.templateId);
15272 };
15273 const ensureStyle = (entry) => {
15274 const url = entry.styleUrl;
15275 if (!url || loadedStyles.has(url)) {
15276 return;
15277 }
15278 const safeUrl = url.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
15279 const existing = document.head.querySelector(
15280 `link[rel="stylesheet"][href="${safeUrl}"]`
15281 );
15282 if (!existing) {
15283 const link = document.createElement("link");
15284 link.rel = "stylesheet";
15285 link.href = url;
15286 if (entry.styleHandle) {
15287 link.dataset.desktopModeStyleHandle = entry.styleHandle;
15288 }
15289 document.head.appendChild(link);
15290 }
15291 if (Array.isArray(entry.styleInline)) {
15292 for (const css2 of entry.styleInline) {
15293 if (typeof css2 !== "string" || css2 === "") {
15294 continue;
15295 }
15296 const style = document.createElement("style");
15297 if (entry.styleHandle) {
15298 style.dataset.desktopModeStyleHandle = entry.styleHandle;
15299 }
15300 style.textContent = css2;
15301 document.head.appendChild(style);
15302 }
15303 }
15304 loadedStyles.add(url);
15305 };
15306 const ensureScript = async (entry) => {
15307 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
15308 return;
15309 }
15310 try {
15311 await loadVendorScript(entry.scriptUrl, {
15312 translations: entry.scriptTranslations,
15313 l10n: entry.scriptL10n,
15314 before: entry.scriptBefore,
15315 after: entry.scriptAfter
15316 });
15317 } catch (err) {
15318 doAction(HOOKS.SHELL_ERROR, {
15319 scope: "native-window-script-load",
15320 id: entry.id,
15321 error: err
15322 });
15323 }
15324 loadedScripts.add(entry.scriptUrl);
15325 };
15326 const openFromEntry = (entry) => {
15327 const render2 = readGlobalRegistry()[entry.id];
15328 const finalRender = (body, ctx) => {
15329 body.appendChild(cloneTemplate(entry.templateId));
15330 return render2?.(body, ctx);
15331 };
15332 const size = resolveSizeForEntry(entry);
15333 void manager.open({
15334 id: entry.id,
15335 baseId: entry.id,
15336 native: true,
15337 url: `#${entry.id}`,
15338 title: entry.title,
15339 icon: entry.icon,
15340 width: size.width,
15341 height: size.height,
15342 minWidth: entry.minWidth,
15343 minHeight: entry.minHeight,
15344 render: finalRender,
15345 autofocus: entry.autofocus,
15346 ownerHandle: entry.ownerHandle || entry.scriptHandle
15347 });
15348 };
15349 const openNewFromEntry = (entry) => {
15350 const render2 = readGlobalRegistry()[entry.id];
15351 const finalRender = (body, ctx) => {
15352 body.appendChild(cloneTemplate(entry.templateId));
15353 return render2?.(body, ctx);
15354 };
15355 const size = resolveSizeForEntry(entry);
15356 void manager.openNew({
15357 id: entry.id,
15358 baseId: entry.id,
15359 native: true,
15360 url: `#${entry.id}`,
15361 title: entry.title,
15362 icon: entry.icon,
15363 width: size.width,
15364 height: size.height,
15365 minWidth: entry.minWidth,
15366 minHeight: entry.minHeight,
15367 initialState: "normal",
15368 render: finalRender,
15369 autofocus: entry.autofocus,
15370 ownerHandle: entry.ownerHandle || entry.scriptHandle
15371 });
15372 };
15373 const registerTile = async (entry) => {
15374 if (registered.has(entry.id)) {
15375 return;
15376 }
15377 if ("none" === entry.placement) {
15378 ensureTemplate(entry);
15379 ensureStyle(entry);
15380 await ensureScript(entry);
15381 registered.add(entry.id);
15382 return;
15383 }
15384 ensureTemplate(entry);
15385 ensureStyle(entry);
15386 await ensureScript(entry);
15387 appendSystemTile({
15388 id: entry.id,
15389 title: entry.title,
15390 icon: entry.icon,
15391 isOpen: () => !!manager.getById(entry.id),
15392 onOpen: () => openFromEntry(entry)
15393 });
15394 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: entry.id });
15395 registered.add(entry.id);
15396 };
15397 const unregisterTile = (id) => {
15398 if (!registered.has(id)) {
15399 return;
15400 }
15401 removeSystemTile(id);
15402 registered.delete(id);
15403 entriesById.delete(id);
15404 };
15405 const sync = async (list2) => {
15406 const incoming = /* @__PURE__ */ new Set();
15407 for (const entry of list2) {
15408 incoming.add(entry.id);
15409 entriesById.set(entry.id, entry);
15410 }
15411 for (const id of Array.from(registered)) {
15412 if (!incoming.has(id)) {
15413 unregisterTile(id);
15414 }
15415 }
15416 for (const entry of list2) {
15417 if (!registered.has(entry.id)) {
15418 await registerTile(entry);
15419 }
15420 }
15421 };
15422 const openById = (id, opts = {}) => {
15423 const entry = entriesById.get(id);
15424 if (!entry) {
15425 return false;
15426 }
15427 activity.publish("desktop-mode/open-requested", {
15428 windowId: id,
15429 source: opts.source ?? "api"
15430 });
15431 openFromEntry(entry);
15432 return true;
15433 };
15434 const openNewById = (id, opts = {}) => {
15435 const entry = entriesById.get(id);
15436 if (!entry) {
15437 return false;
15438 }
15439 activity.publish("desktop-mode/open-requested", {
15440 windowId: id,
15441 source: opts.source ?? "api"
15442 });
15443 openNewFromEntry(entry);
15444 return true;
15445 };
15446 addAction(
15447 HOOKS.WINDOW_RESIZE_END,
15448 "desktop-mode-native-window-geometry",
15449 (payload) => {
15450 const p = payload;
15451 const windowId = p?.windowId;
15452 const width = p?.width;
15453 const height = p?.height;
15454 if (!windowId || typeof width !== "number" || typeof height !== "number") {
15455 return;
15456 }
15457 const win = manager.getById(windowId);
15458 if (!win) {
15459 return;
15460 }
15461 if (win.state !== "normal") {
15462 return;
15463 }
15464 const baseId = win.config.baseId || win.id;
15465 saveNativeWindowGeometry(baseId, { width, height });
15466 if (win.element) {
15467 saveNativeWindowPosition(baseId, {
15468 x: win.element.offsetLeft,
15469 y: win.element.offsetTop
15470 });
15471 }
15472 }
15473 );
15474 addAction(
15475 HOOKS.WINDOW_DRAG_END,
15476 "desktop-mode-native-window-geometry",
15477 (payload) => {
15478 const windowId = payload?.windowId;
15479 if (!windowId) {
15480 return;
15481 }
15482 const win = manager.getById(windowId);
15483 if (!win) {
15484 return;
15485 }
15486 if (win.state !== "normal") {
15487 return;
15488 }
15489 if (!win.element) {
15490 return;
15491 }
15492 const baseId = win.config.baseId || win.id;
15493 saveNativeWindowGeometry(baseId, {
15494 width: win.element.offsetWidth,
15495 height: win.element.offsetHeight
15496 });
15497 saveNativeWindowPosition(baseId, {
15498 x: win.element.offsetLeft,
15499 y: win.element.offsetTop
15500 });
15501 }
15502 );
15503 addAction(
15504 HOOKS.WINDOW_MAXIMIZED,
15505 "desktop-mode-native-window-geometry",
15506 (payload) => {
15507 const windowId = payload?.windowId;
15508 if (!windowId) {
15509 return;
15510 }
15511 const win = manager.getById(windowId);
15512 if (!win) {
15513 return;
15514 }
15515 const baseId = win.config.baseId || win.id;
15516 const entry = entriesById.get(baseId);
15517 const defaults = entry ? { width: entry.width, height: entry.height } : { width: win.config.width, height: win.config.height };
15518 setNativeWindowSavedState(baseId, "maximized", defaults);
15519 }
15520 );
15521 addAction(
15522 HOOKS.WINDOW_UNMAXIMIZED,
15523 "desktop-mode-native-window-geometry",
15524 (payload) => {
15525 const windowId = payload?.windowId;
15526 if (!windowId) {
15527 return;
15528 }
15529 const win = manager.getById(windowId);
15530 if (!win) {
15531 return;
15532 }
15533 const baseId = win.config.baseId || win.id;
15534 setNativeWindowSavedState(baseId, null);
15535 }
15536 );
15537 return { sync, openById, openNewById };
15538 }
15539 function cloneTemplate(template) {
15540 let tpl = null;
15541 if (typeof template === "string") {
15542 const found = document.getElementById(template);
15543 if (found instanceof HTMLTemplateElement) {
15544 tpl = found;
15545 }
15546 } else {
15547 tpl = template;
15548 }
15549 if (!tpl) {
15550 throw new Error(
15551 `[desktop-mode] cloneTemplate: no <template> found for ${typeof template === "string" ? `#${template}` : "<reference>"}`
15552 );
15553 }
15554 return tpl.content.cloneNode(true);
15555 }
15556 function findMenuEntryForUrl(url) {
15557 const wp = window.wp?.desktop;
15558 const bootConfig = window.desktopModeConfig;
15559 const adminUrl = wp?.config?.adminUrl ?? bootConfig?.adminUrl;
15560 if (!adminUrl) {
15561 return null;
15562 }
15563 const items = wp?.getMenuItems?.() ?? bootConfig?.dockItems ?? [];
15564 const targetId = deriveWindowId(url, adminUrl);
15565 return items.find(
15566 (item) => deriveWindowId(item.url, adminUrl) === targetId || (item.submenu ?? []).some(
15567 (sub) => deriveWindowId(sub.url, adminUrl) === targetId
15568 )
15569 ) ?? null;
15570 }
15571 function renderIcon(icon, opts) {
15572 const className = opts.className ?? "";
15573 const title = opts.title ?? "";
15574 if (typeof icon === "string" && icon.startsWith("dashicons-")) {
15575 const el = document.createElement("span");
15576 el.className = `dashicons ${icon} ${className}`.trim();
15577 el.setAttribute("aria-hidden", "true");
15578 return el;
15579 }
15580 if (typeof icon === "string" && icon.startsWith("data:image/svg+xml;base64,")) {
15581 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
15582 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
15583 const el = document.createElement("span");
15584 el.className = className;
15585 el.setAttribute("aria-hidden", "true");
15586 el.style.backgroundImage = `url("${icon}")`;
15587 el.style.backgroundRepeat = "no-repeat";
15588 el.style.backgroundPosition = "center";
15589 el.style.backgroundSize = "contain";
15590 el.style.display = "inline-block";
15591 return el;
15592 }
15593 }
15594 if (typeof icon === "string" && /^data:image\/(png|jpeg|jpg|gif|webp|x-icon|vnd\.microsoft\.icon);base64,/i.test(icon)) {
15595 const commaIdx = icon.indexOf(",");
15596 const payload = commaIdx >= 0 ? icon.slice(commaIdx + 1) : "";
15597 if (/^[A-Za-z0-9+/=]+$/.test(payload)) {
15598 return makeImgIcon(icon, className);
15599 }
15600 }
15601 if (typeof icon === "string" && (icon.startsWith("http://") || icon.startsWith("https://"))) {
15602 return makeImgIcon(icon, className);
15603 }
15604 const span = document.createElement("span");
15605 span.className = `${className} desktop-mode-icon-letter`.trim();
15606 span.setAttribute("aria-hidden", "true");
15607 const letters = letterFromTitle(title);
15608 span.textContent = letters;
15609 const hue = hashTitleToHue(title);
15610 span.style.backgroundColor = `hsl( ${hue}, 60%, 45% )`;
15611 span.style.color = "#fff";
15612 span.style.display = "inline-flex";
15613 span.style.alignItems = "center";
15614 span.style.justifyContent = "center";
15615 span.style.fontWeight = "600";
15616 span.style.borderRadius = "4px";
15617 return span;
15618 }
15619 function makeImgIcon(src, className) {
15620 const img = document.createElement("img");
15621 img.className = className;
15622 img.src = src;
15623 img.alt = "";
15624 img.setAttribute("aria-hidden", "true");
15625 img.draggable = false;
15626 return img;
15627 }
15628 function letterFromTitle(title) {
15629 const trimmed = (title ?? "").trim();
15630 if (trimmed === "") {
15631 return "?";
15632 }
15633 const words = trimmed.split(/\s+/);
15634 if (words.length >= 2) {
15635 return (words[0][0] + words[1][0]).toUpperCase();
15636 }
15637 const first = words[0];
15638 if (first.length >= 2) {
15639 return first.slice(0, 2).toUpperCase();
15640 }
15641 return first.toUpperCase();
15642 }
15643 const BADGE_CLASS = "desktop-mode-icon__badge";
15644 const _badges = /* @__PURE__ */ new Map();
15645 function _safeBadge(count) {
15646 return Math.max(0, Math.floor(Number(count) || 0));
15647 }
15648 function setIconBadge(iconId, count) {
15649 if (!iconId) {
15650 return;
15651 }
15652 const tile2 = _findIconTile(iconId);
15653 if (!tile2) {
15654 return;
15655 }
15656 const safe = _safeBadge(count);
15657 const previous = _badges.get(iconId) ?? 0;
15658 if (safe === previous) {
15659 return;
15660 }
15661 if (safe === 0) {
15662 _badges.delete(iconId);
15663 } else {
15664 _badges.set(iconId, safe);
15665 }
15666 _paintBadgeNode(tile2, safe);
15667 activity.publish("desktop-mode/badge-changed", {
15668 itemId: iconId,
15669 count: safe,
15670 rail: "icon"
15671 });
15672 doAction(HOOKS.ICON_BADGE_CHANGED, {
15673 iconId,
15674 count: safe,
15675 previousCount: previous
15676 });
15677 }
15678 function clearIconBadge(iconId) {
15679 setIconBadge(iconId, 0);
15680 }
15681 function getIconBadge(iconId) {
15682 return _badges.get(iconId) ?? 0;
15683 }
15684 const iconsApi = {
15685 setBadge: setIconBadge,
15686 clearBadge: clearIconBadge,
15687 getBadge: getIconBadge
15688 };
15689 function fingerprintIcons(icons) {
15690 if (!icons || icons.length === 0) {
15691 return "";
15692 }
15693 return icons.map(
15694 (i) => `${i.id}|${i.title}|${i.icon}|${i.window ?? ""}|${i.url ?? ""}|${i.position ?? 0}|${i.pinned ? 1 : 0}`
15695 ).join(";");
15696 }
15697 let _lastFingerprint = "";
15698 function renderDesktopIcons(host, icons, deps2) {
15699 const fp = fingerprintIcons(icons);
15700 if (fp === _lastFingerprint && host.querySelector(":scope > .desktop-mode-icons")) {
15701 return;
15702 }
15703 _lastFingerprint = fp;
15704 const existing = host.querySelector(":scope > .desktop-mode-icons");
15705 if (existing) {
15706 existing.remove();
15707 }
15708 if (!icons || icons.length === 0) {
15709 return;
15710 }
15711 const container = document.createElement("div");
15712 container.className = "desktop-mode-icons";
15713 container.setAttribute("role", "list");
15714 container.setAttribute("aria-label", __("Desktop icons"));
15715 const ordered = [...icons].sort((a, b) => {
15716 const ap = a.pinned ? 0 : 1;
15717 const bp = b.pinned ? 0 : 1;
15718 return ap - bp;
15719 });
15720 const tiles = /* @__PURE__ */ new Map();
15721 for (const entry of ordered) {
15722 const tile2 = buildIcon(entry, deps2);
15723 const stored = _badges.get(entry.id) ?? 0;
15724 if (stored > 0) {
15725 _paintBadgeNode(tile2, stored);
15726 }
15727 container.appendChild(tile2);
15728 tiles.set(entry.id, tile2);
15729 }
15730 host.appendChild(container);
15731 doAction(HOOKS.DESKTOP_ICONS_RENDERED, {
15732 ids: (icons ?? []).map((i) => i.id),
15733 container,
15734 tiles
15735 });
15736 }
15737 function _findIconTile(iconId) {
15738 if (!iconId) {
15739 return null;
15740 }
15741 const container = document.querySelector(
15742 ".desktop-mode-icons"
15743 );
15744 if (!container) {
15745 return null;
15746 }
15747 return container.querySelector(
15748 `[data-icon-id="${_cssEscape(iconId)}"]`
15749 );
15750 }
15751 function _paintBadgeNode(host, count) {
15752 const existing = host.querySelector(
15753 `:scope > .${BADGE_CLASS}`
15754 );
15755 if (count <= 0) {
15756 existing?.remove();
15757 return;
15758 }
15759 const display = count > 99 ? "99+" : String(count);
15760 const ariaLabel = sprintf(
15761 // translators: %d is the number of pending items in a desktop-icon badge.
15762 _n("%d notification", "%d notifications", count),
15763 count
15764 );
15765 if (existing) {
15766 if (existing.textContent !== display) {
15767 existing.textContent = display;
15768 }
15769 existing.setAttribute("aria-label", ariaLabel);
15770 return;
15771 }
15772 const badge = document.createElement("span");
15773 badge.className = BADGE_CLASS;
15774 badge.textContent = display;
15775 badge.setAttribute("aria-label", ariaLabel);
15776 host.appendChild(badge);
15777 }
15778 function _cssEscape(value) {
15779 const c = window.CSS;
15780 return c?.escape ? c.escape(value) : value;
15781 }
15782 function buildIcon(entry, deps2) {
15783 const tile2 = document.createElement("button");
15784 tile2.type = "button";
15785 tile2.className = entry.pinned ? "desktop-mode-icon desktop-mode-icon--pinned" : "desktop-mode-icon";
15786 tile2.dataset.iconId = entry.id;
15787 if (entry.pinned) {
15788 tile2.dataset.pinned = "1";
15789 }
15790 tile2.setAttribute("role", "listitem");
15791 tile2.setAttribute("aria-label", entry.title);
15792 const icon = renderIcon(entry.icon, {
15793 title: entry.title,
15794 className: "desktop-mode-icon__image"
15795 });
15796 tile2.appendChild(icon);
15797 const label = document.createElement("span");
15798 label.className = "desktop-mode-icon__label";
15799 label.textContent = entry.title;
15800 tile2.appendChild(label);
15801 tile2.addEventListener("click", (e) => {
15802 e.stopPropagation();
15803 doAction(HOOKS.DESKTOP_ICON_CLICKED, {
15804 id: entry.id,
15805 target: entry.window ? "window" : "url"
15806 });
15807 openTarget(entry, deps2);
15808 });
15809 tile2.addEventListener("contextmenu", (e) => {
15810 if (entry.pinned) {
15811 return;
15812 }
15813 e.preventDefault();
15814 e.stopPropagation();
15815 openItemVisibilityMenu({
15816 x: e.clientX,
15817 y: e.clientY,
15818 id: entry.id,
15819 title: entry.title,
15820 surface: "desktop"
15821 });
15822 });
15823 return tile2;
15824 }
15825 function openTarget(entry, deps2) {
15826 if (entry.window) {
15827 const opened = deps2.openWindow(entry.window);
15828 if (!opened) {
15829 return;
15830 }
15831 return;
15832 }
15833 if (entry.url) {
15834 if (tryOpenExternalUrl(entry.url)) {
15835 return;
15836 }
15837 try {
15838 const parsed = new URL(entry.url, window.location.origin);
15839 const windowId = deps2.deriveWindowId(parsed.toString());
15840 const menuEntry = findMenuEntryForUrl(parsed.toString());
15841 void deps2.manager.open({
15842 id: windowId,
15843 baseId: windowId,
15844 url: parsed.toString(),
15845 parentUrl: menuEntry?.url ?? parsed.toString(),
15846 title: entry.title,
15847 icon: entry.icon,
15848 submenu: menuEntry?.submenu,
15849 multi: !!menuEntry?.multi
15850 });
15851 } catch {
15852 }
15853 }
15854 }
15855 const SIDE_DOCK_ID = "desktop-mode-side-dock";
15856 function coreItemToIconEntry(item, index2) {
15857 return {
15858 id: `dock-core:${item.id}`,
15859 title: item.title,
15860 icon: item.icon,
15861 window: "",
15862 url: item.url,
15863 // Synthesized icons render after server-registered ones; the
15864 // large offset leaves headroom for plugin authors who set
15865 // explicit `position` values.
15866 position: 1e3 + index2
15867 };
15868 }
15869 function createLayoutDispatcher(deps2, initialLayout, initialDockItems, initialServerIcons) {
15870 let layout = initialLayout;
15871 let items = initialDockItems;
15872 let serverIcons = initialServerIcons ?? [];
15873 let primary = null;
15874 let side = null;
15875 let primaryDock = null;
15876 let sideDock = null;
15877 let sideDockEl = null;
15878 const systemTiles = /* @__PURE__ */ new Map();
15879 const railFor = (affinity) => {
15880 if (affinity === "core" && side) {
15881 return side;
15882 }
15883 return primary;
15884 };
15885 const ensureSideDockEl = () => {
15886 const existing = document.getElementById(
15887 SIDE_DOCK_ID
15888 );
15889 if (existing) {
15890 return existing;
15891 }
15892 const el = document.createElement("nav");
15893 el.id = SIDE_DOCK_ID;
15894 el.className = "desktop-mode-dock";
15895 el.setAttribute("role", "toolbar");
15896 el.setAttribute("aria-label", "Core admin navigation");
15897 deps2.shellBody.insertBefore(el, deps2.shellBody.firstChild);
15898 return el;
15899 };
15900 const removeSideDockEl = () => {
15901 if (sideDockEl && sideDockEl.parentNode) {
15902 sideDockEl.parentNode.removeChild(sideDockEl);
15903 }
15904 sideDockEl = null;
15905 };
15906 const readSettings = () => deps2.getSettings?.() ?? { itemVisibility: {}, dockOrder: [] };
15907 const effectiveDockItems = () => {
15908 const dockedNativeWindows = /* @__PURE__ */ new Set();
15909 for (const entry of systemTiles.values()) {
15910 dockedNativeWindows.add(entry.item.id);
15911 }
15912 return applyDockPlacement(
15913 items,
15914 serverIcons,
15915 readSettings(),
15916 dockedNativeWindows
15917 );
15918 };
15919 const partition = () => {
15920 const effective = effectiveDockItems();
15921 const core = [];
15922 const plugin = [];
15923 for (const item of effective) {
15924 if (item.isCore) {
15925 core.push(item);
15926 } else {
15927 plugin.push(item);
15928 }
15929 }
15930 return { core, plugin };
15931 };
15932 const repaintIcons = () => {
15933 const settings = readSettings();
15934 if (layout !== "spatial") {
15935 deps2.renderIcons(
15936 applyDesktopPlacement(serverIcons, items, settings.itemVisibility)
15937 );
15938 return;
15939 }
15940 const { core } = partition();
15941 const synthesized = core.map(coreItemToIconEntry);
15942 const keptServerIcons = serverIcons.filter((icon) => {
15943 const override = settings.itemVisibility[icon.id];
15944 if (override) {
15945 return override === "desktop" || override === "both";
15946 }
15947 return Boolean(icon.pinned);
15948 });
15949 const explicitlyPromoted = [];
15950 let synthIndex = 0;
15951 for (const item of items) {
15952 const placement = settings.itemVisibility[item.id];
15953 if (placement === "desktop" || placement === "both") {
15954 explicitlyPromoted.push({
15955 id: `dock:${item.id}`,
15956 title: item.title,
15957 icon: item.icon,
15958 window: "",
15959 url: item.url || "",
15960 position: 2e3 + synthIndex++
15961 });
15962 }
15963 }
15964 deps2.renderIcons([
15965 ...synthesized,
15966 ...keptServerIcons,
15967 ...explicitlyPromoted
15968 ]);
15969 };
15970 const tearDownDocks = () => {
15971 if (primary) {
15972 try {
15973 primary.destroy();
15974 } catch (err) {
15975 doAction(HOOKS.SHELL_ERROR, {
15976 scope: "dock-rail-renderer/destroy",
15977 error: err
15978 });
15979 }
15980 primary = null;
15981 primaryDock = null;
15982 }
15983 if (side) {
15984 try {
15985 side.destroy();
15986 } catch (err) {
15987 doAction(HOOKS.SHELL_ERROR, {
15988 scope: "dock-rail-renderer/destroy",
15989 error: err
15990 });
15991 }
15992 side = null;
15993 sideDock = null;
15994 }
15995 };
15996 const mountRail = (mountDeps) => {
15997 const renderer = resolveActive();
15998 if (!renderer) {
15999 doAction(HOOKS.SHELL_ERROR, {
16000 scope: "dock-rail-renderer",
16001 message: "No dock rail renderer is registered."
16002 });
16003 return null;
16004 }
16005 try {
16006 return renderer.mount(mountDeps);
16007 } catch (err) {
16008 doAction(HOOKS.SHELL_ERROR, {
16009 scope: "dock-rail-renderer/mount",
16010 rendererId: renderer.id,
16011 error: err
16012 });
16013 if (renderer === defaultDockRailRenderer) {
16014 return null;
16015 }
16016 try {
16017 return defaultDockRailRenderer.mount(mountDeps);
16018 } catch {
16019 return null;
16020 }
16021 }
16022 };
16023 const buildMountDeps = (container, railItems, orientation) => ({
16024 container,
16025 items: railItems,
16026 // `fullMenu` is the complete admin-menu list. Renderers that
16027 // want to ignore the layout's partitioning (e.g., paint
16028 // every menu item in one ring regardless of `isCore`) read
16029 // this instead of `items`. Snapshot per-mount so a renderer
16030 // holding the array sees a stable list; live updates flow
16031 // through `replaceItems`.
16032 fullMenu: items.slice(),
16033 // Same idea for system tiles — OS Settings, plugin-owned
16034 // native-window launchers, etc. Lets a renderer apply
16035 // uniform treatment across menu + system cohorts in one
16036 // pass. Live updates flow through `appendSystemItem` /
16037 // `removeSystemItem`.
16038 fullSystemTiles: Array.from(systemTiles.values()).map(
16039 (entry) => entry.item
16040 ),
16041 orientation,
16042 windowManager: deps2.windowManager,
16043 adminUrl: deps2.adminUrl,
16044 // `openItem` / `openSubmenuPick` / `openSystemItem` are
16045 // routing callbacks for custom renderers. They mirror
16046 // exactly what the default renderer (`Dock.openPage` /
16047 // `Dock.openSubmenuPick`) does internally — same
16048 // `deriveWindowId(url, adminUrl)` call, same window-
16049 // config shape — so a custom renderer addresses the same
16050 // window with the same id at runtime. Switching renderer
16051 // mid-session doesn't lose the user's open windows.
16052 openItem: (item) => {
16053 const baseId = deriveWindowId(item.url, deps2.adminUrl);
16054 deps2.windowManager.open({
16055 id: baseId,
16056 baseId,
16057 url: item.url,
16058 parentUrl: item.url,
16059 title: item.title,
16060 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
16061 submenu: item.submenu,
16062 multi: !!item.multi
16063 });
16064 },
16065 openSubmenuPick: (item, sub) => {
16066 deps2.windowManager.open({
16067 id: deriveWindowId(sub.url, deps2.adminUrl),
16068 baseId: deriveWindowId(item.url, deps2.adminUrl),
16069 url: sub.url,
16070 // Pin the synthetic parent tab to the dock landing
16071 // page, not to the sub-page the user picked. Without
16072 // this, a submenu-pick (e.g. clicking "Editor" inside
16073 // Appearance's submenu popover) would open at
16074 // site-editor.php with no way back to themes.php.
16075 parentUrl: item.url,
16076 title: item.title,
16077 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
16078 submenu: item.submenu,
16079 multi: !!item.multi
16080 });
16081 },
16082 openSystemItem: (item) => item.onOpen()
16083 });
16084 const buildDocksForCurrentLayout = () => {
16085 tearDownDocks();
16086 const { core, plugin } = partition();
16087 if (layout === "classic") {
16088 sideDockEl = ensureSideDockEl();
16089 side = mountRail(
16090 buildMountDeps(sideDockEl, core, "left")
16091 );
16092 sideDock = unwrapDefaultDock(side);
16093 primary = mountRail(
16094 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
16095 );
16096 primaryDock = unwrapDefaultDock(primary);
16097 } else if (layout === "unified") {
16098 removeSideDockEl();
16099 primary = mountRail(
16100 buildMountDeps(deps2.bottomDockEl, effectiveDockItems(), "bottom")
16101 );
16102 primaryDock = unwrapDefaultDock(primary);
16103 } else {
16104 removeSideDockEl();
16105 primary = mountRail(
16106 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
16107 );
16108 primaryDock = unwrapDefaultDock(primary);
16109 }
16110 for (const entry of systemTiles.values()) {
16111 railFor(entry.affinity)?.appendSystemItem(entry.item);
16112 }
16113 };
16114 const dispatcher = {
16115 getLayout: () => layout,
16116 getPrimary: () => primaryDock,
16117 getSide: () => sideDock,
16118 setLayout: (next) => {
16119 if (next === layout) {
16120 return;
16121 }
16122 layout = next;
16123 deps2.shellRoot.setAttribute("data-desktop-mode-layout", next);
16124 buildDocksForCurrentLayout();
16125 repaintIcons();
16126 document.dispatchEvent(
16127 new CustomEvent("desktop-mode-layout-changed", {
16128 detail: {
16129 layout: next,
16130 primary: primaryDock,
16131 side: sideDock
16132 }
16133 })
16134 );
16135 },
16136 applyDockItems: (nextItems) => {
16137 items = nextItems;
16138 const { core, plugin } = partition();
16139 if (layout === "classic") {
16140 side?.replaceItems(core);
16141 primary?.replaceItems(plugin);
16142 } else if (layout === "unified") {
16143 primary?.replaceItems(effectiveDockItems());
16144 } else {
16145 primary?.replaceItems(plugin);
16146 }
16147 repaintIcons();
16148 },
16149 applyDesktopIcons: (next) => {
16150 serverIcons = next ?? [];
16151 repaintIcons();
16152 },
16153 appendSystemTile: (item, affinity = "plugin") => {
16154 systemTiles.set(item.id, { item, affinity });
16155 railFor(affinity)?.appendSystemItem(item);
16156 },
16157 removeSystemTile: (id) => {
16158 const entry = systemTiles.get(id);
16159 if (!entry) {
16160 return;
16161 }
16162 systemTiles.delete(id);
16163 railFor(entry.affinity)?.removeSystemItem(id);
16164 },
16165 listSystemTiles: () => Array.from(systemTiles.values()).map((entry) => ({
16166 id: entry.item.id,
16167 title: entry.item.title,
16168 icon: entry.item.icon,
16169 affinity: entry.affinity
16170 })),
16171 getSystemTile: (id) => systemTiles.get(id)?.item ?? null,
16172 getMenuItems: () => items.slice(),
16173 refresh: () => {
16174 const { core, plugin } = partition();
16175 if (layout === "classic") {
16176 side?.replaceItems(core);
16177 primary?.replaceItems(plugin);
16178 } else if (layout === "unified") {
16179 primary?.replaceItems(effectiveDockItems());
16180 } else {
16181 primary?.replaceItems(plugin);
16182 }
16183 repaintIcons();
16184 },
16185 destroy: () => {
16186 tearDownDocks();
16187 removeSideDockEl();
16188 }
16189 };
16190 deps2.shellRoot.setAttribute("data-desktop-mode-layout", layout);
16191 buildDocksForCurrentLayout();
16192 repaintIcons();
16193 let lastResolvedId = resolveActive()?.id ?? null;
16194 subscribe$3(() => {
16195 const nextId2 = resolveActive()?.id ?? null;
16196 if (nextId2 === lastResolvedId) {
16197 return;
16198 }
16199 lastResolvedId = nextId2;
16200 buildDocksForCurrentLayout();
16201 repaintIcons();
16202 document.dispatchEvent(
16203 new CustomEvent("desktop-mode-layout-changed", {
16204 detail: {
16205 layout,
16206 primary: primaryDock,
16207 side: sideDock
16208 }
16209 })
16210 );
16211 });
16212 return dispatcher;
16213 }
16214 function loadImpl(scriptUrl) {
16215 if (window.desktopModeCreateAiAssistant) {
16216 return Promise.resolve(window.desktopModeCreateAiAssistant);
16217 }
16218 return new Promise((resolve2, reject) => {
16219 const existing = document.querySelector(
16220 `script[data-desktop-mode-ai="1"]`
16221 );
16222 const finish = () => {
16223 const factory = window.desktopModeCreateAiAssistant;
16224 if (!factory) {
16225 reject(
16226 new Error(
16227 "[desktop-mode] ai-assistant bundle loaded but did not register desktopModeCreateAiAssistant"
16228 )
16229 );
16230 return;
16231 }
16232 resolve2(factory);
16233 };
16234 if (existing) {
16235 if (window.desktopModeCreateAiAssistant) {
16236 finish();
16237 } else {
16238 existing.addEventListener("load", finish);
16239 existing.addEventListener(
16240 "error",
16241 () => reject(new Error("failed to load ai-assistant bundle"))
16242 );
16243 }
16244 return;
16245 }
16246 const s = document.createElement("script");
16247 s.src = scriptUrl;
16248 s.async = true;
16249 s.dataset.desktopModeAi = "1";
16250 s.addEventListener("load", finish);
16251 s.addEventListener(
16252 "error",
16253 () => reject(new Error("failed to load ai-assistant bundle"))
16254 );
16255 document.head.appendChild(s);
16256 });
16257 }
16258 class AiAssistantStub {
16259 constructor(config, scriptUrl) {
16260 this._real = null;
16261 this._loadPromise = null;
16262 this._pendingAsk = null;
16263 this._intendOpen = false;
16264 this.ask = (...args) => {
16265 return this._ensure().then((r) => r.ask(...args));
16266 };
16267 this._config = config;
16268 this._scriptUrl = scriptUrl;
16269 }
16270 _ensure() {
16271 if (this._loadPromise) {
16272 return this._loadPromise;
16273 }
16274 this._loadPromise = loadImpl(this._scriptUrl).then((factory) => {
16275 const real = factory(this._config);
16276 if (this._pendingAsk) {
16277 real.attachAsk(this._pendingAsk);
16278 }
16279 this._real = real;
16280 return real;
16281 });
16282 return this._loadPromise;
16283 }
16284 open() {
16285 this._intendOpen = true;
16286 void this._ensure().then((r) => r.open());
16287 }
16288 close() {
16289 this._intendOpen = false;
16290 if (this._real) {
16291 this._real.close();
16292 }
16293 }
16294 toggle() {
16295 if (this.isOpen) {
16296 this.close();
16297 } else {
16298 this.open();
16299 }
16300 }
16301 get isOpen() {
16302 return this._real ? this._real.isOpen : this._intendOpen;
16303 }
16304 /**
16305 * Late-bind the programmatic `ask` callback. Mirrors the real
16306 * class's `attachAsk` signature so `desktop.ts`'s call site is
16307 * identical whether it's wiring the stub or the impl.
16308 */
16309 attachAsk(fn) {
16310 this._pendingAsk = fn;
16311 if (this._real) {
16312 this._real.attachAsk(fn);
16313 }
16314 }
16315 }
16316 const isAbortError = (err) => {
16317 if (!err || typeof err !== "object") {
16318 return false;
16319 }
16320 return err.name === "AbortError";
16321 };
16322 const normaliseToolsOpt = (tools) => {
16323 if (!tools) {
16324 return [];
16325 }
16326 const all2 = listAiCallableCommands();
16327 if (tools === true || tools === "aiCallable") {
16328 return all2;
16329 }
16330 if (Array.isArray(tools)) {
16331 const allowed = new Set(tools.map((s) => s.toLowerCase()));
16332 return all2.filter((c) => allowed.has(c.slug));
16333 }
16334 if (typeof tools === "function") {
16335 return all2.filter((c) => {
16336 try {
16337 return tools(c.slug) === true;
16338 } catch {
16339 return false;
16340 }
16341 });
16342 }
16343 return [];
16344 };
16345 const normaliseSystemPrompt = (sp) => {
16346 if (!sp) {
16347 return null;
16348 }
16349 if (typeof sp === "string") {
16350 return { text: sp, mode: "append" };
16351 }
16352 if (typeof sp === "object" && typeof sp.text === "string" && sp.text !== "") {
16353 return {
16354 text: sp.text,
16355 mode: sp.mode === "replace" ? "replace" : "append"
16356 };
16357 }
16358 return null;
16359 };
16360 function liftMessage(payloadMessage, result) {
16361 const seed2 = payloadMessage ?? "";
16362 if (seed2 !== "") {
16363 return seed2;
16364 }
16365 if (typeof result === "string" && result !== "") {
16366 return result;
16367 }
16368 if (result && typeof result === "object" && "message" in result && typeof result.message === "string") {
16369 return result.message;
16370 }
16371 return "";
16372 }
16373 function serialiseOutcome(result) {
16374 if (result === void 0) {
16375 return { value: null };
16376 }
16377 if (typeof result === "object" && result !== null) {
16378 return result;
16379 }
16380 return { value: result };
16381 }
16382 function createAsk(deps2) {
16383 const postToSearch = async (body, signal) => {
16384 const config = deps2.config();
16385 const url = config.aiSearchUrl ?? "";
16386 const nonce = config.restNonce ?? "";
16387 if (!url || !nonce) {
16388 throw new Error(
16389 "[desktop-mode] wp.desktop.ai.ask: aiSearchUrl / restNonce missing from config. AI Copilot may not be enabled."
16390 );
16391 }
16392 try {
16393 return await trackedFetch$1(
16394 url,
16395 {
16396 method: "POST",
16397 credentials: "same-origin",
16398 headers: {
16399 "Content-Type": "application/json",
16400 "X-WP-Nonce": nonce
16401 },
16402 body: JSON.stringify(body),
16403 signal
16404 },
16405 { source: "desktop-mode/ai-ask" }
16406 );
16407 } catch (err) {
16408 if (isAbortError(err)) {
16409 throw err;
16410 }
16411 throw new Error(
16412 `[desktop-mode] wp.desktop.ai.ask: network error — ${String(
16413 err?.message ?? err
16414 )}`
16415 );
16416 }
16417 };
16418 const dispatchToolCall = async (payload, opts) => {
16419 const slug = payload.tool?.slug ?? "";
16420 const args = payload.tool?.args ?? "";
16421 const cmd = findCommand(slug);
16422 if (!cmd) {
16423 return {
16424 ok: false,
16425 response: {
16426 answer_type: "tool_call",
16427 message: `Command /${slug} was not registered on this page.`,
16428 entity: null,
16429 admin_links: null,
16430 toolCall: {
16431 slug,
16432 args,
16433 result: { error: "command_not_found" }
16434 },
16435 request_id: payload.request_id
16436 }
16437 };
16438 }
16439 const ctx = opts.commandContext ?? deps2.fallbackContext();
16440 let result;
16441 try {
16442 result = await Promise.resolve(cmd.run(args, ctx));
16443 } catch (err) {
16444 result = { error: String(err?.message ?? err) };
16445 }
16446 return { ok: true, slug, args, result };
16447 };
16448 const composeFollowUp = async (text, slug, args, result, sp, signal) => {
16449 const body = {
16450 query: text,
16451 follow_up: {
16452 tool: { slug, args },
16453 result: serialiseOutcome(result)
16454 }
16455 };
16456 if (sp) {
16457 body.system_prompt_text = sp.text;
16458 body.system_prompt_mode = sp.mode;
16459 }
16460 let res;
16461 try {
16462 res = await postToSearch(body, signal);
16463 } catch (err) {
16464 if (isAbortError(err)) {
16465 throw err;
16466 }
16467 return null;
16468 }
16469 if (!res.ok) {
16470 return null;
16471 }
16472 const payload = await res.json().catch(() => ({}));
16473 const message = typeof payload.message === "string" ? payload.message.trim() : "";
16474 return message !== "" ? payload.message ?? null : null;
16475 };
16476 return async function ask(query, opts = {}) {
16477 const text = (query ?? "").trim();
16478 if (text === "") {
16479 const hasMeaningfulOpts = opts.tools !== void 0 || opts.systemPrompt !== void 0 || opts.followUp === true || opts.resumeTool !== void 0 || opts.commandContext !== void 0;
16480 if (hasMeaningfulOpts) {
16481 throw new Error(
16482 "[desktop-mode] wp.desktop.ai.ask: empty query passed with non-default options — likely a caller bug. Provide a query or call without options."
16483 );
16484 }
16485 return {
16486 answer_type: "chat",
16487 message: "",
16488 entity: null,
16489 admin_links: null
16490 };
16491 }
16492 const commandTools = normaliseToolsOpt(opts.tools);
16493 const sp = normaliseSystemPrompt(opts.systemPrompt);
16494 const body = { query: text };
16495 if (opts.resumeTool) {
16496 body.resume_tool = opts.resumeTool;
16497 }
16498 if (typeof opts.startOffset === "number") {
16499 body.start_offset = opts.startOffset;
16500 }
16501 if (commandTools.length > 0) {
16502 body.command_tools = commandTools;
16503 }
16504 if (sp) {
16505 body.system_prompt_text = sp.text;
16506 body.system_prompt_mode = sp.mode;
16507 }
16508 const res = await postToSearch(body, opts.signal);
16509 if (!res.ok) {
16510 const detail = await res.json().catch(() => ({ message: res.statusText }));
16511 throw new Error(
16512 `[desktop-mode] wp.desktop.ai.ask: HTTP ${res.status} — ${detail.message ?? res.statusText}`
16513 );
16514 }
16515 const payload = await res.json();
16516 if (payload.answer_type !== "tool_call" || !payload.tool) {
16517 return {
16518 answer_type: payload.answer_type,
16519 message: payload.message ?? "",
16520 entity: payload.entity ?? null,
16521 admin_links: payload.admin_links ?? null,
16522 request_id: payload.request_id,
16523 continue: payload.continue ?? null
16524 };
16525 }
16526 const dispatch2 = await dispatchToolCall(payload, opts);
16527 if (!dispatch2.ok) {
16528 return dispatch2.response;
16529 }
16530 const { slug, args, result } = dispatch2;
16531 let message = liftMessage(payload.message, result);
16532 if (opts.followUp === true) {
16533 const composed = await composeFollowUp(
16534 text,
16535 slug,
16536 args,
16537 result,
16538 sp,
16539 opts.signal
16540 );
16541 if (composed !== null) {
16542 message = composed;
16543 }
16544 }
16545 return {
16546 answer_type: "tool_call",
16547 message,
16548 entity: null,
16549 admin_links: null,
16550 toolCall: { slug, args, result },
16551 request_id: payload.request_id
16552 };
16553 };
16554 }
16555 const EVENT_NAME = "desktop-mode-broadcast";
16556 const POSTMESSAGE_TYPE = "desktop-mode-broadcast";
16557 const ORIGIN = window.location.origin;
16558 let _manager = null;
16559 function attachBroadcastBus(manager) {
16560 _manager = manager;
16561 }
16562 function broadcast(topic, payload) {
16563 const filteredTopic = String(
16564 applyFilters("desktop-mode.broadcast.topic", topic, { payload }) ?? topic
16565 );
16566 const filteredPayload = applyFilters(
16567 "desktop-mode.broadcast.payload",
16568 payload,
16569 { topic: filteredTopic }
16570 );
16571 const detail = {
16572 topic: filteredTopic,
16573 payload: filteredPayload
16574 };
16575 document.dispatchEvent(new CustomEvent(EVENT_NAME, { detail }));
16576 doAction(HOOKS.BROADCAST, detail);
16577 activity.publish(
16578 filteredTopic,
16579 filteredPayload
16580 );
16581 if (!_manager) {
16582 return;
16583 }
16584 const message = {
16585 type: POSTMESSAGE_TYPE,
16586 topic: filteredTopic,
16587 payload: filteredPayload
16588 };
16589 for (const win of _manager._stack) {
16590 const target2 = win.iframe?.contentWindow;
16591 if (!target2) {
16592 continue;
16593 }
16594 try {
16595 target2.postMessage(message, ORIGIN);
16596 } catch (err) {
16597 }
16598 }
16599 }
16600 function subscribe$2(topic, cb) {
16601 const handler = (e) => {
16602 const detail = e.detail;
16603 if (!detail) {
16604 return;
16605 }
16606 if (topic !== "*" && detail.topic !== topic) {
16607 return;
16608 }
16609 try {
16610 cb(detail.payload, { topic: detail.topic });
16611 } catch (err) {
16612 doAction(HOOKS.SHELL_ERROR, {
16613 scope: "broadcast-subscriber",
16614 topic: detail.topic,
16615 error: err
16616 });
16617 }
16618 };
16619 document.addEventListener(EVENT_NAME, handler);
16620 return () => document.removeEventListener(EVENT_NAME, handler);
16621 }
16622 function installBroadcastReceiver() {
16623 window.addEventListener("message", (e) => {
16624 if (e.origin !== ORIGIN) {
16625 return;
16626 }
16627 const data = e.data;
16628 if (!data || data.type !== POSTMESSAGE_TYPE) {
16629 return;
16630 }
16631 if (data._fromParent) {
16632 return;
16633 }
16634 if (typeof data.topic !== "string") {
16635 return;
16636 }
16637 broadcast(data.topic, data.payload);
16638 });
16639 }
16640 const LOG_PREFIX = "[desktop-mode-bin badge]";
16641 function log(...args) {
16642 try {
16643 if (window.localStorage?.getItem("desktopModeBinDebug")) {
16644 console.info(LOG_PREFIX, ...args);
16645 }
16646 } catch {
16647 }
16648 }
16649 function warn(...args) {
16650 console.warn(LOG_PREFIX, ...args);
16651 }
16652 const TARGET_ID = "desktop-mode-recycle-bin";
16653 const HEARTBEAT_FIELD$1 = "desktop_mode_recycle_bin_seen_ts";
16654 function getDesktopApi() {
16655 return window.wp?.desktop;
16656 }
16657 const store$3 = createSharedStore(
16658 "desktop-mode/recycle-bin/badge",
16659 () => ({
16660 current: 0,
16661 seenTs: 0,
16662 started: false,
16663 countUrl: ""
16664 })
16665 );
16666 function setRecycleBinBadge(next) {
16667 const safe = Math.max(0, Math.floor(next));
16668 const prev = store$3.state.current;
16669 store$3.state.current = safe;
16670 log("setRecycleBinBadge", { prev, next: safe });
16671 paintBadge(safe);
16672 }
16673 function adjustRecycleBinBadge(delta) {
16674 setRecycleBinBadge(store$3.state.current + delta);
16675 }
16676 function _currentRecycleBinBadge() {
16677 return store$3.state.current;
16678 }
16679 function paintBadge(count) {
16680 const desktop = getDesktopApi();
16681 const active2 = isBinWindowActive();
16682 const visible = active2 ? 0 : count;
16683 log("paintBadge", { count, visible, active: active2 });
16684 desktop?.dock?.setBadge?.(TARGET_ID, visible);
16685 desktop?.taskbar?.setBadge?.(TARGET_ID, visible);
16686 desktop?.icons?.setBadge?.(TARGET_ID, visible);
16687 }
16688 function isBinWindowActive() {
16689 const mgr = getDesktopApi()?.windowManager;
16690 if (mgr?.isActiveByBaseId) {
16691 return mgr.isActiveByBaseId(TARGET_ID);
16692 }
16693 return !!mgr?.isActive?.(TARGET_ID);
16694 }
16695 function startRecycleBinBadge(initialRaw, countUrl = "") {
16696 const initial = Number(initialRaw) || 0;
16697 const cfg = window.desktopModeConfig;
16698 const cfgCount = cfg?.recycleBinCount;
16699 const cfgUrl = cfg?.recycleBinCountUrl;
16700 const cfgDebug = cfg?.desktopModeBinDebug;
16701 log("startRecycleBinBadge entry", {
16702 initial,
16703 countUrl,
16704 alreadyStarted: store$3.state.started,
16705 cfgCount,
16706 cfgUrl,
16707 cfgDebug,
16708 readyState: document.readyState
16709 });
16710 const cfgCountNum = Number(cfgCount);
16711 const cfgCountIsHealthy = (typeof cfgCount === "number" || typeof cfgCount === "string") && Number.isFinite(cfgCountNum);
16712 if (!cfgCountIsHealthy) {
16713 warn(
16714 "desktopModeConfig.recycleBinCount is missing — PHP filter `desktop_mode_shell_config` did not deliver. Check your PHP error log for `[desktop-mode-bin debug]` lines.",
16715 { cfg }
16716 );
16717 }
16718 if (store$3.state.started) {
16719 setRecycleBinBadge(initial);
16720 return;
16721 }
16722 store$3.state.started = true;
16723 store$3.state.countUrl = countUrl;
16724 store$3.state.seenTs = Date.now();
16725 setRecycleBinBadge(initial);
16726 wireDockTileSignal();
16727 wireDesktopIconsSignal();
16728 wireBroadcastDeltas();
16729 wirePostMessageFastPath();
16730 wireHeartbeatProbe();
16731 wireWindowLifecycleSignals();
16732 }
16733 function wireWindowLifecycleSignals() {
16734 const ns = "desktop-mode/recycle-bin/badge-lifecycle";
16735 const repaint = (payload) => {
16736 const detail = payload;
16737 const windowId = detail?.windowId;
16738 if (!windowId) {
16739 return;
16740 }
16741 const isBin = windowId === TARGET_ID || windowId.startsWith(TARGET_ID + "-");
16742 if (!isBin) {
16743 return;
16744 }
16745 paintBadge(store$3.state.current);
16746 };
16747 addAction(HOOKS.WINDOW_OPENED, ns, repaint);
16748 addAction(HOOKS.WINDOW_FOCUSED, ns, repaint);
16749 addAction(HOOKS.WINDOW_BLURRED, ns, repaint);
16750 addAction(HOOKS.WINDOW_MINIMIZED, ns, repaint);
16751 addAction(HOOKS.WINDOW_RESTORED, ns, repaint);
16752 addAction(HOOKS.WINDOW_CLOSED, ns, repaint);
16753 addAction(HOOKS.WINDOW_REOPENED, ns, repaint);
16754 }
16755 function wireDockTileSignal() {
16756 addAction(
16757 HOOKS.DOCK_ITEM_APPENDED,
16758 "desktop-mode/recycle-bin/badge",
16759 (payload) => {
16760 if (payload?.id === TARGET_ID) {
16761 paintBadge(store$3.state.current);
16762 }
16763 }
16764 );
16765 }
16766 function wireDesktopIconsSignal() {
16767 addAction(
16768 HOOKS.DESKTOP_ICONS_RENDERED,
16769 "desktop-mode/recycle-bin/badge",
16770 (payload) => {
16771 if (payload?.ids?.includes(TARGET_ID)) {
16772 paintBadge(store$3.state.current);
16773 }
16774 }
16775 );
16776 }
16777 function wireBroadcastDeltas() {
16778 const onDomain = (payload) => {
16779 const detail = payload;
16780 if (!detail) {
16781 return;
16782 }
16783 const ids = Array.isArray(detail.ids) ? detail.ids.length : 0;
16784 switch (detail.action) {
16785 case "trashed":
16786 adjustRecycleBinBadge(+ids);
16787 break;
16788 case "untrashed":
16789 case "deleted":
16790 adjustRecycleBinBadge(-ids);
16791 break;
16792 }
16793 };
16794 subscribe$2("desktop-mode.post.changed", onDomain);
16795 subscribe$2("desktop-mode.page.changed", onDomain);
16796 subscribe$2("desktop-mode.attachment.changed", onDomain);
16797 subscribe$2("desktop-mode.comment.changed", onDomain);
16798 subscribe$2("desktop-mode.placement.changed", onDomain);
16799 subscribe$2("desktop-mode.shortcut.changed", onDomain);
16800 subscribe$2("desktop-mode.folder.changed", onDomain);
16801 }
16802 function wirePostMessageFastPath() {
16803 const expectedOrigin = window.location.origin;
16804 window.addEventListener("message", (e) => {
16805 if (e.origin !== expectedOrigin) {
16806 return;
16807 }
16808 const data = e.data;
16809 if (!data || data.type !== "desktop-mode-recycle-bin-changed") {
16810 return;
16811 }
16812 const ts = typeof data.ts === "number" ? data.ts : Date.now();
16813 if (ts <= store$3.state.seenTs) {
16814 log("postMessage skipped (ts <= seenTs)", { ts, seenTs: store$3.state.seenTs });
16815 return;
16816 }
16817 log("postMessage triggers refetch", { ts, prevSeenTs: store$3.state.seenTs });
16818 store$3.state.seenTs = ts;
16819 void refetchCount();
16820 });
16821 }
16822 function wireHeartbeatProbe() {
16823 const $ = window.jQuery;
16824 if (!$) {
16825 warn("wireHeartbeatProbe: window.jQuery not available — heartbeat path disabled");
16826 return;
16827 }
16828 log("wireHeartbeatProbe: jQuery + heartbeat hooks attached");
16829 $(document).on("heartbeat-send", (...args) => {
16830 const data = args[1];
16831 if (data) {
16832 data[HEARTBEAT_FIELD$1] = store$3.state.seenTs;
16833 }
16834 });
16835 $(document).on("heartbeat-tick", (...args) => {
16836 const response = args[1];
16837 const block = response?.desktop_mode_recycle_bin;
16838 log("heartbeat-tick", { hasBlock: !!block, block });
16839 if (!block) {
16840 return;
16841 }
16842 if (typeof block.ts === "number" && block.ts > store$3.state.seenTs) {
16843 store$3.state.seenTs = block.ts;
16844 }
16845 if (typeof block.count === "number") {
16846 setRecycleBinBadge(block.count);
16847 }
16848 });
16849 }
16850 async function refetchCount() {
16851 if (!store$3.state.countUrl) {
16852 log("refetchCount: no countUrl, skip");
16853 return;
16854 }
16855 log("refetchCount: hitting", store$3.state.countUrl);
16856 try {
16857 const response = await fetch(store$3.state.countUrl, {
16858 credentials: "same-origin",
16859 headers: { Accept: "application/json" }
16860 });
16861 if (!response.ok) {
16862 warn("refetchCount: non-OK", response.status, response.statusText);
16863 return;
16864 }
16865 const json = await response.json();
16866 log("refetchCount: response", json);
16867 if (typeof json.count === "number") {
16868 setRecycleBinBadge(json.count);
16869 }
16870 } catch (err) {
16871 warn("refetchCount: fetch failed", err);
16872 }
16873 }
16874 const OS_SETTINGS_ID = "desktop-mode-os-settings";
16875 const RECYCLE_BIN_ID = "desktop-mode-recycle-bin";
16876 function registerBuiltInPeekRenderers(opts) {
16877 const wpHooks = getWpHooks();
16878 if (!wpHooks) {
16879 return;
16880 }
16881 wpHooks.addFilter(
16882 "desktop-mode.dock.peek-card-content",
16883 "desktop-mode/built-in-peek-renderers",
16884 (body, ctx) => {
16885 const context = ctx;
16886 const id = context.window.id;
16887 if (id === OS_SETTINGS_ID) {
16888 return renderOsSettings();
16889 }
16890 if (id === RECYCLE_BIN_ID) {
16891 return renderRecycleBin(context, opts.getRecycleBinCount);
16892 }
16893 return body;
16894 }
16895 );
16896 }
16897 function renderOsSettings(_ctx) {
16898 const root = document.createElement("span");
16899 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--os-settings";
16900 root.setAttribute("aria-hidden", "true");
16901 const hero = document.createElement("span");
16902 hero.className = "desktop-mode-dock-peek__os-hero dashicons dashicons-admin-generic";
16903 root.appendChild(hero);
16904 const subtitle = document.createElement("span");
16905 subtitle.className = "desktop-mode-dock-peek__os-subtitle";
16906 subtitle.textContent = __("System Preferences");
16907 root.appendChild(subtitle);
16908 const tabs = document.createElement("span");
16909 tabs.className = "desktop-mode-dock-peek__os-tabs";
16910 for (const cls of [
16911 "dashicons-art",
16912 "dashicons-admin-customizer",
16913 "dashicons-editor-help"
16914 ]) {
16915 const tab = document.createElement("span");
16916 tab.className = `desktop-mode-dock-peek__os-tab dashicons ${cls}`;
16917 tabs.appendChild(tab);
16918 }
16919 root.appendChild(tabs);
16920 return root;
16921 }
16922 function renderRecycleBin(_ctx, getCount) {
16923 const root = document.createElement("span");
16924 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--recycle-bin";
16925 root.setAttribute("aria-hidden", "true");
16926 const count = Math.max(0, Math.floor(getCount() || 0));
16927 root.dataset.empty = count === 0 ? "true" : "false";
16928 const stage = document.createElement("span");
16929 stage.className = "desktop-mode-dock-peek__bin-stage";
16930 const stack = document.createElement("span");
16931 stack.className = "desktop-mode-dock-peek__bin-stack";
16932 for (let i = 0; i < 3; i++) {
16933 const slip = document.createElement("span");
16934 slip.className = "desktop-mode-dock-peek__bin-slip";
16935 stack.appendChild(slip);
16936 }
16937 stage.appendChild(stack);
16938 const icon = document.createElement("span");
16939 icon.className = `desktop-mode-dock-peek__bin-icon dashicons ${count === 0 ? "dashicons-trash" : "dashicons-trash"}`;
16940 stage.appendChild(icon);
16941 root.appendChild(stage);
16942 const label = document.createElement("span");
16943 label.className = "desktop-mode-dock-peek__bin-label";
16944 if (count === 0) {
16945 label.textContent = __("Recycle Bin — empty");
16946 } else if (count === 1) {
16947 label.textContent = __("1 item");
16948 } else if (count > 99) {
16949 label.textContent = "99+ items";
16950 } else {
16951 label.textContent = `${count} items`;
16952 }
16953 root.appendChild(label);
16954 return root;
16955 }
16956 function getWpHooks() {
16957 const wp = window.wp;
16958 return wp?.hooks ?? null;
16959 }
16960 const BUG_REPORT_WINDOW_ID = "desktop-mode-bug-report";
16961 const REPO_OWNER = "WordPress";
16962 const REPO_NAME = "desktop-mode";
16963 const MAX_BODY_LENGTH = 6e3;
16964 function renderBugReport(body) {
16965 body.classList.add("desktop-mode-bug-report");
16966 body.replaceChildren();
16967 const form = document.createElement("form");
16968 form.className = "desktop-mode-bug-report__form";
16969 form.setAttribute("novalidate", "");
16970 const intro = document.createElement("p");
16971 intro.className = "desktop-mode-bug-report__intro";
16972 intro.textContent = __(
16973 "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."
16974 );
16975 form.appendChild(intro);
16976 form.appendChild(buildTypeField());
16977 form.appendChild(buildTextField("title", __("Title"), {
16978 placeholder: __("A short summary"),
16979 required: true
16980 }));
16981 form.appendChild(buildTextareaField("description", __("What happened? What did you expect?"), {
16982 placeholder: __("Describe the issue or the feature you have in mind."),
16983 rows: 5,
16984 required: true
16985 }));
16986 form.appendChild(buildTextareaField("steps", __("Steps to reproduce (bug only)"), {
16987 placeholder: __("One step per line"),
16988 rows: 4
16989 }));
16990 const meta = buildMetadataPreview();
16991 form.appendChild(meta);
16992 const actions = document.createElement("div");
16993 actions.className = "desktop-mode-bug-report__actions";
16994 const submit = document.createElement("button");
16995 submit.type = "submit";
16996 submit.className = "desktop-mode-bug-report__submit";
16997 submit.textContent = __("Open issue on GitHub");
16998 actions.appendChild(submit);
16999 const hint = document.createElement("span");
17000 hint.className = "desktop-mode-bug-report__hint";
17001 hint.textContent = __("You will review and submit on GitHub.");
17002 actions.appendChild(hint);
17003 form.appendChild(actions);
17004 form.addEventListener("submit", (e) => {
17005 e.preventDefault();
17006 const state2 = readFormState(form);
17007 if (!state2.title.trim() || !state2.description.trim()) {
17008 showInlineError(form, __("Title and description are both required."));
17009 return;
17010 }
17011 const url = buildGithubIssueUrl(state2);
17012 window.open(url, "_blank", "noopener");
17013 });
17014 body.appendChild(form);
17015 }
17016 function buildTypeField() {
17017 const wrap = document.createElement("div");
17018 wrap.className = "desktop-mode-bug-report__field desktop-mode-bug-report__field--type";
17019 const label = document.createElement("span");
17020 label.className = "desktop-mode-bug-report__label";
17021 label.textContent = __("Type");
17022 wrap.appendChild(label);
17023 const group = document.createElement("div");
17024 group.className = "desktop-mode-bug-report__radio-group";
17025 group.setAttribute("role", "radiogroup");
17026 const options = [
17027 { value: "bug", label: __("Bug"), checked: true },
17028 { value: "feature", label: __("Feature request") },
17029 { value: "question", label: __("Question") }
17030 ];
17031 for (const opt of options) {
17032 const radioLabel = document.createElement("label");
17033 radioLabel.className = "desktop-mode-bug-report__radio";
17034 const input = document.createElement("input");
17035 input.type = "radio";
17036 input.name = "type";
17037 input.value = opt.value;
17038 if (opt.checked) {
17039 input.checked = true;
17040 }
17041 radioLabel.appendChild(input);
17042 const text = document.createElement("span");
17043 text.textContent = opt.label;
17044 radioLabel.appendChild(text);
17045 group.appendChild(radioLabel);
17046 }
17047 wrap.appendChild(group);
17048 return wrap;
17049 }
17050 function buildTextField(name, labelText, opts = {}) {
17051 const wrap = document.createElement("div");
17052 wrap.className = "desktop-mode-bug-report__field";
17053 const label = document.createElement("label");
17054 label.className = "desktop-mode-bug-report__label";
17055 label.textContent = labelText;
17056 wrap.appendChild(label);
17057 const input = document.createElement("input");
17058 input.type = "text";
17059 input.name = name;
17060 input.className = "desktop-mode-bug-report__input";
17061 if (opts.placeholder) {
17062 input.placeholder = opts.placeholder;
17063 }
17064 if (opts.required) {
17065 input.setAttribute("aria-required", "true");
17066 }
17067 label.appendChild(input);
17068 return wrap;
17069 }
17070 function buildTextareaField(name, labelText, opts = {}) {
17071 const wrap = document.createElement("div");
17072 wrap.className = "desktop-mode-bug-report__field";
17073 const label = document.createElement("label");
17074 label.className = "desktop-mode-bug-report__label";
17075 label.textContent = labelText;
17076 wrap.appendChild(label);
17077 const textarea = document.createElement("textarea");
17078 textarea.name = name;
17079 textarea.className = "desktop-mode-bug-report__textarea";
17080 textarea.rows = opts.rows ?? 4;
17081 if (opts.placeholder) {
17082 textarea.placeholder = opts.placeholder;
17083 }
17084 if (opts.required) {
17085 textarea.setAttribute("aria-required", "true");
17086 }
17087 label.appendChild(textarea);
17088 return wrap;
17089 }
17090 function buildMetadataPreview() {
17091 const details = document.createElement("details");
17092 details.className = "desktop-mode-bug-report__metadata";
17093 const summary = document.createElement("summary");
17094 summary.textContent = __("Environment included with the report");
17095 details.appendChild(summary);
17096 const pre = document.createElement("pre");
17097 pre.className = "desktop-mode-bug-report__metadata-body";
17098 pre.textContent = formatMetadata(collectMetadata());
17099 details.appendChild(pre);
17100 return details;
17101 }
17102 function showInlineError(form, msg) {
17103 let banner = form.querySelector(".desktop-mode-bug-report__error");
17104 if (!banner) {
17105 banner = document.createElement("div");
17106 banner.className = "desktop-mode-bug-report__error";
17107 banner.setAttribute("role", "alert");
17108 form.prepend(banner);
17109 }
17110 banner.textContent = msg;
17111 }
17112 function readFormState(form) {
17113 const data = new FormData(form);
17114 return {
17115 type: data.get("type") ?? "bug",
17116 title: data.get("title") ?? "",
17117 description: data.get("description") ?? "",
17118 steps: data.get("steps") ?? ""
17119 };
17120 }
17121 function buildGithubIssueUrl(state2) {
17122 const labels = labelsForType(state2.type);
17123 const body = composeIssueBody(state2);
17124 const params = new URLSearchParams();
17125 params.set("title", state2.title.trim());
17126 params.set("body", body);
17127 if (labels.length) {
17128 params.set("labels", labels.join(","));
17129 }
17130 return `https://github.com/${REPO_OWNER}/${REPO_NAME}/issues/new?${params.toString()}`;
17131 }
17132 function labelsForType(type) {
17133 switch (type) {
17134 case "bug":
17135 return ["bug"];
17136 case "feature":
17137 return ["enhancement"];
17138 case "question":
17139 return ["question"];
17140 default:
17141 return [];
17142 }
17143 }
17144 function composeIssueBody(state2) {
17145 const parts = [];
17146 parts.push(state2.description.trim());
17147 if (state2.type === "bug" && state2.steps.trim()) {
17148 parts.push("");
17149 parts.push("## Steps to reproduce");
17150 parts.push("");
17151 parts.push(state2.steps.trim());
17152 }
17153 parts.push("");
17154 parts.push("<details><summary>Environment</summary>");
17155 parts.push("");
17156 parts.push("```");
17157 parts.push(formatMetadata(collectMetadata()));
17158 parts.push("```");
17159 parts.push("");
17160 parts.push("</details>");
17161 let out = parts.join("\n");
17162 if (out.length > MAX_BODY_LENGTH) {
17163 out = out.slice(0, MAX_BODY_LENGTH) + "\n\n…(truncated to fit GitHub URL length limit)";
17164 }
17165 return out;
17166 }
17167 function collectMetadata() {
17168 const cfg = window.wp?.desktop?.config;
17169 return {
17170 pluginVersion: cfg?.pluginVersion ?? "unknown",
17171 wordpressVersion: cfg?.wordpressVersion ?? "unknown",
17172 userAgent: navigator.userAgent,
17173 viewport: `${window.innerWidth}x${window.innerHeight}`,
17174 platform: navigator.platform || "unknown",
17175 currentUrl: window.location.href
17176 };
17177 }
17178 function formatMetadata(m) {
17179 return [
17180 `Plugin version: ${m.pluginVersion}`,
17181 `WordPress version: ${m.wordpressVersion}`,
17182 `User agent: ${m.userAgent}`,
17183 `Viewport: ${m.viewport}`,
17184 `Platform: ${m.platform}`,
17185 `Current URL: ${m.currentUrl}`
17186 ].join("\n");
17187 }
17188 let _config = null;
17189 let _state = {
17190 installHintDismissed: false,
17191 notificationsEnabled: false
17192 };
17193 const _listeners = /* @__PURE__ */ new Set();
17194 function initPwaState(config) {
17195 if (!config) {
17196 _config = null;
17197 return;
17198 }
17199 _config = config;
17200 _state = { ...config.state };
17201 notify$4();
17202 }
17203 function getPwaState() {
17204 return { ..._state };
17205 }
17206 function updatePwaState(patch) {
17207 _state = { ..._state, ...patch };
17208 notify$4();
17209 if (!_config) {
17210 return getPwaState();
17211 }
17212 const body = JSON.stringify(patch);
17213 const nonce = readRestNonce$2();
17214 void fetch(_config.stateUrl, {
17215 method: "POST",
17216 credentials: "same-origin",
17217 headers: {
17218 "Content-Type": "application/json",
17219 ...nonce ? { "X-WP-Nonce": nonce } : {}
17220 },
17221 body
17222 }).catch((err) => {
17223 if (typeof console !== "undefined") {
17224 console.warn("[desktop-mode] pwa-state write failed:", err);
17225 }
17226 });
17227 return getPwaState();
17228 }
17229 function subscribePwaState(cb) {
17230 _listeners.add(cb);
17231 return () => {
17232 _listeners.delete(cb);
17233 };
17234 }
17235 function notify$4() {
17236 const snapshot = getPwaState();
17237 for (const cb of Array.from(_listeners)) {
17238 try {
17239 cb(snapshot);
17240 } catch (err) {
17241 if (typeof console !== "undefined") {
17242 console.error(
17243 "[desktop-mode] pwa-state listener threw:",
17244 err
17245 );
17246 }
17247 }
17248 }
17249 }
17250 function readRestNonce$2() {
17251 const cfg = window.desktopModeConfig;
17252 return cfg?.restNonce ?? "";
17253 }
17254 const state = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
17255 __proto__: null,
17256 getPwaState,
17257 initPwaState,
17258 subscribePwaState,
17259 updatePwaState
17260 }, Symbol.toStringTag, { value: "Module" }));
17261 let _registration = null;
17262 let _registrationFailed = false;
17263 let _controllerChangeBound = false;
17264 let _reloadingForSwUpdate = false;
17265 let _status = "pending";
17266 function bindControllerChangeReload() {
17267 if (_controllerChangeBound) {
17268 return;
17269 }
17270 _controllerChangeBound = true;
17271 const hadInitialController = !!navigator.serviceWorker.controller;
17272 navigator.serviceWorker.addEventListener("controllerchange", () => {
17273 if (!hadInitialController) {
17274 return;
17275 }
17276 if (_reloadingForSwUpdate) {
17277 return;
17278 }
17279 if (wasRecentlyReloadedForSwUpdate()) {
17280 return;
17281 }
17282 markReloadedForSwUpdate();
17283 _reloadingForSwUpdate = true;
17284 setTimeout(() => window.location.reload(), 0);
17285 });
17286 }
17287 const SW_RELOAD_THROTTLE_KEY = "wpd-sw-reload-ts";
17288 const SW_RELOAD_THROTTLE_MS = 3e4;
17289 function wasRecentlyReloadedForSwUpdate() {
17290 try {
17291 const raw = sessionStorage.getItem(SW_RELOAD_THROTTLE_KEY);
17292 const last = raw ? Number.parseInt(raw, 10) : 0;
17293 if (!Number.isFinite(last) || last <= 0) {
17294 return false;
17295 }
17296 return Date.now() - last < SW_RELOAD_THROTTLE_MS;
17297 } catch {
17298 return false;
17299 }
17300 }
17301 function markReloadedForSwUpdate() {
17302 try {
17303 sessionStorage.setItem(SW_RELOAD_THROTTLE_KEY, String(Date.now()));
17304 } catch {
17305 }
17306 }
17307 async function registerServiceWorker(config, options = {}) {
17308 if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
17309 _status = "unsupported";
17310 return null;
17311 }
17312 if (!config?.swUrl) {
17313 _status = "unsupported";
17314 return null;
17315 }
17316 if (!window.isSecureContext) {
17317 _status = "unsupported";
17318 return null;
17319 }
17320 if (_registration || _registrationFailed) {
17321 return _registration;
17322 }
17323 if (!options.forceReplace) {
17324 const existing = await navigator.serviceWorker.getRegistrations().catch(() => []);
17325 const foreign = existing.find((reg) => {
17326 const url = reg.active?.scriptURL ?? reg.installing?.scriptURL ?? "";
17327 return url !== "" && url !== config.swUrl;
17328 });
17329 if (foreign) {
17330 _status = "foreign-sw";
17331 if (typeof console !== "undefined") {
17332 console.warn(
17333 "[desktop-mode] another service worker is already registered (" + foreign.scope + "); skipping desktop-mode SW. Set desktop_mode_pwa_force_replace_sw=true to override."
17334 );
17335 }
17336 return null;
17337 }
17338 }
17339 try {
17340 _registration = await navigator.serviceWorker.register(config.swUrl, {
17341 scope: "/",
17342 updateViaCache: "none"
17343 });
17344 _status = "registered";
17345 bindControllerChangeReload();
17346 return _registration;
17347 } catch (err) {
17348 _registrationFailed = true;
17349 _status = "failed";
17350 if (typeof console !== "undefined") {
17351 console.warn("[desktop-mode] SW registration failed:", err);
17352 }
17353 return null;
17354 }
17355 }
17356 function getSwRegistrationStatus() {
17357 return _status;
17358 }
17359 const PWA_INSTALL_TILE_ID = "desktop-mode-pwa-install";
17360 function isStandaloneDisplay() {
17361 if (typeof window === "undefined") {
17362 return false;
17363 }
17364 if (window.matchMedia?.("(display-mode: standalone)").matches) {
17365 return true;
17366 }
17367 const nav = window.navigator;
17368 return nav.standalone === true;
17369 }
17370 async function isLikelyInstalled() {
17371 if (isStandaloneDisplay()) {
17372 return true;
17373 }
17374 const nav = window.navigator;
17375 if (typeof nav.getInstalledRelatedApps !== "function") {
17376 return false;
17377 }
17378 try {
17379 const apps = await nav.getInstalledRelatedApps();
17380 return Array.isArray(apps) && apps.length > 0;
17381 } catch {
17382 return false;
17383 }
17384 }
17385 let _deferred = null;
17386 function installPwaInstallAffordance(siteName, showToast2) {
17387 if (typeof window === "undefined") {
17388 return;
17389 }
17390 window.removeEventListener(
17391 "beforeinstallprompt",
17392 _handleBeforeInstall
17393 );
17394 window.addEventListener(
17395 "beforeinstallprompt",
17396 _handleBeforeInstall
17397 );
17398 window.removeEventListener("appinstalled", _handleAppInstalled);
17399 window.addEventListener("appinstalled", _handleAppInstalled);
17400 function _handleBeforeInstall(ev) {
17401 ev.preventDefault();
17402 _deferred = ev;
17403 }
17404 function _handleAppInstalled() {
17405 _deferred = null;
17406 showToast2({
17407 message: sprintf(
17408 /* translators: %s: site name */
17409 __("Installed %s as an app."),
17410 siteName
17411 )
17412 });
17413 }
17414 }
17415 function getInstallTileDef(siteName, showToast2) {
17416 return {
17417 id: PWA_INSTALL_TILE_ID,
17418 title: sprintf(
17419 /* translators: %s: site name */
17420 __("Install %s as an app"),
17421 siteName
17422 ),
17423 // Dashicons class — the dock renderer prefers Dashicons
17424 // strings. `dashicons-download` is the closest match for
17425 // "install" in the WordPress glyph set without shipping
17426 // bespoke artwork.
17427 icon: "dashicons-download",
17428 onOpen: () => {
17429 void onTileClick(siteName, showToast2);
17430 }
17431 };
17432 }
17433 async function onTileClick(siteName, showToast2) {
17434 if (_deferred) {
17435 const event = _deferred;
17436 _deferred = null;
17437 try {
17438 await event.prompt();
17439 const choice = await event.userChoice;
17440 if (choice.outcome === "dismissed") {
17441 showToast2({
17442 message: __("Install cancelled.")
17443 });
17444 }
17445 } catch (err) {
17446 if (typeof console !== "undefined") {
17447 console.warn(
17448 "[desktop-mode] install prompt failed:",
17449 err
17450 );
17451 }
17452 }
17453 return;
17454 }
17455 if (await isLikelyInstalled()) {
17456 showToast2({
17457 message: sprintf(
17458 /* translators: %s: site name */
17459 __(
17460 "%s is already installed. Open it from your apps menu or home screen."
17461 ),
17462 siteName
17463 )
17464 });
17465 return;
17466 }
17467 if (getSwRegistrationStatus() === "foreign-sw") {
17468 showToast2({
17469 message: __(
17470 "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."
17471 )
17472 });
17473 return;
17474 }
17475 showToast2({
17476 message: __(
17477 "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."
17478 )
17479 });
17480 }
17481 async function promptInstall() {
17482 if (!_deferred) {
17483 return "unavailable";
17484 }
17485 const event = _deferred;
17486 _deferred = null;
17487 try {
17488 await event.prompt();
17489 const choice = await event.userChoice;
17490 return choice.outcome;
17491 } catch {
17492 return "unavailable";
17493 }
17494 }
17495 function undismissInstallHint() {
17496 Promise.resolve().then(() => state).then((m) => {
17497 m.updatePwaState({ installHintDismissed: false });
17498 });
17499 }
17500 function notify$3(options) {
17501 const intent = activity.filter(
17502 "desktop-mode/notification-requested",
17503 { ...options }
17504 );
17505 if (!intent || intent.cancel === true || !intent.title) {
17506 return () => void 0;
17507 }
17508 let dismissed = false;
17509 let dismissNative = null;
17510 let dismissToast = null;
17511 const dismiss = () => {
17512 if (dismissed) {
17513 return;
17514 }
17515 dismissed = true;
17516 if (dismissNative) {
17517 dismissNative();
17518 }
17519 if (dismissToast) {
17520 dismissToast();
17521 }
17522 };
17523 const fallback = () => {
17524 dismissToast = showToast({
17525 message: intent.body ? intent.title + " — " + intent.body : intent.title
17526 });
17527 activity.publish("desktop-mode/notification-shown", {
17528 ...intent,
17529 fallback: "toast"
17530 });
17531 };
17532 if (typeof window === "undefined" || typeof Notification === "undefined") {
17533 fallback();
17534 return dismiss;
17535 }
17536 const perm = Notification.permission;
17537 if (perm === "granted") {
17538 dismissNative = renderNative(intent);
17539 if (!dismissNative) {
17540 fallback();
17541 }
17542 return dismiss;
17543 }
17544 if (perm === "denied") {
17545 fallback();
17546 return dismiss;
17547 }
17548 void Notification.requestPermission().then((result) => {
17549 if (dismissed) {
17550 return;
17551 }
17552 if (result === "granted") {
17553 updatePwaState({ notificationsEnabled: true });
17554 dismissNative = renderNative(intent);
17555 if (!dismissNative) {
17556 fallback();
17557 }
17558 return;
17559 }
17560 fallback();
17561 });
17562 return dismiss;
17563 }
17564 function renderNative(intent) {
17565 let n = null;
17566 try {
17567 n = new Notification(intent.title, {
17568 body: intent.body,
17569 icon: intent.icon,
17570 tag: intent.tag,
17571 requireInteraction: intent.requireInteraction
17572 });
17573 } catch (err) {
17574 if (typeof console !== "undefined") {
17575 console.warn("[desktop-mode] Notification ctor threw:", err);
17576 }
17577 return null;
17578 }
17579 if (intent.onClick) {
17580 const handler = intent.onClick;
17581 n.onclick = () => {
17582 try {
17583 handler(n);
17584 } catch (hErr) {
17585 if (typeof console !== "undefined") {
17586 console.error(
17587 "[desktop-mode] notification onClick threw:",
17588 hErr
17589 );
17590 }
17591 }
17592 };
17593 }
17594 activity.publish("desktop-mode/notification-shown", {
17595 ...intent,
17596 fallback: null
17597 });
17598 return () => {
17599 if (n) {
17600 n.close();
17601 }
17602 };
17603 }
17604 async function requestNotificationPermission() {
17605 if (typeof Notification === "undefined") {
17606 return "unsupported";
17607 }
17608 if (Notification.permission !== "default") {
17609 return Notification.permission;
17610 }
17611 const result = await Notification.requestPermission();
17612 if (result === "granted") {
17613 updatePwaState({ notificationsEnabled: true });
17614 }
17615 return result;
17616 }
17617 function getNotificationPermission() {
17618 if (typeof Notification === "undefined") {
17619 return "unsupported";
17620 }
17621 return Notification.permission;
17622 }
17623 function bootstrapPwa(config, showToast2) {
17624 if (!config.pwa) {
17625 return;
17626 }
17627 initPwaState(config.pwa);
17628 installPwaInstallAffordance(
17629 config.pwa.appName || "WordPress",
17630 showToast2
17631 );
17632 void registerServiceWorker(config.pwa, {
17633 forceReplace: !!config.pwa.forceReplaceSw
17634 });
17635 }
17636 const DRAG_BRIDGE_EVENTS = {
17637 START: "desktop-mode-cross-frame-drag-start",
17638 END: "desktop-mode-cross-frame-drag-end"
17639 };
17640 function isStart(m) {
17641 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-start" && !!m.payload && typeof m.payload === "object";
17642 }
17643 function isEnd(m) {
17644 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-end";
17645 }
17646 function isPayloadRequest(m) {
17647 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-payload-request";
17648 }
17649 function normalizeLegacyPayload(payload) {
17650 const obj = payload;
17651 if (obj.kind !== void 0 && obj.kind !== null) {
17652 return payload;
17653 }
17654 if (typeof obj.id === "number" && typeof obj.url === "string" && typeof obj.mime === "string") {
17655 return {
17656 kind: "attachment",
17657 id: obj.id,
17658 url: obj.url,
17659 title: typeof obj.title === "string" ? obj.title : "",
17660 alt: typeof obj.alt === "string" ? obj.alt : "",
17661 mime: obj.mime,
17662 thumbnailUrl: typeof obj.thumbnailUrl === "string" ? obj.thumbnailUrl : void 0,
17663 sizes: obj.sizes && typeof obj.sizes === "object" ? obj.sizes : void 0
17664 };
17665 }
17666 return payload;
17667 }
17668 class DragBridge {
17669 constructor() {
17670 this._payload = null;
17671 this._onMessage = (e) => {
17672 if (e.origin !== this._origin) {
17673 return;
17674 }
17675 const msg = e.data;
17676 if (isStart(msg)) {
17677 this._startDrag(msg.payload);
17678 return;
17679 }
17680 if (isEnd(msg)) {
17681 this._endDrag();
17682 return;
17683 }
17684 if (isPayloadRequest(msg) && this._payload && e.source) {
17685 try {
17686 e.source.postMessage(
17687 { type: "desktop-mode-drag-payload", payload: this._payload },
17688 this._origin
17689 );
17690 } catch {
17691 }
17692 }
17693 };
17694 this._origin = window.location.origin;
17695 window.addEventListener("message", this._onMessage);
17696 }
17697 getPayload() {
17698 return this._payload;
17699 }
17700 isDragging() {
17701 return this._payload !== null;
17702 }
17703 start(payload) {
17704 if (this._payload === payload) {
17705 return;
17706 }
17707 this._startDrag(payload);
17708 }
17709 end() {
17710 this._endDrag();
17711 }
17712 _startDrag(payload) {
17713 const normalized = normalizeLegacyPayload(payload);
17714 this._payload = normalized;
17715 document.dispatchEvent(
17716 new CustomEvent(DRAG_BRIDGE_EVENTS.START, {
17717 detail: { payload: normalized }
17718 })
17719 );
17720 }
17721 _endDrag() {
17722 if (this._payload === null) {
17723 return;
17724 }
17725 const payload = this._payload;
17726 this._payload = null;
17727 document.dispatchEvent(
17728 new CustomEvent(DRAG_BRIDGE_EVENTS.END, { detail: { payload } })
17729 );
17730 }
17731 }
17732 class DropTargetRegistry {
17733 constructor() {
17734 this._targets = /* @__PURE__ */ new Map();
17735 this._byElement = /* @__PURE__ */ new Map();
17736 }
17737 register(target2) {
17738 const prev = this._targets.get(target2.id);
17739 if (prev) {
17740 this._byElement.delete(prev.element);
17741 }
17742 this._targets.set(target2.id, target2);
17743 this._byElement.set(target2.element, target2);
17744 return () => {
17745 const cur = this._targets.get(target2.id);
17746 if (cur === target2) {
17747 this._targets.delete(target2.id);
17748 this._byElement.delete(target2.element);
17749 }
17750 };
17751 }
17752 list() {
17753 return Array.from(this._targets.values());
17754 }
17755 clear() {
17756 this._targets.clear();
17757 this._byElement.clear();
17758 }
17759 /**
17760 * Find the deepest registered target whose element is `el` or an
17761 * ancestor of `el`. Walks the DOM tree once (O(depth)).
17762 *
17763 * Window claim boundary: if the walk crosses a `.desktop-mode-window`
17764 * element BEFORE finding a registered target, hit-testing stops
17765 * there and returns null. This is the rule that makes "drag over
17766 * a Gutenberg admin window" produce reject feedback instead of
17767 * silently routing the drop to the wallpaper canvas underneath.
17768 *
17769 * A window can opt INTO accepting drops by registering a target
17770 * on its own body (e.g. Recycle Bin's `[data-desktop-mode-recycle-bin-root]`):
17771 * since that element sits inside the window, the walk hits it
17772 * before reaching the window boundary and the body's target wins.
17773 */
17774 hitTest(el) {
17775 let cur = el;
17776 while (cur) {
17777 if (cur instanceof HTMLElement) {
17778 const t = this._byElement.get(cur);
17779 if (t) {
17780 return t;
17781 }
17782 if (cur.classList.contains("desktop-mode-window")) {
17783 return null;
17784 }
17785 }
17786 cur = cur.parentElement;
17787 }
17788 return null;
17789 }
17790 /**
17791 * Convenience: pick the target at viewport `(clientX, clientY)`.
17792 * Caller is responsible for hiding any obscuring ghost element
17793 * before calling — see `GhostHandle.withHidden()`.
17794 */
17795 hitTestPoint(clientX, clientY) {
17796 const el = document.elementFromPoint(clientX, clientY);
17797 const target2 = this.hitTest(el);
17798 return { target: target2, element: el, accepted: false };
17799 }
17800 }
17801 const GHOST_CLASS = "desktop-mode-drag-ghost";
17802 const GHOST_ACCEPT_CLASS = "desktop-mode-drag-ghost--accept";
17803 const GHOST_REJECT_CLASS = "desktop-mode-drag-ghost--reject";
17804 const HINT_CLASS = "desktop-mode-drag-hint";
17805 const HINT_ACCEPT_CLASS = "desktop-mode-drag-hint--accept";
17806 const HINT_REJECT_CLASS = "desktop-mode-drag-hint--reject";
17807 const HINT_NEUTRAL_CLASS = "desktop-mode-drag-hint--neutral";
17808 const HINT_OFFSET_X = 16;
17809 const HINT_OFFSET_Y = 18;
17810 function mountGhost(payload, clientX, clientY) {
17811 const ghost = buildGhost(payload);
17812 const offsetX = payload.ghost?.offsetX ?? defaultOffsetX(payload.source);
17813 const offsetY = payload.ghost?.offsetY ?? defaultOffsetY(payload.source);
17814 ghost.classList.add(GHOST_CLASS);
17815 ghost.setAttribute("aria-hidden", "true");
17816 ghost.style.position = "fixed";
17817 ghost.style.left = "0";
17818 ghost.style.top = "0";
17819 ghost.style.margin = "0";
17820 ghost.style.pointerEvents = "none";
17821 ghost.style.zIndex = "2147483647";
17822 ghost.style.willChange = "transform";
17823 document.body.appendChild(ghost);
17824 const labels = resolveHintLabels(payload);
17825 const hint = labels ? buildHintChip() : null;
17826 if (hint) {
17827 document.body.appendChild(hint);
17828 }
17829 const handle = {
17830 get element() {
17831 return ghost;
17832 },
17833 moveTo(cx, cy) {
17834 ghost.style.transform = `translate3d(${cx - offsetX}px, ${cy - offsetY}px, 0)`;
17835 if (hint) {
17836 hint.style.transform = `translate3d(${cx + HINT_OFFSET_X}px, ${cy + HINT_OFFSET_Y}px, 0)`;
17837 }
17838 },
17839 setMode(mode, overrides) {
17840 ghost.classList.remove(GHOST_ACCEPT_CLASS, GHOST_REJECT_CLASS);
17841 if (mode === "accept") {
17842 ghost.classList.add(GHOST_ACCEPT_CLASS);
17843 } else if (mode === "reject") {
17844 ghost.classList.add(GHOST_REJECT_CLASS);
17845 }
17846 if (hint && labels) {
17847 hint.classList.remove(
17848 HINT_ACCEPT_CLASS,
17849 HINT_REJECT_CLASS,
17850 HINT_NEUTRAL_CLASS
17851 );
17852 if (mode === "accept") {
17853 hint.classList.add(HINT_ACCEPT_CLASS);
17854 hint.textContent = overrides?.acceptLabel ?? labels.accept;
17855 } else if (mode === "reject") {
17856 hint.classList.add(HINT_REJECT_CLASS);
17857 hint.textContent = labels.reject;
17858 } else {
17859 hint.classList.add(HINT_NEUTRAL_CLASS);
17860 hint.textContent = labels.neutral;
17861 }
17862 hint.hidden = !hint.textContent;
17863 }
17864 },
17865 withHidden(fn) {
17866 const prevG = ghost.style.visibility;
17867 const prevH = hint?.style.visibility ?? "";
17868 ghost.style.visibility = "hidden";
17869 if (hint) {
17870 hint.style.visibility = "hidden";
17871 }
17872 try {
17873 return fn();
17874 } finally {
17875 ghost.style.visibility = prevG;
17876 if (hint) {
17877 hint.style.visibility = prevH;
17878 }
17879 }
17880 },
17881 dispose() {
17882 if (ghost.isConnected) {
17883 ghost.remove();
17884 }
17885 if (hint?.isConnected) {
17886 hint.remove();
17887 }
17888 }
17889 };
17890 handle.moveTo(clientX, clientY);
17891 handle.setMode("neutral");
17892 return handle;
17893 }
17894 function buildHintChip() {
17895 const chip = document.createElement("div");
17896 chip.className = HINT_CLASS;
17897 chip.setAttribute("aria-hidden", "true");
17898 chip.setAttribute("role", "presentation");
17899 chip.style.position = "fixed";
17900 chip.style.left = "0";
17901 chip.style.top = "0";
17902 chip.style.margin = "0";
17903 chip.style.pointerEvents = "none";
17904 chip.style.zIndex = "2147483647";
17905 chip.style.willChange = "transform";
17906 return chip;
17907 }
17908 function resolveHintLabels(payload) {
17909 const cfg = payload.ghost?.hint;
17910 if (cfg?.hidden) {
17911 return null;
17912 }
17913 return {
17914 accept: cfg?.accept ?? defaultAcceptLabel(payload),
17915 reject: cfg?.reject ?? defaultRejectLabel(),
17916 neutral: cfg?.neutral ?? defaultNeutralLabel(payload)
17917 };
17918 }
17919 function defaultAcceptLabel(payload) {
17920 if (payload.type === "shortcut") {
17921 return __("Drop here to create shortcut", "desktop-mode");
17922 }
17923 if (payload.type === "desktop-file") {
17924 return __("Drop here to move", "desktop-mode");
17925 }
17926 return __("Drop here", "desktop-mode");
17927 }
17928 function defaultRejectLabel(_payload) {
17929 return __("Can’t drop here", "desktop-mode");
17930 }
17931 function defaultNeutralLabel(payload) {
17932 if (payload.type === "shortcut") {
17933 return __(
17934 "Drop on the desktop or a folder",
17935 "desktop-mode"
17936 );
17937 }
17938 if (payload.type === "desktop-file") {
17939 return __("Drop in a folder", "desktop-mode");
17940 }
17941 return "";
17942 }
17943 function buildGhost(payload) {
17944 if (payload.ghost?.element) {
17945 return payload.ghost.element;
17946 }
17947 const clone = payload.source.cloneNode(true);
17948 clone.removeAttribute("id");
17949 const rect = payload.source.getBoundingClientRect();
17950 clone.style.width = `${rect.width}px`;
17951 clone.style.height = `${rect.height}px`;
17952 return clone;
17953 }
17954 function defaultOffsetX(source) {
17955 return source.offsetWidth / 2;
17956 }
17957 function defaultOffsetY(source) {
17958 return source.offsetHeight / 2;
17959 }
17960 let _installed$3 = false;
17961 function installRecovery(cancelActive) {
17962 if (_installed$3) {
17963 return;
17964 }
17965 _installed$3 = true;
17966 document.addEventListener("keydown", (e) => {
17967 if (e.key === "Escape") {
17968 cancelActive("escape");
17969 }
17970 });
17971 window.addEventListener("blur", () => {
17972 cancelActive("blur");
17973 });
17974 document.addEventListener("visibilitychange", () => {
17975 if (document.hidden) {
17976 cancelActive("visibility");
17977 }
17978 });
17979 }
17980 const DRAG_THRESHOLD_PX = 4;
17981 const DRAG_EVENTS = {
17982 START: "desktop-mode.drag.start",
17983 MOVE: "desktop-mode.drag.move",
17984 ENTER: "desktop-mode.drag.enter",
17985 LEAVE: "desktop-mode.drag.leave",
17986 REJECTED: "desktop-mode.drag.rejected",
17987 COMMIT: "desktop-mode.drag.commit",
17988 CANCEL: "desktop-mode.drag.cancel",
17989 END: "desktop-mode.drag.end"
17990 };
17991 const SOURCE_DRAGGING_CLASS = "desktop-mode-file-tile--dragging";
17992 const TARGET_DROP_ACTIVE_CLASS = "desktop-mode-file-tile--drop-target";
17993 const TRASH_DROP_ACTIVE_ATTR$1 = "data-desktop-mode-trash-drop-active";
17994 const FILES_DROP_ACTIVE_ATTR = "data-files-drop-active";
17995 const BODY_DRAGGING_ATTR = "data-desktop-mode-dragging";
17996 const BODY_DRAG_TYPE_ATTR = "data-desktop-mode-drag-type";
17997 const BODY_DRAG_MODE_ATTR = "data-desktop-mode-drag-mode";
17998 class DragManager {
17999 constructor() {
18000 this._registry = new DropTargetRegistry();
18001 this._active = null;
18002 this._docListenersAttached = false;
18003 this._lastLiftedEndAt = 0;
18004 this._onPointerMove = (e) => {
18005 const session = this._active;
18006 if (!session || session._pointerId !== e.pointerId) {
18007 return;
18008 }
18009 const dx = e.clientX - session._origin.clientX;
18010 const dy = e.clientY - session._origin.clientY;
18011 if (!session._lifted) {
18012 if (Math.abs(dx) < DRAG_THRESHOLD_PX && Math.abs(dy) < DRAG_THRESHOLD_PX) {
18013 return;
18014 }
18015 this._lift(session, e);
18016 }
18017 if (!session._ghost) {
18018 return;
18019 }
18020 session._ghost.moveTo(e.clientX, e.clientY);
18021 this._updateHover(session, e.clientX, e.clientY);
18022 dispatchOnDocument(DRAG_EVENTS.MOVE, {
18023 payload: session.payload,
18024 clientX: e.clientX,
18025 clientY: e.clientY
18026 });
18027 };
18028 this._onPointerUp = (e) => {
18029 const session = this._active;
18030 if (!session || session._pointerId !== e.pointerId) {
18031 return;
18032 }
18033 if (!session._lifted) {
18034 session._finished = true;
18035 this._active = null;
18036 try {
18037 session._callbacks.onClickOnly?.();
18038 } catch (err) {
18039 console.error("[desktop-mode] drag onClickOnly threw:", err);
18040 }
18041 return;
18042 }
18043 const hit = this._hitTestNow(session, e.clientX, e.clientY);
18044 if (hit && hit.accepted && hit.target) {
18045 this._commit(session, hit.target, e.clientX, e.clientY);
18046 return;
18047 }
18048 this._cancel(session, hit && hit.target ? "rejected" : "no-target");
18049 };
18050 this._onPointerCancel = (e) => {
18051 const session = this._active;
18052 if (!session || session._pointerId !== e.pointerId) {
18053 return;
18054 }
18055 this._cancel(session, "pointercancel");
18056 };
18057 }
18058 start(opts) {
18059 if (this._active) {
18060 return null;
18061 }
18062 if (opts.origin.button !== 0) {
18063 return null;
18064 }
18065 const session = {
18066 payload: opts.payload,
18067 isFinished: () => session._finished,
18068 cancel: (reason) => this._cancel(session, reason ?? "caller"),
18069 _origin: opts.origin,
18070 _pointerId: opts.origin.pointerId,
18071 _lifted: false,
18072 _finished: false,
18073 _callbacks: {
18074 onClickOnly: opts.onClickOnly,
18075 onCancel: opts.onCancel,
18076 onCommit: opts.onCommit
18077 },
18078 _ghost: null,
18079 _currentTarget: null,
18080 _currentAccepted: false
18081 };
18082 this._active = session;
18083 this._ensureDocListeners();
18084 installRecovery((reason) => {
18085 if (this._active) {
18086 this._cancel(this._active, reason);
18087 }
18088 });
18089 return session;
18090 }
18091 registerDropTarget(target2) {
18092 return this._registry.register(target2);
18093 }
18094 isDragging() {
18095 return this._active !== null && this._active._lifted;
18096 }
18097 /**
18098 * Whether a real (lifted) drag ended within `withinMs` of now.
18099 * Surfaces that bind plain `click` listeners use this to ignore
18100 * the synthesized click that fires after a drop. 500 ms is a
18101 * generous default — browsers fire the click within 10–50 ms of
18102 * pointerup, but plugins may chain post-drag work into a
18103 * `requestAnimationFrame` and call back into a click-driven API.
18104 *
18105 * @public
18106 * @since 0.8.5
18107 */
18108 recentlyEndedDrag(withinMs = 500) {
18109 if (this._lastLiftedEndAt === 0) {
18110 return false;
18111 }
18112 return Date.now() - this._lastLiftedEndAt < withinMs;
18113 }
18114 getActive() {
18115 return this._active;
18116 }
18117 debug() {
18118 return {
18119 findOrphans: () => findOrphans(),
18120 listTargets: () => this._registry.list()
18121 };
18122 }
18123 // -----------------------------------------------------------------
18124 // Internals
18125 // -----------------------------------------------------------------
18126 _ensureDocListeners() {
18127 if (this._docListenersAttached) {
18128 return;
18129 }
18130 this._docListenersAttached = true;
18131 document.addEventListener("pointermove", this._onPointerMove, true);
18132 document.addEventListener("pointerup", this._onPointerUp, true);
18133 document.addEventListener("pointercancel", this._onPointerCancel, true);
18134 }
18135 _lift(session, e) {
18136 session._lifted = true;
18137 session.payload.source.classList.add(SOURCE_DRAGGING_CLASS);
18138 session._ghost = mountGhost(session.payload, e.clientX, e.clientY);
18139 if (typeof document !== "undefined" && document.body) {
18140 document.body.setAttribute(BODY_DRAGGING_ATTR, "");
18141 document.body.setAttribute(
18142 BODY_DRAG_TYPE_ATTR,
18143 String(session.payload.type)
18144 );
18145 document.body.setAttribute(BODY_DRAG_MODE_ATTR, "neutral");
18146 }
18147 dispatchOnDocument(DRAG_EVENTS.START, { payload: session.payload });
18148 }
18149 _hitTestNow(session, clientX, clientY) {
18150 const run = () => {
18151 const el = document.elementFromPoint(clientX, clientY);
18152 const target2 = this._registry.hitTest(el);
18153 if (!target2) {
18154 return { target: null, accepted: false };
18155 }
18156 let accepted = false;
18157 try {
18158 accepted = target2.accept(session.payload);
18159 } catch (err) {
18160 console.error("[desktop-mode] drop target accept() threw:", target2.id, err);
18161 }
18162 return { target: target2, accepted };
18163 };
18164 if (session._ghost) {
18165 return session._ghost.withHidden(run);
18166 }
18167 return run();
18168 }
18169 _updateHover(session, clientX, clientY) {
18170 const next = this._hitTestNow(session, clientX, clientY);
18171 const prevTarget = session._currentTarget;
18172 if (next.target === prevTarget && next.accepted === session._currentAccepted) {
18173 return;
18174 }
18175 if (prevTarget) {
18176 fireLeave(prevTarget, session);
18177 }
18178 session._currentTarget = next.target;
18179 session._currentAccepted = next.accepted;
18180 let mode;
18181 if (next.target) {
18182 if (next.accepted) {
18183 fireEnter(next.target, session);
18184 session._ghost?.setMode("accept", {
18185 acceptLabel: next.target.acceptLabel
18186 });
18187 mode = "accept";
18188 } else {
18189 session._ghost?.setMode("reject");
18190 dispatchOnDocument(DRAG_EVENTS.REJECTED, {
18191 payload: session.payload,
18192 targetId: next.target.id
18193 });
18194 mode = "reject";
18195 }
18196 } else {
18197 session._ghost?.setMode("reject");
18198 mode = "reject";
18199 }
18200 if (typeof document !== "undefined" && document.body) {
18201 document.body.setAttribute(BODY_DRAG_MODE_ATTR, mode);
18202 }
18203 }
18204 _commit(session, target2, clientX, clientY) {
18205 session._finished = true;
18206 this._lastLiftedEndAt = Date.now();
18207 fireLeave(target2, session);
18208 this._cleanupDom(session);
18209 const prevActive = this._active;
18210 this._active = null;
18211 try {
18212 void target2.onDrop(session, { clientX, clientY });
18213 } catch (err) {
18214 console.error("[desktop-mode] drop target onDrop threw:", target2.id, err);
18215 }
18216 try {
18217 session._callbacks.onCommit?.(target2);
18218 } catch (err) {
18219 console.error("[desktop-mode] drag onCommit threw:", err);
18220 }
18221 dispatchOnDocument(DRAG_EVENTS.COMMIT, {
18222 payload: session.payload,
18223 targetId: target2.id
18224 });
18225 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason: "commit" });
18226 if (this._active === prevActive) {
18227 this._active = null;
18228 }
18229 }
18230 _cancel(session, reason) {
18231 if (session._finished) {
18232 return;
18233 }
18234 session._finished = true;
18235 if (session._lifted) {
18236 this._lastLiftedEndAt = Date.now();
18237 }
18238 if (session._currentTarget) {
18239 fireLeave(session._currentTarget, session);
18240 }
18241 this._cleanupDom(session);
18242 this._active = null;
18243 try {
18244 session._callbacks.onCancel?.(reason);
18245 } catch (err) {
18246 console.error("[desktop-mode] drag onCancel threw:", err);
18247 }
18248 dispatchOnDocument(DRAG_EVENTS.CANCEL, { payload: session.payload, reason });
18249 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason });
18250 }
18251 _cleanupDom(session) {
18252 try {
18253 session.payload.source.classList.remove(SOURCE_DRAGGING_CLASS);
18254 } catch {
18255 }
18256 session._ghost?.dispose();
18257 session._ghost = null;
18258 session._currentTarget = null;
18259 session._currentAccepted = false;
18260 if (typeof document !== "undefined" && document.body) {
18261 document.body.removeAttribute(BODY_DRAGGING_ATTR);
18262 document.body.removeAttribute(BODY_DRAG_TYPE_ATTR);
18263 document.body.removeAttribute(BODY_DRAG_MODE_ATTR);
18264 }
18265 scrubOrphans();
18266 }
18267 }
18268 function dispatchOnDocument(type, detail) {
18269 if (typeof document === "undefined") {
18270 return;
18271 }
18272 document.dispatchEvent(new CustomEvent(type, { detail }));
18273 }
18274 function fireEnter(target2, session) {
18275 try {
18276 target2.onEnter?.(session);
18277 } catch (err) {
18278 console.error("[desktop-mode] drop target onEnter threw:", target2.id, err);
18279 }
18280 dispatchOnDocument(DRAG_EVENTS.ENTER, {
18281 payload: session.payload,
18282 targetId: target2.id
18283 });
18284 }
18285 function fireLeave(target2, session) {
18286 try {
18287 target2.onLeave?.(session);
18288 } catch (err) {
18289 console.error("[desktop-mode] drop target onLeave threw:", target2.id, err);
18290 }
18291 dispatchOnDocument(DRAG_EVENTS.LEAVE, {
18292 payload: session.payload,
18293 targetId: target2.id
18294 });
18295 }
18296 function findOrphans() {
18297 if (typeof document === "undefined") {
18298 return [];
18299 }
18300 const out = [];
18301 for (const sel of [
18302 `.${SOURCE_DRAGGING_CLASS}`,
18303 `.${TARGET_DROP_ACTIVE_CLASS}`,
18304 `[${TRASH_DROP_ACTIVE_ATTR$1}]`,
18305 `[${FILES_DROP_ACTIVE_ATTR}]`
18306 ]) {
18307 document.querySelectorAll(sel).forEach((el) => out.push(el));
18308 }
18309 return out;
18310 }
18311 function scrubOrphans() {
18312 for (const el of findOrphans()) {
18313 el.classList.remove(SOURCE_DRAGGING_CLASS, TARGET_DROP_ACTIVE_CLASS);
18314 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR$1);
18315 el.removeAttribute(FILES_DROP_ACTIVE_ATTR);
18316 }
18317 }
18318 const WINDOW_ROOT_SELECTOR = ".desktop-mode-window";
18319 const WINDOW_ID_PREFIX = "wp-window-";
18320 function findWindowRootAtPoint(clientX, clientY) {
18321 const el = document.elementFromPoint(clientX, clientY);
18322 if (!el) {
18323 return null;
18324 }
18325 const root = el.closest(WINDOW_ROOT_SELECTOR);
18326 return root instanceof HTMLElement ? root : null;
18327 }
18328 function windowIdFromRoot(root) {
18329 if (!root.id.startsWith(WINDOW_ID_PREFIX)) {
18330 return null;
18331 }
18332 const id = root.id.slice(WINDOW_ID_PREFIX.length);
18333 return id.length > 0 ? id : null;
18334 }
18335 const TARGET_ID_PREFIX = "desktop-mode-iframe-drop-";
18336 const IFRAME_SELECTOR = "iframe.desktop-mode-window__iframe";
18337 const DROP_ACTIVE_ATTR = "data-desktop-mode-iframe-drop-active";
18338 let _installed$2 = false;
18339 let _dragManager = null;
18340 const _suppressedIframes = /* @__PURE__ */ new Map();
18341 const _activeRegistrations = /* @__PURE__ */ new Map();
18342 let _bridgeInterceptPayload = null;
18343 let _lastHoveredBridgeIframe = null;
18344 function suppressIframePointerEventsBridge() {
18345 const iframes = document.querySelectorAll(
18346 IFRAME_SELECTOR
18347 );
18348 iframes.forEach((iframe) => {
18349 if (_suppressedIframes.has(iframe)) {
18350 return;
18351 }
18352 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
18353 iframe.style.pointerEvents = "none";
18354 });
18355 }
18356 function restoreIframePointerEvents() {
18357 _suppressedIframes.forEach((prev, iframe) => {
18358 iframe.style.pointerEvents = prev;
18359 });
18360 _suppressedIframes.clear();
18361 }
18362 function findIframeAtCursor(clientX, clientY) {
18363 const win = findWindowRootAtPoint(clientX, clientY);
18364 if (!win) {
18365 return null;
18366 }
18367 const iframe = win.querySelector(IFRAME_SELECTOR);
18368 return iframe instanceof HTMLIFrameElement ? iframe : null;
18369 }
18370 const onBridgeDragOver = (e) => {
18371 if (!_bridgeInterceptPayload) {
18372 return;
18373 }
18374 e.preventDefault();
18375 if (e.dataTransfer) {
18376 e.dataTransfer.dropEffect = "copy";
18377 }
18378 const iframe = findIframeAtCursor(e.clientX, e.clientY);
18379 if (iframe === _lastHoveredBridgeIframe) {
18380 return;
18381 }
18382 if (_lastHoveredBridgeIframe) {
18383 postIntoIframe(_lastHoveredBridgeIframe, {
18384 type: "desktop-mode-drag-leave"
18385 });
18386 }
18387 _lastHoveredBridgeIframe = iframe;
18388 if (iframe) {
18389 postIntoIframe(iframe, {
18390 type: "desktop-mode-drag-over",
18391 payload: _bridgeInterceptPayload
18392 });
18393 }
18394 };
18395 const onBridgeDrop = (e) => {
18396 if (!_bridgeInterceptPayload) {
18397 return;
18398 }
18399 e.preventDefault();
18400 e.stopPropagation();
18401 if (typeof e.stopImmediatePropagation === "function") {
18402 e.stopImmediatePropagation();
18403 }
18404 const iframe = findIframeAtCursor(e.clientX, e.clientY);
18405 const payload = _bridgeInterceptPayload;
18406 stopBridgeIntercept();
18407 if (!iframe) {
18408 return;
18409 }
18410 const rect = iframe.getBoundingClientRect();
18411 postIntoIframe(iframe, {
18412 type: "desktop-mode-drop",
18413 payload,
18414 position: {
18415 x: e.clientX - rect.left,
18416 y: e.clientY - rect.top
18417 }
18418 });
18419 };
18420 const onBridgeDragEnd = () => {
18421 stopBridgeIntercept();
18422 };
18423 function startBridgeIntercept(payload) {
18424 if (_bridgeInterceptPayload) {
18425 _bridgeInterceptPayload = payload;
18426 return;
18427 }
18428 _bridgeInterceptPayload = payload;
18429 suppressIframePointerEventsBridge();
18430 document.addEventListener("dragover", onBridgeDragOver, true);
18431 document.addEventListener("drop", onBridgeDrop, true);
18432 document.addEventListener("dragend", onBridgeDragEnd, true);
18433 }
18434 function stopBridgeIntercept() {
18435 if (!_bridgeInterceptPayload) {
18436 return;
18437 }
18438 _bridgeInterceptPayload = null;
18439 if (_lastHoveredBridgeIframe) {
18440 postIntoIframe(_lastHoveredBridgeIframe, {
18441 type: "desktop-mode-drag-leave"
18442 });
18443 _lastHoveredBridgeIframe = null;
18444 }
18445 document.removeEventListener("dragover", onBridgeDragOver, true);
18446 document.removeEventListener("drop", onBridgeDrop, true);
18447 document.removeEventListener("dragend", onBridgeDragEnd, true);
18448 restoreIframePointerEvents();
18449 }
18450 function extractBridgePayload(payload) {
18451 if (!payload || typeof payload !== "object") {
18452 return void 0;
18453 }
18454 const obj = payload;
18455 if (obj.type !== "shortcut" && obj.type !== "desktop-file") {
18456 return void 0;
18457 }
18458 const data = obj.data;
18459 return data?.bridgePayload;
18460 }
18461 function postIntoIframe(iframe, msg) {
18462 const w = iframe.contentWindow;
18463 if (!w) {
18464 return;
18465 }
18466 try {
18467 w.postMessage(msg, window.location.origin);
18468 } catch {
18469 }
18470 }
18471 function registerDropTargetFor(dragManager, iframe, target2, windowId) {
18472 return dragManager.registerDropTarget({
18473 id: `${TARGET_ID_PREFIX}${windowId}`,
18474 element: target2,
18475 accept: (payload) => !!extractBridgePayload(payload),
18476 onEnter: (session) => {
18477 const bridge = extractBridgePayload(session.payload);
18478 if (!bridge) {
18479 return;
18480 }
18481 target2.setAttribute(DROP_ACTIVE_ATTR, "");
18482 postIntoIframe(iframe, {
18483 type: "desktop-mode-drag-over",
18484 payload: bridge
18485 });
18486 },
18487 onLeave: () => {
18488 target2.removeAttribute(DROP_ACTIVE_ATTR);
18489 postIntoIframe(iframe, { type: "desktop-mode-drag-leave" });
18490 },
18491 onDrop: (session, ev) => {
18492 target2.removeAttribute(DROP_ACTIVE_ATTR);
18493 const bridge = extractBridgePayload(session.payload);
18494 if (!bridge) {
18495 return;
18496 }
18497 const rect = iframe.getBoundingClientRect();
18498 postIntoIframe(iframe, {
18499 type: "desktop-mode-drop",
18500 payload: bridge,
18501 position: {
18502 x: ev.clientX - rect.left,
18503 y: ev.clientY - rect.top
18504 }
18505 });
18506 }
18507 });
18508 }
18509 function deriveWindowIdFromIframe(iframe) {
18510 let cur = iframe.parentElement;
18511 while (cur) {
18512 if (cur.id.startsWith("wp-window-")) {
18513 return cur.id.slice("wp-window-".length);
18514 }
18515 cur = cur.parentElement;
18516 }
18517 return `unknown-${Math.random().toString(36).slice(2, 10)}`;
18518 }
18519 function onDragStart(payload) {
18520 const dragManager = _dragManager;
18521 if (!dragManager) {
18522 return;
18523 }
18524 const iframes = document.querySelectorAll(IFRAME_SELECTOR);
18525 const isBridgeable = !!extractBridgePayload(payload);
18526 console.info(
18527 "[desktop-mode] drag-start: suppressing %d iframe(s); bridgeable=%s",
18528 iframes.length,
18529 isBridgeable,
18530 payload
18531 );
18532 iframes.forEach((iframe) => {
18533 if (!_suppressedIframes.has(iframe)) {
18534 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
18535 iframe.style.pointerEvents = "none";
18536 }
18537 if (!isBridgeable) {
18538 return;
18539 }
18540 if (_activeRegistrations.has(iframe)) {
18541 return;
18542 }
18543 const dropTargetEl = iframe.parentElement;
18544 if (!dropTargetEl) {
18545 return;
18546 }
18547 const windowId = deriveWindowIdFromIframe(iframe);
18548 const deregister = registerDropTargetFor(
18549 dragManager,
18550 iframe,
18551 dropTargetEl,
18552 windowId
18553 );
18554 _activeRegistrations.set(iframe, deregister);
18555 });
18556 }
18557 function onDragEnd$1() {
18558 _suppressedIframes.forEach((prev, iframe) => {
18559 iframe.style.pointerEvents = prev;
18560 });
18561 _suppressedIframes.clear();
18562 _activeRegistrations.forEach((deregister) => {
18563 try {
18564 deregister();
18565 } catch {
18566 }
18567 });
18568 _activeRegistrations.clear();
18569 }
18570 function installIframeDropTargets(dragManager) {
18571 if (_installed$2) {
18572 return;
18573 }
18574 _installed$2 = true;
18575 _dragManager = dragManager;
18576 document.addEventListener(DRAG_EVENTS.START, (e) => {
18577 const detail = e.detail;
18578 onDragStart(detail?.payload);
18579 });
18580 document.addEventListener(DRAG_EVENTS.END, () => {
18581 onDragEnd$1();
18582 });
18583 document.addEventListener(DRAG_BRIDGE_EVENTS.START, (e) => {
18584 const detail = e.detail;
18585 if (!detail?.payload) {
18586 return;
18587 }
18588 startBridgeIntercept(detail.payload);
18589 });
18590 document.addEventListener(DRAG_BRIDGE_EVENTS.END, () => {
18591 stopBridgeIntercept();
18592 });
18593 addAction(
18594 HOOKS.WINDOW_CLOSED,
18595 "desktop-mode/drag/iframe-drop-targets-window-close",
18596 () => {
18597 for (const [iframe] of Array.from(_suppressedIframes)) {
18598 if (!iframe.isConnected) {
18599 _suppressedIframes.delete(iframe);
18600 }
18601 }
18602 for (const [iframe, deregister] of Array.from(_activeRegistrations)) {
18603 if (!iframe.isConnected) {
18604 try {
18605 deregister();
18606 } catch {
18607 }
18608 _activeRegistrations.delete(iframe);
18609 }
18610 }
18611 }
18612 );
18613 window.__desktopModeIframeDropDebug = () => ({
18614 installed: _installed$2,
18615 iframesInDom: document.querySelectorAll(IFRAME_SELECTOR).length,
18616 suppressedCount: _suppressedIframes.size,
18617 registeredCount: _activeRegistrations.size,
18618 suppressedIframeIds: Array.from(_suppressedIframes.keys()).map(
18619 deriveWindowIdFromIframe
18620 )
18621 });
18622 }
18623 const FOCUS_ON_DRAG_HOVER_DWELL_MS = 250;
18624 const FOCUS_ON_DRAG_HOVER_WATCHDOG_MS = 1e3;
18625 const DRAG_HOVER_MESSAGE_TYPE = "desktop-mode-drag-hover";
18626 let _installed$1 = false;
18627 let _host = null;
18628 let _lastHoverWindowId = null;
18629 let _dwellTimer = null;
18630 let _watchdogTimer = null;
18631 let _bridgePayloadKind = null;
18632 function clearDwell() {
18633 if (_dwellTimer !== null) {
18634 clearTimeout(_dwellTimer);
18635 _dwellTimer = null;
18636 }
18637 }
18638 function clearWatchdog() {
18639 if (_watchdogTimer !== null) {
18640 clearTimeout(_watchdogTimer);
18641 _watchdogTimer = null;
18642 }
18643 }
18644 function resetHoverState() {
18645 clearDwell();
18646 clearWatchdog();
18647 _lastHoverWindowId = null;
18648 }
18649 function bumpWatchdog() {
18650 clearWatchdog();
18651 _watchdogTimer = setTimeout(() => {
18652 _watchdogTimer = null;
18653 resetHoverState();
18654 }, FOCUS_ON_DRAG_HOVER_WATCHDOG_MS);
18655 }
18656 function fireFocus(windowId, payloadType) {
18657 const win = _host?.getById(windowId);
18658 if (!win || win.isFocused()) {
18659 return;
18660 }
18661 const shouldFocus = applyFilters(
18662 HOOKS.WINDOW_FOCUS_ON_DRAG_HOVER,
18663 true,
18664 { windowId, payloadType }
18665 );
18666 if (!shouldFocus) {
18667 return;
18668 }
18669 try {
18670 _host?.focus(win);
18671 } catch (err) {
18672 console.error("[desktop-mode] focus-on-drag-hover focus() threw:", windowId, err);
18673 }
18674 }
18675 function trackHoverWindowId(windowId, payloadType) {
18676 if (windowId === _lastHoverWindowId) {
18677 return;
18678 }
18679 clearDwell();
18680 _lastHoverWindowId = windowId;
18681 if (windowId === null) {
18682 return;
18683 }
18684 _dwellTimer = setTimeout(() => {
18685 _dwellTimer = null;
18686 fireFocus(windowId, payloadType);
18687 }, FOCUS_ON_DRAG_HOVER_DWELL_MS);
18688 }
18689 function trackHoverAtPoint(clientX, clientY, payloadType) {
18690 const root = findWindowRootAtPoint(clientX, clientY);
18691 trackHoverWindowId(root ? windowIdFromRoot(root) : null, payloadType);
18692 }
18693 const onDragMove = (e) => {
18694 const detail = e.detail;
18695 if (typeof detail?.clientX !== "number" || typeof detail?.clientY !== "number") {
18696 return;
18697 }
18698 trackHoverAtPoint(detail.clientX, detail.clientY, detail.payload?.type ?? "");
18699 };
18700 const onDragEnd = () => {
18701 resetHoverState();
18702 };
18703 function dragHasFiles$1(e) {
18704 const types = e.dataTransfer?.types;
18705 if (!types) {
18706 return false;
18707 }
18708 const list2 = types;
18709 if (typeof list2.includes === "function") {
18710 return list2.includes("Files");
18711 }
18712 return typeof list2.contains === "function" && list2.contains("Files");
18713 }
18714 const onNativeDragOver = (e) => {
18715 bumpWatchdog();
18716 const payloadType = _bridgePayloadKind ?? (dragHasFiles$1(e) ? "os-file" : "external");
18717 trackHoverAtPoint(e.clientX, e.clientY, payloadType);
18718 };
18719 const onNativeDragSettled = () => {
18720 resetHoverState();
18721 };
18722 const onNativeDragLeave = (e) => {
18723 if (e.relatedTarget === null) {
18724 resetHoverState();
18725 }
18726 };
18727 function windowIdFromMessageSource(source) {
18728 if (!source) {
18729 return null;
18730 }
18731 const iframes = document.querySelectorAll("iframe");
18732 for (const f of Array.from(iframes)) {
18733 if (f.contentWindow === source) {
18734 const host = f.closest("[data-window-id]");
18735 return host?.getAttribute("data-window-id") || null;
18736 }
18737 }
18738 return null;
18739 }
18740 const onHoverMessage = (e) => {
18741 if (e.origin !== window.location.origin) {
18742 return;
18743 }
18744 const data = e.data;
18745 if (!data || data.type !== DRAG_HOVER_MESSAGE_TYPE) {
18746 return;
18747 }
18748 const windowId = windowIdFromMessageSource(e.source);
18749 if (!windowId) {
18750 return;
18751 }
18752 bumpWatchdog();
18753 trackHoverWindowId(
18754 windowId,
18755 typeof data.payloadType === "string" ? data.payloadType : "external"
18756 );
18757 };
18758 const onBridgeStart = (e) => {
18759 const detail = e.detail;
18760 if (detail?.payload) {
18761 _bridgePayloadKind = detail.payload.kind ?? "";
18762 }
18763 };
18764 const onBridgeEnd = () => {
18765 _bridgePayloadKind = null;
18766 resetHoverState();
18767 };
18768 function installFocusWindowOnDragHover(host) {
18769 if (_installed$1) {
18770 return;
18771 }
18772 _installed$1 = true;
18773 _host = host;
18774 document.addEventListener(DRAG_EVENTS.MOVE, onDragMove);
18775 document.addEventListener(DRAG_EVENTS.END, onDragEnd);
18776 document.addEventListener(DRAG_BRIDGE_EVENTS.START, onBridgeStart);
18777 document.addEventListener(DRAG_BRIDGE_EVENTS.END, onBridgeEnd);
18778 document.addEventListener("dragover", onNativeDragOver, true);
18779 document.addEventListener("drop", onNativeDragSettled, true);
18780 document.addEventListener("dragend", onNativeDragSettled, true);
18781 document.addEventListener("dragleave", onNativeDragLeave, true);
18782 window.addEventListener("message", onHoverMessage);
18783 }
18784 function collectOpenables() {
18785 const desktop = window.wp?.desktop;
18786 if (!desktop) {
18787 return [];
18788 }
18789 const wm = desktop.windowManager;
18790 const config = desktop.config;
18791 if (!wm || !config) {
18792 return [];
18793 }
18794 const items = [];
18795 const fromMenu = (item, group) => ({
18796 id: item.id,
18797 label: item.title,
18798 description: group,
18799 icon: item.icon,
18800 open: () => wm.open({
18801 id: item.id,
18802 baseId: item.id,
18803 url: item.url,
18804 title: item.title,
18805 icon: item.icon
18806 })
18807 });
18808 for (const item of config.dockItems ?? []) {
18809 items.push(fromMenu(item, "Admin menu"));
18810 }
18811 const filtered = applyFilters(
18812 "desktop-mode.open-command.items",
18813 items
18814 );
18815 return Array.isArray(filtered) ? filtered : items;
18816 }
18817 const openCommand = {
18818 slug: "open",
18819 label: "Open",
18820 description: "Open an admin page or registered window.",
18821 hint: "[window]",
18822 icon: "dashicons-external",
18823 /**
18824 * Suggest matching windows as the user types args. Simple
18825 * case-insensitive substring match against label AND id so
18826 * "add" finds "Add New Post" and "jorvy" finds Jorvy whether
18827 * the plugin listed it with a friendly label or the slug.
18828 */
18829 suggest(args) {
18830 const q = args.trim().toLowerCase();
18831 const list2 = collectOpenables();
18832 const hits = q === "" ? list2 : list2.filter(
18833 (w) => w.label.toLowerCase().includes(q) || w.id.toLowerCase().includes(q)
18834 );
18835 return hits.slice(0, 12).map((w) => ({
18836 value: w.label,
18837 label: w.label,
18838 description: w.description,
18839 icon: w.icon ?? "dashicons-external"
18840 }));
18841 },
18842 run(args, ctx) {
18843 const q = args.trim();
18844 if (!q) {
18845 return "Type the name of a window to open, for example `/open Posts`.";
18846 }
18847 const list2 = collectOpenables();
18848 const ql = q.toLowerCase();
18849 const match = list2.find((w) => w.label.toLowerCase() === ql || w.id.toLowerCase() === ql) ?? list2.find(
18850 (w) => w.label.toLowerCase().includes(ql) || w.id.toLowerCase().includes(ql)
18851 );
18852 if (!match) {
18853 return `No window matching **${q}** — try \`/open\` alone to see available options.`;
18854 }
18855 match.open();
18856 ctx.close();
18857 }
18858 };
18859 function registerBuiltInCommands() {
18860 registerCommand(openCommand);
18861 }
18862 const palettes = [];
18863 const listeners$2 = /* @__PURE__ */ new Set();
18864 function registerPalette(p) {
18865 if (!p || typeof p.id !== "string" || p.id === "") {
18866 return () => {
18867 };
18868 }
18869 if (typeof p.open !== "function" || typeof p.close !== "function" || typeof p.isOpen !== "function") {
18870 return () => {
18871 };
18872 }
18873 const idx = palettes.findIndex((x) => x.id === p.id);
18874 if (idx >= 0) {
18875 palettes[idx] = p;
18876 } else {
18877 palettes.push(p);
18878 }
18879 notify$2();
18880 return () => {
18881 const i = palettes.findIndex((x) => x.id === p.id);
18882 if (i >= 0) {
18883 palettes.splice(i, 1);
18884 notify$2();
18885 }
18886 };
18887 }
18888 function unregisterPalette(id) {
18889 const idx = palettes.findIndex((x) => x.id === id);
18890 if (idx >= 0) {
18891 palettes.splice(idx, 1);
18892 notify$2();
18893 }
18894 }
18895 function listPalettes() {
18896 return palettes.slice();
18897 }
18898 function notify$2() {
18899 for (const cb of Array.from(listeners$2)) {
18900 try {
18901 cb();
18902 } catch (err) {
18903 if (typeof console !== "undefined") {
18904 console.error("[desktop-mode] palette-registry listener threw:", err);
18905 }
18906 }
18907 }
18908 }
18909 function cyclePalettes() {
18910 if (palettes.length === 0) {
18911 return;
18912 }
18913 const cur = palettes.findIndex((p) => {
18914 try {
18915 return p.isOpen();
18916 } catch {
18917 return false;
18918 }
18919 });
18920 if (cur === -1) {
18921 try {
18922 palettes[0].open();
18923 } catch {
18924 }
18925 return;
18926 }
18927 try {
18928 palettes[cur].close();
18929 } catch {
18930 }
18931 const next = cur + 1;
18932 if (next < palettes.length) {
18933 try {
18934 palettes[next].open();
18935 } catch {
18936 }
18937 }
18938 }
18939 function openPaletteOnly(id) {
18940 const target2 = palettes.find((p) => p.id === id);
18941 if (!target2) {
18942 return;
18943 }
18944 for (const p of palettes) {
18945 if (p.id !== id) {
18946 try {
18947 if (p.isOpen()) {
18948 p.close();
18949 }
18950 } catch {
18951 }
18952 }
18953 }
18954 try {
18955 target2.open();
18956 } catch {
18957 }
18958 }
18959 let installed$1 = false;
18960 function installPaletteShortcut() {
18961 if (installed$1) {
18962 return;
18963 }
18964 installed$1 = true;
18965 document.addEventListener(
18966 "keydown",
18967 (e) => {
18968 if (!(e.metaKey || e.ctrlKey) || e.key !== "k") {
18969 return;
18970 }
18971 if (e.shiftKey || e.altKey) {
18972 return;
18973 }
18974 e.preventDefault();
18975 e.stopImmediatePropagation();
18976 cyclePalettes();
18977 },
18978 true
18979 );
18980 const origin = window.location.origin;
18981 window.addEventListener("message", (e) => {
18982 if (e.origin !== origin) {
18983 return;
18984 }
18985 const data = e.data;
18986 if (data && data.type === "desktop-mode-palette-cycle") {
18987 cyclePalettes();
18988 }
18989 });
18990 }
18991 const suppliers = /* @__PURE__ */ new Map();
18992 const subscribers = /* @__PURE__ */ new Map();
18993 let booted$2 = false;
18994 const heartbeat = {
18995 contribute(field, supplier) {
18996 suppliers.set(field, supplier);
18997 return () => {
18998 if (suppliers.get(field) === supplier) {
18999 suppliers.delete(field);
19000 }
19001 };
19002 },
19003 subscribe(field, cb) {
19004 let set = subscribers.get(field);
19005 if (!set) {
19006 set = /* @__PURE__ */ new Set();
19007 subscribers.set(field, set);
19008 }
19009 set.add(cb);
19010 return () => {
19011 set.delete(cb);
19012 };
19013 }
19014 };
19015 function bootHeartbeatBus() {
19016 if (booted$2) {
19017 return;
19018 }
19019 booted$2 = true;
19020 const $ = window.jQuery;
19021 if (!$) {
19022 console.warn(
19023 "[desktop-mode/heartbeat] jQuery missing — Heartbeat bus disabled."
19024 );
19025 return;
19026 }
19027 $(document).on("heartbeat-send", (...args) => {
19028 const data = args[1];
19029 if (!data) {
19030 return;
19031 }
19032 for (const [field, supplier] of suppliers) {
19033 try {
19034 data[field] = supplier();
19035 } catch (err) {
19036 console.error(
19037 `[desktop-mode/heartbeat] supplier for "${field}" threw:`,
19038 err
19039 );
19040 }
19041 }
19042 });
19043 $(document).on("heartbeat-tick", (...args) => {
19044 const response = args[1];
19045 if (!response) {
19046 return;
19047 }
19048 for (const [field, set] of subscribers) {
19049 const value = response[field];
19050 if (value === void 0) {
19051 continue;
19052 }
19053 for (const cb of set) {
19054 try {
19055 cb(value);
19056 } catch (err) {
19057 console.error(
19058 `[desktop-mode/heartbeat] subscriber for "${field}" threw:`,
19059 err
19060 );
19061 }
19062 }
19063 }
19064 });
19065 }
19066 const store$2 = createSharedStore(
19067 "desktop-mode/presence",
19068 () => ({ byUser: /* @__PURE__ */ new Map(), serverTimeMs: 0 })
19069 );
19070 const ACTIVE_THRESHOLD_MS = 5 * 60 * 1e3;
19071 let lastInputMs = Date.now();
19072 let booted$1 = false;
19073 function noteUserActivity() {
19074 lastInputMs = Date.now();
19075 }
19076 function applySnapshot(block) {
19077 if (!block || !block.snapshot) {
19078 return;
19079 }
19080 const previous = store$2.state.byUser;
19081 const next = new Map(previous);
19082 const transitions = [];
19083 for (const [rawId, raw] of Object.entries(block.snapshot)) {
19084 const userId = Number(rawId);
19085 if (!Number.isFinite(userId) || userId <= 0) {
19086 continue;
19087 }
19088 const status = raw?.status ?? "offline";
19089 const entry = {
19090 status,
19091 lastSeenMs: Number(raw?.lastSeenMs ?? 0) || 0,
19092 lastActiveMs: Number(raw?.lastActiveMs ?? 0) || 0
19093 };
19094 const old = previous.get(userId);
19095 next.set(userId, entry);
19096 if (!old || old.status !== entry.status) {
19097 transitions.push({
19098 userId,
19099 oldStatus: old ? old.status : null,
19100 newStatus: entry.status,
19101 entry
19102 });
19103 }
19104 }
19105 store$2.state.byUser = next;
19106 if (typeof block.serverTimeMs === "number") {
19107 store$2.state.serverTimeMs = block.serverTimeMs;
19108 }
19109 store$2.notify();
19110 for (const t of transitions) {
19111 const detail = {
19112 userId: t.userId,
19113 oldStatus: t.oldStatus,
19114 newStatus: t.newStatus,
19115 lastSeenMs: t.entry.lastSeenMs,
19116 lastActiveMs: t.entry.lastActiveMs
19117 };
19118 document.dispatchEvent(
19119 new CustomEvent("desktop-mode-presence-changed", { detail })
19120 );
19121 activity.publish("desktop-mode/presence-changed", detail);
19122 }
19123 activity.publish("desktop-mode/presence-snapshot-applied", {
19124 applied: Object.keys(block.snapshot).length,
19125 transitions: transitions.length
19126 });
19127 }
19128 function bootPresenceProbe() {
19129 if (booted$1) {
19130 return;
19131 }
19132 booted$1 = true;
19133 document.addEventListener("pointerdown", noteUserActivity, {
19134 capture: true,
19135 passive: true
19136 });
19137 document.addEventListener("keydown", noteUserActivity, {
19138 capture: true,
19139 passive: true
19140 });
19141 document.addEventListener("visibilitychange", () => {
19142 if (!document.hidden) {
19143 noteUserActivity();
19144 }
19145 });
19146 heartbeat.contribute("desktop_mode_presence_active", () => true);
19147 heartbeat.contribute(
19148 "desktop_mode_user_active",
19149 () => Date.now() - lastInputMs < ACTIVE_THRESHOLD_MS
19150 );
19151 heartbeat.subscribe("desktop_mode_presence", (block) => {
19152 applySnapshot(block);
19153 });
19154 }
19155 function getStatus(userId) {
19156 const entry = store$2.state.byUser.get(userId);
19157 return entry ? entry.status : "offline";
19158 }
19159 function getAll() {
19160 return new Map(store$2.state.byUser);
19161 }
19162 function getEntry(userId) {
19163 return store$2.state.byUser.get(userId) ?? null;
19164 }
19165 function subscribe$1(cb) {
19166 return store$2.subscribe((s) => cb(s));
19167 }
19168 function markActive() {
19169 noteUserActivity();
19170 }
19171 function applyPresenceBatch(updates) {
19172 if (!Array.isArray(updates) || updates.length === 0) {
19173 return;
19174 }
19175 const previous = store$2.state.byUser;
19176 const next = new Map(previous);
19177 const transitions = [];
19178 for (const u of updates) {
19179 const userId = Number(u.userId);
19180 if (!Number.isFinite(userId) || userId <= 0) {
19181 continue;
19182 }
19183 const old = previous.get(userId);
19184 const entry = {
19185 status: u.status,
19186 lastSeenMs: typeof u.lastSeenMs === "number" ? u.lastSeenMs : old?.lastSeenMs ?? 0,
19187 lastActiveMs: typeof u.lastActiveMs === "number" ? u.lastActiveMs : old?.lastActiveMs ?? 0
19188 };
19189 next.set(userId, entry);
19190 if (!old || old.status !== entry.status) {
19191 transitions.push({
19192 userId,
19193 oldStatus: old ? old.status : null,
19194 newStatus: entry.status,
19195 entry
19196 });
19197 }
19198 }
19199 if (transitions.length === 0 && next.size === previous.size) {
19200 return;
19201 }
19202 store$2.state.byUser = next;
19203 store$2.notify();
19204 for (const t of transitions) {
19205 const detail = {
19206 userId: t.userId,
19207 oldStatus: t.oldStatus,
19208 newStatus: t.newStatus,
19209 lastSeenMs: t.entry.lastSeenMs,
19210 lastActiveMs: t.entry.lastActiveMs
19211 };
19212 document.dispatchEvent(
19213 new CustomEvent("desktop-mode-presence-changed", { detail })
19214 );
19215 activity.publish("desktop-mode/presence-changed", detail);
19216 }
19217 activity.publish("desktop-mode/presence-snapshot-applied", {
19218 applied: updates.length,
19219 transitions: transitions.length
19220 });
19221 }
19222 const presenceApi = Object.freeze({
19223 getStatus,
19224 getAll,
19225 getEntry,
19226 subscribe: subscribe$1,
19227 markActive,
19228 applyBatch: applyPresenceBatch
19229 });
19230 const HEARTBEAT_FIELD = "desktop_mode_nonces";
19231 const targets = /* @__PURE__ */ new Map();
19232 let booted = false;
19233 function registerNonceTarget(action, updater) {
19234 if (typeof action !== "string" || action === "") {
19235 return () => {
19236 };
19237 }
19238 let set = targets.get(action);
19239 if (!set) {
19240 set = /* @__PURE__ */ new Set();
19241 targets.set(action, set);
19242 }
19243 set.add(updater);
19244 return () => {
19245 set.delete(updater);
19246 };
19247 }
19248 function bootNonceRefresh() {
19249 if (booted) {
19250 return;
19251 }
19252 booted = true;
19253 heartbeat.subscribe(HEARTBEAT_FIELD, (payload) => {
19254 if (!payload || typeof payload !== "object") {
19255 return;
19256 }
19257 for (const [action, value] of Object.entries(payload)) {
19258 if (typeof value !== "string" || value === "") {
19259 continue;
19260 }
19261 const set = targets.get(action);
19262 if (!set) {
19263 continue;
19264 }
19265 for (const updater of set) {
19266 try {
19267 updater(value);
19268 } catch (err) {
19269 console.error(
19270 `[desktop-mode/nonce-refresh] updater for "${action}" threw:`,
19271 err
19272 );
19273 }
19274 }
19275 }
19276 });
19277 registerShellAndPluginsWindowTargets();
19278 }
19279 function registerShellAndPluginsWindowTargets() {
19280 registerNonceTarget("wp_rest", updateAllRestNonces);
19281 registerNonceTarget("desktop-mode-plugins", (fresh) => {
19282 writeWindowConfigField("desktop-mode-plugins", "ajaxNonce", fresh);
19283 });
19284 registerNonceTarget("updates", (fresh) => {
19285 writeWindowConfigField("desktop-mode-plugins", "updatesNonce", fresh);
19286 });
19287 }
19288 function updateAllRestNonces(fresh) {
19289 const cfg = readShellConfig();
19290 if (cfg && typeof cfg.restNonce === "string") {
19291 cfg.restNonce = fresh;
19292 }
19293 const windowConfigs = readWindowConfigs();
19294 if (!windowConfigs) {
19295 return;
19296 }
19297 for (const blob of Object.values(windowConfigs)) {
19298 if (blob && typeof blob === "object" && typeof blob.restNonce === "string") {
19299 blob.restNonce = fresh;
19300 }
19301 }
19302 }
19303 function writeWindowConfigField(windowId, field, value) {
19304 const blobs = readWindowConfigs();
19305 const blob = blobs?.[windowId];
19306 if (blob && typeof blob === "object") {
19307 blob[field] = value;
19308 }
19309 }
19310 function readShellConfig() {
19311 if (typeof window === "undefined") {
19312 return void 0;
19313 }
19314 return window.desktopModeConfig;
19315 }
19316 function readWindowConfigs() {
19317 if (typeof window === "undefined") {
19318 return void 0;
19319 }
19320 return window.desktopModeWindowConfig;
19321 }
19322 const VIEWPORT_CLAMP_MARGIN = 12;
19323 function findDockEntryForUrl(url, config) {
19324 const windowId = deriveWindowId(url, config.adminUrl);
19325 return (config.dockItems || []).find(
19326 (i) => deriveWindowId(i.url, config.adminUrl) === windowId || (i.submenu || []).some(
19327 (s) => deriveWindowId(s.url, config.adminUrl) === windowId
19328 )
19329 );
19330 }
19331 function clampGeometryToViewport(win, rect) {
19332 const maxW = Math.max(200, rect.width - VIEWPORT_CLAMP_MARGIN * 2);
19333 const maxH = Math.max(200, rect.height - VIEWPORT_CLAMP_MARGIN * 2);
19334 const width = Math.min(win.width, maxW);
19335 const height = Math.min(win.height, maxH);
19336 const maxX = Math.max(0, rect.width - width - VIEWPORT_CLAMP_MARGIN);
19337 const maxY = Math.max(0, rect.height - height - VIEWPORT_CLAMP_MARGIN);
19338 const x = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.x, maxX));
19339 const y = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.y, maxY));
19340 return { x, y, width, height };
19341 }
19342 const INITIAL_ORIGIN$1 = window.location.origin;
19343 function bindTopWindowLinkInterceptor(manager, config) {
19344 document.addEventListener(
19345 "click",
19346 (e) => {
19347 if (e.defaultPrevented) {
19348 return;
19349 }
19350 if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
19351 return;
19352 }
19353 const target2 = e.target;
19354 const link = target2 && target2.closest ? target2.closest("a[href]") : null;
19355 if (!link) {
19356 return;
19357 }
19358 const anchor = link;
19359 const linkTarget = anchor.getAttribute("target");
19360 if (linkTarget && linkTarget !== "" && linkTarget !== "_self") {
19361 return;
19362 }
19363 if (anchor.hasAttribute("download")) {
19364 return;
19365 }
19366 const rawHref = anchor.getAttribute("href");
19367 if (!rawHref || rawHref.charAt(0) === "#") {
19368 return;
19369 }
19370 if (/^(mailto:|tel:|javascript:|data:)/i.test(rawHref)) {
19371 return;
19372 }
19373 let url;
19374 try {
19375 url = new URL(rawHref, window.location.href);
19376 } catch (err) {
19377 if (typeof console !== "undefined") {
19378 console.warn(
19379 "[desktop-mode] Couldn’t parse href; letting the browser handle the click:",
19380 rawHref,
19381 err
19382 );
19383 }
19384 return;
19385 }
19386 if (url.origin !== INITIAL_ORIGIN$1) {
19387 return;
19388 }
19389 let adminPath;
19390 try {
19391 adminPath = new URL(config.adminUrl).pathname;
19392 } catch (err) {
19393 if (typeof console !== "undefined") {
19394 console.error(
19395 "[desktop-mode] config.adminUrl is not a valid URL; falling back to /wp-admin/:",
19396 config.adminUrl,
19397 err
19398 );
19399 }
19400 adminPath = "/wp-admin/";
19401 }
19402 if (!url.pathname.startsWith(adminPath)) {
19403 return;
19404 }
19405 if (/\/(admin-post|admin-ajax)\.php$/.test(url.pathname)) {
19406 return;
19407 }
19408 if (url.searchParams.has("action") && url.searchParams.get("action") === "logout") {
19409 return;
19410 }
19411 if (url.searchParams.has("desktop_mode_classic")) {
19412 return;
19413 }
19414 e.preventDefault();
19415 e.stopPropagation();
19416 if (tryNativeUrlRemap(url.href)) {
19417 return;
19418 }
19419 const windowId = deriveWindowId(url.href, config.adminUrl);
19420 const dockEntry = findDockEntryForUrl(url.href, config);
19421 const fallbackTitle = (anchor.textContent || "").trim() || dockEntry?.title || "";
19422 const isAdminBarNew = !!anchor.closest("#wp-admin-bar-new-content");
19423 const openOpts = {
19424 id: windowId,
19425 baseId: windowId,
19426 multi: !!dockEntry?.multi || isAdminBarNew,
19427 url: url.href,
19428 parentUrl: dockEntry?.url ?? url.href,
19429 title: dockEntry?.title || fallbackTitle,
19430 icon: dockEntry?.icon || "dashicons-admin-generic",
19431 submenu: dockEntry?.submenu
19432 };
19433 if (isAdminBarNew) {
19434 void manager.openNew(openOpts);
19435 return;
19436 }
19437 void manager.open(openOpts);
19438 },
19439 true
19440 );
19441 }
19442 const REGISTRY_CHANGED_EVENT = "desktop-mode-registry-changed";
19443 function diffIds(prev, next) {
19444 const prevIds = /* @__PURE__ */ new Set();
19445 if (Array.isArray(prev)) {
19446 for (const item of prev) {
19447 if (item && typeof item.id === "string") {
19448 prevIds.add(item.id);
19449 }
19450 }
19451 }
19452 const nextIds = /* @__PURE__ */ new Set();
19453 for (const item of next) {
19454 if (item && typeof item.id === "string") {
19455 nextIds.add(item.id);
19456 }
19457 }
19458 const added = [];
19459 for (const id of nextIds) {
19460 if (!prevIds.has(id)) {
19461 added.push(id);
19462 }
19463 }
19464 const removed = [];
19465 for (const id of prevIds) {
19466 if (!nextIds.has(id)) {
19467 removed.push(id);
19468 }
19469 }
19470 return { added, removed };
19471 }
19472 function emitRegistryChanged(registry2, prev, next) {
19473 const { added, removed } = diffIds(prev, next);
19474 if (added.length === 0 && removed.length === 0) {
19475 return;
19476 }
19477 if (typeof document === "undefined") {
19478 return;
19479 }
19480 const detail = { registry: registry2, added, removed };
19481 document.dispatchEvent(
19482 new CustomEvent(REGISTRY_CHANGED_EVENT, { detail })
19483 );
19484 }
19485 function createApplyPayload(deps2) {
19486 const {
19487 applyDockItems,
19488 config,
19489 syncNativeWindows,
19490 syncServerWidgets,
19491 syncServerWallpapers,
19492 syncServerCommands,
19493 syncServerSettingsTabs,
19494 syncServerTitleBarButtons,
19495 syncServerUnfocusEffects,
19496 syncServerWindowLinkRenderers,
19497 syncServerDockRailRenderers,
19498 renderIcons,
19499 syncShortcuts
19500 } = deps2;
19501 return function applyPayload(payload) {
19502 const dockItems = payload.dockItems;
19503 const nativeWindows = payload.nativeWindows;
19504 const serverWidgets = payload.serverWidgets;
19505 const serverWallpapers = payload.serverWallpapers;
19506 const serverCommandScripts = payload.serverCommandScripts;
19507 const serverCommands = payload.serverCommands;
19508 const serverSettingsTabScripts = payload.serverSettingsTabScripts;
19509 const serverSettingsTabs = payload.serverSettingsTabs;
19510 const serverDockRailRendererScripts = payload.serverDockRailRendererScripts;
19511 const serverTitleBarButtonScripts = payload.serverTitleBarButtonScripts;
19512 const serverUnfocusEffectScripts = payload.serverUnfocusEffectScripts;
19513 const serverWindowLinkRendererScripts = payload.serverWindowLinkRendererScripts;
19514 const serverWindowNotices = payload.serverWindowNotices;
19515 const desktopIcons = payload.desktopIcons;
19516 if (!Array.isArray(dockItems) || dockItems.length === 0) {
19517 return;
19518 }
19519 const prevDockItems = config.dockItems;
19520 applyDockItems(dockItems);
19521 config.dockItems = dockItems;
19522 emitRegistryChanged(
19523 "dock-items",
19524 prevDockItems,
19525 dockItems
19526 );
19527 syncShortcuts?.();
19528 if (Array.isArray(nativeWindows)) {
19529 const prevNativeWindows = config.nativeWindows;
19530 void syncNativeWindows(
19531 nativeWindows
19532 );
19533 config.nativeWindows = nativeWindows;
19534 emitRegistryChanged(
19535 "native-windows",
19536 prevNativeWindows,
19537 nativeWindows
19538 );
19539 }
19540 if (Array.isArray(serverWidgets)) {
19541 void syncServerWidgets(
19542 serverWidgets
19543 );
19544 config.serverWidgets = serverWidgets;
19545 }
19546 if (Array.isArray(serverWallpapers)) {
19547 void syncServerWallpapers(
19548 serverWallpapers
19549 );
19550 config.serverWallpapers = serverWallpapers;
19551 }
19552 if (Array.isArray(serverCommandScripts)) {
19553 void syncServerCommands(
19554 serverCommandScripts,
19555 Array.isArray(serverCommands) ? serverCommands : void 0
19556 );
19557 config.serverCommandScripts = serverCommandScripts;
19558 if (Array.isArray(serverCommands)) {
19559 config.serverCommands = serverCommands;
19560 }
19561 }
19562 if (Array.isArray(serverSettingsTabScripts)) {
19563 void syncServerSettingsTabs(
19564 serverSettingsTabScripts,
19565 Array.isArray(serverSettingsTabs) ? serverSettingsTabs : void 0
19566 );
19567 config.serverSettingsTabScripts = serverSettingsTabScripts;
19568 if (Array.isArray(serverSettingsTabs)) {
19569 config.serverSettingsTabs = serverSettingsTabs;
19570 }
19571 }
19572 if (Array.isArray(serverTitleBarButtonScripts)) {
19573 void syncServerTitleBarButtons(
19574 serverTitleBarButtonScripts
19575 );
19576 config.serverTitleBarButtonScripts = serverTitleBarButtonScripts;
19577 }
19578 if (Array.isArray(serverUnfocusEffectScripts)) {
19579 void syncServerUnfocusEffects(
19580 serverUnfocusEffectScripts
19581 );
19582 config.serverUnfocusEffectScripts = serverUnfocusEffectScripts;
19583 }
19584 if (Array.isArray(serverWindowLinkRendererScripts)) {
19585 void syncServerWindowLinkRenderers(
19586 serverWindowLinkRendererScripts
19587 );
19588 config.serverWindowLinkRendererScripts = serverWindowLinkRendererScripts;
19589 }
19590 if (Array.isArray(serverDockRailRendererScripts)) {
19591 void syncServerDockRailRenderers(
19592 serverDockRailRendererScripts
19593 );
19594 config.serverDockRailRendererScripts = serverDockRailRendererScripts;
19595 }
19596 if (Array.isArray(serverWindowNotices)) {
19597 applyServerWindowNotices(
19598 serverWindowNotices
19599 );
19600 config.serverWindowNotices = serverWindowNotices;
19601 }
19602 if (Array.isArray(desktopIcons)) {
19603 const prevDesktopIcons = config.desktopIcons;
19604 renderIcons(desktopIcons);
19605 config.desktopIcons = desktopIcons;
19606 emitRegistryChanged(
19607 "desktop-icons",
19608 prevDesktopIcons,
19609 desktopIcons
19610 );
19611 }
19612 };
19613 }
19614 const MENU_REFRESH_TIMEOUT_MS = 8e3;
19615 function bindMenuRefresh(deps2) {
19616 const {
19617 layoutDispatcher,
19618 desktopArea,
19619 config,
19620 syncNativeWindows,
19621 syncServerWidgets,
19622 syncServerWallpapers,
19623 syncServerCommands,
19624 syncServerSettingsTabs,
19625 syncServerTitleBarButtons,
19626 syncServerUnfocusEffects,
19627 syncServerWindowLinkRenderers,
19628 syncServerDockRailRenderers,
19629 renderIcons,
19630 syncShortcuts
19631 } = deps2;
19632 const applyPayload = createApplyPayload({
19633 applyDockItems: (items) => layoutDispatcher?.applyDockItems(items),
19634 config,
19635 syncNativeWindows,
19636 syncServerWidgets,
19637 syncServerWallpapers,
19638 syncServerCommands,
19639 syncServerSettingsTabs,
19640 syncServerTitleBarButtons,
19641 syncServerUnfocusEffects,
19642 syncServerWindowLinkRenderers,
19643 syncServerDockRailRenderers,
19644 renderIcons,
19645 syncShortcuts
19646 });
19647 let lastMenuSig = typeof config.menuSig === "string" ? config.menuSig : "";
19648 let sigRefreshInFlight = false;
19649 const refresh = () => {
19650 if (!config.adminUrl) {
19651 return Promise.resolve();
19652 }
19653 const probeUrl = (() => {
19654 try {
19655 const url = new URL("admin.php", config.adminUrl);
19656 url.searchParams.set("desktop_mode_chromeless", "1");
19657 url.searchParams.set("desktop_mode_menu_refresh", "1");
19658 return url.toString();
19659 } catch (_err) {
19660 return null;
19661 }
19662 })();
19663 if (!probeUrl) {
19664 return Promise.resolve();
19665 }
19666 return new Promise((resolve2) => {
19667 const iframe = document.createElement("iframe");
19668 iframe.setAttribute("aria-hidden", "true");
19669 iframe.tabIndex = -1;
19670 iframe.style.cssText = "position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;border:0;opacity:0;pointer-events:none;";
19671 iframe.src = probeUrl;
19672 let done = false;
19673 const cleanup = () => {
19674 if (done) {
19675 return;
19676 }
19677 done = true;
19678 window.clearTimeout(timeoutId);
19679 window.removeEventListener("message", onMessage);
19680 if (iframe.parentNode) {
19681 iframe.parentNode.removeChild(iframe);
19682 }
19683 resolve2();
19684 };
19685 const onMessage = (e) => {
19686 if (e.source !== iframe.contentWindow) {
19687 return;
19688 }
19689 const data = e.data;
19690 if (!data || data.type !== "desktop-mode-plugins-changed") {
19691 return;
19692 }
19693 cleanup();
19694 };
19695 const timeoutId = window.setTimeout(() => {
19696 doAction(HOOKS.SHELL_ERROR, {
19697 scope: "menu-refresh",
19698 error: new Error("menu refresh probe timed out")
19699 });
19700 cleanup();
19701 }, MENU_REFRESH_TIMEOUT_MS);
19702 window.addEventListener("message", onMessage);
19703 document.body.appendChild(iframe);
19704 });
19705 };
19706 window.addEventListener("message", (e) => {
19707 if (e.origin !== INITIAL_ORIGIN$1) {
19708 return;
19709 }
19710 const data = e.data;
19711 if (!data) {
19712 return;
19713 }
19714 if (data.type === "desktop-mode-plugins-changed") {
19715 if (data.payload) {
19716 applyPayload(data.payload);
19717 if (typeof data.payload.menuSig === "string") {
19718 lastMenuSig = data.payload.menuSig;
19719 }
19720 }
19721 return;
19722 }
19723 if (data.type === "desktop-mode-menu-signature") {
19724 const sig = data.sig;
19725 if (typeof sig === "string" && sig !== "" && sig !== lastMenuSig && !sigRefreshInFlight) {
19726 sigRefreshInFlight = true;
19727 void refresh().finally(() => {
19728 sigRefreshInFlight = false;
19729 });
19730 }
19731 }
19732 });
19733 return refresh;
19734 }
19735 function hasRestorableSession(session) {
19736 if (!session) {
19737 return false;
19738 }
19739 if (Array.isArray(session.windows) && session.windows.length > 0) {
19740 return true;
19741 }
19742 if (typeof session.updated !== "number" || session.updated <= 0 || !Array.isArray(session.desktops) || session.desktops.length === 0) {
19743 return false;
19744 }
19745 if (session.desktops.length > 1) {
19746 return true;
19747 }
19748 const onlyDesktop = session.desktops[0];
19749 if (onlyDesktop?.id && onlyDesktop.id !== "desktop-1") {
19750 return true;
19751 }
19752 return !!session.activeDesktop && session.activeDesktop !== "desktop-1";
19753 }
19754 async function restoreSession(manager, config, desktopArea) {
19755 const rect = desktopArea.getBoundingClientRect();
19756 if (Array.isArray(config.session.desktops) && config.session.desktops.length > 0) {
19757 manager.seedDesktops(
19758 config.session.desktops,
19759 config.session.activeDesktop || config.session.desktops[0].id
19760 );
19761 }
19762 for (const win of config.session.windows) {
19763 const clamped = clampGeometryToViewport(win, rect);
19764 const dockEntry = findDockEntryForUrl(win.url, config);
19765 const opened = await manager.open({
19766 id: win.id,
19767 baseId: win.baseId || win.id,
19768 desktopId: win.desktopId,
19769 multi: !!dockEntry?.multi,
19770 url: win.url,
19771 // `dockEntry?.url` is the parent menu's landing page —
19772 // recover it so the synthetic "back to parent" tab in
19773 // the in-window strip points at the dock URL even when
19774 // the saved `win.url` is a sub-page (e.g. theme-install.php
19775 // under Appearance, or a deep wc-admin route under
19776 // WooCommerce). Without this the dedup check in
19777 // `dom.ts` sees the iframe URL match a submenu entry
19778 // and suppresses the parent tab — losing the only
19779 // affordance to navigate back.
19780 parentUrl: dockEntry?.url ?? win.url,
19781 title: win.title,
19782 icon: win.icon || "dashicons-admin-generic",
19783 x: clamped.x,
19784 y: clamped.y,
19785 width: clamped.width,
19786 height: clamped.height,
19787 initialState: win.state,
19788 submenu: dockEntry?.submenu
19789 });
19790 if (Array.isArray(win.externalTabs)) {
19791 for (const ext of win.externalTabs) {
19792 if (ext && typeof ext.url === "string" && ext.url !== "") {
19793 opened.addExternalTab(
19794 ext.url,
19795 typeof ext.label === "string" && ext.label !== "" ? ext.label : ext.url
19796 );
19797 }
19798 }
19799 }
19800 }
19801 if (config.session.focused) {
19802 const focused = manager.getById(config.session.focused);
19803 if (focused) {
19804 manager.focus(focused);
19805 }
19806 }
19807 }
19808 async function openCurrentPage(manager, config) {
19809 if (tryNativeUrlRemap(config.currentPage)) {
19810 return;
19811 }
19812 const windowId = deriveWindowId(config.currentPage, config.adminUrl);
19813 const dockEntry = findDockEntryForUrl(config.currentPage, config);
19814 await manager.open({
19815 id: windowId,
19816 baseId: windowId,
19817 multi: !!dockEntry?.multi,
19818 url: config.currentPage,
19819 parentUrl: dockEntry?.url ?? config.currentPage,
19820 title: config.currentTitle,
19821 icon: config.currentIcon,
19822 submenu: dockEntry?.submenu
19823 });
19824 }
19825 function shouldAutoOpenCurrentPage(inputs) {
19826 const suppress = inputs.fromPortal && !inputs.fromPortalIntent && (inputs.hasSession || !inputs.defaultEnabled || inputs.isNativeDefault);
19827 return !suppress;
19828 }
19829 function trackedFetch(manager, input, requestInit, opts) {
19830 const finalInit = injectRestNonce(input, requestInit);
19831 const promise = window.fetch(input, finalInit);
19832 if (opts?.silent) {
19833 return promise;
19834 }
19835 let target2 = opts?.window;
19836 if (!target2 && opts?.windowId) {
19837 target2 = manager.getById(opts.windowId) ?? null;
19838 }
19839 if (!target2) {
19840 target2 = manager.getFocused();
19841 }
19842 if (target2 && typeof target2.trackActivity === "function") {
19843 void target2.trackActivity(promise).catch(() => {
19844 });
19845 }
19846 return promise;
19847 }
19848 const SESSION_SAVE_DEBOUNCE_MS = 500;
19849 function createSessionSaver(manager, config) {
19850 let debounceTimer = null;
19851 let inFlight = false;
19852 const doSave = async () => {
19853 if (inFlight) {
19854 return;
19855 }
19856 const payload = manager.snapshot();
19857 inFlight = true;
19858 try {
19859 await trackedFetch(
19860 manager,
19861 config.sessionUrl,
19862 {
19863 method: "POST",
19864 credentials: "same-origin",
19865 headers: {
19866 "Content-Type": "application/json",
19867 "X-WP-Nonce": config.restNonce
19868 },
19869 body: JSON.stringify({ session: payload }),
19870 // Best-effort: we don't block the UI on persistence.
19871 keepalive: true
19872 },
19873 { silent: true }
19874 );
19875 } catch (err) {
19876 doAction(HOOKS.SHELL_ERROR, { scope: "session-save", error: err });
19877 } finally {
19878 inFlight = false;
19879 }
19880 };
19881 const flushImmediately = () => {
19882 if (debounceTimer !== null) {
19883 clearTimeout(debounceTimer);
19884 debounceTimer = null;
19885 }
19886 const payload = manager.snapshot();
19887 const body = new Blob(
19888 [JSON.stringify({ session: payload })],
19889 { type: "application/json" }
19890 );
19891 const beaconUrl = config.sessionUrl + (config.sessionUrl.includes("?") ? "&" : "?") + "_wpnonce=" + encodeURIComponent(config.restNonce);
19892 if (navigator.sendBeacon && navigator.sendBeacon(beaconUrl, body)) {
19893 return;
19894 }
19895 void doSave();
19896 };
19897 const schedule = () => {
19898 if (debounceTimer !== null) {
19899 clearTimeout(debounceTimer);
19900 }
19901 debounceTimer = window.setTimeout(() => {
19902 debounceTimer = null;
19903 void doSave();
19904 }, SESSION_SAVE_DEBOUNCE_MS);
19905 };
19906 window.addEventListener("pagehide", flushImmediately);
19907 document.addEventListener("visibilitychange", () => {
19908 if (document.visibilityState === "hidden") {
19909 flushImmediately();
19910 }
19911 });
19912 return schedule;
19913 }
19914 const SHELL_RESIZE_DEBOUNCE_MS = 120;
19915 function wireSessionEvents(save) {
19916 document.addEventListener("desktop-mode-window-opened", save);
19917 document.addEventListener("desktop-mode-window-closed", save);
19918 document.addEventListener("desktop-mode-window-focused", save);
19919 document.addEventListener("desktop-mode-window-changed", save);
19920 addAction(HOOKS.DESKTOP_CREATED, "desktop-mode/session-save", save);
19921 addAction(HOOKS.DESKTOP_CLOSED, "desktop-mode/session-save", save);
19922 addAction(HOOKS.DESKTOP_SWITCHED, "desktop-mode/session-save", save);
19923 }
19924 function bindShellLifecycle() {
19925 const shellEl = document.getElementById("desktop-mode-shell");
19926 let resizeTimer = null;
19927 const fireShellResize = () => {
19928 resizeTimer = null;
19929 const rect = shellEl ? shellEl.getBoundingClientRect() : null;
19930 doAction(HOOKS.SHELL_RESIZED, {
19931 width: rect ? Math.round(rect.width) : window.innerWidth,
19932 height: rect ? Math.round(rect.height) : window.innerHeight
19933 });
19934 };
19935 window.addEventListener("resize", () => {
19936 if (resizeTimer !== null) {
19937 window.clearTimeout(resizeTimer);
19938 }
19939 resizeTimer = window.setTimeout(
19940 fireShellResize,
19941 SHELL_RESIZE_DEBOUNCE_MS
19942 );
19943 });
19944 document.addEventListener("visibilitychange", () => {
19945 doAction(HOOKS.SHELL_VISIBILITY, {
19946 state: document.hidden ? "hidden" : "visible"
19947 });
19948 });
19949 }
19950 function applyTileClasses(baseClasses, item, ctx) {
19951 const fullCtx = {
19952 rail: ctx.rail ?? "dock",
19953 orientation: ctx.orientation,
19954 dockId: ctx.dockId,
19955 container: ctx.container ?? document.body,
19956 item,
19957 isSystem: ctx.isSystem
19958 };
19959 return applyFilters(
19960 HOOKS.DOCK_TILE_CLASS,
19961 baseClasses,
19962 fullCtx
19963 );
19964 }
19965 function applyTileElement(tile2, item, ctx) {
19966 const fullCtx = {
19967 rail: ctx.rail ?? "dock",
19968 orientation: ctx.orientation,
19969 dockId: ctx.dockId,
19970 container: ctx.container ?? document.body,
19971 item,
19972 isSystem: ctx.isSystem
19973 };
19974 return applyFilters(
19975 HOOKS.DOCK_TILE_ELEMENT,
19976 tile2,
19977 fullCtx
19978 );
19979 }
19980 function applyTileTooltip(label, item, ctx) {
19981 const fullCtx = {
19982 rail: ctx.rail ?? "dock",
19983 orientation: ctx.orientation,
19984 dockId: ctx.dockId,
19985 container: ctx.container ?? document.body,
19986 item,
19987 isSystem: ctx.isSystem
19988 };
19989 return applyFilters(
19990 HOOKS.DOCK_TILE_TOOLTIP,
19991 label,
19992 fullCtx
19993 );
19994 }
19995 function dispatchTileRendered(el, item, ctx) {
19996 const fullCtx = {
19997 rail: ctx.rail ?? "dock",
19998 orientation: ctx.orientation,
19999 dockId: ctx.dockId,
20000 container: ctx.container ?? document.body,
20001 item,
20002 isSystem: ctx.isSystem
20003 };
20004 doAction(HOOKS.DOCK_TILE_RENDERED, { ...fullCtx, el });
20005 }
20006 const DEFAULT_DOCK_SELECTOR = [
20007 ".desktop-mode-dock",
20008 "#desktop-mode-dock",
20009 "#desktop-mode-side-dock",
20010 ".desktop-mode-dock__tooltip",
20011 ".desktop-mode-dock-submenu"
20012 ].join(",");
20013 const customSelectors = /* @__PURE__ */ new Set();
20014 function isDockElement(target2) {
20015 if (!target2 || typeof target2.closest !== "function") {
20016 return false;
20017 }
20018 const el = target2;
20019 if (el.closest(DEFAULT_DOCK_SELECTOR)) {
20020 return true;
20021 }
20022 for (const selector of customSelectors) {
20023 if (el.closest(selector)) {
20024 return true;
20025 }
20026 }
20027 return false;
20028 }
20029 function registerDockSelector(selector) {
20030 if (typeof selector !== "string" || selector.trim() === "") {
20031 return () => void 0;
20032 }
20033 customSelectors.add(selector);
20034 return () => {
20035 customSelectors.delete(selector);
20036 };
20037 }
20038 const states = /* @__PURE__ */ new Map();
20039 const INITIAL_ORIGIN = window.location.origin;
20040 function ensureState(windowId) {
20041 let s = states.get(windowId);
20042 if (!s) {
20043 s = {
20044 headers: /* @__PURE__ */ new Map(),
20045 observers: /* @__PURE__ */ new Set(),
20046 observeCount: 0,
20047 loadHandler: null,
20048 loadHandlerTarget: null
20049 };
20050 states.set(windowId, s);
20051 }
20052 ensureLoadHandler(windowId, s);
20053 return s;
20054 }
20055 function ensureLoadHandler(windowId, s) {
20056 const iframe = findIframe(windowId);
20057 if (!iframe) {
20058 return;
20059 }
20060 if (s.loadHandlerTarget === iframe && s.loadHandler) {
20061 return;
20062 }
20063 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
20064 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
20065 }
20066 if (typeof iframe.addEventListener !== "function") {
20067 return;
20068 }
20069 const handler = () => {
20070 queueMicrotask(() => pushInstrumentation(windowId));
20071 };
20072 iframe.addEventListener("load", handler);
20073 s.loadHandler = handler;
20074 s.loadHandlerTarget = iframe;
20075 }
20076 function detachLoadHandler(s) {
20077 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
20078 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
20079 }
20080 s.loadHandler = null;
20081 s.loadHandlerTarget = null;
20082 }
20083 function findIframe(windowId) {
20084 const wpd = window.wp?.desktop?.windowManager;
20085 if (wpd && typeof wpd.getById === "function") {
20086 const win = wpd.getById(windowId);
20087 if (win?.iframe) {
20088 return win.iframe;
20089 }
20090 if (win?.element) {
20091 const synth = win.element.querySelector("iframe");
20092 if (synth) {
20093 return synth;
20094 }
20095 }
20096 }
20097 const fallback = document.getElementById(`wp-window-${windowId}`);
20098 return fallback?.querySelector("iframe") ?? null;
20099 }
20100 function snapshotHeaders(s) {
20101 const out = {};
20102 for (const [name, contributions] of s.headers) {
20103 const parts = [];
20104 for (const c of contributions) {
20105 let v;
20106 try {
20107 v = typeof c.value === "function" ? c.value() : c.value;
20108 } catch {
20109 continue;
20110 }
20111 if (typeof v === "string" && v !== "") {
20112 parts.push(v);
20113 }
20114 }
20115 if (parts.length > 0) {
20116 out[name] = parts.join(", ");
20117 }
20118 }
20119 return out;
20120 }
20121 function pushInstrumentation(windowId) {
20122 const iframe = findIframe(windowId);
20123 if (!iframe || !iframe.contentWindow) {
20124 return;
20125 }
20126 const s = states.get(windowId);
20127 const headers = s ? snapshotHeaders(s) : {};
20128 const observe = !!s && s.observeCount > 0;
20129 try {
20130 iframe.contentWindow.postMessage(
20131 {
20132 type: "desktop-mode-instrument-set",
20133 headers,
20134 observe
20135 },
20136 INITIAL_ORIGIN
20137 );
20138 } catch {
20139 }
20140 }
20141 addAction(HOOKS.IFRAME_READY, "desktop-mode/devtools/replay", (payload) => {
20142 const p = payload;
20143 if (p && typeof p.windowId === "string" && states.has(p.windowId)) {
20144 pushInstrumentation(p.windowId);
20145 }
20146 });
20147 addAction(
20148 HOOKS.IFRAME_NETWORK_COMPLETED,
20149 "desktop-mode/devtools/dispatch",
20150 (payload) => {
20151 const p = payload;
20152 if (!p || typeof p.windowId !== "string") {
20153 return;
20154 }
20155 const s = states.get(p.windowId);
20156 if (!s) {
20157 return;
20158 }
20159 for (const cb of s.observers) {
20160 try {
20161 cb(p);
20162 } catch {
20163 }
20164 }
20165 }
20166 );
20167 const sessions = /* @__PURE__ */ new Map();
20168 const POLL_INTERVAL_MS = 1e3;
20169 function pollOnce(sessionId, restUrl2, restNonce) {
20170 const sp = sessions.get(sessionId);
20171 if (!sp || sp.inflight) {
20172 return;
20173 }
20174 sp.inflight = true;
20175 const u = new URL(restUrl2 + "desktop-mode/v1/debug", window.location.origin);
20176 u.searchParams.set("sessionId", sessionId);
20177 u.searchParams.set("since", String(sp.cursor));
20178 for (const ch of sp.channels.keys()) {
20179 u.searchParams.append("channels[]", ch);
20180 }
20181 const url = u.toString();
20182 fetch(url, {
20183 credentials: "same-origin",
20184 headers: { "X-WP-Nonce": restNonce }
20185 }).then((r) => r.ok ? r.json() : { events: [], cursor: sp.cursor }).then((body) => {
20186 sp.inflight = false;
20187 if (!sessions.has(sessionId)) {
20188 return;
20189 }
20190 if (typeof body.cursor === "number") {
20191 sp.cursor = body.cursor;
20192 }
20193 for (const ev of body.events || []) {
20194 const bucket2 = sp.channels.get(ev.channel);
20195 if (!bucket2) {
20196 continue;
20197 }
20198 for (const cb of bucket2) {
20199 try {
20200 cb(ev);
20201 } catch {
20202 }
20203 }
20204 }
20205 }).catch(() => {
20206 sp.inflight = false;
20207 }).finally(() => {
20208 const stillThere = sessions.get(sessionId);
20209 if (stillThere && stillThere.channels.size > 0) {
20210 stillThere.timer = setTimeout(
20211 () => pollOnce(sessionId, restUrl2, restNonce),
20212 POLL_INTERVAL_MS
20213 );
20214 }
20215 });
20216 }
20217 function getRestEndpoint() {
20218 const cfg = window.desktopModeConfig;
20219 if (!cfg || !cfg.restUrl || !cfg.restNonce) {
20220 return null;
20221 }
20222 return { restUrl: cfg.restUrl, restNonce: cfg.restNonce };
20223 }
20224 function dispatchLocal(sessionId, ev) {
20225 const sp = sessions.get(sessionId);
20226 if (!sp) {
20227 return;
20228 }
20229 const bucket2 = sp.channels.get(ev.channel);
20230 if (!bucket2) {
20231 return;
20232 }
20233 for (const cb of bucket2) {
20234 try {
20235 cb(ev);
20236 } catch {
20237 }
20238 }
20239 }
20240 let _localEventCounter = 0;
20241 const debugBus = {
20242 startSession() {
20243 const cryptoApi = window.crypto;
20244 if (cryptoApi && typeof cryptoApi.randomUUID === "function") {
20245 return cryptoApi.randomUUID();
20246 }
20247 return "wpdbg-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
20248 },
20249 publish(sessionId, channel, payload) {
20250 dispatchLocal(sessionId, {
20251 id: ++_localEventCounter,
20252 t: Date.now(),
20253 channel,
20254 payload
20255 });
20256 },
20257 subscribe(sessionId, channel, cb) {
20258 let sp = sessions.get(sessionId);
20259 const startedFresh = !sp;
20260 if (!sp) {
20261 sp = {
20262 channels: /* @__PURE__ */ new Map(),
20263 cursor: 0,
20264 timer: null,
20265 inflight: false
20266 };
20267 sessions.set(sessionId, sp);
20268 }
20269 let bucket2 = sp.channels.get(channel);
20270 if (!bucket2) {
20271 bucket2 = /* @__PURE__ */ new Set();
20272 sp.channels.set(channel, bucket2);
20273 }
20274 bucket2.add(cb);
20275 if (startedFresh) {
20276 const ep = getRestEndpoint();
20277 if (ep) {
20278 pollOnce(sessionId, ep.restUrl, ep.restNonce);
20279 }
20280 }
20281 return () => {
20282 const cur = sessions.get(sessionId);
20283 if (!cur) {
20284 return;
20285 }
20286 const b = cur.channels.get(channel);
20287 if (b) {
20288 b.delete(cb);
20289 if (b.size === 0) {
20290 cur.channels.delete(channel);
20291 }
20292 }
20293 if (cur.channels.size === 0) {
20294 if (cur.timer) {
20295 clearTimeout(cur.timer);
20296 }
20297 sessions.delete(sessionId);
20298 }
20299 };
20300 }
20301 };
20302 const devtools = {
20303 addRequestHeader(windowId, name, value) {
20304 if (typeof windowId !== "string" || windowId === "") {
20305 return () => {
20306 };
20307 }
20308 if (typeof name !== "string" || name === "") {
20309 return () => {
20310 };
20311 }
20312 const s = ensureState(windowId);
20313 const contribution = { value };
20314 let bucket2 = s.headers.get(name);
20315 if (!bucket2) {
20316 bucket2 = [];
20317 s.headers.set(name, bucket2);
20318 }
20319 bucket2.push(contribution);
20320 pushInstrumentation(windowId);
20321 return () => {
20322 const cur = states.get(windowId);
20323 if (!cur) {
20324 return;
20325 }
20326 const b = cur.headers.get(name);
20327 if (!b) {
20328 return;
20329 }
20330 const i = b.indexOf(contribution);
20331 if (i >= 0) {
20332 b.splice(i, 1);
20333 }
20334 if (b.length === 0) {
20335 cur.headers.delete(name);
20336 }
20337 pushInstrumentation(windowId);
20338 gcWindowState(windowId);
20339 };
20340 },
20341 onRequest(windowId, cb, opts) {
20342 if (typeof windowId !== "string" || windowId === "") {
20343 return () => {
20344 };
20345 }
20346 if (typeof cb !== "function") {
20347 return () => {
20348 };
20349 }
20350 const s = ensureState(windowId);
20351 s.observers.add(cb);
20352 const wantsObserve = !!opts?.observe;
20353 if (wantsObserve) {
20354 s.observeCount++;
20355 pushInstrumentation(windowId);
20356 }
20357 return () => {
20358 const cur = states.get(windowId);
20359 if (!cur) {
20360 return;
20361 }
20362 cur.observers.delete(cb);
20363 if (wantsObserve) {
20364 cur.observeCount = Math.max(0, cur.observeCount - 1);
20365 pushInstrumentation(windowId);
20366 }
20367 gcWindowState(windowId);
20368 };
20369 },
20370 reloadWithDebugSession(windowId, sessionId, opts) {
20371 if (typeof windowId !== "string" || windowId === "" || typeof sessionId !== "string" || sessionId === "") {
20372 return null;
20373 }
20374 const iframe = findIframe(windowId);
20375 if (!iframe) {
20376 return null;
20377 }
20378 const headerName = opts?.headerName || "X-WP-Debug-Session";
20379 const queryArg = opts?.queryArg || "wp_debug_session";
20380 const stopHeader = devtools.addRequestHeader(windowId, headerName, sessionId);
20381 try {
20382 const currentSrc = iframe.getAttribute("src") || iframe.src || "";
20383 const u = new URL(currentSrc, window.location.origin);
20384 u.searchParams.set(queryArg, sessionId);
20385 iframe.src = u.toString();
20386 } catch {
20387 }
20388 return {
20389 dispose: () => {
20390 stopHeader();
20391 }
20392 };
20393 },
20394 debug: debugBus
20395 };
20396 function gcWindowState(windowId) {
20397 const s = states.get(windowId);
20398 if (!s) {
20399 return;
20400 }
20401 if (s.headers.size === 0 && s.observers.size === 0) {
20402 detachLoadHandler(s);
20403 states.delete(windowId);
20404 }
20405 }
20406 async function wpdConfirm(options) {
20407 await ensureShellOverlaysLoaded(shellOverlaysBundleUrl());
20408 return new Promise((resolve2) => {
20409 const dialog2 = document.createElement("wpd-confirm-dialog");
20410 dialog2.setAttribute("open", "");
20411 if (options.title) {
20412 dialog2.setAttribute("title", options.title);
20413 }
20414 dialog2.setAttribute("message", options.message);
20415 if (options.confirmLabel) {
20416 dialog2.setAttribute("confirm-label", options.confirmLabel);
20417 }
20418 if (options.cancelLabel) {
20419 dialog2.setAttribute("cancel-label", options.cancelLabel);
20420 }
20421 if (options.danger) {
20422 dialog2.setAttribute("danger", "");
20423 }
20424 if (options.hideCancel) {
20425 dialog2.setAttribute("hide-cancel", "");
20426 }
20427 if (options.dismissable) {
20428 dialog2.setAttribute("dismissable", "");
20429 }
20430 const cleanup = (ok) => {
20431 dialog2.remove();
20432 resolve2(ok);
20433 };
20434 dialog2.addEventListener("wpd-confirm", () => cleanup(true));
20435 dialog2.addEventListener("wpd-cancel", () => cleanup(false));
20436 document.body.appendChild(dialog2);
20437 const inner = dialog2.shadowRoot?.querySelector(".dialog");
20438 (inner ?? dialog2).focus?.();
20439 });
20440 }
20441 function collectWallpaperSurfaces(manager) {
20442 const seed2 = [];
20443 for (const w of manager.getVisibleRects()) {
20444 if (w.state === "minimized") {
20445 continue;
20446 }
20447 if (w.element.offsetParent === null) {
20448 continue;
20449 }
20450 const r = w.element.getBoundingClientRect();
20451 seed2.push({
20452 id: `window:${w.windowId}`,
20453 kind: "window",
20454 rect: rectFromDom(r),
20455 face: "top",
20456 element: w.element
20457 });
20458 }
20459 const shellEl = document.getElementById("desktop-mode-shell");
20460 if (shellEl) {
20461 const r = shellEl.getBoundingClientRect();
20462 seed2.push({
20463 id: "shell:floor",
20464 kind: "shell",
20465 rect: {
20466 x: r.left,
20467 y: r.bottom - 1,
20468 width: r.width,
20469 height: 1
20470 },
20471 face: "top",
20472 element: shellEl
20473 });
20474 }
20475 const dockEls = document.querySelectorAll(
20476 ".desktop-mode-dock"
20477 );
20478 let dockIndex = 0;
20479 for (const dockEl of Array.from(dockEls)) {
20480 const r = dockEl.getBoundingClientRect();
20481 if (r.width <= 0 || r.height <= 0) {
20482 continue;
20483 }
20484 const placement = dockEl.getAttribute("data-desktop-mode-dock-placement") ?? "bottom";
20485 const id = dockIndex === 0 ? "dock:edge" : `dock:edge:${dockIndex}`;
20486 dockIndex++;
20487 if (placement === "bottom") {
20488 seed2.push({
20489 id,
20490 kind: "dock",
20491 rect: { x: r.left, y: r.top, width: r.width, height: 1 },
20492 face: "top",
20493 element: dockEl
20494 });
20495 } else if (placement === "right") {
20496 seed2.push({
20497 id,
20498 kind: "dock",
20499 rect: { x: r.left, y: r.top, width: 1, height: r.height },
20500 face: "left",
20501 element: dockEl
20502 });
20503 } else {
20504 seed2.push({
20505 id,
20506 kind: "dock",
20507 rect: {
20508 x: r.right - 1,
20509 y: r.top,
20510 width: 1,
20511 height: r.height
20512 },
20513 face: "right",
20514 element: dockEl
20515 });
20516 }
20517 }
20518 const widgetCards = document.querySelectorAll(
20519 ".desktop-mode-widgets__card"
20520 );
20521 let widgetIndex = 0;
20522 widgetCards.forEach((card) => {
20523 const r = card.getBoundingClientRect();
20524 if (r.width === 0 && r.height === 0) {
20525 return;
20526 }
20527 const id = card.dataset.widgetId ?? String(widgetIndex++);
20528 seed2.push({
20529 id: `widget:${id}`,
20530 kind: "widget",
20531 rect: rectFromDom(r),
20532 face: "top",
20533 element: card
20534 });
20535 });
20536 const filtered = applyFilters(HOOKS.WALLPAPER_SURFACES, seed2);
20537 return Array.isArray(filtered) ? filtered : seed2;
20538 }
20539 function rectFromDom(r) {
20540 return {
20541 x: r.left,
20542 y: r.top,
20543 width: r.width,
20544 height: r.height
20545 };
20546 }
20547 const NODE_KEY_PROP = "__desktop_modeKeyedListKey";
20548 const NODE_DATA_PROP = "__desktop_modeKeyedListData";
20549 function getHostState(host) {
20550 const cached = host.__desktop_modeKeyedList;
20551 if (cached) {
20552 return cached;
20553 }
20554 const fresh = { byKey: /* @__PURE__ */ new Map() };
20555 host.__desktop_modeKeyedList = fresh;
20556 return fresh;
20557 }
20558 function renderKeyedList(host, items, opts) {
20559 const state2 = getHostState(host);
20560 const prev = state2.byKey;
20561 const next = /* @__PURE__ */ new Map();
20562 const ordered = [];
20563 const seenKeys = /* @__PURE__ */ new Set();
20564 for (const item of items) {
20565 const key = String(opts.keyOf(item));
20566 if (seenKeys.has(key)) {
20567 console.warn(
20568 "[desktop-mode/keyed-list] duplicate key — only the last item with this key will render:",
20569 key
20570 );
20571 }
20572 seenKeys.add(key);
20573 const reused = prev.get(key);
20574 if (reused) {
20575 const prevData = reused.data;
20576 opts.updateItem?.(reused.el, item, prevData);
20577 reused.data = item;
20578 next.set(key, reused);
20579 ordered.push(reused.el);
20580 continue;
20581 }
20582 const el = opts.buildItem(item);
20583 el[NODE_KEY_PROP] = key;
20584 el[NODE_DATA_PROP] = item;
20585 next.set(key, { el, data: item });
20586 ordered.push(el);
20587 }
20588 for (const [key, entry] of prev) {
20589 if (!next.has(key)) {
20590 entry.el.remove();
20591 }
20592 }
20593 for (let i = 0; i < ordered.length; i++) {
20594 const desired = ordered[i];
20595 const live = host.children[i];
20596 if (live === desired) {
20597 continue;
20598 }
20599 host.insertBefore(desired, live ?? null);
20600 }
20601 state2.byKey = next;
20602 }
20603 function clearKeyedList(host) {
20604 const cached = host.__desktop_modeKeyedList;
20605 if (!cached) {
20606 return;
20607 }
20608 for (const entry of cached.byKey.values()) {
20609 entry.el.remove();
20610 }
20611 cached.byKey.clear();
20612 delete host.__desktop_modeKeyedList;
20613 }
20614 function createInfiniteList(options) {
20615 const {
20616 root,
20617 fetchPage,
20618 getId,
20619 renderItem,
20620 rootMargin = "200px",
20621 initialCursor = null,
20622 onLoadingChange = () => void 0,
20623 onError = (err) => {
20624 if (typeof console !== "undefined") {
20625 console.error("[desktop-mode] createInfiniteList:", err);
20626 }
20627 }
20628 } = options;
20629 let sentinel = options.sentinel ?? null;
20630 if (!sentinel) {
20631 sentinel = document.createElement("div");
20632 sentinel.dataset.wpdInfiniteListSentinel = "";
20633 sentinel.style.height = "1px";
20634 root.appendChild(sentinel);
20635 }
20636 const seen = /* @__PURE__ */ new Set();
20637 let cursor = initialCursor;
20638 let hasMoreInternal = true;
20639 let loading = false;
20640 let controller = null;
20641 let renderedCount = 0;
20642 let destroyed = false;
20643 let observer = null;
20644 const setLoading = (next) => {
20645 if (loading === next) {
20646 return;
20647 }
20648 loading = next;
20649 try {
20650 onLoadingChange(next);
20651 } catch (err) {
20652 onError(err);
20653 }
20654 };
20655 const detachObserver = () => {
20656 if (observer) {
20657 observer.disconnect();
20658 observer = null;
20659 }
20660 };
20661 const ensureObserver = () => {
20662 if (observer || !sentinel || destroyed) {
20663 return;
20664 }
20665 observer = new IntersectionObserver(
20666 (entries) => {
20667 for (const entry of entries) {
20668 if (entry.isIntersecting) {
20669 void loadMore();
20670 }
20671 }
20672 },
20673 { rootMargin }
20674 );
20675 observer.observe(sentinel);
20676 };
20677 const loadMore = async () => {
20678 if (destroyed || loading || !hasMoreInternal) {
20679 return;
20680 }
20681 setLoading(true);
20682 controller = new AbortController();
20683 const localController = controller;
20684 try {
20685 const page = await fetchPage(cursor, localController.signal);
20686 if (destroyed || localController !== controller) {
20687 return;
20688 }
20689 let appended = 0;
20690 const frag = document.createDocumentFragment();
20691 for (const item of page.items ?? []) {
20692 const key = String(getId(item));
20693 if (seen.has(key)) {
20694 continue;
20695 }
20696 seen.add(key);
20697 const el = renderItem(item, renderedCount + appended);
20698 frag.appendChild(el);
20699 appended++;
20700 }
20701 if (appended > 0) {
20702 if (sentinel && sentinel.parentNode === root) {
20703 root.insertBefore(frag, sentinel);
20704 } else {
20705 root.appendChild(frag);
20706 }
20707 renderedCount += appended;
20708 }
20709 cursor = page.nextCursor ?? null;
20710 if (!cursor) {
20711 hasMoreInternal = false;
20712 detachObserver();
20713 }
20714 } catch (err) {
20715 if (err?.name === "AbortError") {
20716 return;
20717 }
20718 onError(err);
20719 } finally {
20720 if (localController === controller) {
20721 setLoading(false);
20722 controller = null;
20723 }
20724 }
20725 };
20726 const reset = () => {
20727 if (destroyed) {
20728 return;
20729 }
20730 controller?.abort();
20731 controller = null;
20732 seen.clear();
20733 cursor = initialCursor;
20734 hasMoreInternal = true;
20735 renderedCount = 0;
20736 const sentinelInRoot = sentinel && sentinel.parentNode === root;
20737 while (root.firstChild) {
20738 root.removeChild(root.firstChild);
20739 }
20740 if (sentinelInRoot && sentinel) {
20741 root.appendChild(sentinel);
20742 }
20743 setLoading(false);
20744 ensureObserver();
20745 void loadMore();
20746 };
20747 const destroy = () => {
20748 if (destroyed) {
20749 return;
20750 }
20751 destroyed = true;
20752 detachObserver();
20753 controller?.abort();
20754 controller = null;
20755 if (!options.sentinel && sentinel && sentinel.parentNode === root) {
20756 root.removeChild(sentinel);
20757 }
20758 sentinel = null;
20759 setLoading(false);
20760 };
20761 ensureObserver();
20762 void loadMore();
20763 return {
20764 reset,
20765 loadMore,
20766 hasMore: () => hasMoreInternal,
20767 isLoading: () => loading,
20768 destroy
20769 };
20770 }
20771 const POPUP_DEFAULT_WIDTH = 520;
20772 const POPUP_DEFAULT_HEIGHT = 720;
20773 const POPUP_CLOSE_POLL_MS = 500;
20774 function startOAuth(service, options = {}) {
20775 if (typeof service !== "string" || service === "") {
20776 return Promise.reject(
20777 new Error("[desktop-mode] startOAuth requires a non-empty service slug.")
20778 );
20779 }
20780 const restRoot2 = readRestRoot$1();
20781 const restNonce = readRestNonce$1();
20782 return trackedFetch$1(
20783 joinRestUrl(restRoot2, "desktop-mode/v1/oauth/start"),
20784 {
20785 method: "POST",
20786 headers: {
20787 "Content-Type": "application/json",
20788 "X-WP-Nonce": restNonce ?? ""
20789 },
20790 body: JSON.stringify({ service })
20791 },
20792 { source: "desktop-mode/oauth-start" }
20793 ).then(async (res) => {
20794 if (!res.ok) {
20795 const text = await res.text().catch(() => "");
20796 throw new Error(
20797 `[desktop-mode] OAuth start failed (${res.status}): ${text}`
20798 );
20799 }
20800 return await res.json();
20801 }).then((startBody) => openPopupAndWait(startBody, service, options));
20802 }
20803 function openPopupAndWait(body, service, options) {
20804 return new Promise((resolve2, reject) => {
20805 const width = options.width ?? POPUP_DEFAULT_WIDTH;
20806 const height = options.height ?? POPUP_DEFAULT_HEIGHT;
20807 const left = Math.max(0, Math.floor((window.screen.width - width) / 2));
20808 const top = Math.max(0, Math.floor((window.screen.height - height) / 2));
20809 const features = [
20810 `width=${width}`,
20811 `height=${height}`,
20812 `left=${left}`,
20813 `top=${top}`,
20814 "menubar=no",
20815 "toolbar=no",
20816 "location=yes",
20817 "status=no",
20818 "resizable=yes",
20819 "scrollbars=yes"
20820 ].join(",");
20821 const popup = window.open(
20822 body.authorize_url,
20823 `desktop-mode-oauth-${service}`,
20824 features
20825 );
20826 if (!popup) {
20827 reject(
20828 new Error(
20829 "[desktop-mode] OAuth popup blocked. Tell users to allow popups for this site."
20830 )
20831 );
20832 return;
20833 }
20834 const expectedOrigin = window.location.origin;
20835 let pollTimer = null;
20836 let detached = false;
20837 const cleanup = () => {
20838 if (detached) {
20839 return;
20840 }
20841 detached = true;
20842 window.removeEventListener("message", onMessage);
20843 if (pollTimer !== null) {
20844 window.clearInterval(pollTimer);
20845 pollTimer = null;
20846 }
20847 };
20848 const onMessage = (e) => {
20849 if (e.origin !== expectedOrigin) {
20850 return;
20851 }
20852 const data = e.data;
20853 if (!data || data.type !== "desktop-mode-oauth-callback") {
20854 return;
20855 }
20856 const payload = data.payload;
20857 cleanup();
20858 if (payload && payload.ok) {
20859 resolve2(payload);
20860 } else {
20861 const reason = payload?.reason ?? "unknown";
20862 const message = payload?.message ?? "OAuth flow failed";
20863 const err = new Error(
20864 `[desktop-mode] startOAuth(${service}) failed: ${reason} — ${message}`
20865 );
20866 err.cause = payload;
20867 reject(err);
20868 }
20869 };
20870 window.addEventListener("message", onMessage);
20871 pollTimer = window.setInterval(() => {
20872 if (popup.closed) {
20873 cleanup();
20874 reject(
20875 new Error(
20876 `[desktop-mode] startOAuth(${service}) cancelled — popup closed before completing.`
20877 )
20878 );
20879 }
20880 }, POPUP_CLOSE_POLL_MS);
20881 });
20882 }
20883 function readDesktopConfig() {
20884 return window.desktopModeConfig ?? {};
20885 }
20886 function readRestRoot$1() {
20887 const root = readDesktopConfig().restRoot;
20888 if (typeof root === "string" && root !== "") {
20889 return root;
20890 }
20891 return `${window.location.origin}/wp-json/`;
20892 }
20893 function readRestNonce$1() {
20894 const nonce = readDesktopConfig().restNonce;
20895 return typeof nonce === "string" && nonce !== "" ? nonce : null;
20896 }
20897 const RESERVED_NAMESPACE_KEYS = /* @__PURE__ */ new Set([
20898 "windowManager",
20899 "dock",
20900 "sideDock",
20901 "taskbar",
20902 "desktopLayout",
20903 "icons",
20904 "files",
20905 "confirm",
20906 "saveSession",
20907 "hooks",
20908 "HOOKS",
20909 "isActive",
20910 "registerWallpaper",
20911 "registerWidget",
20912 "widgetLayer",
20913 "widgets",
20914 "registerSystemTile",
20915 "registerWindow",
20916 "openWindow",
20917 "openNewWindow",
20918 "cloneTemplate",
20919 "onWindow",
20920 "createInfiniteList",
20921 "startOAuth",
20922 "repaintLoadingOverlays",
20923 "loadVendorScript",
20924 "getWallpaperSurfaces",
20925 "registerModule",
20926 "loadModules",
20927 "whenReady",
20928 "ready",
20929 "isReady",
20930 "setDefaultWindow",
20931 "refreshMenu",
20932 "config",
20933 "ai",
20934 "dragBridge",
20935 "dragManager",
20936 "registerCommand",
20937 "unregisterCommand",
20938 "listCommands",
20939 "registerDestructiveAdminAction",
20940 "unregisterDestructiveAdminAction",
20941 "listDestructiveAdminActions",
20942 "registerSettingsTab",
20943 "unregisterSettingsTab",
20944 "listSettingsTabs",
20945 "registerDockRailRenderer",
20946 "unregisterDockRailRenderer",
20947 "listDockRailRenderers",
20948 "openOsSettings",
20949 "getOsSettings",
20950 "subscribeOsSettings",
20951 "updateOsSettings",
20952 "deriveWindowId",
20953 "listSystemTiles",
20954 "getSystemTile",
20955 "getMenuItems",
20956 "renderIcon",
20957 "applyTileClasses",
20958 "applyTileElement",
20959 "applyTileTooltip",
20960 "dispatchTileRendered",
20961 "isDockElement",
20962 "registerDockSelector",
20963 "registerTitleBarButton",
20964 "unregisterTitleBarButton",
20965 "listTitleBarButtons",
20966 "registerUnfocusEffect",
20967 "unregisterUnfocusEffect",
20968 "listUnfocusEffects",
20969 "relations",
20970 "registerWindowLinkRenderer",
20971 "unregisterWindowLinkRenderer",
20972 "listWindowLinkRenderers",
20973 "registerWindowTheme",
20974 "unregisterWindowTheme",
20975 "listWindowThemes",
20976 "applyWindowTheme",
20977 "registerWindowControl",
20978 "unregisterWindowControl",
20979 "listWindowControls",
20980 "applyWindowControls",
20981 "registerWindowSlot",
20982 "unregisterWindowSlot",
20983 "listWindowSlots",
20984 "applyWindowSlot",
20985 "registerWindowNotice",
20986 "unregisterWindowNotice",
20987 "listWindowNotices",
20988 "dismissWindowNotice",
20989 "undismissWindowNotice",
20990 "registerWindowChrome",
20991 "unregisterWindowChrome",
20992 "listWindowChromes",
20993 "applyWindowChrome",
20994 "connect",
20995 "getConnection",
20996 "broadcast",
20997 "subscribe",
20998 "registerPalette",
20999 "unregisterPalette",
21000 "listPalettes",
21001 "openPalette",
21002 "devtools",
21003 "createSharedStore",
21004 "presence",
21005 "activity",
21006 "heartbeat",
21007 "showToast",
21008 "renderKeyedList",
21009 "clearKeyedList",
21010 "registerNamespace",
21011 "notify",
21012 "pwa",
21013 "getWindowConfig",
21014 "debug",
21015 "fetch"
21016 ]);
21017 function buildPublicApi(deps2) {
21018 const {
21019 manager,
21020 dock,
21021 layoutDispatcher,
21022 osSettings,
21023 iconsApi: iconsApi2,
21024 filesApi: filesApi2,
21025 saveSession,
21026 widgetLayer,
21027 registerWindow,
21028 openWindowById,
21029 openNewWindowById,
21030 placeSystemTile,
21031 setDefaultWindow,
21032 refreshMenu,
21033 openOsSettings,
21034 aiAssistant,
21035 dragBridge,
21036 dragManager,
21037 connect,
21038 getConnection,
21039 config
21040 } = deps2;
21041 const desktopApi = {
21042 windowManager: manager,
21043 dock,
21044 sideDock: layoutDispatcher?.getSide() ?? null,
21045 desktopLayout: osSettings.getOsSettingsSnapshot().desktopLayout,
21046 icons: iconsApi2,
21047 files: filesApi2,
21048 confirm: wpdConfirm,
21049 saveSession,
21050 hooks: rawHooks(),
21051 HOOKS,
21052 isActive: () => !!document.getElementById("desktop-mode-shell"),
21053 registerWallpaper: (def) => {
21054 register$2(def);
21055 osSettings.apply();
21056 },
21057 registerWidget: (def) => {
21058 register(def);
21059 },
21060 widgetLayer,
21061 widgets: {
21062 redock: (id) => {
21063 widgetLayer?.redock(id);
21064 }
21065 },
21066 loadVendorScript,
21067 getWallpaperSurfaces: () => collectWallpaperSurfaces(manager),
21068 registerWindow,
21069 openWindow: openWindowById,
21070 openNewWindow: openNewWindowById,
21071 fetch: (input, requestInit, opts) => trackedFetch(manager, input, requestInit, opts),
21072 repaintLoadingOverlays,
21073 cloneTemplate,
21074 onWindow,
21075 createInfiniteList,
21076 startOAuth,
21077 registerSystemTile: (item) => {
21078 placeSystemTile(item);
21079 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: item.id });
21080 },
21081 registerModule,
21082 loadModules,
21083 whenReady,
21084 ready: whenReady,
21085 isReady,
21086 setDefaultWindow,
21087 refreshMenu,
21088 config,
21089 ai: aiAssistant,
21090 dragBridge,
21091 dragManager,
21092 registerCommand,
21093 unregisterCommand,
21094 listCommands,
21095 registerDestructiveAdminAction,
21096 unregisterDestructiveAdminAction,
21097 listDestructiveAdminActions,
21098 registerSettingsTab,
21099 unregisterSettingsTab,
21100 listSettingsTabs,
21101 registerDockRailRenderer: register$1,
21102 unregisterDockRailRenderer: unregister$1,
21103 listDockRailRenderers: list,
21104 openOsSettings,
21105 getOsSettings: () => osSettings.getOsSettingsSnapshot(),
21106 subscribeOsSettings: (cb) => osSettings.subscribeOsSettings(cb),
21107 updateOsSettings: (patch, opts = {}) => {
21108 if (typeof patch.wallpaper === "string") {
21109 osSettings.state.wallpaper = patch.wallpaper;
21110 }
21111 if (typeof patch.accent === "string") {
21112 osSettings.state.accent = patch.accent;
21113 }
21114 if (typeof patch.dockSize === "string") {
21115 osSettings.state.dockSize = patch.dockSize;
21116 }
21117 if (typeof patch.desktopLayout === "string") {
21118 osSettings.state.desktopLayout = patch.desktopLayout;
21119 }
21120 if (typeof patch.dockRailRenderer === "string") {
21121 osSettings.state.dockRailRenderer = patch.dockRailRenderer;
21122 }
21123 if (typeof patch.windowLinkRenderer === "string") {
21124 osSettings.state.windowLinkRenderer = patch.windowLinkRenderer;
21125 }
21126 if (patch.windowLinkVisibility === "focus" || patch.windowLinkVisibility === "always" || patch.windowLinkVisibility === "off") {
21127 osSettings.state.windowLinkVisibility = patch.windowLinkVisibility;
21128 }
21129 if (typeof patch.windowLinksEnabled === "boolean") {
21130 osSettings.state.windowLinksEnabled = patch.windowLinksEnabled;
21131 }
21132 if (typeof patch.windowLinkRaiseOnFocus === "boolean") {
21133 osSettings.state.windowLinkRaiseOnFocus = patch.windowLinkRaiseOnFocus;
21134 }
21135 if (typeof patch.windowLinkHighlight === "boolean") {
21136 osSettings.state.windowLinkHighlight = patch.windowLinkHighlight;
21137 }
21138 if (patch.ai && typeof patch.ai === "object") {
21139 osSettings.state.ai = { ...osSettings.state.ai, ...patch.ai };
21140 }
21141 if (typeof patch.nativePostsEnabled === "boolean") {
21142 osSettings.state.nativePostsEnabled = patch.nativePostsEnabled;
21143 }
21144 if (typeof patch.nativePagesEnabled === "boolean") {
21145 osSettings.state.nativePagesEnabled = patch.nativePagesEnabled;
21146 }
21147 if (typeof patch.nativeUsersEnabled === "boolean") {
21148 osSettings.state.nativeUsersEnabled = patch.nativeUsersEnabled;
21149 }
21150 if (typeof patch.nativePluginsEnabled === "boolean") {
21151 osSettings.state.nativePluginsEnabled = patch.nativePluginsEnabled;
21152 }
21153 if (typeof patch.nativeCommentsEnabled === "boolean") {
21154 osSettings.state.nativeCommentsEnabled = patch.nativeCommentsEnabled;
21155 }
21156 if (typeof patch.foldersSharingEnabled === "boolean") {
21157 osSettings.state.foldersSharingEnabled = patch.foldersSharingEnabled;
21158 }
21159 if (typeof patch.developerModeEnabled === "boolean") {
21160 osSettings.state.developerModeEnabled = patch.developerModeEnabled;
21161 }
21162 if (Array.isArray(patch.nativePostsHiddenColumns)) {
21163 osSettings.state.nativePostsHiddenColumns = patch.nativePostsHiddenColumns.filter(
21164 (v) => typeof v === "string" && v !== ""
21165 ).slice(0, 32);
21166 }
21167 if (patch.itemVisibility && typeof patch.itemVisibility === "object") {
21168 const allowed = ["both", "dock", "desktop", "hidden"];
21169 const next = {};
21170 for (const [k, v] of Object.entries(
21171 patch.itemVisibility
21172 )) {
21173 if (typeof k !== "string" || k === "") {
21174 continue;
21175 }
21176 if (typeof v !== "string" || !allowed.includes(v)) {
21177 continue;
21178 }
21179 next[k] = v;
21180 }
21181 osSettings.state.itemVisibility = next;
21182 }
21183 if (Array.isArray(patch.dockOrder)) {
21184 osSettings.state.dockOrder = patch.dockOrder.filter(
21185 (v) => typeof v === "string" && v !== ""
21186 ).slice(0, 256);
21187 }
21188 if (patch.dockPromotedPositions && typeof patch.dockPromotedPositions === "object") {
21189 const MAX_COORD = 1e5;
21190 const next = {};
21191 for (const [k, v] of Object.entries(
21192 patch.dockPromotedPositions
21193 )) {
21194 if (typeof k !== "string" || k === "") {
21195 continue;
21196 }
21197 if (!v || typeof v !== "object") {
21198 continue;
21199 }
21200 const pos = v;
21201 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) {
21202 continue;
21203 }
21204 next[k] = { x: pos.x, y: pos.y };
21205 if (Object.keys(next).length >= 256) {
21206 break;
21207 }
21208 }
21209 osSettings.state.dockPromotedPositions = next;
21210 }
21211 osSettings.save(opts);
21212 if (patch.itemVisibility || patch.dockOrder) {
21213 layoutDispatcher?.refresh();
21214 }
21215 },
21216 deriveWindowId: (url, overrideAdminUrl) => deriveWindowId(url, overrideAdminUrl ?? config.adminUrl),
21217 listSystemTiles: () => layoutDispatcher?.listSystemTiles() ?? [],
21218 getSystemTile: (id) => layoutDispatcher?.getSystemTile(id) ?? null,
21219 getMenuItems: () => layoutDispatcher?.getMenuItems() ?? [],
21220 renderIcon,
21221 applyTileClasses,
21222 applyTileElement,
21223 applyTileTooltip,
21224 dispatchTileRendered,
21225 isDockElement,
21226 registerDockSelector,
21227 registerTitleBarButton,
21228 unregisterTitleBarButton,
21229 listTitleBarButtons,
21230 registerUnfocusEffect,
21231 unregisterUnfocusEffect,
21232 listUnfocusEffects,
21233 relations: relationsApi,
21234 registerWindowLinkRenderer,
21235 unregisterWindowLinkRenderer,
21236 listWindowLinkRenderers,
21237 registerWindowTheme,
21238 unregisterWindowTheme,
21239 listWindowThemes,
21240 applyWindowTheme: (windowId, override) => {
21241 const win = manager.getById(windowId);
21242 if (!win) {
21243 return;
21244 }
21245 win.setAppearanceTheme(override);
21246 },
21247 registerWindowControl,
21248 unregisterWindowControl,
21249 listWindowControls,
21250 applyWindowControls: (windowId, override) => {
21251 const win = manager.getById(windowId);
21252 if (!win) {
21253 return;
21254 }
21255 win.setAppearanceControls(override);
21256 },
21257 registerWindowSlot,
21258 unregisterWindowSlot,
21259 listWindowSlots,
21260 applyWindowSlot: (windowId, slot, slotConfig) => {
21261 const win = manager.getById(windowId);
21262 if (!win) {
21263 return;
21264 }
21265 win.setAppearanceSlot(slot, slotConfig);
21266 },
21267 registerWindowNotice,
21268 unregisterWindowNotice,
21269 listWindowNotices,
21270 dismissWindowNotice,
21271 undismissWindowNotice,
21272 registerWindowChrome,
21273 unregisterWindowChrome,
21274 listWindowChromes,
21275 applyWindowChrome: (windowId, chromeId) => {
21276 const win = manager.getById(windowId);
21277 if (!win) {
21278 return;
21279 }
21280 win.setAppearanceChrome(chromeId);
21281 },
21282 connect,
21283 getConnection,
21284 broadcast,
21285 subscribe: subscribe$2,
21286 registerPalette,
21287 unregisterPalette,
21288 listPalettes,
21289 openPalette: openPaletteOnly,
21290 devtools,
21291 createSharedStore,
21292 presence: presenceApi,
21293 activity,
21294 heartbeat,
21295 showToast,
21296 notify: notify$3,
21297 pwa: {
21298 promptInstall,
21299 undismissInstallHint,
21300 getState: getPwaState,
21301 subscribe: subscribePwaState,
21302 requestNotificationPermission,
21303 getNotificationPermission
21304 },
21305 renderKeyedList,
21306 clearKeyedList,
21307 registerNamespace: (name, api) => {
21308 if (typeof name !== "string" || name === "") {
21309 console.warn(
21310 "[desktop-mode] registerNamespace: name must be a non-empty string"
21311 );
21312 return;
21313 }
21314 if (!api || typeof api !== "object") {
21315 console.warn(
21316 `[desktop-mode] registerNamespace("${name}"): api must be an object`
21317 );
21318 return;
21319 }
21320 if (RESERVED_NAMESPACE_KEYS.has(name)) {
21321 console.warn(
21322 `[desktop-mode] registerNamespace("${name}"): name is reserved by the shell — pick a plugin-specific key`
21323 );
21324 return;
21325 }
21326 desktopApi[name] = api;
21327 },
21328 getWindowConfig: (id) => {
21329 const store2 = window.desktopModeWindowConfig;
21330 if (!store2 || typeof store2 !== "object") {
21331 return void 0;
21332 }
21333 const value = store2[id];
21334 return value === void 0 ? void 0 : value;
21335 },
21336 debug: {
21337 window: (id) => {
21338 const entry = (config.nativeWindows ?? []).find(
21339 (e) => e.id === id
21340 );
21341 if (!entry) {
21342 return null;
21343 }
21344 const url = entry.scriptUrl || "";
21345 let loadPath = "unknown";
21346 let tagInDom = false;
21347 if (url) {
21348 const lazyTag = document.querySelector(
21349 `script[data-desktop-mode-vendor="${url.replace(/"/g, '\\"')}"]`
21350 );
21351 if (lazyTag) {
21352 loadPath = "lazy";
21353 tagInDom = true;
21354 } else {
21355 const eagerTag = Array.from(
21356 document.querySelectorAll(
21357 "script[src]"
21358 )
21359 ).find((s) => s.src === url);
21360 if (eagerTag) {
21361 loadPath = "eager";
21362 tagInDom = true;
21363 }
21364 }
21365 }
21366 const cfgStore = window.desktopModeWindowConfig;
21367 const configPresent = !!(cfgStore && typeof cfgStore === "object" && Object.prototype.hasOwnProperty.call(cfgStore, id));
21368 return {
21369 id,
21370 scriptHandle: entry.scriptHandle || "",
21371 scriptUrl: url,
21372 loadPath,
21373 tagInDom,
21374 configPresent,
21375 extras: {
21376 hasTranslations: !!entry.scriptTranslations,
21377 l10nCount: (entry.scriptL10n ?? []).length,
21378 beforeCount: (entry.scriptBefore ?? []).length,
21379 afterCount: (entry.scriptAfter ?? []).length
21380 }
21381 };
21382 }
21383 }
21384 };
21385 return desktopApi;
21386 }
21387 function installPublicApi(api) {
21388 if (!window.wp) {
21389 window.wp = {};
21390 }
21391 if (!window.wp.desktop) {
21392 window.wp.desktop = api;
21393 return;
21394 }
21395 Object.assign(
21396 window.wp.desktop,
21397 api
21398 );
21399 }
21400 const store$1 = createSharedStore("desktop-mode/layout", () => ({
21401 // Default mirrors the OsSettingsSnapshot default; the shell
21402 // re-publishes the persisted value as soon as it boots.
21403 layout: "classic"
21404 }));
21405 function setCurrentLayout(layout) {
21406 if (store$1.state.layout === layout) {
21407 return;
21408 }
21409 store$1.state.layout = layout;
21410 store$1.notify();
21411 }
21412 class DesktopFile {
21413 constructor(shape) {
21414 this.shape = shape;
21415 }
21416 /** Title shown under the tile. Defaults to `shape.title`. */
21417 title() {
21418 return this.shape.title;
21419 }
21420 /** Dashicon class or data URI. Defaults to `shape.icon`. */
21421 icon() {
21422 return this.shape.icon;
21423 }
21424 /** Optional preview-image URL. Defaults to `shape.previewUrl`. */
21425 previewUrl() {
21426 return this.shape.previewUrl;
21427 }
21428 /** Reference (id, URL, …). */
21429 ref() {
21430 return this.shape.ref;
21431 }
21432 /** Whether the underlying entity still exists. */
21433 exists() {
21434 return this.shape.exists;
21435 }
21436 }
21437 class DefaultDesktopFile extends DesktopFile {
21438 constructor(shape, typeSlug) {
21439 super(shape);
21440 this.typeSlug = typeSlug;
21441 }
21442 type() {
21443 return this.typeSlug;
21444 }
21445 }
21446 const seed$1 = /* @__PURE__ */ new Map();
21447 const listeners$1 = /* @__PURE__ */ new Set();
21448 function registerType(def) {
21449 if (!def.type) {
21450 throw new Error("[desktop-mode] registerType: `type` is required.");
21451 }
21452 if (!def.label) {
21453 throw new Error("[desktop-mode] registerType: `label` is required.");
21454 }
21455 seed$1.set(def.type, {
21456 type: def.type,
21457 label: def.label,
21458 sort: typeof def.sort === "number" ? def.sort : 100,
21459 DesktopFile: def.DesktopFile
21460 });
21461 doAction("desktop-mode.files.type-registered", def.type, def);
21462 notify$1();
21463 }
21464 function unregisterType(typeSlug) {
21465 if (seed$1.delete(typeSlug)) {
21466 doAction("desktop-mode.files.type-unregistered", typeSlug);
21467 notify$1();
21468 }
21469 }
21470 function getType(typeSlug) {
21471 const entry = seed$1.get(typeSlug);
21472 return entry ? entry : null;
21473 }
21474 function getTypes() {
21475 const list2 = Array.from(seed$1.values()).slice();
21476 const filtered = applyFilters(
21477 "desktop-mode.files.types",
21478 list2
21479 );
21480 const arr = Array.isArray(filtered) ? filtered : list2;
21481 arr.sort((a, b) => {
21482 if (a.sort !== b.sort) {
21483 return a.sort - b.sort;
21484 }
21485 return a.label.localeCompare(b.label);
21486 });
21487 return arr;
21488 }
21489 function resolve(shape) {
21490 const entry = seed$1.get(shape.type);
21491 if (entry?.DesktopFile) {
21492 return new entry.DesktopFile(shape);
21493 }
21494 return new DefaultDesktopFile(shape, shape.type);
21495 }
21496 function subscribe(cb) {
21497 listeners$1.add(cb);
21498 return () => listeners$1.delete(cb);
21499 }
21500 function notify$1() {
21501 for (const cb of listeners$1) {
21502 try {
21503 cb();
21504 } catch (err) {
21505 console.error("[desktop-mode] files registry subscriber threw:", err);
21506 }
21507 }
21508 }
21509 const seed = /* @__PURE__ */ new Map();
21510 const listeners = /* @__PURE__ */ new Set();
21511 let userAssociations = {};
21512 function setUserAssociations(map) {
21513 userAssociations = { ...map };
21514 notify();
21515 }
21516 function getUserAssociations() {
21517 return { ...userAssociations };
21518 }
21519 function registerOpener(def) {
21520 if (!def.id) {
21521 throw new Error("[desktop-mode] registerOpener: `id` is required.");
21522 }
21523 if (!def.label) {
21524 throw new Error("[desktop-mode] registerOpener: `label` is required.");
21525 }
21526 if (!Array.isArray(def.types) || def.types.length === 0) {
21527 throw new Error("[desktop-mode] registerOpener: `types` must be a non-empty array.");
21528 }
21529 if (!def.handler || typeof def.handler !== "object") {
21530 throw new Error("[desktop-mode] registerOpener: `handler` is required.");
21531 }
21532 seed.set(def.id, {
21533 id: def.id,
21534 label: def.label,
21535 types: def.types.slice(),
21536 isDefault: !!def.isDefault,
21537 sort: typeof def.sort === "number" ? def.sort : 100,
21538 handler: def.handler
21539 });
21540 doAction("desktop-mode.files.opener-registered", def.id, def);
21541 notify();
21542 }
21543 function unregisterOpener(id) {
21544 if (seed.delete(id)) {
21545 doAction("desktop-mode.files.opener-unregistered", id);
21546 notify();
21547 }
21548 }
21549 function getOpener(id) {
21550 return seed.get(id) ?? null;
21551 }
21552 function getOpeners() {
21553 const list2 = Array.from(seed.values()).slice();
21554 const filtered = applyFilters(
21555 "desktop-mode.files.openers",
21556 list2
21557 );
21558 const arr = Array.isArray(filtered) ? filtered : list2;
21559 arr.sort((a, b) => {
21560 const sa = typeof a.sort === "number" ? a.sort : 100;
21561 const sb = typeof b.sort === "number" ? b.sort : 100;
21562 if (sa !== sb) {
21563 return sa - sb;
21564 }
21565 return a.label.localeCompare(b.label);
21566 });
21567 return arr;
21568 }
21569 function getOpenersForType(type) {
21570 return getOpeners().filter((e) => e.types.includes(type));
21571 }
21572 function resolveOpener(type) {
21573 const candidates = getOpenersForType(type);
21574 if (candidates.length === 0) {
21575 return null;
21576 }
21577 const override = userAssociations[type];
21578 let resolved = null;
21579 if (override) {
21580 resolved = candidates.find((e) => e.id === override) ?? null;
21581 }
21582 if (!resolved) {
21583 resolved = candidates.find((e) => e.isDefault) ?? null;
21584 }
21585 if (!resolved) {
21586 resolved = candidates[0];
21587 }
21588 const filtered = applyFilters(
21589 "desktop-mode.files.resolve-opener",
21590 resolved,
21591 type
21592 );
21593 return filtered ?? null;
21594 }
21595 function subscribeOpeners(cb) {
21596 listeners.add(cb);
21597 return () => listeners.delete(cb);
21598 }
21599 function notify() {
21600 for (const cb of listeners) {
21601 try {
21602 cb();
21603 } catch (err) {
21604 console.error("[desktop-mode] openers subscriber threw:", err);
21605 }
21606 }
21607 }
21608 let deps$1 = null;
21609 function installOpenDeps(next) {
21610 deps$1 = next;
21611 }
21612 async function openFile(file, ctx) {
21613 if (!deps$1) {
21614 console.warn(
21615 "[desktop-mode] wp.desktop.files.open() called before the shell installed open deps. The file will not open."
21616 );
21617 return false;
21618 }
21619 const opener = resolveOpener(file.type());
21620 if (!opener) {
21621 doAction("desktop-mode.files.open-failed", {
21622 reason: "no-opener",
21623 type: file.type(),
21624 ref: file.ref()
21625 });
21626 return false;
21627 }
21628 doAction("desktop-mode.files.opening", { file, openerId: opener.id });
21629 try {
21630 const handler = opener.handler;
21631 if (handler.kind === "url") {
21632 const url = await handler.url(file);
21633 if (!url) {
21634 return false;
21635 }
21636 const id = handler.windowId ? handler.windowId(file) : deps$1.deriveWindowId(url);
21637 const title = handler.title ? handler.title(file) : file.title();
21638 const icon = file.icon();
21639 const opened = deps$1.openUrl({ id, url, title, icon });
21640 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "url" });
21641 return opened;
21642 }
21643 if (handler.kind === "window") {
21644 const config = handler.config ? handler.config(file) : void 0;
21645 const opened = deps$1.openNativeWindow(handler.windowId, config);
21646 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "window" });
21647 return opened;
21648 }
21649 await handler.open(file, ctx);
21650 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "js" });
21651 return true;
21652 } catch (err) {
21653 doAction("desktop-mode.files.open-failed", {
21654 reason: "handler-threw",
21655 type: file.type(),
21656 ref: file.ref(),
21657 openerId: opener.id,
21658 error: err
21659 });
21660 console.error("[desktop-mode] file opener threw:", err);
21661 return false;
21662 }
21663 }
21664 function registerBuiltInFileTypes() {
21665 registerType({ type: "shortcut", label: "Plugin shortcut", sort: 1 });
21666 registerType({ type: "folder", label: "Folder", sort: 5 });
21667 registerType({ type: "post", label: "Post", sort: 10 });
21668 registerType({ type: "attachment", label: "Media", sort: 20 });
21669 registerType({ type: "user", label: "User", sort: 30 });
21670 registerType({ type: "term", label: "Taxonomy term", sort: 40 });
21671 registerType({ type: "comment", label: "Comment", sort: 50 });
21672 registerType({ type: "bookmark", label: "Bookmark", sort: 60 });
21673 registerType({ type: "link", label: "Web link", sort: 70 });
21674 registerType({ type: "embed", label: "Embedded web window", sort: 80 });
21675 }
21676 let deps = null;
21677 function installRestDeps(next) {
21678 deps = next;
21679 }
21680 function ensureDeps() {
21681 if (!deps) {
21682 throw new Error("[desktop-mode] files REST client called before installRestDeps().");
21683 }
21684 return deps;
21685 }
21686 class FilesConflictError extends Error {
21687 constructor(detail) {
21688 super(
21689 `Row was changed by ${detail.actor.name || "another session"} (parent="${detail.current.parentName}")`
21690 );
21691 this.name = "FilesConflictError";
21692 this.status = 409;
21693 this.detail = detail;
21694 }
21695 }
21696 async function call(path, init2) {
21697 const { baseUrl, nonce } = ensureDeps();
21698 const url = joinRestUrl(baseUrl, path);
21699 const headers = new Headers(init2.headers ?? {});
21700 headers.set("X-WP-Nonce", nonce);
21701 if (init2.body && !headers.has("Content-Type")) {
21702 headers.set("Content-Type", "application/json");
21703 }
21704 const res = await trackedFetch$1(
21705 url,
21706 { ...init2, headers, credentials: "same-origin" },
21707 { source: "desktop-mode/files" }
21708 );
21709 const text = await res.text();
21710 let body = null;
21711 let parseError = null;
21712 if (text) {
21713 try {
21714 body = JSON.parse(text);
21715 } catch (e) {
21716 body = null;
21717 parseError = e;
21718 }
21719 }
21720 if (!res.ok) {
21721 if (res.status === 409) {
21722 const data = body?.data?.data ?? body?.data;
21723 if (data && typeof data === "object") {
21724 throw new FilesConflictError(data);
21725 }
21726 }
21727 const err = body;
21728 throw new Error(
21729 `[desktop-mode] files REST ${res.status}: ${err?.code ?? ""} ${err?.message ?? ""}`.trim()
21730 );
21731 }
21732 if (null === body) {
21733 if (parseError && text) {
21734 const head = text.slice(0, 120).replace(/\s+/g, " ");
21735 throw new Error(
21736 `[desktop-mode] files REST ${res.status} returned non-JSON body — ${parseError.message}. First 120 chars: ${head}`
21737 );
21738 }
21739 throw new Error(
21740 `[desktop-mode] files REST ${res.status}: empty or unparseable body.`
21741 );
21742 }
21743 return body;
21744 }
21745 function listPlacements(folderId = 0) {
21746 return call(
21747 `/placements?folder=${encodeURIComponent(String(folderId))}`,
21748 { method: "GET" }
21749 );
21750 }
21751 function createPlacement(body) {
21752 return call("/placements", {
21753 method: "POST",
21754 body: JSON.stringify(body)
21755 });
21756 }
21757 function updatePlacement(id, body, ifMatchMs) {
21758 const headers = {};
21759 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
21760 headers["If-Match"] = String(ifMatchMs);
21761 }
21762 return call(`/placements/${id}`, {
21763 method: "PATCH",
21764 body: JSON.stringify(body),
21765 headers
21766 });
21767 }
21768 function deletePlacement(id) {
21769 return call(`/placements/${id}`, { method: "DELETE" });
21770 }
21771 async function restoreTrashedItem(id, type) {
21772 const { baseUrl, nonce } = ensureDeps();
21773 const root = baseUrl.replace(/\/files\/?$/, "");
21774 const url = `${root}/recycle-bin/restore`;
21775 const res = await trackedFetch$1(
21776 url,
21777 {
21778 method: "POST",
21779 headers: {
21780 "Content-Type": "application/json",
21781 "X-WP-Nonce": nonce
21782 },
21783 credentials: "same-origin",
21784 body: JSON.stringify({ items: [{ id, type }] })
21785 },
21786 { source: "desktop-mode/files" }
21787 );
21788 if (!res.ok) {
21789 throw new Error(`[desktop-mode] restore ${res.status}`);
21790 }
21791 return await res.json();
21792 }
21793 function listFolders() {
21794 return call("/folders", { method: "GET" });
21795 }
21796 function createFolder(body) {
21797 return call("/folders", {
21798 method: "POST",
21799 body: JSON.stringify(body)
21800 });
21801 }
21802 function updateFolder(id, body, ifMatchMs) {
21803 const headers = {};
21804 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
21805 headers["If-Match"] = String(ifMatchMs);
21806 }
21807 return call(`/folders/${id}`, {
21808 method: "PATCH",
21809 body: JSON.stringify(body),
21810 headers
21811 });
21812 }
21813 function deleteFolder(id) {
21814 return call(`/folders/${id}`, { method: "DELETE" });
21815 }
21816 function saveAssociations(associations) {
21817 return call("/associations", {
21818 method: "PUT",
21819 body: JSON.stringify({ associations })
21820 });
21821 }
21822 function listShares(folderId) {
21823 return call(`/folders/${folderId}/shares`, { method: "GET" });
21824 }
21825 function inviteShare(folderId, body) {
21826 return call(`/folders/${folderId}/shares`, {
21827 method: "POST",
21828 body: JSON.stringify(body)
21829 });
21830 }
21831 function updateShareCapability(folderId, shareId, capability) {
21832 return call(`/folders/${folderId}/shares/${shareId}`, {
21833 method: "PATCH",
21834 body: JSON.stringify({ capability })
21835 });
21836 }
21837 function revokeShare(folderId, shareId) {
21838 return call(`/folders/${folderId}/shares/${shareId}`, {
21839 method: "DELETE"
21840 });
21841 }
21842 function acceptShare(folderId, shareId) {
21843 return call(`/folders/${folderId}/shares/${shareId}/accept`, {
21844 method: "POST"
21845 });
21846 }
21847 function denyShare(folderId, shareId) {
21848 return call(`/folders/${folderId}/shares/${shareId}/deny`, {
21849 method: "POST"
21850 });
21851 }
21852 function leaveShare(folderId) {
21853 return call(`/folders/${folderId}/leave`, {
21854 method: "POST"
21855 });
21856 }
21857 function purgeFolderSharingTables() {
21858 return call(
21859 "/folder-sharing-tables/purge",
21860 { method: "POST" }
21861 );
21862 }
21863 const filesRest = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
21864 __proto__: null,
21865 FilesConflictError,
21866 acceptShare,
21867 createFolder,
21868 createPlacement,
21869 deleteFolder,
21870 deletePlacement,
21871 denyShare,
21872 installRestDeps,
21873 inviteShare,
21874 leaveShare,
21875 listFolders,
21876 listPlacements,
21877 listShares,
21878 purgeFolderSharingTables,
21879 restoreTrashedItem,
21880 revokeShare,
21881 saveAssociations,
21882 updateFolder,
21883 updatePlacement,
21884 updateShareCapability
21885 }, Symbol.toStringTag, { value: "Module" }));
21886 const STORE_KEY = "desktop-mode/files";
21887 function getFilesStore() {
21888 return createSharedStore(STORE_KEY, () => ({
21889 placementsByFolder: /* @__PURE__ */ new Map(),
21890 folders: /* @__PURE__ */ new Map(),
21891 hydratedFolders: /* @__PURE__ */ new Set()
21892 }));
21893 }
21894 function fireChanged(detail) {
21895 if (typeof document === "undefined") {
21896 return;
21897 }
21898 document.dispatchEvent(
21899 new CustomEvent("desktop-mode-files-changed", {
21900 detail: { source: "local", ...detail }
21901 })
21902 );
21903 }
21904 function setFolderPlacements(folderId, placements) {
21905 const store2 = getFilesStore();
21906 const next = new Map(store2.state.placementsByFolder);
21907 next.set(folderId, placements.slice());
21908 const hydrated = new Set(store2.state.hydratedFolders);
21909 hydrated.add(folderId);
21910 store2.state = { ...store2.state, placementsByFolder: next, hydratedFolders: hydrated };
21911 store2.notify();
21912 fireChanged({ kind: "placements-set", folderId });
21913 }
21914 function upsertPlacement(placement, source = "local") {
21915 if (!placement || typeof placement.id !== "number") {
21916 console.warn(
21917 "[desktop-mode] upsertPlacement called with a non-placement value; ignoring.",
21918 placement
21919 );
21920 return;
21921 }
21922 const store2 = getFilesStore();
21923 const next = new Map(store2.state.placementsByFolder);
21924 for (const [folderId, list2] of next) {
21925 const idx2 = list2.findIndex((p) => p && p.id === placement.id);
21926 if (idx2 >= 0 && folderId !== placement.parentId) {
21927 const copy = list2.filter(Boolean);
21928 const removeAt = copy.findIndex((p) => p.id === placement.id);
21929 if (removeAt >= 0) {
21930 copy.splice(removeAt, 1);
21931 }
21932 next.set(folderId, copy);
21933 }
21934 }
21935 const rawTarget = next.get(placement.parentId)?.slice() ?? [];
21936 const target2 = rawTarget.filter(Boolean);
21937 const idx = target2.findIndex((p) => p.id === placement.id);
21938 if (idx >= 0) {
21939 target2[idx] = placement;
21940 } else {
21941 target2.push(placement);
21942 }
21943 next.set(placement.parentId, target2);
21944 store2.state = { ...store2.state, placementsByFolder: next };
21945 store2.notify();
21946 fireChanged({ kind: "placement-upserted", placementId: placement.id, folderId: placement.parentId, source });
21947 }
21948 function removePlacement(placementId, source = "local") {
21949 const store2 = getFilesStore();
21950 const next = new Map(store2.state.placementsByFolder);
21951 let touchedFolder;
21952 for (const [folderId, list2] of next) {
21953 const idx = list2.findIndex((p) => p && p.id === placementId);
21954 if (idx >= 0) {
21955 const copy = list2.filter(Boolean).filter(
21956 (p) => p.id !== placementId
21957 );
21958 next.set(folderId, copy);
21959 touchedFolder = folderId;
21960 }
21961 }
21962 if (touchedFolder === void 0) {
21963 return;
21964 }
21965 store2.state = { ...store2.state, placementsByFolder: next };
21966 store2.notify();
21967 fireChanged({ kind: "placement-removed", placementId, folderId: touchedFolder, source });
21968 }
21969 function setFolders(folders) {
21970 const store2 = getFilesStore();
21971 const next = /* @__PURE__ */ new Map();
21972 for (const f of folders) {
21973 next.set(f.id, f);
21974 }
21975 store2.state = { ...store2.state, folders: next };
21976 store2.notify();
21977 fireChanged({ kind: "folders-set" });
21978 }
21979 function upsertFolder(folder, source = "local") {
21980 const store2 = getFilesStore();
21981 const next = new Map(store2.state.folders);
21982 next.set(folder.id, folder);
21983 store2.state = { ...store2.state, folders: next };
21984 store2.notify();
21985 fireChanged({ kind: "folder-upserted", folderRowId: folder.id, source });
21986 }
21987 function removeFolder(folderId, source = "local") {
21988 const store2 = getFilesStore();
21989 const folders = new Map(store2.state.folders);
21990 folders.delete(folderId);
21991 const placements = new Map(store2.state.placementsByFolder);
21992 placements.delete(folderId);
21993 store2.state = { ...store2.state, folders, placementsByFolder: placements };
21994 store2.notify();
21995 fireChanged({ kind: "folder-removed", folderRowId: folderId, source });
21996 }
21997 function subscribeFilesStore(cb) {
21998 const store2 = getFilesStore();
21999 const off = store2.subscribe(cb);
22000 return off;
22001 }
22002 function getFilesState() {
22003 return getFilesStore().getState();
22004 }
22005 const store = {
22006 getState: getFilesState,
22007 subscribe: subscribeFilesStore,
22008 setFolderPlacements,
22009 upsertPlacement,
22010 upsertFolder,
22011 removePlacement,
22012 removeFolder
22013 };
22014 const styles$4 = css`:host{display:inline-block}`;
22015 const styles$3 = 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 )}`;
22016 const _WpdRibbon = class _WpdRibbon extends Component {
22017 render() {
22018 return html`<span class="banner" part="banner"><slot></slot></span>`;
22019 }
22020 };
22021 _WpdRibbon.props = ["placement", "tone"];
22022 _WpdRibbon.styles = [styles$3];
22023 _WpdRibbon.help = {
22024 title: "Ribbon",
22025 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.",
22026 status: "experimental",
22027 since: "0.8.6",
22028 props: [
22029 {
22030 name: "placement",
22031 type: '"top-end" | "top-start" | "bottom-end" | "bottom-start"',
22032 description: "Which corner of the parent the ribbon hugs. Defaults to `top-end` (logical right in LTR, left in RTL)."
22033 },
22034 {
22035 name: "tone",
22036 type: '"primary" | "success" | "warning" | "danger" | "info" | "neutral"',
22037 description: "Background color tone. Defaults to `primary` (the admin theme accent)."
22038 }
22039 ],
22040 slots: [{ name: "(default)", description: "Ribbon label text. Keep short." }],
22041 cssProps: [
22042 { name: "--wpd-ribbon-size", default: "90px", description: "Square clipping window edge." },
22043 { name: "--wpd-ribbon-banner-width", default: "140px", description: "Width of the rotated strip." },
22044 { name: "--wpd-ribbon-banner-offset", default: "20px", description: "Distance from corner to strip center." },
22045 { name: "--wpd-ribbon-banner-pull", default: "-36px", description: "How far the strip overhangs the clip edge." },
22046 { name: "--wpd-ribbon-bg", default: "var(--wp-admin-theme-color, #2271b1)" },
22047 { name: "--wpd-ribbon-fg", default: "#fff" },
22048 { name: "--wpd-ribbon-shadow", default: "0 2px 4px rgba(0,0,0,0.2)" },
22049 { name: "--wpd-ribbon-padding", default: "4px 0" },
22050 { name: "--wpd-ribbon-font", default: "700 10px/1.4 system-ui" },
22051 { name: "--wpd-ribbon-tracking", default: "0.06em" },
22052 { name: "--wpd-ribbon-z", default: "2" }
22053 ],
22054 example: html`
22055 <div
22056 style="position: relative; width: 240px; height: 120px;
22057 border: 1px solid #ccc; border-radius: 8px;
22058 padding: 16px; box-sizing: border-box;"
22059 >
22060 <wpd-ribbon>Featured</wpd-ribbon>
22061 Card body…
22062 </div>
22063 `
22064 };
22065 let WpdRibbon = _WpdRibbon;
22066 defineComponent("wpd-ribbon", WpdRibbon);
22067 const TILE_CLASS = "desktop-mode-file-tile";
22068 const STATUS_LABEL = {
22069 draft: "Draft",
22070 pending: "Pending",
22071 private: "Private",
22072 future: "Scheduled"
22073 };
22074 function statusRibbonsEnabled() {
22075 const get2 = window.wp?.desktop?.getOsSettings;
22076 if (typeof get2 !== "function") {
22077 return true;
22078 }
22079 try {
22080 return get2()?.showPostStatusRibbons !== false;
22081 } catch {
22082 return true;
22083 }
22084 }
22085 function getDragManager$1() {
22086 const api = window.wp?.desktop?.dragManager;
22087 return api ?? null;
22088 }
22089 const REACTIVE_PROPS = [
22090 "type",
22091 "ref",
22092 "label",
22093 "icon",
22094 "thumbnail",
22095 "kind",
22096 "status",
22097 "selected",
22098 "missing",
22099 "access-gated",
22100 "drag-kind",
22101 "drag-title",
22102 "drag-icon"
22103 ];
22104 const _WpdTile = class _WpdTile extends Component {
22105 constructor() {
22106 super(...arguments);
22107 this._pointerdownHandler = null;
22108 this._keydownHandler = null;
22109 }
22110 connectedCallback() {
22111 super.connectedCallback();
22112 if (!this._keydownHandler) {
22113 this._keydownHandler = (e) => {
22114 if (e.key === "Enter" || e.key === " ") {
22115 e.preventDefault();
22116 this.click();
22117 }
22118 };
22119 this.addEventListener("keydown", this._keydownHandler);
22120 }
22121 this._paint();
22122 }
22123 disconnectedCallback() {
22124 if (this._pointerdownHandler) {
22125 this.removeEventListener(
22126 "pointerdown",
22127 this._pointerdownHandler
22128 );
22129 this._pointerdownHandler = null;
22130 }
22131 if (this._keydownHandler) {
22132 this.removeEventListener(
22133 "keydown",
22134 this._keydownHandler
22135 );
22136 this._keydownHandler = null;
22137 }
22138 }
22139 /**
22140 * Bypass the templated render loop. Lit-html's `render(template,
22141 * root)` would wipe the host's light-DOM children every tick —
22142 * including the visual / label / ribbon `_paint()` just
22143 * inserted. We override `requestUpdate` directly so attribute
22144 * changes call `_paint` (idempotent) without lit-html getting
22145 * involved.
22146 */
22147 requestUpdate() {
22148 if (!this.isConnected) {
22149 return;
22150 }
22151 this._paint();
22152 }
22153 render() {
22154 return html``;
22155 }
22156 _paint() {
22157 const type = this.getAttribute("type") ?? "";
22158 const ref = this.getAttribute("ref") ?? "";
22159 const label = this.getAttribute("label") ?? "";
22160 const icon = this.getAttribute("icon") ?? "";
22161 const thumbnail = this.getAttribute("thumbnail") ?? "";
22162 const kind = this.getAttribute("kind") ?? "entry";
22163 const status = this.getAttribute("status") ?? "";
22164 const selected = this.hasAttribute("selected");
22165 const missing = this.hasAttribute("missing");
22166 const accessGated = this.hasAttribute("access-gated");
22167 const ownedClasses = [
22168 TILE_CLASS,
22169 `${TILE_CLASS}--folder`,
22170 `${TILE_CLASS}--missing`,
22171 `${TILE_CLASS}--access-gated`,
22172 `${TILE_CLASS}--selected`
22173 ];
22174 for (const c of ownedClasses) {
22175 this.classList.remove(c);
22176 }
22177 this.classList.add(TILE_CLASS);
22178 if (kind === "folder") {
22179 this.classList.add(`${TILE_CLASS}--folder`);
22180 }
22181 if (missing) {
22182 this.classList.add(`${TILE_CLASS}--missing`);
22183 }
22184 if (accessGated) {
22185 this.classList.add(`${TILE_CLASS}--access-gated`);
22186 }
22187 if (selected) {
22188 this.classList.add(`${TILE_CLASS}--selected`);
22189 }
22190 this.dataset.fileType = type;
22191 this.dataset.fileRef = ref;
22192 if (kind) {
22193 this.dataset.role = kind;
22194 }
22195 this.setAttribute("role", "listitem");
22196 this.setAttribute("aria-label", label);
22197 if (!this.hasAttribute("tabindex")) {
22198 this.setAttribute("tabindex", "0");
22199 }
22200 const accessGatedTitle = "You don’t have permission to open this — ask the folder owner for access.";
22201 if (accessGated) {
22202 this.title = accessGatedTitle;
22203 this.setAttribute("aria-disabled", "true");
22204 } else {
22205 this.removeAttribute("aria-disabled");
22206 if (this.title === accessGatedTitle) {
22207 this.removeAttribute("title");
22208 }
22209 }
22210 const SLOTS = [
22211 `${TILE_CLASS}__visual`,
22212 `${TILE_CLASS}__label`,
22213 `${TILE_CLASS}__lock`
22214 ];
22215 for (const cls of SLOTS) {
22216 this.querySelectorAll(`:scope > .${cls}`).forEach(
22217 (n) => n.remove()
22218 );
22219 }
22220 this.querySelectorAll(":scope > wpd-ribbon").forEach(
22221 (n) => n.remove()
22222 );
22223 const visual = document.createElement("span");
22224 visual.className = `${TILE_CLASS}__visual`;
22225 if (thumbnail) {
22226 const img = document.createElement("img");
22227 img.src = thumbnail;
22228 img.alt = "";
22229 img.loading = "lazy";
22230 img.decoding = "async";
22231 img.className = `${TILE_CLASS}__preview`;
22232 img.draggable = false;
22233 visual.appendChild(img);
22234 } else if (icon) {
22235 const iconNode = renderIcon(icon, {
22236 title: label,
22237 className: `${TILE_CLASS}__icon`
22238 });
22239 visual.appendChild(iconNode);
22240 }
22241 this.appendChild(visual);
22242 const labelNode = document.createElement("span");
22243 labelNode.className = `${TILE_CLASS}__label`;
22244 labelNode.textContent = label;
22245 this.appendChild(labelNode);
22246 if (accessGated) {
22247 const lock = document.createElement("span");
22248 lock.className = `${TILE_CLASS}__lock dashicons dashicons-lock`;
22249 lock.setAttribute("aria-hidden", "true");
22250 this.appendChild(lock);
22251 }
22252 if (status && status !== "publish" && STATUS_LABEL[status] && statusRibbonsEnabled()) {
22253 const ribbon = document.createElement("wpd-ribbon");
22254 ribbon.setAttribute("placement", "top-end");
22255 ribbon.setAttribute("tone", ribbonToneFor(status));
22256 ribbon.textContent = STATUS_LABEL[status];
22257 this.appendChild(ribbon);
22258 }
22259 applyTileEntryStagger(this);
22260 doAction("desktop-mode.tile.rendered", { tile: this });
22261 this._wireDragOut();
22262 }
22263 _wireDragOut() {
22264 if (this._pointerdownHandler) {
22265 this.removeEventListener(
22266 "pointerdown",
22267 this._pointerdownHandler
22268 );
22269 this._pointerdownHandler = null;
22270 }
22271 const dragKind = this.getAttribute("drag-kind");
22272 if (!dragKind) {
22273 return;
22274 }
22275 const handler = (e) => {
22276 if (e.button !== 0) {
22277 return;
22278 }
22279 const dragManager = getDragManager$1();
22280 if (!dragManager) {
22281 return;
22282 }
22283 const ref = this.getAttribute("ref") ?? "";
22284 const title = this.getAttribute("drag-title") ?? this.getAttribute("label") ?? void 0;
22285 const icon = this.getAttribute("drag-icon") ?? this.getAttribute("icon") ?? void 0;
22286 const rect = this.getBoundingClientRect();
22287 dragManager.start({
22288 payload: {
22289 type: "shortcut",
22290 source: this,
22291 data: {
22292 kind: dragKind,
22293 ref,
22294 title,
22295 icon
22296 },
22297 ghost: {
22298 offsetX: e.clientX - rect.left,
22299 offsetY: e.clientY - rect.top
22300 }
22301 },
22302 origin: e
22303 });
22304 };
22305 this._pointerdownHandler = handler;
22306 this.addEventListener("pointerdown", handler);
22307 }
22308 };
22309 _WpdTile.shadow = false;
22310 _WpdTile.props = REACTIVE_PROPS;
22311 _WpdTile.styles = [styles$4];
22312 _WpdTile.help = {
22313 title: "Tile",
22314 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.",
22315 status: "experimental",
22316 since: "0.8.6",
22317 props: [
22318 { name: "type", type: "string" },
22319 { name: "ref", type: "string" },
22320 { name: "label", type: "string" },
22321 { name: "icon", type: "string", description: "Dashicon class / URL / data URI. Ignored when `thumbnail` is set." },
22322 { name: "thumbnail", type: "string", description: "Preview image URL. Renders as `<img>` and wins over `icon`." },
22323 { name: "kind", type: "`entry` | `folder`" },
22324 { name: "status", type: "`draft` | `pending` | `private` | `future` | `publish`" },
22325 { name: "selected", type: "boolean" },
22326 { name: "missing", type: "boolean" },
22327 { name: "access-gated", type: "boolean" },
22328 { name: "drag-kind", type: "string", description: "When set, the component wires pointerdown → DragManager." },
22329 { name: "drag-title", type: "string" },
22330 { name: "drag-icon", type: "string" }
22331 ]
22332 };
22333 let WpdTile = _WpdTile;
22334 function ribbonToneFor(status) {
22335 switch (status) {
22336 case "draft":
22337 return "warning";
22338 case "pending":
22339 return "info";
22340 case "private":
22341 return "danger";
22342 case "future":
22343 return "primary";
22344 default:
22345 return "primary";
22346 }
22347 }
22348 defineComponent("wpd-tile", WpdTile);
22349 function buildTileFromSpec(spec) {
22350 const tile2 = document.createElement("wpd-tile");
22351 tile2.setAttribute("type", spec.type);
22352 tile2.setAttribute("ref", spec.ref);
22353 tile2.setAttribute("label", spec.label);
22354 if (spec.icon) {
22355 tile2.setAttribute("icon", spec.icon);
22356 }
22357 if (spec.thumbnail) {
22358 tile2.setAttribute("thumbnail", spec.thumbnail);
22359 }
22360 if (spec.role) {
22361 tile2.setAttribute("kind", spec.role);
22362 }
22363 if (spec.status) {
22364 tile2.setAttribute("status", spec.status);
22365 }
22366 if (spec.missing) {
22367 tile2.setAttribute("missing", "");
22368 }
22369 if (spec.accessGated) {
22370 tile2.setAttribute("access-gated", "");
22371 }
22372 if (spec.dataset) {
22373 for (const [key, raw] of Object.entries(spec.dataset)) {
22374 if (raw === void 0 || raw === null) {
22375 continue;
22376 }
22377 tile2.dataset[key] = String(raw);
22378 }
22379 }
22380 if (Array.isArray(spec.extraClasses)) {
22381 for (const c of spec.extraClasses) {
22382 if (c) {
22383 tile2.classList.add(c);
22384 }
22385 }
22386 }
22387 const classFiltered = applyFilters(
22388 "desktop-mode.tile.class",
22389 tile2.className,
22390 spec
22391 );
22392 if (classFiltered && classFiltered !== tile2.className) {
22393 tile2.className = classFiltered;
22394 }
22395 if (typeof spec.x === "number" && typeof spec.y === "number") {
22396 tile2.style.position = "absolute";
22397 tile2.style.left = `${spec.x}px`;
22398 tile2.style.top = `${spec.y}px`;
22399 }
22400 return tile2;
22401 }
22402 function placementToSpec(placement, folderId) {
22403 const file = resolve(placement.file);
22404 const previewUrl = file.previewUrl();
22405 const metaName = placement.meta && typeof placement.meta.name === "string" ? placement.meta.name.trim() : "";
22406 const label = metaName !== "" ? metaName : file.title();
22407 const metaIconUrl = placement.meta && typeof placement.meta.iconUrl === "string" ? placement.meta.iconUrl.trim() : "";
22408 return {
22409 type: placement.file.type,
22410 ref: placement.file.ref,
22411 label,
22412 // Preview wins over icon (matches the previous behavior).
22413 thumbnail: previewUrl || void 0,
22414 icon: previewUrl ? void 0 : metaIconUrl || file.icon(),
22415 x: placement.x,
22416 y: placement.y,
22417 dataset: {
22418 placementId: placement.id,
22419 folderId
22420 },
22421 meta: placement.meta,
22422 missing: !placement.file.exists,
22423 accessGated: Boolean(placement.accessGated),
22424 ariaLabel: label
22425 };
22426 }
22427 function buildTile(placement, folderId) {
22428 const file = resolve(placement.file);
22429 const tile2 = buildTileFromSpec(placementToSpec(placement, folderId));
22430 const classFiltered = applyFilters(
22431 "desktop-mode.files.tile-class",
22432 TILE_CLASS,
22433 placement
22434 );
22435 if (classFiltered && classFiltered !== TILE_CLASS) {
22436 tile2.className = classFiltered;
22437 }
22438 const extra = applyFilters(
22439 "desktop-mode.files.tile-element",
22440 null,
22441 placement
22442 );
22443 if (extra instanceof Element) {
22444 tile2.appendChild(extra);
22445 }
22446 tile2.addEventListener("dblclick", (e) => {
22447 e.preventDefault();
22448 e.stopPropagation();
22449 if (placement.accessGated) {
22450 showToast({
22451 message: `You don’t have permission to open "${placement.file.title || file.title()}". Ask the folder owner if you need access to this item.`,
22452 duration: 6e3
22453 });
22454 return;
22455 }
22456 void openFile(file, {
22457 placement: {
22458 id: placement.id,
22459 x: placement.x,
22460 y: placement.y,
22461 meta: placement.meta
22462 }
22463 });
22464 });
22465 doAction("desktop-mode.files.tile-rendered", { tile: tile2, placement });
22466 return tile2;
22467 }
22468 function setTilePosition(tile2, x, y) {
22469 tile2.style.left = `${x}px`;
22470 tile2.style.top = `${y}px`;
22471 }
22472 function attachDismissable(host, options) {
22473 const onAway = (e) => {
22474 if (e.target instanceof Node && host.contains(e.target)) {
22475 return;
22476 }
22477 if (e.target instanceof Node) {
22478 for (const sel of options.siblingSelectors ?? []) {
22479 const matches = Array.from(
22480 document.querySelectorAll(sel)
22481 );
22482 for (const m of matches) {
22483 if (m.contains(e.target)) {
22484 return;
22485 }
22486 }
22487 }
22488 }
22489 if (options.excludeOutsideTarget && e.target instanceof Node && options.excludeOutsideTarget.contains(e.target)) {
22490 return;
22491 }
22492 options.close();
22493 };
22494 const onKey = (e) => {
22495 if (e.key === "Escape") {
22496 options.close();
22497 }
22498 };
22499 document.addEventListener("mousedown", onAway, { capture: true });
22500 document.addEventListener("keydown", onKey);
22501 return () => {
22502 document.removeEventListener("mousedown", onAway, { capture: true });
22503 document.removeEventListener("keydown", onKey);
22504 };
22505 }
22506 const MENU_CLASS$2 = "desktop-mode-wallpaper-menu";
22507 let activeMenu$2 = null;
22508 function closeTileMenu() {
22509 if (!activeMenu$2) {
22510 return;
22511 }
22512 activeMenu$2.dispatchEvent(new CustomEvent("tile-menu-closed"));
22513 activeMenu$2.remove();
22514 activeMenu$2 = null;
22515 doAction("desktop-mode.files.tile-menu.closed", {});
22516 }
22517 let openGeneration$1 = 0;
22518 function openTileMenu(pos, opts) {
22519 closeTileMenu();
22520 const myGen = ++openGeneration$1;
22521 openWithShellOverlays(
22522 () => myGen === openGeneration$1,
22523 () => openTileMenuImmediate(pos, opts)
22524 );
22525 }
22526 function openTileMenuImmediate(pos, { placement, items }) {
22527 const list2 = applyFilters(
22528 "desktop-mode.files.tile-menu",
22529 items.slice(),
22530 placement
22531 );
22532 const sorted = (Array.isArray(list2) ? list2 : items).slice().sort((a, b) => {
22533 const sa = typeof a.sort === "number" ? a.sort : 100;
22534 const sb = typeof b.sort === "number" ? b.sort : 100;
22535 if (sa !== sb) {
22536 return sa - sb;
22537 }
22538 return a.label.localeCompare(b.label);
22539 });
22540 if (sorted.length === 0) {
22541 return;
22542 }
22543 const menu = document.createElement("wpd-context-menu");
22544 menu.setAttribute("open", "");
22545 menu.classList.add(MENU_CLASS$2);
22546 menu.dataset.placementId = String(placement.id);
22547 menu.style.left = `${pos.x}px`;
22548 menu.style.top = `${pos.y}px`;
22549 const itemById = /* @__PURE__ */ new Map();
22550 for (const item of sorted) {
22551 itemById.set(item.id, item);
22552 const opt = document.createElement("wpd-context-menu-option");
22553 opt.dataset.menuItemId = item.id;
22554 opt.setAttribute("value", item.id);
22555 if (item.danger) {
22556 opt.setAttribute("danger", "");
22557 }
22558 if (item.disabled) {
22559 opt.setAttribute("disabled", "");
22560 }
22561 if (item.icon) {
22562 opt.setAttribute("icon", sanitizeClass$2(item.icon));
22563 }
22564 opt.textContent = item.label;
22565 menu.appendChild(opt);
22566 }
22567 menu.addEventListener("wpd-context-menu-pick", (e) => {
22568 const detail = e.detail;
22569 const item = itemById.get(detail.id);
22570 if (!item) {
22571 return;
22572 }
22573 closeTileMenu();
22574 void item.onClick(new MouseEvent("click"));
22575 });
22576 document.body.appendChild(menu);
22577 activeMenu$2 = menu;
22578 const rect = menu.getBoundingClientRect();
22579 if (rect.right > window.innerWidth) {
22580 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
22581 }
22582 if (rect.bottom > window.innerHeight) {
22583 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
22584 }
22585 const detach = attachDismissable(menu, {
22586 close: () => closeTileMenu()
22587 });
22588 menu.addEventListener("tile-menu-closed", detach);
22589 doAction("desktop-mode.files.tile-menu.opened", {
22590 placementId: placement.id,
22591 items: sorted.map((i) => i.id)
22592 });
22593 }
22594 function sanitizeClass$2(raw) {
22595 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
22596 }
22597 const ROOT_CLASS$3 = "desktop-mode-create-folder-dialog";
22598 let active$1 = null;
22599 function closeCreateFolderDialog() {
22600 if (!active$1) {
22601 return;
22602 }
22603 active$1.dispatchEvent(new CustomEvent("create-folder-dialog-closed"));
22604 active$1.remove();
22605 active$1 = null;
22606 doAction("desktop-mode.files.create-folder.closed", {});
22607 }
22608 function openCreateFolderDialog(options) {
22609 closeCreateFolderDialog();
22610 const decision = applyFilters(
22611 "desktop-mode.files.create-folder.dialog",
22612 null,
22613 options
22614 );
22615 if (decision === false) {
22616 return;
22617 }
22618 const initial = (options.initialName ?? "Untitled folder").trim();
22619 const overlay = document.createElement("div");
22620 overlay.className = `${ROOT_CLASS$3}__overlay`;
22621 overlay.setAttribute("role", "presentation");
22622 const dialog2 = document.createElement("div");
22623 dialog2.className = ROOT_CLASS$3;
22624 dialog2.setAttribute("role", "dialog");
22625 dialog2.setAttribute("aria-modal", "true");
22626 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS$3}-title`);
22627 const title = document.createElement("h2");
22628 title.id = `${ROOT_CLASS$3}-title`;
22629 title.className = `${ROOT_CLASS$3}__title`;
22630 title.textContent = options.title ?? "New folder";
22631 dialog2.appendChild(title);
22632 const label = document.createElement("label");
22633 label.className = `${ROOT_CLASS$3}__label`;
22634 label.htmlFor = `${ROOT_CLASS$3}-input`;
22635 label.textContent = options.label ?? "Folder name";
22636 dialog2.appendChild(label);
22637 const input = document.createElement("input");
22638 input.type = "text";
22639 input.id = `${ROOT_CLASS$3}-input`;
22640 input.className = `${ROOT_CLASS$3}__input`;
22641 input.value = initial;
22642 input.setAttribute("autocomplete", "off");
22643 input.setAttribute("spellcheck", "false");
22644 dialog2.appendChild(input);
22645 const error = document.createElement("p");
22646 error.className = `${ROOT_CLASS$3}__error`;
22647 error.hidden = true;
22648 error.setAttribute("role", "alert");
22649 dialog2.appendChild(error);
22650 const actions = document.createElement("div");
22651 actions.className = `${ROOT_CLASS$3}__actions`;
22652 const cancel = document.createElement("button");
22653 cancel.type = "button";
22654 cancel.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--secondary`;
22655 cancel.textContent = "Cancel";
22656 const submit = document.createElement("button");
22657 submit.type = "button";
22658 submit.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--primary`;
22659 submit.textContent = options.submitLabel ?? "Create";
22660 actions.appendChild(cancel);
22661 actions.appendChild(submit);
22662 dialog2.appendChild(actions);
22663 overlay.appendChild(dialog2);
22664 document.body.appendChild(overlay);
22665 active$1 = overlay;
22666 input.focus();
22667 input.select();
22668 doAction("desktop-mode.files.create-folder.opened", {});
22669 const setBusy = (busy) => {
22670 input.disabled = busy;
22671 cancel.disabled = busy;
22672 submit.disabled = busy;
22673 dialog2.classList.toggle(`${ROOT_CLASS$3}--busy`, busy);
22674 };
22675 const showError = (msg) => {
22676 error.textContent = msg;
22677 error.hidden = false;
22678 };
22679 const doCancel = () => {
22680 closeCreateFolderDialog();
22681 options.onCancel?.();
22682 };
22683 const doSubmit = async () => {
22684 const name = input.value.trim();
22685 if (!name) {
22686 showError("Please enter a name.");
22687 input.focus();
22688 return;
22689 }
22690 error.hidden = true;
22691 setBusy(true);
22692 try {
22693 await options.onSubmit(name);
22694 closeCreateFolderDialog();
22695 } catch (err) {
22696 setBusy(false);
22697 showError(
22698 err instanceof Error ? err.message : "Could not create the folder."
22699 );
22700 input.focus();
22701 input.select();
22702 }
22703 };
22704 cancel.addEventListener("click", () => doCancel());
22705 submit.addEventListener("click", () => void doSubmit());
22706 overlay.addEventListener("click", (e) => {
22707 if (e.target === overlay) {
22708 doCancel();
22709 }
22710 });
22711 const onKey = (e) => {
22712 if (e.key === "Escape") {
22713 e.preventDefault();
22714 doCancel();
22715 } else if (e.key === "Enter" && !e.isComposing) {
22716 e.preventDefault();
22717 void doSubmit();
22718 }
22719 };
22720 dialog2.addEventListener("keydown", onKey);
22721 overlay.addEventListener("create-folder-dialog-closed", () => {
22722 dialog2.removeEventListener("keydown", onKey);
22723 });
22724 }
22725 const GRID_PADDING = 16;
22726 const GRID_CELL_W = 96;
22727 const GRID_CELL_H = 110;
22728 function pointToCell(x, y) {
22729 const col = Math.max(0, Math.round((x - GRID_PADDING) / GRID_CELL_W));
22730 const row = Math.max(0, Math.round((y - GRID_PADDING) / GRID_CELL_H));
22731 return cellToPos(col, row);
22732 }
22733 function cellToPos(col, row) {
22734 return {
22735 col,
22736 row,
22737 x: GRID_PADDING + col * GRID_CELL_W,
22738 y: GRID_PADDING + row * GRID_CELL_H
22739 };
22740 }
22741 function snapToEmptyCell(x, y, occupied, host) {
22742 const target2 = pointToCell(x, y);
22743 if (!occupied.has(cellKey(target2.col, target2.row))) {
22744 return target2;
22745 }
22746 const maxRows = host ? Math.max(1, Math.floor((host.clientHeight - GRID_PADDING) / GRID_CELL_H)) : 999;
22747 for (let col = 0; col < 999; col++) {
22748 for (let row = 0; row < maxRows; row++) {
22749 if (!occupied.has(cellKey(col, row))) {
22750 return cellToPos(col, row);
22751 }
22752 }
22753 }
22754 return target2;
22755 }
22756 function nextRowMajorCell(occupied, host) {
22757 const cols = host ? Math.max(
22758 1,
22759 Math.floor((host.clientWidth - GRID_PADDING) / GRID_CELL_W)
22760 ) : 4;
22761 const maxCols = Math.max(1, cols);
22762 for (let row = 0; row < 999; row++) {
22763 for (let col = 0; col < maxCols; col++) {
22764 if (!occupied.has(cellKey(col, row))) {
22765 return cellToPos(col, row);
22766 }
22767 }
22768 }
22769 return cellToPos(0, 0);
22770 }
22771 function buildOccupiedSet(placements, excludeId) {
22772 const out = /* @__PURE__ */ new Set();
22773 for (const p of placements) {
22774 const cell = pointToCell(p.x, p.y);
22775 out.add(cellKey(cell.col, cell.row));
22776 }
22777 return out;
22778 }
22779 function cellKey(col, row) {
22780 return `${col},${row}`;
22781 }
22782 function isConflict(err) {
22783 return err instanceof FilesConflictError;
22784 }
22785 function buildReason(err) {
22786 const actor = err.detail.actor.name || "Someone else";
22787 const where = err.detail.current.parentName || "another folder";
22788 if (err.detail.reason === "trashed") {
22789 return "This item is in the recycle bin.";
22790 }
22791 if (err.detail.reason === "forbidden") {
22792 return "You no longer have access.";
22793 }
22794 if (err.detail.reason === "gone") {
22795 return "This item was deleted.";
22796 }
22797 return `${actor} moved this to "${where}".`;
22798 }
22799 function showConflictToast(err) {
22800 const reason = buildReason(err);
22801 const targetParentId = err.detail.current.parentId;
22802 let action;
22803 if (targetParentId > 0) {
22804 action = {
22805 label: "View folder",
22806 onClick: () => {
22807 const winId = `desktop-mode-folder-${targetParentId}`;
22808 const mgr = window.desktopMode?.windowManager;
22809 if (mgr?.focus) {
22810 const w = mgr.focus(winId);
22811 if (w) {
22812 return;
22813 }
22814 }
22815 if (mgr?.open) {
22816 void mgr.open(winId);
22817 }
22818 }
22819 };
22820 }
22821 showToast({
22822 message: reason,
22823 action,
22824 duration: 7e3
22825 });
22826 }
22827 function broadcastFilesChange(kind, action, ids) {
22828 const api = window.wp?.desktop;
22829 api?.broadcast?.(`desktop-mode.${kind}.changed`, {
22830 source: "desktop-files",
22831 action,
22832 ids
22833 });
22834 }
22835 function showTrashErrorToast(err) {
22836 const api = window.wp?.desktop;
22837 if (!api?.showToast) {
22838 return;
22839 }
22840 const raw = err instanceof Error ? err.message : String(err);
22841 const friendly = raw.replace(/^\[desktop-mode\][^:]*:\s*/, "").replace(/^desktop_mode_files_[a-z_]+\s*/, "");
22842 api.showToast({
22843 message: friendly || "Could not move this item to the recycle bin.",
22844 duration: 5e3
22845 });
22846 }
22847 function showTrashedToast(message, onUndo) {
22848 const api = window.wp?.desktop;
22849 if (!api?.showToast) {
22850 return;
22851 }
22852 api.showToast({
22853 message,
22854 duration: 6e3,
22855 action: {
22856 label: "Undo",
22857 onClick: onUndo
22858 }
22859 });
22860 }
22861 async function trashPlacementWithUndo(placement) {
22862 const placementId = placement.id;
22863 const parentId = placement.parentId;
22864 const title = placement.file?.title ?? "Item";
22865 const kind = placement.file?.type === "shortcut" ? "shortcut" : "placement";
22866 store.removePlacement(placementId);
22867 try {
22868 await deletePlacement(placementId);
22869 broadcastFilesChange(kind, "trashed", [placementId]);
22870 showTrashedToast(`"${title}" moved to Trash`, async () => {
22871 try {
22872 await restoreTrashedItem(placementId, "placement");
22873 const res = await listPlacements(parentId);
22874 store.setFolderPlacements(parentId, res.placements);
22875 broadcastFilesChange(kind, "untrashed", [placementId]);
22876 } catch (err) {
22877 console.error("[desktop-mode] restore failed:", err);
22878 }
22879 });
22880 } catch (err) {
22881 console.error("[desktop-mode] deletePlacement failed:", err);
22882 showTrashErrorToast(err);
22883 void listPlacements(parentId).then((res) => {
22884 store.setFolderPlacements(parentId, res.placements);
22885 });
22886 }
22887 }
22888 async function trashFolderWithUndo(placement) {
22889 const folderId = parseInt(placement.file.ref, 10);
22890 if (!folderId) {
22891 return;
22892 }
22893 const placementId = placement.id;
22894 const parentId = placement.parentId;
22895 const title = placement.file?.title ?? "Folder";
22896 store.removePlacement(placementId);
22897 store.removeFolder(folderId);
22898 try {
22899 await deleteFolder(folderId);
22900 broadcastFilesChange("folder", "trashed", [folderId]);
22901 showTrashedToast(`"${title}" moved to Trash`, async () => {
22902 try {
22903 await restoreTrashedItem(folderId, "folder");
22904 const res = await listPlacements(parentId);
22905 store.setFolderPlacements(parentId, res.placements);
22906 broadcastFilesChange("folder", "untrashed", [folderId]);
22907 } catch (err) {
22908 console.error("[desktop-mode] restore folder failed:", err);
22909 }
22910 });
22911 } catch (err) {
22912 console.error("[desktop-mode] deleteFolder failed:", err);
22913 showTrashErrorToast(err);
22914 void listPlacements(parentId).then((res) => {
22915 store.setFolderPlacements(parentId, res.placements);
22916 });
22917 }
22918 }
22919 function trashByFileType(placement) {
22920 if (placement.file?.type === "folder") {
22921 return trashFolderWithUndo(placement);
22922 }
22923 return trashPlacementWithUndo(placement);
22924 }
22925 function buildBridgePayloadFromPlacement(placement) {
22926 const file = placement.file;
22927 if (!file) {
22928 return void 0;
22929 }
22930 const id = parseInt(String(file.ref ?? ""), 10);
22931 if (!Number.isFinite(id) || id <= 0) {
22932 return void 0;
22933 }
22934 const title = String(file.title ?? "");
22935 if (file.type === "attachment") {
22936 const url = String(file.sourceUrl ?? file.previewUrl ?? "");
22937 return {
22938 kind: "attachment",
22939 id,
22940 url,
22941 title,
22942 alt: String(file.alt ?? ""),
22943 mime: String(file.mime ?? ""),
22944 thumbnailUrl: file.previewUrl ? String(file.previewUrl) : void 0
22945 };
22946 }
22947 if (file.type === "post") {
22948 return {
22949 kind: "post",
22950 id,
22951 postType: String(file.postType ?? "post"),
22952 url: String(file.link ?? ""),
22953 title
22954 };
22955 }
22956 if (file.type === "user") {
22957 return {
22958 kind: "user",
22959 id,
22960 url: String(file.link ?? ""),
22961 title
22962 };
22963 }
22964 return void 0;
22965 }
22966 function getDragManager() {
22967 const api = window.wp?.desktop?.dragManager;
22968 return api ?? null;
22969 }
22970 const LAYER_CLASS = "desktop-mode-files-layer";
22971 function mountFilesLayer(host, folderId = 0) {
22972 const container = document.createElement("div");
22973 container.className = LAYER_CLASS;
22974 container.setAttribute("role", "list");
22975 container.dataset.folderId = String(folderId);
22976 host.appendChild(container);
22977 let lastFingerprint = "";
22978 let selectedId = null;
22979 const selectionListeners = /* @__PURE__ */ new Set();
22980 const notifySelection = (placement) => {
22981 for (const cb of selectionListeners) {
22982 try {
22983 cb(placement);
22984 } catch (err) {
22985 console.error(
22986 "[desktop-mode] files: selection listener threw:",
22987 err
22988 );
22989 }
22990 }
22991 };
22992 const setSelected = (placement) => {
22993 const newId = placement ? placement.id : null;
22994 if (newId === selectedId) {
22995 return;
22996 }
22997 container.querySelectorAll(`.${TILE_CLASS}--selected`).forEach((n) => n.removeAttribute("selected"));
22998 if (placement) {
22999 const tile2 = container.querySelector(
23000 `[data-placement-id="${placement.id}"]`
23001 );
23002 tile2?.setAttribute("selected", "");
23003 }
23004 selectedId = newId;
23005 notifySelection(placement);
23006 };
23007 const computeLayout = (list2) => {
23008 const pinnedSlots = /* @__PURE__ */ new Map();
23009 const occupiedCells = /* @__PURE__ */ new Set();
23010 let pinnedIdx = 0;
23011 for (const placement of list2) {
23012 if (!isPinned(placement)) {
23013 continue;
23014 }
23015 const slot = cellToPos(0, pinnedIdx);
23016 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
23017 occupiedCells.add(cellKey(slot.col, slot.row));
23018 pinnedIdx += 1;
23019 }
23020 const displaced = /* @__PURE__ */ new Map();
23021 for (const placement of list2) {
23022 if (pinnedSlots.has(placement.id)) {
23023 continue;
23024 }
23025 const target2 = pointToCell(placement.x, placement.y);
23026 const key = cellKey(target2.col, target2.row);
23027 if (!occupiedCells.has(key)) {
23028 occupiedCells.add(key);
23029 continue;
23030 }
23031 const free = snapToEmptyCell(
23032 placement.x,
23033 placement.y,
23034 occupiedCells,
23035 host
23036 );
23037 occupiedCells.add(cellKey(free.col, free.row));
23038 displaced.set(placement.id, { x: free.x, y: free.y });
23039 }
23040 return { pinnedSlots, displaced };
23041 };
23042 const applyTilePosition = (tile2, placement, pinnedSlots, displaced) => {
23043 const pinned = pinnedSlots.get(placement.id);
23044 const moved = displaced.get(placement.id);
23045 if (pinned) {
23046 setTilePosition(tile2, pinned.x, pinned.y);
23047 } else if (moved) {
23048 setTilePosition(tile2, moved.x, moved.y);
23049 } else {
23050 setTilePosition(tile2, placement.x, placement.y);
23051 }
23052 };
23053 const wireTile = (placement, pinnedSlots, displaced) => {
23054 const tile2 = buildTile(placement, folderId);
23055 const pinnedSlot = pinnedSlots.get(placement.id);
23056 if (pinnedSlot) {
23057 setTilePosition(tile2, pinnedSlot.x, pinnedSlot.y);
23058 tile2.classList.add(`${TILE_CLASS}--pinned`);
23059 attachContextMenu(tile2, placement);
23060 attachSelectOnClick(tile2, placement);
23061 if (shouldRejectTileDrops(placement)) {
23062 const dragManager = getDragManager();
23063 if (dragManager) {
23064 const deregister = dragManager.registerDropTarget({
23065 id: `desktop-mode-files-tile-${placement.id}-reject`,
23066 element: tile2,
23067 accept: () => false,
23068 onDrop: () => {
23069 }
23070 });
23071 tileRejectDeregisters.set(placement.id, deregister);
23072 }
23073 }
23074 return tile2;
23075 }
23076 const moved = displaced.get(placement.id);
23077 if (moved) {
23078 setTilePosition(tile2, moved.x, moved.y);
23079 }
23080 attachTileDrag(tile2, placement, folderId);
23081 attachContextMenu(tile2, placement);
23082 attachSelectOnClick(tile2, placement);
23083 if (placement.file.type === "folder") {
23084 const targetFolderId = parseInt(placement.file.ref, 10);
23085 if (targetFolderId > 0) {
23086 const dragManager = getDragManager();
23087 if (dragManager) {
23088 const deregister = registerFolderDropTarget(
23089 dragManager,
23090 tile2,
23091 targetFolderId
23092 );
23093 folderDropDeregisters.set(placement.id, deregister);
23094 }
23095 }
23096 } else if (shouldRejectTileDrops(placement)) {
23097 const dragManager = getDragManager();
23098 if (dragManager) {
23099 const deregister = dragManager.registerDropTarget({
23100 id: `desktop-mode-files-tile-${placement.id}-reject`,
23101 element: tile2,
23102 accept: () => false,
23103 onDrop: () => {
23104 }
23105 });
23106 tileRejectDeregisters.set(placement.id, deregister);
23107 }
23108 }
23109 return tile2;
23110 };
23111 const tryPatchIncremental = (list2) => {
23112 const existing = /* @__PURE__ */ new Map();
23113 for (const tile2 of container.querySelectorAll(
23114 "[data-placement-id]"
23115 )) {
23116 const raw = tile2.dataset.placementId ?? "";
23117 const id = parseInt(raw, 10);
23118 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
23119 return false;
23120 }
23121 existing.set(id, tile2);
23122 }
23123 const wantIds = /* @__PURE__ */ new Set();
23124 for (const placement of list2) {
23125 wantIds.add(placement.id);
23126 }
23127 for (const placement of list2) {
23128 const tile2 = existing.get(placement.id);
23129 if (!tile2) {
23130 continue;
23131 }
23132 if (tile2.dataset.fileType !== placement.file.type) {
23133 return false;
23134 }
23135 if (tile2.dataset.fileRef !== placement.file.ref) {
23136 return false;
23137 }
23138 const wasPinned = tile2.classList.contains(
23139 `${TILE_CLASS}--pinned`
23140 );
23141 if (wasPinned !== isPinned(placement)) {
23142 return false;
23143 }
23144 }
23145 for (const [id, tile2] of existing) {
23146 if (wantIds.has(id)) {
23147 continue;
23148 }
23149 const folderDereg = folderDropDeregisters.get(id);
23150 if (folderDereg) {
23151 try {
23152 folderDereg();
23153 } catch {
23154 }
23155 folderDropDeregisters.delete(id);
23156 }
23157 const rejectDereg = tileRejectDeregisters.get(id);
23158 if (rejectDereg) {
23159 try {
23160 rejectDereg();
23161 } catch {
23162 }
23163 tileRejectDeregisters.delete(id);
23164 }
23165 tile2.remove();
23166 }
23167 const { pinnedSlots, displaced } = computeLayout(list2);
23168 for (const placement of list2) {
23169 const tile2 = existing.get(placement.id);
23170 if (tile2) {
23171 applyTilePosition(tile2, placement, pinnedSlots, displaced);
23172 continue;
23173 }
23174 container.appendChild(
23175 wireTile(placement, pinnedSlots, displaced)
23176 );
23177 }
23178 if (selectedId !== null && !container.querySelector(
23179 `[data-placement-id="${selectedId}"]`
23180 )) {
23181 selectedId = null;
23182 notifySelection(null);
23183 }
23184 doAction("desktop-mode.files.grid-rendered", {
23185 folderId,
23186 count: list2.length
23187 });
23188 return true;
23189 };
23190 const repaint = (state2) => {
23191 const raw = state2.placementsByFolder.get(folderId) ?? [];
23192 const list2 = raw.slice().sort((a, b) => {
23193 const ap = isPinned(a) ? 0 : 1;
23194 const bp = isPinned(b) ? 0 : 1;
23195 return ap - bp;
23196 });
23197 const fp = fingerprint(list2);
23198 if (fp === lastFingerprint) {
23199 return;
23200 }
23201 lastFingerprint = fp;
23202 if (tryPatchPositions(list2, container, host)) {
23203 return;
23204 }
23205 if (tryPatchIncremental(list2)) {
23206 return;
23207 }
23208 container.replaceChildren();
23209 for (const [, deregister] of folderDropDeregisters) {
23210 try {
23211 deregister();
23212 } catch {
23213 }
23214 }
23215 folderDropDeregisters.clear();
23216 for (const [, deregister] of tileRejectDeregisters) {
23217 try {
23218 deregister();
23219 } catch {
23220 }
23221 }
23222 tileRejectDeregisters.clear();
23223 const { pinnedSlots, displaced } = computeLayout(list2);
23224 for (const placement of list2) {
23225 container.appendChild(
23226 wireTile(placement, pinnedSlots, displaced)
23227 );
23228 }
23229 if (selectedId !== null && !container.querySelector(`[data-placement-id="${selectedId}"]`)) {
23230 selectedId = null;
23231 notifySelection(null);
23232 } else if (selectedId !== null) {
23233 const tile2 = container.querySelector(
23234 `[data-placement-id="${selectedId}"]`
23235 );
23236 tile2?.setAttribute("selected", "");
23237 }
23238 doAction("desktop-mode.files.grid-rendered", {
23239 folderId,
23240 count: list2.length
23241 });
23242 };
23243 const dropTargetDeregisters = [];
23244 const folderDropDeregisters = /* @__PURE__ */ new Map();
23245 const tileRejectDeregisters = /* @__PURE__ */ new Map();
23246 let dropPreviewEl = null;
23247 let dropPreviewMoveHandler = null;
23248 const installCanvasDropPreview = (session) => {
23249 if (dropPreviewEl) {
23250 return;
23251 }
23252 if (session.payload.type !== "desktop-file") {
23253 return;
23254 }
23255 const previewEl = document.createElement("div");
23256 previewEl.className = "desktop-mode-files-drop-preview";
23257 previewEl.setAttribute("aria-hidden", "true");
23258 container.appendChild(previewEl);
23259 dropPreviewEl = previewEl;
23260 const ghost = session.payload.ghost;
23261 const offsetX = ghost?.offsetX ?? 0;
23262 const offsetY = ghost?.offsetY ?? 0;
23263 const data = session.payload.data;
23264 const movingId = data?.placement?.id;
23265 const updatePreview = (clientX, clientY) => {
23266 const rect = container.getBoundingClientRect();
23267 const rawX = Math.max(0, clientX - rect.left - offsetX);
23268 const rawY = Math.max(0, clientY - rect.top - offsetY);
23269 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
23270 const occupied = buildVisualOccupiedSet(peers, movingId);
23271 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
23272 previewEl.style.transform = `translate3d(${cell.x}px, ${cell.y}px, 0)`;
23273 };
23274 const sourceRect = session.payload.source.getBoundingClientRect();
23275 updatePreview(
23276 sourceRect.left + offsetX,
23277 sourceRect.top + offsetY
23278 );
23279 const moveHandler = (ev) => {
23280 updatePreview(ev.clientX, ev.clientY);
23281 };
23282 document.addEventListener("pointermove", moveHandler);
23283 dropPreviewMoveHandler = moveHandler;
23284 };
23285 const teardownCanvasDropPreview = () => {
23286 if (dropPreviewMoveHandler) {
23287 document.removeEventListener("pointermove", dropPreviewMoveHandler);
23288 dropPreviewMoveHandler = null;
23289 }
23290 if (dropPreviewEl) {
23291 dropPreviewEl.remove();
23292 dropPreviewEl = null;
23293 }
23294 };
23295 const canvasDropTarget = {
23296 id: `desktop-mode-files-canvas-${folderId}`,
23297 element: host,
23298 accept: (payload) => {
23299 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
23300 return false;
23301 }
23302 if (folderId > 0 && payload.type === "desktop-file") {
23303 const data = payload.data;
23304 if (data.placement.file?.type === "folder") {
23305 const movingFolderId = parseInt(data.placement.file.ref, 10);
23306 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, folderId)) {
23307 return false;
23308 }
23309 }
23310 }
23311 return true;
23312 },
23313 onEnter: (session) => {
23314 host.setAttribute("data-files-drop-active", "");
23315 installCanvasDropPreview(session);
23316 },
23317 onLeave: () => {
23318 host.removeAttribute("data-files-drop-active");
23319 teardownCanvasDropPreview();
23320 },
23321 onDrop: (session, ev) => {
23322 host.removeAttribute("data-files-drop-active");
23323 teardownCanvasDropPreview();
23324 const rect = container.getBoundingClientRect();
23325 const ghost = session.payload.ghost;
23326 const offsetX = ghost?.offsetX ?? 0;
23327 const offsetY = ghost?.offsetY ?? 0;
23328 const rawX = Math.max(0, ev.clientX - rect.left - offsetX);
23329 const rawY = Math.max(0, ev.clientY - rect.top - offsetY);
23330 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
23331 if (session.payload.type === "desktop-file") {
23332 const data = session.payload.data;
23333 const occupied = buildVisualOccupiedSet(peers, data.placement.id);
23334 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
23335 const next = {
23336 ...data.placement,
23337 x: cell.x,
23338 y: cell.y,
23339 parentId: folderId
23340 };
23341 store.upsertPlacement(next);
23342 doAction("desktop-mode.files.tile-manually-placed", {
23343 folderId,
23344 placementId: data.placement.id
23345 });
23346 if (isSyntheticPlacement(data.placement)) {
23347 const dockItemId = readSynthSource(data.placement);
23348 if (dockItemId) {
23349 persistDockPromotedPosition(
23350 dockItemId,
23351 cell.x,
23352 cell.y
23353 );
23354 }
23355 return;
23356 }
23357 void updatePlacement(
23358 data.placement.id,
23359 {
23360 x: cell.x,
23361 y: cell.y,
23362 parentId: folderId
23363 },
23364 data.placement.updatedAtMs
23365 ).then((server) => {
23366 store.upsertPlacement(server, "remote");
23367 }).catch((err) => {
23368 if (isConflict(err)) {
23369 showConflictToast(err);
23370 } else {
23371 console.error(
23372 "[desktop-mode] files: drag persist failed",
23373 err
23374 );
23375 }
23376 store.upsertPlacement(data.placement);
23377 });
23378 return;
23379 }
23380 if (session.payload.type === "shortcut") {
23381 const data = session.payload.data;
23382 const occupied = buildVisualOccupiedSet(peers);
23383 const cell = nextRowMajorCell(occupied, host);
23384 void createPlacement({
23385 parentId: folderId,
23386 type: data.kind,
23387 ref: data.ref,
23388 x: cell.x,
23389 y: cell.y
23390 }).then((placement) => {
23391 store.upsertPlacement(placement);
23392 doAction("desktop-mode.files.shortcut-dropped", {
23393 folderId,
23394 placement
23395 });
23396 }).catch((err) => {
23397 console.error(
23398 "[desktop-mode] shortcut drop failed:",
23399 err
23400 );
23401 });
23402 }
23403 }
23404 };
23405 const dragManagerForLayer = getDragManager();
23406 if (dragManagerForLayer) {
23407 dropTargetDeregisters.push(
23408 dragManagerForLayer.registerDropTarget(canvasDropTarget)
23409 );
23410 }
23411 const onCanvasClick = (e) => {
23412 if (e.target instanceof Element && e.target.closest(`.${TILE_CLASS}`)) {
23413 return;
23414 }
23415 setSelected(null);
23416 };
23417 host.addEventListener("click", onCanvasClick);
23418 function attachSelectOnClick(tile2, placement) {
23419 tile2.addEventListener("click", (e) => {
23420 e.stopPropagation();
23421 setSelected(placement);
23422 });
23423 }
23424 repaint(store.getState());
23425 const off = store.subscribe(repaint);
23426 let resolveHydrated = () => void 0;
23427 const hydrated = new Promise((resolve2) => {
23428 resolveHydrated = resolve2;
23429 });
23430 if (!store.getState().hydratedFolders.has(folderId)) {
23431 void listPlacements(folderId).then((res) => {
23432 store.setFolderPlacements(folderId, res.placements);
23433 }).catch((err) => {
23434 console.error("[desktop-mode] files: failed to hydrate folder", folderId, err);
23435 }).finally(() => {
23436 resolveHydrated();
23437 });
23438 } else {
23439 queueMicrotask(resolveHydrated);
23440 }
23441 const colsForWidth = () => {
23442 const w = host.clientWidth > 0 ? host.clientWidth : 4 * GRID_CELL_W;
23443 return Math.max(1, Math.floor((w - GRID_PADDING) / GRID_CELL_W));
23444 };
23445 const sortPlacements = (list2, mode) => {
23446 const sorted = list2.slice();
23447 switch (mode) {
23448 case "name-asc":
23449 sorted.sort(
23450 (a, b) => a.file.title.localeCompare(b.file.title)
23451 );
23452 break;
23453 case "name-desc":
23454 sorted.sort(
23455 (a, b) => b.file.title.localeCompare(a.file.title)
23456 );
23457 break;
23458 case "date-asc":
23459 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
23460 break;
23461 case "date-desc":
23462 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
23463 break;
23464 }
23465 return sorted;
23466 };
23467 const sort = (mode) => {
23468 const live = store.getState().placementsByFolder.get(folderId);
23469 if (!live || live.length === 0) {
23470 return;
23471 }
23472 const pinned = live.filter((p) => isPinned(p));
23473 const draggable = live.filter((p) => !isPinned(p));
23474 const sorted = sortPlacements(draggable, mode);
23475 const cols = colsForWidth();
23476 const occupied = /* @__PURE__ */ new Set();
23477 for (let i = 0; i < pinned.length; i += 1) {
23478 occupied.add(cellKey(0, i));
23479 }
23480 let idx = 0;
23481 const nextCell = () => {
23482 while (true) {
23483 const row = Math.floor(idx / cols);
23484 const col = idx % cols;
23485 idx += 1;
23486 if (!occupied.has(cellKey(col, row))) {
23487 return { col, row };
23488 }
23489 }
23490 };
23491 sorted.forEach((p, i) => {
23492 const cell = nextCell();
23493 const x = GRID_PADDING + cell.col * GRID_CELL_W;
23494 const y = GRID_PADDING + cell.row * GRID_CELL_H;
23495 const next = {
23496 ...p,
23497 x,
23498 y,
23499 sortOrder: i
23500 };
23501 store.upsertPlacement(next);
23502 if (isSyntheticPlacement(p)) {
23503 return;
23504 }
23505 void updatePlacement(p.id, { x, y, sortOrder: i }).catch((err) => {
23506 console.error(
23507 "[desktop-mode] files: sort persist failed",
23508 err
23509 );
23510 });
23511 });
23512 };
23513 const reflow = () => {
23514 const live = store.getState().placementsByFolder.get(folderId);
23515 if (!live || live.length === 0) {
23516 return;
23517 }
23518 const w = host.clientWidth > 0 ? host.clientWidth : Infinity;
23519 const overflowing = live.some((p) => {
23520 const right = p.x + GRID_CELL_W;
23521 return right > w;
23522 });
23523 if (!overflowing) {
23524 return;
23525 }
23526 const cols = colsForWidth();
23527 const pinned = live.filter((p) => isPinned(p));
23528 const draggable = live.filter((p) => !isPinned(p));
23529 const occupied = /* @__PURE__ */ new Set();
23530 for (let i = 0; i < pinned.length; i += 1) {
23531 occupied.add(cellKey(0, i));
23532 }
23533 let idx = 0;
23534 const nextCell = () => {
23535 while (true) {
23536 const row = Math.floor(idx / cols);
23537 const col = idx % cols;
23538 idx += 1;
23539 if (!occupied.has(cellKey(col, row))) {
23540 return { col, row };
23541 }
23542 }
23543 };
23544 for (const p of draggable) {
23545 const cell = nextCell();
23546 const x = GRID_PADDING + cell.col * GRID_CELL_W;
23547 const y = GRID_PADDING + cell.row * GRID_CELL_H;
23548 const tile2 = container.querySelector(
23549 `[data-placement-id="${p.id}"]`
23550 );
23551 if (tile2) {
23552 setTilePosition(tile2, x, y);
23553 }
23554 }
23555 };
23556 let lastWidth = host.clientWidth;
23557 let resizeObserver = null;
23558 if (typeof ResizeObserver !== "undefined") {
23559 resizeObserver = new ResizeObserver(() => {
23560 const w = host.clientWidth;
23561 if (w === lastWidth) {
23562 return;
23563 }
23564 lastWidth = w;
23565 reflow();
23566 });
23567 resizeObserver.observe(host);
23568 }
23569 return {
23570 host,
23571 folderId,
23572 onSelectionChange(cb) {
23573 selectionListeners.add(cb);
23574 return () => {
23575 selectionListeners.delete(cb);
23576 };
23577 },
23578 sort,
23579 reflow,
23580 hydrated,
23581 dispose() {
23582 off();
23583 resizeObserver?.disconnect();
23584 resizeObserver = null;
23585 for (const deregister of dropTargetDeregisters) {
23586 try {
23587 deregister();
23588 } catch {
23589 }
23590 }
23591 dropTargetDeregisters.length = 0;
23592 for (const deregister of folderDropDeregisters.values()) {
23593 try {
23594 deregister();
23595 } catch {
23596 }
23597 }
23598 folderDropDeregisters.clear();
23599 for (const deregister of tileRejectDeregisters.values()) {
23600 try {
23601 deregister();
23602 } catch {
23603 }
23604 }
23605 tileRejectDeregisters.clear();
23606 host.removeEventListener("click", onCanvasClick);
23607 selectionListeners.clear();
23608 container.remove();
23609 }
23610 };
23611 }
23612 function fingerprint(list2) {
23613 if (list2.length === 0) {
23614 return "0";
23615 }
23616 const parts = [];
23617 for (const p of list2) {
23618 parts.push(
23619 `${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}`
23620 );
23621 }
23622 return parts.join("|");
23623 }
23624 function isPinned(placement) {
23625 return Boolean(placement.file.pinned);
23626 }
23627 function readSynthSource(placement) {
23628 const meta = placement.meta;
23629 if (!meta || typeof meta !== "object") {
23630 return null;
23631 }
23632 const v = meta.__synthFromDockItem;
23633 return typeof v === "string" && v !== "" ? v : null;
23634 }
23635 function isSyntheticPlacement(placement) {
23636 return placement.id <= 0 || readSynthSource(placement) !== null;
23637 }
23638 const RECYCLE_BIN_REF = "desktop-mode-recycle-bin";
23639 function shouldRejectTileDrops(placement) {
23640 if (placement.file?.type === "folder") {
23641 return false;
23642 }
23643 if (placement.file?.ref === RECYCLE_BIN_REF) {
23644 return false;
23645 }
23646 return true;
23647 }
23648 function buildVisualOccupiedSet(placements, excludeId) {
23649 const sorted = placements.slice().sort((a, b) => {
23650 const ap = isPinned(a) ? 0 : 1;
23651 const bp = isPinned(b) ? 0 : 1;
23652 return ap - bp;
23653 });
23654 const set = /* @__PURE__ */ new Set();
23655 let pinnedIdx = 0;
23656 for (const p of sorted) {
23657 if (excludeId !== void 0 && p.id === excludeId) {
23658 continue;
23659 }
23660 if (isPinned(p)) {
23661 set.add(cellKey(0, pinnedIdx));
23662 pinnedIdx += 1;
23663 } else {
23664 const cell = pointToCell(p.x, p.y);
23665 set.add(cellKey(cell.col, cell.row));
23666 }
23667 }
23668 return set;
23669 }
23670 function wouldCreateFolderCycle(movingFolderId, targetParentId) {
23671 if (targetParentId <= 0 || movingFolderId <= 0) {
23672 return false;
23673 }
23674 if (movingFolderId === targetParentId) {
23675 return true;
23676 }
23677 const parentByFolderId = /* @__PURE__ */ new Map();
23678 const state2 = store.getState();
23679 for (const bucket2 of state2.placementsByFolder.values()) {
23680 for (const p of bucket2) {
23681 if (p.file?.type !== "folder") {
23682 continue;
23683 }
23684 const fid = parseInt(p.file.ref, 10);
23685 if (Number.isNaN(fid) || fid <= 0) {
23686 continue;
23687 }
23688 if (!parentByFolderId.has(fid)) {
23689 parentByFolderId.set(fid, p.parentId);
23690 }
23691 }
23692 }
23693 const visited = /* @__PURE__ */ new Set();
23694 let cursor = targetParentId;
23695 let maxDepth = 256;
23696 while (cursor > 0 && maxDepth-- > 0) {
23697 if (cursor === movingFolderId) {
23698 return true;
23699 }
23700 if (visited.has(cursor)) {
23701 return true;
23702 }
23703 visited.add(cursor);
23704 const next = parentByFolderId.get(cursor);
23705 if (next === void 0) {
23706 return false;
23707 }
23708 cursor = next;
23709 }
23710 return false;
23711 }
23712 function persistDockPromotedPosition(dockItemId, x, y) {
23713 const api = window.wp?.desktop;
23714 if (!api?.getOsSettings || !api?.updateOsSettings) {
23715 return;
23716 }
23717 const current = api.getOsSettings().dockPromotedPositions ?? {};
23718 api.updateOsSettings({
23719 dockPromotedPositions: {
23720 ...current,
23721 [dockItemId]: { x, y }
23722 }
23723 });
23724 }
23725 function tryPatchPositions(list2, container, host) {
23726 const tiles = Array.from(
23727 container.querySelectorAll("[data-placement-id]")
23728 );
23729 if (tiles.length !== list2.length) {
23730 return false;
23731 }
23732 const byId = /* @__PURE__ */ new Map();
23733 for (const tile2 of tiles) {
23734 const raw = tile2.dataset.placementId ?? "";
23735 const id = parseInt(raw, 10);
23736 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
23737 return false;
23738 }
23739 byId.set(id, tile2);
23740 }
23741 for (const placement of list2) {
23742 const tile2 = byId.get(placement.id);
23743 if (!tile2) {
23744 return false;
23745 }
23746 if (tile2.dataset.fileType !== placement.file.type) {
23747 return false;
23748 }
23749 if (tile2.dataset.fileRef !== placement.file.ref) {
23750 return false;
23751 }
23752 const wasPinned = tile2.classList.contains(`${TILE_CLASS}--pinned`);
23753 if (wasPinned !== isPinned(placement)) {
23754 return false;
23755 }
23756 }
23757 const pinnedSlots = /* @__PURE__ */ new Map();
23758 const occupiedCells = /* @__PURE__ */ new Set();
23759 let pinnedIdx = 0;
23760 for (const placement of list2) {
23761 if (!isPinned(placement)) {
23762 continue;
23763 }
23764 const slot = cellToPos(0, pinnedIdx);
23765 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
23766 occupiedCells.add(cellKey(slot.col, slot.row));
23767 pinnedIdx += 1;
23768 }
23769 const displaced = /* @__PURE__ */ new Map();
23770 for (const placement of list2) {
23771 if (pinnedSlots.has(placement.id)) {
23772 continue;
23773 }
23774 const target2 = pointToCell(placement.x, placement.y);
23775 const key = cellKey(target2.col, target2.row);
23776 if (!occupiedCells.has(key)) {
23777 occupiedCells.add(key);
23778 continue;
23779 }
23780 const free = snapToEmptyCell(
23781 placement.x,
23782 placement.y,
23783 occupiedCells,
23784 host
23785 );
23786 occupiedCells.add(cellKey(free.col, free.row));
23787 displaced.set(placement.id, { x: free.x, y: free.y });
23788 }
23789 for (const placement of list2) {
23790 const tile2 = byId.get(placement.id);
23791 if (!tile2) {
23792 continue;
23793 }
23794 const pinned = pinnedSlots.get(placement.id);
23795 const disp = displaced.get(placement.id);
23796 if (pinned) {
23797 setTilePosition(tile2, pinned.x, pinned.y);
23798 } else if (disp) {
23799 setTilePosition(tile2, disp.x, disp.y);
23800 } else {
23801 setTilePosition(tile2, placement.x, placement.y);
23802 }
23803 }
23804 return true;
23805 }
23806 function hidePromotedDockItem(dockItemId) {
23807 const api = window.wp?.desktop;
23808 if (!api?.getOsSettings || !api?.updateOsSettings) {
23809 return;
23810 }
23811 const current = api.getOsSettings().itemVisibility ?? {};
23812 const next = { ...current, [dockItemId]: "dock" };
23813 api.updateOsSettings({ itemVisibility: next });
23814 }
23815 function registerFolderDropTarget(dragManager, tile2, targetFolderId, currentFolderId) {
23816 const target2 = {
23817 id: `desktop-mode-files-folder-${targetFolderId}-tile-${tile2.dataset.placementId ?? "?"}`,
23818 element: tile2,
23819 accept: (payload) => {
23820 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
23821 return false;
23822 }
23823 if (payload.type === "desktop-file") {
23824 const data = payload.data;
23825 if (data.placement.file.type === "folder" && parseInt(data.placement.file.ref, 10) === targetFolderId) {
23826 return false;
23827 }
23828 if (data.placement.parentId === targetFolderId) {
23829 return false;
23830 }
23831 if (isSyntheticPlacement(data.placement)) {
23832 return false;
23833 }
23834 if (data.placement.file.type === "folder") {
23835 const movingFolderId = parseInt(data.placement.file.ref, 10);
23836 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, targetFolderId)) {
23837 return false;
23838 }
23839 }
23840 }
23841 return true;
23842 },
23843 onEnter: () => {
23844 tile2.classList.add(`${TILE_CLASS}--drop-target`);
23845 },
23846 onLeave: () => {
23847 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
23848 },
23849 onDrop: (session) => {
23850 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
23851 if (session.payload.type === "desktop-file") {
23852 const data = session.payload.data;
23853 const next = {
23854 ...data.placement,
23855 parentId: targetFolderId
23856 };
23857 store.upsertPlacement(next);
23858 void updatePlacement(
23859 data.placement.id,
23860 { parentId: targetFolderId },
23861 data.placement.updatedAtMs
23862 ).then((server) => {
23863 store.upsertPlacement(server, "remote");
23864 }).catch((err) => {
23865 if (isConflict(err)) {
23866 showConflictToast(err);
23867 } else {
23868 console.error(
23869 "[desktop-mode] files: move-into-folder persist failed",
23870 err
23871 );
23872 }
23873 store.upsertPlacement(data.placement);
23874 });
23875 return;
23876 }
23877 if (session.payload.type === "shortcut") {
23878 const data = session.payload.data;
23879 const peers = store.getState().placementsByFolder.get(targetFolderId) ?? [];
23880 const cell = nextRowMajorCell(buildVisualOccupiedSet(peers));
23881 void createPlacement({
23882 parentId: targetFolderId,
23883 type: data.kind,
23884 ref: data.ref,
23885 x: cell.x,
23886 y: cell.y
23887 }).then((placement) => {
23888 store.upsertPlacement(placement);
23889 doAction("desktop-mode.files.shortcut-dropped", {
23890 folderId: targetFolderId,
23891 placement
23892 });
23893 }).catch((err) => {
23894 console.error(
23895 "[desktop-mode] shortcut drop into folder failed:",
23896 err
23897 );
23898 });
23899 }
23900 }
23901 };
23902 return dragManager.registerDropTarget(target2);
23903 }
23904 function attachTileDrag(tile2, placement, folderId) {
23905 tile2.addEventListener("pointerdown", (e) => {
23906 if (e.button !== 0) {
23907 return;
23908 }
23909 const dragManager = getDragManager();
23910 if (!dragManager) {
23911 return;
23912 }
23913 const liveBucket = store.getState().placementsByFolder.get(folderId);
23914 const livePlacement = liveBucket?.find((p) => p.id === placement.id) ?? placement;
23915 parseFloat(tile2.style.left) || livePlacement.x;
23916 parseFloat(tile2.style.top) || livePlacement.y;
23917 dragManager.start({
23918 payload: {
23919 type: "desktop-file",
23920 source: tile2,
23921 data: {
23922 placement: livePlacement,
23923 sourceFolderId: folderId,
23924 // Synthesize a cross-frame bridge payload from the
23925 // placement's file shape so a wallpaper-placed
23926 // shortcut can be dropped into an open Gutenberg
23927 // iframe and inserted as the matching block. The
23928 // PHP serialize() methods (`Desktop_Mode_Post_File`,
23929 // `Desktop_Mode_User_File`, `Desktop_Mode_Attachment_File`)
23930 // surface the URL fields this needs.
23931 bridgePayload: buildBridgePayloadFromPlacement(livePlacement)
23932 },
23933 ghost: {
23934 offsetX: e.clientX - tile2.getBoundingClientRect().left,
23935 offsetY: e.clientY - tile2.getBoundingClientRect().top
23936 }
23937 },
23938 origin: e
23939 // `onClickOnly` intentionally empty — a tile click is
23940 // handled by the dedicated `attachSelectOnClick` listener
23941 // below, which fires from the regular `click` event after
23942 // a sub-threshold pointerup. The manager won't fire a
23943 // `click` itself; the browser does.
23944 });
23945 });
23946 }
23947 function attachContextMenu(tile2, placement) {
23948 tile2.addEventListener("contextmenu", (e) => {
23949 e.preventDefault();
23950 e.stopPropagation();
23951 const items = [
23952 {
23953 id: "open",
23954 label: "Open",
23955 icon: "dashicons-external",
23956 sort: 10,
23957 onClick: () => {
23958 const file = resolve(placement.file);
23959 void openFile(file);
23960 }
23961 }
23962 ];
23963 if (placement.file.type === "post") {
23964 items.push({
23965 id: "navigate-into",
23966 label: "Navigate into",
23967 icon: "dashicons-category",
23968 sort: 20,
23969 onClick: () => {
23970 const postId = parseInt(placement.file.ref, 10);
23971 if (!postId) {
23972 return;
23973 }
23974 const api = window.wp?.desktop?.myWordpress;
23975 const postType = typeof placement.file.postType === "string" ? placement.file.postType : "post";
23976 const entityId = postType === "page" ? "pages" : "posts";
23977 api?.openDetail({
23978 entityId,
23979 postId,
23980 postTitle: placement.file.title || `#${postId}`
23981 });
23982 }
23983 });
23984 }
23985 const isFolder = placement.file.type === "folder";
23986 if (isFolder) {
23987 items.push({
23988 id: "rename-folder",
23989 label: "Rename…",
23990 icon: "dashicons-edit",
23991 sort: 30,
23992 onClick: () => {
23993 const folderId = parseInt(placement.file.ref, 10);
23994 if (!folderId) {
23995 return;
23996 }
23997 openCreateFolderDialog({
23998 title: "Rename folder",
23999 label: "New name",
24000 submitLabel: "Rename",
24001 initialName: placement.file.title,
24002 onSubmit: async (name) => {
24003 const trimmed = name.trim();
24004 if (!trimmed || trimmed === placement.file.title) {
24005 return;
24006 }
24007 const previousTitle = placement.file.title;
24008 const optimistic = {
24009 ...placement,
24010 file: { ...placement.file, title: trimmed }
24011 };
24012 store.upsertPlacement(optimistic);
24013 try {
24014 const folderUpdatedAtMs = store.getState().folders.get(folderId)?.updatedAtMs ?? 0;
24015 const updated = await updateFolder(
24016 folderId,
24017 { name: trimmed },
24018 folderUpdatedAtMs
24019 );
24020 store.upsertFolder(updated);
24021 const refreshed = await listPlacements(
24022 placement.parentId
24023 );
24024 store.setFolderPlacements(
24025 placement.parentId,
24026 refreshed.placements
24027 );
24028 } catch (err) {
24029 console.error(
24030 "[desktop-mode] rename folder failed:",
24031 err
24032 );
24033 store.upsertPlacement({
24034 ...placement,
24035 file: {
24036 ...placement.file,
24037 title: previousTitle
24038 }
24039 });
24040 }
24041 }
24042 });
24043 }
24044 });
24045 if (placement.canTrash !== false) {
24046 items.push({
24047 id: "delete-folder",
24048 label: "Move folder to Trash",
24049 icon: "dashicons-trash",
24050 sort: 90,
24051 danger: true,
24052 onClick: () => trashFolderWithUndo(placement)
24053 });
24054 }
24055 } else {
24056 const synthFromDockItem = readSynthSource(placement);
24057 const isRegisteredIcon = placement.file.type === "shortcut";
24058 if (synthFromDockItem || isRegisteredIcon) {
24059 const hideId = synthFromDockItem ?? placement.file.ref;
24060 items.push({
24061 id: "hide-from-desktop",
24062 label: "Hide from desktop",
24063 icon: "dashicons-hidden",
24064 sort: 90,
24065 onClick: () => hidePromotedDockItem(hideId)
24066 });
24067 } else if (placement.canTrash !== false) {
24068 items.push({
24069 id: "remove",
24070 label: "Move to Trash",
24071 icon: "dashicons-trash",
24072 sort: 90,
24073 danger: true,
24074 onClick: () => trashPlacementWithUndo(placement)
24075 });
24076 }
24077 }
24078 openTileMenu({ x: e.clientX, y: e.clientY }, { placement, items });
24079 });
24080 }
24081 const STATUS_BAR_CLASS = "desktop-mode-folder-status-bar";
24082 const ROOT_CLASS$2 = STATUS_BAR_CLASS;
24083 function mountFolderStatusBar(host, folderId) {
24084 const bar = document.createElement("div");
24085 bar.className = ROOT_CLASS$2;
24086 bar.setAttribute("role", "status");
24087 bar.dataset.folderId = String(folderId);
24088 host.appendChild(bar);
24089 const repaint = () => {
24090 const list2 = getFilesState().placementsByFolder.get(folderId) ?? [];
24091 const folders = list2.filter((p) => p.file.type === "folder").length;
24092 const files = list2.length - folders;
24093 const ctx = {
24094 folderId,
24095 totals: { files, folders, total: list2.length }
24096 };
24097 const segments = computeSegments(ctx);
24098 render(bar, segments);
24099 };
24100 repaint();
24101 const off = subscribeFilesStore(() => repaint());
24102 return {
24103 dispose() {
24104 off();
24105 bar.remove();
24106 }
24107 };
24108 }
24109 function computeSegments(ctx) {
24110 const { folders, files } = ctx.totals;
24111 const builtIns = [
24112 {
24113 id: "count",
24114 label: pluralize(files, "file", "files") + (folders > 0 ? `, ${pluralize(folders, "folder", "folders")}` : ""),
24115 align: "start",
24116 sort: 10
24117 }
24118 ];
24119 const filtered = applyFilters(
24120 "desktop-mode.files.folder-window.status-bar",
24121 builtIns,
24122 ctx
24123 );
24124 return Array.isArray(filtered) ? filtered : builtIns;
24125 }
24126 function render(bar, segments) {
24127 const sort = (a, b) => {
24128 const sa = typeof a.sort === "number" ? a.sort : 100;
24129 const sb = typeof b.sort === "number" ? b.sort : 100;
24130 if (sa !== sb) {
24131 return sa - sb;
24132 }
24133 return a.label.localeCompare(b.label);
24134 };
24135 const start = segments.filter((s) => (s.align ?? "start") === "start").sort(sort);
24136 const end = segments.filter((s) => s.align === "end").sort(sort);
24137 bar.replaceChildren();
24138 bar.appendChild(buildCluster("start", start));
24139 bar.appendChild(buildCluster("end", end));
24140 }
24141 function buildCluster(align, segs) {
24142 const cluster = document.createElement("div");
24143 cluster.className = `${ROOT_CLASS$2}__cluster ${ROOT_CLASS$2}__cluster--${align}`;
24144 for (const seg of segs) {
24145 cluster.appendChild(buildSegment(seg));
24146 }
24147 return cluster;
24148 }
24149 function buildSegment(seg) {
24150 const interactive = typeof seg.onClick === "function";
24151 const el = document.createElement(interactive ? "button" : "span");
24152 el.className = `${ROOT_CLASS$2}__segment`;
24153 el.dataset.segmentId = seg.id;
24154 if (interactive) {
24155 el.type = "button";
24156 el.addEventListener("click", (e) => seg.onClick(e));
24157 }
24158 if (seg.icon) {
24159 const icon = document.createElement("span");
24160 icon.className = `${ROOT_CLASS$2}__icon dashicons ${seg.icon.replace(/[^a-zA-Z0-9_-]/g, "")}`;
24161 icon.setAttribute("aria-hidden", "true");
24162 el.appendChild(icon);
24163 }
24164 const label = document.createElement("span");
24165 label.className = `${ROOT_CLASS$2}__label`;
24166 label.textContent = seg.label;
24167 el.appendChild(label);
24168 return el;
24169 }
24170 function pluralize(n, singular, plural) {
24171 return `${n} ${n === 1 ? singular : plural}`;
24172 }
24173 const MENU_CLASS$1 = "desktop-mode-icon-canvas-menu";
24174 let activeMenu$1 = null;
24175 let activeFlyout = null;
24176 let activeCanvas = null;
24177 let outsideHandler = null;
24178 let escHandler = null;
24179 function attachIconCanvasMenu(canvas, deps2) {
24180 deps2.openOnBackgroundClick !== false;
24181 const onContextMenu = (e) => {
24182 if (isInsideTile(e.target) || isInsideMenu(e.target)) {
24183 return;
24184 }
24185 e.preventDefault();
24186 toggle(e.clientX, e.clientY);
24187 };
24188 let toggleGen = 0;
24189 const toggle = (x, y) => {
24190 if (activeCanvas === canvas && activeMenu$1) {
24191 closeMenu();
24192 return;
24193 }
24194 const items = buildItems(deps2);
24195 const filtered = applyFilters(
24196 "desktop-mode.icon-canvas.menu",
24197 items,
24198 deps2.scope
24199 );
24200 const finalItems = Array.isArray(filtered) ? filtered : items;
24201 const myGen = ++toggleGen;
24202 openWithShellOverlays(
24203 () => myGen === toggleGen,
24204 () => openMenu(finalItems, { x, y }, canvas)
24205 );
24206 };
24207 canvas.addEventListener("contextmenu", onContextMenu);
24208 return {
24209 dispose: () => {
24210 canvas.removeEventListener("contextmenu", onContextMenu);
24211 closeMenu();
24212 }
24213 };
24214 }
24215 function isInsideTile(target2) {
24216 if (!(target2 instanceof Element)) {
24217 return false;
24218 }
24219 return target2.closest(".desktop-mode-file-tile") !== null;
24220 }
24221 function isInsideMenu(target2) {
24222 if (!(target2 instanceof Element)) {
24223 return false;
24224 }
24225 return target2.closest(`.${MENU_CLASS$1}`) !== null;
24226 }
24227 function buildItems(deps2) {
24228 const sortItem = {
24229 id: "sort-by",
24230 label: __("Sort by", "desktop-mode"),
24231 icon: "dashicons-sort",
24232 sort: 10,
24233 children: [
24234 {
24235 id: "sort-name-asc",
24236 label: __("Name (A → Z)", "desktop-mode"),
24237 sort: 10,
24238 onClick: () => deps2.onSort("name-asc")
24239 },
24240 {
24241 id: "sort-name-desc",
24242 label: __("Name (Z → A)", "desktop-mode"),
24243 sort: 20,
24244 onClick: () => deps2.onSort("name-desc")
24245 },
24246 {
24247 id: "sort-date-desc",
24248 label: __("Newest first", "desktop-mode"),
24249 sort: 30,
24250 onClick: () => deps2.onSort("date-desc")
24251 },
24252 {
24253 id: "sort-date-asc",
24254 label: __("Oldest first", "desktop-mode"),
24255 sort: 40,
24256 onClick: () => deps2.onSort("date-asc")
24257 }
24258 ]
24259 };
24260 const items = [sortItem];
24261 if (Array.isArray(deps2.extraItems)) {
24262 items.push(...deps2.extraItems);
24263 }
24264 return items;
24265 }
24266 function sortItems(items) {
24267 return items.slice().sort((a, b) => {
24268 const sa = typeof a.sort === "number" ? a.sort : 100;
24269 const sb = typeof b.sort === "number" ? b.sort : 100;
24270 if (sa !== sb) {
24271 return sa - sb;
24272 }
24273 return a.label.localeCompare(b.label);
24274 });
24275 }
24276 function openMenu(items, pos, canvas) {
24277 closeMenu();
24278 if (items.length === 0) {
24279 return;
24280 }
24281 activeCanvas = canvas;
24282 const sorted = sortItems(items);
24283 const menu = document.createElement("wpd-context-menu");
24284 menu.setAttribute("open", "");
24285 menu.classList.add(MENU_CLASS$1);
24286 menu.style.left = `${pos.x}px`;
24287 menu.style.top = `${pos.y}px`;
24288 const itemById = /* @__PURE__ */ new Map();
24289 for (const item of sorted) {
24290 itemById.set(item.id, item);
24291 const opt = appendOption(menu, item);
24292 if (hasChildren(item)) {
24293 opt.addEventListener("mouseenter", () => {
24294 openFlyout(item, opt);
24295 });
24296 }
24297 }
24298 menu.addEventListener("wpd-context-menu-pick", (e) => {
24299 const detail = e.detail;
24300 const item = itemById.get(detail.id);
24301 if (!item) {
24302 return;
24303 }
24304 if (hasChildren(item)) {
24305 e.stopPropagation();
24306 const anchor = menu.querySelector(
24307 `[data-menu-item-id="${item.id}"]`
24308 );
24309 if (anchor) {
24310 openFlyout(item, anchor);
24311 }
24312 return;
24313 }
24314 closeMenu();
24315 item.onClick?.();
24316 });
24317 document.body.appendChild(menu);
24318 activeMenu$1 = menu;
24319 clampToViewport(menu);
24320 queueMicrotask(() => {
24321 outsideHandler = (e) => {
24322 if (isInsideMenu(e.target)) {
24323 return;
24324 }
24325 closeMenu();
24326 };
24327 escHandler = (e) => {
24328 if (e.key === "Escape") {
24329 closeMenu();
24330 }
24331 };
24332 document.addEventListener("mousedown", outsideHandler);
24333 document.addEventListener("keydown", escHandler);
24334 });
24335 }
24336 function appendOption(host, item) {
24337 const opt = document.createElement("wpd-context-menu-option");
24338 opt.dataset.menuItemId = item.id;
24339 opt.setAttribute("value", item.id);
24340 if (item.heading) {
24341 opt.setAttribute("heading", "");
24342 }
24343 if (item.disabled) {
24344 opt.setAttribute("disabled", "");
24345 }
24346 if (item.icon) {
24347 opt.setAttribute("icon", sanitizeClass$1(item.icon));
24348 }
24349 if (hasChildren(item)) {
24350 opt.setAttribute("has-children", "");
24351 }
24352 opt.textContent = item.label;
24353 host.appendChild(opt);
24354 return opt;
24355 }
24356 function openFlyout(parent, anchor) {
24357 closeFlyout();
24358 if (!hasChildren(parent)) {
24359 return;
24360 }
24361 const fly = document.createElement("wpd-context-menu");
24362 fly.setAttribute("open", "");
24363 fly.classList.add(MENU_CLASS$1, `${MENU_CLASS$1}--flyout`);
24364 const childById = /* @__PURE__ */ new Map();
24365 for (const child of sortItems(parent.children ?? [])) {
24366 childById.set(child.id, child);
24367 appendOption(fly, child);
24368 }
24369 fly.addEventListener("wpd-context-menu-pick", (e) => {
24370 const detail = e.detail;
24371 const child = childById.get(detail.id);
24372 if (!child) {
24373 return;
24374 }
24375 e.stopPropagation();
24376 closeMenu();
24377 child.onClick?.();
24378 });
24379 document.body.appendChild(fly);
24380 activeFlyout = fly;
24381 positionFlyout(fly, anchor);
24382 }
24383 function positionFlyout(fly, anchor) {
24384 const ar = anchor.getBoundingClientRect();
24385 fly.style.position = "fixed";
24386 fly.style.left = `${ar.right}px`;
24387 fly.style.top = `${ar.top}px`;
24388 const fr = fly.getBoundingClientRect();
24389 if (fr.right > window.innerWidth) {
24390 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
24391 }
24392 if (fr.bottom > window.innerHeight) {
24393 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
24394 }
24395 }
24396 function clampToViewport(menu) {
24397 const rect = menu.getBoundingClientRect();
24398 if (rect.right > window.innerWidth) {
24399 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
24400 }
24401 if (rect.bottom > window.innerHeight) {
24402 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
24403 }
24404 }
24405 function hasChildren(item) {
24406 return Array.isArray(item.children) && item.children.length > 0;
24407 }
24408 function closeFlyout() {
24409 if (activeFlyout) {
24410 activeFlyout.remove();
24411 activeFlyout = null;
24412 }
24413 }
24414 function closeMenu() {
24415 closeFlyout();
24416 if (activeMenu$1) {
24417 activeMenu$1.remove();
24418 activeMenu$1 = null;
24419 }
24420 activeCanvas = null;
24421 if (outsideHandler) {
24422 document.removeEventListener("mousedown", outsideHandler);
24423 outsideHandler = null;
24424 }
24425 if (escHandler) {
24426 document.removeEventListener("keydown", escHandler);
24427 escHandler = null;
24428 }
24429 }
24430 function sanitizeClass$1(raw) {
24431 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
24432 }
24433 const ROOT_CLASS$1 = "desktop-mode-breadcrumbs";
24434 function renderBreadcrumbs(host, segments, opts = {}) {
24435 host.replaceChildren();
24436 host.classList.add(ROOT_CLASS$1);
24437 if (opts.onBack) {
24438 const back = document.createElement("button");
24439 back.type = "button";
24440 back.className = `${ROOT_CLASS$1}__back`;
24441 back.setAttribute("aria-label", __("Back", "desktop-mode"));
24442 back.title = __("Back", "desktop-mode");
24443 const arrow = document.createElement("span");
24444 arrow.className = "dashicons dashicons-arrow-left-alt2";
24445 arrow.setAttribute("aria-hidden", "true");
24446 back.appendChild(arrow);
24447 if (opts.backDisabled) {
24448 back.disabled = true;
24449 }
24450 const onBack = opts.onBack;
24451 back.addEventListener("click", () => {
24452 if (back.disabled) {
24453 return;
24454 }
24455 onBack();
24456 });
24457 host.appendChild(back);
24458 }
24459 const nav = document.createElement("nav");
24460 nav.className = `${ROOT_CLASS$1}__crumbs`;
24461 nav.setAttribute("aria-label", __("Breadcrumb", "desktop-mode"));
24462 segments.forEach((seg, idx) => {
24463 if (idx > 0) {
24464 const sep = document.createElement("span");
24465 sep.className = `${ROOT_CLASS$1}__sep`;
24466 sep.setAttribute("aria-hidden", "true");
24467 sep.textContent = "›";
24468 nav.appendChild(sep);
24469 }
24470 if (!seg.onClick) {
24471 const here = document.createElement("span");
24472 here.className = `${ROOT_CLASS$1}__crumb ${ROOT_CLASS$1}__crumb--current`;
24473 here.setAttribute("aria-current", "page");
24474 here.textContent = seg.label;
24475 nav.appendChild(here);
24476 return;
24477 }
24478 const btn = document.createElement("button");
24479 btn.type = "button";
24480 btn.className = `${ROOT_CLASS$1}__crumb`;
24481 btn.textContent = seg.label;
24482 const onClick = seg.onClick;
24483 btn.addEventListener("click", () => {
24484 onClick();
24485 });
24486 nav.appendChild(btn);
24487 });
24488 host.appendChild(nav);
24489 }
24490 async function getJson(url, init2 = {}) {
24491 const response = await trackedFetch$1(url, {
24492 credentials: "same-origin",
24493 headers: {
24494 Accept: "application/json",
24495 "X-WP-Nonce": readRestNonce(),
24496 ...init2.headers ?? {}
24497 },
24498 ...init2
24499 });
24500 if (!response.ok) {
24501 throw new Error(`${response.status} ${response.statusText}`);
24502 }
24503 return await response.json();
24504 }
24505 function readRestNonce() {
24506 const cfg = window.wp?.desktop?.config;
24507 return cfg?.restNonce ?? "";
24508 }
24509 function readRestRoot() {
24510 const cfg = window.wp?.desktop?.config;
24511 if (cfg?.restUrl) {
24512 return cfg.restUrl.endsWith("/") ? cfg.restUrl : cfg.restUrl + "/";
24513 }
24514 return `${window.location.origin}/wp-json/`;
24515 }
24516 function restUrl(path) {
24517 return joinRestUrl(readRestRoot(), path);
24518 }
24519 function renderPlacementPreview(placement, host) {
24520 const filtered = applyFilters(
24521 "desktop-mode.files.preview",
24522 null,
24523 placement
24524 );
24525 if (filtered instanceof HTMLElement) {
24526 host.replaceChildren(filtered);
24527 return;
24528 }
24529 if (placement.accessGated) {
24530 host.replaceChildren(renderAccessGated(placement));
24531 return;
24532 }
24533 host.replaceChildren(renderLoading());
24534 void renderByType(placement).then((node) => {
24535 host.replaceChildren(node);
24536 }).catch((err) => {
24537 host.replaceChildren(renderError(err));
24538 });
24539 }
24540 function renderAccessGated(placement) {
24541 const wrap = document.createElement("div");
24542 wrap.className = "desktop-mode-files__access-gated";
24543 const ring = document.createElement("div");
24544 ring.className = "desktop-mode-files__access-gated-ring";
24545 const glyph = document.createElement("span");
24546 glyph.className = "dashicons dashicons-lock desktop-mode-files__access-gated-glyph";
24547 glyph.setAttribute("aria-hidden", "true");
24548 ring.appendChild(glyph);
24549 wrap.appendChild(ring);
24550 const title = document.createElement("h2");
24551 title.className = "desktop-mode-files__access-gated-title";
24552 title.textContent = "No permission to view";
24553 wrap.appendChild(title);
24554 const sub = document.createElement("p");
24555 sub.className = "desktop-mode-files__access-gated-sub";
24556 const target2 = placement.file.title || placement.file.type;
24557 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.`;
24558 wrap.appendChild(sub);
24559 const hint = document.createElement("p");
24560 hint.className = "desktop-mode-files__access-gated-hint";
24561 hint.textContent = "Ask the owner to grant access on the underlying item, or to remove it from the shared folder.";
24562 wrap.appendChild(hint);
24563 return wrap;
24564 }
24565 async function renderByType(placement) {
24566 const file = placement.file;
24567 switch (file.type) {
24568 case "post":
24569 return renderPostPreview(file.ref, file);
24570 case "folder":
24571 return renderFolderPreview(file);
24572 case "shortcut":
24573 return renderShortcutPreview(file);
24574 case "attachment":
24575 return renderAttachmentPreview(file.ref, file);
24576 case "user":
24577 return renderUserSummary(file.ref, file);
24578 case "term":
24579 return renderTermSummary(file);
24580 case "comment":
24581 return renderCommentSummary(file.ref, file);
24582 case "bookmark":
24583 return renderBookmarkPreview(file);
24584 default:
24585 return renderGenericPreview(file);
24586 }
24587 }
24588 async function renderPostPreview(ref, file) {
24589 const id = parseInt(ref, 10);
24590 if (!id) {
24591 return renderGenericPreview(file);
24592 }
24593 let data = null;
24594 for (const path of ["wp/v2/posts", "wp/v2/pages"]) {
24595 try {
24596 data = await getJson(
24597 restUrl(
24598 `${path}/${id}?_fields=id,title,content,date,link,status`
24599 )
24600 );
24601 break;
24602 } catch {
24603 }
24604 }
24605 if (!data) {
24606 return renderGenericPreview(file);
24607 }
24608 const wrap = articleShell();
24609 const h = document.createElement("h2");
24610 h.className = "desktop-mode-my-wordpress__article-title";
24611 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
24612 wrap.appendChild(h);
24613 const meta = document.createElement("p");
24614 meta.className = "desktop-mode-my-wordpress__article-meta";
24615 const parts = [];
24616 parts.push(formatDate(data.date));
24617 if (data.status && data.status !== "publish") {
24618 parts.push(data.status);
24619 }
24620 meta.textContent = parts.join(" · ");
24621 wrap.appendChild(meta);
24622 if (data.content?.rendered) {
24623 const body = document.createElement("div");
24624 body.className = "desktop-mode-my-wordpress__article-content";
24625 body.innerHTML = data.content.rendered;
24626 wrap.appendChild(body);
24627 }
24628 const footer = document.createElement("footer");
24629 footer.className = "desktop-mode-my-wordpress__article-footer";
24630 const myWordpressApi = window.wp?.desktop?.myWordpress;
24631 if (myWordpressApi) {
24632 const exploreBtn = document.createElement("wpd-button");
24633 exploreBtn.setAttribute("variant", "secondary");
24634 exploreBtn.textContent = __("Explore details", "desktop-mode");
24635 exploreBtn.title = __(
24636 "See author, comments, categories, tags, attached media, and revisions for this entry.",
24637 "desktop-mode"
24638 );
24639 exploreBtn.addEventListener("click", () => {
24640 const postType = typeof file.postType === "string" ? file.postType : "post";
24641 myWordpressApi.openDetail({
24642 entityId: postType === "page" ? "pages" : "posts",
24643 postId: id,
24644 postTitle: stripTags(data.title.rendered) || `#${id}`
24645 });
24646 });
24647 footer.appendChild(exploreBtn);
24648 }
24649 const editBtn = document.createElement("wpd-button");
24650 editBtn.setAttribute("variant", "primary");
24651 editBtn.textContent = __("Open in editor", "desktop-mode");
24652 editBtn.addEventListener("click", () => {
24653 const adminUrl = window.wp?.desktop?.config?.adminUrl;
24654 if (!adminUrl) {
24655 return;
24656 }
24657 const editUrl = `${adminUrl}post.php?post=${id}&action=edit`;
24658 const wm = window.wp?.desktop?.windowManager;
24659 const postType = typeof file.postType === "string" ? file.postType : "post";
24660 const entityId = postType === "page" ? "pages" : "posts";
24661 wm?.open({
24662 id: `${entityId}-edit-${id}`,
24663 url: editUrl,
24664 title: stripTags(data.title.rendered),
24665 icon: file.icon
24666 });
24667 });
24668 footer.appendChild(editBtn);
24669 wrap.appendChild(footer);
24670 return wrap;
24671 }
24672 async function renderUserSummary(ref, file) {
24673 const id = parseInt(ref, 10);
24674 if (!id) {
24675 return renderGenericPreview(file);
24676 }
24677 let data = null;
24678 try {
24679 data = await getJson(
24680 restUrl(`desktop-mode/v1/user-stats/${id}`)
24681 );
24682 } catch {
24683 return renderGenericPreview(file);
24684 }
24685 const wrap = articleShell("desktop-mode-my-wordpress__user");
24686 const header = document.createElement("header");
24687 header.className = "desktop-mode-my-wordpress__user-header";
24688 if (data.profile.avatarUrl) {
24689 const img = document.createElement("img");
24690 img.className = "desktop-mode-my-wordpress__user-avatar";
24691 img.src = data.profile.avatarUrl;
24692 img.alt = "";
24693 header.appendChild(img);
24694 }
24695 const head = document.createElement("div");
24696 head.className = "desktop-mode-my-wordpress__user-headline";
24697 const h = document.createElement("h2");
24698 h.className = "desktop-mode-my-wordpress__article-title";
24699 h.textContent = data.profile.name || file.title || `#${id}`;
24700 head.appendChild(h);
24701 if (data.profile.roleLabels && data.profile.roleLabels.length > 0) {
24702 const roles = document.createElement("div");
24703 roles.className = "desktop-mode-my-wordpress__user-roles";
24704 for (const r of data.profile.roleLabels) {
24705 const badge = document.createElement("span");
24706 badge.className = "desktop-mode-my-wordpress__user-role";
24707 badge.textContent = r;
24708 roles.appendChild(badge);
24709 }
24710 head.appendChild(roles);
24711 }
24712 header.appendChild(head);
24713 wrap.appendChild(header);
24714 if (data.profile.description) {
24715 const bio = document.createElement("div");
24716 bio.className = "desktop-mode-my-wordpress__user-bio";
24717 bio.textContent = data.profile.description;
24718 wrap.appendChild(bio);
24719 }
24720 const cards = document.createElement("div");
24721 cards.className = "desktop-mode-my-wordpress__user-stats";
24722 cards.appendChild(
24723 statCard(
24724 data.counts.posts.total.toLocaleString(),
24725 __("Posts", "desktop-mode")
24726 )
24727 );
24728 cards.appendChild(
24729 statCard(
24730 data.counts.pages.total.toLocaleString(),
24731 __("Pages", "desktop-mode")
24732 )
24733 );
24734 cards.appendChild(
24735 statCard(
24736 data.counts.commentsReceived.toLocaleString(),
24737 __("Comments received", "desktop-mode")
24738 )
24739 );
24740 wrap.appendChild(cards);
24741 return wrap;
24742 }
24743 async function renderTermSummary(file) {
24744 const id = parseInt(file.ref, 10);
24745 const taxonomy = typeof file.taxonomy === "string" && file.taxonomy ? file.taxonomy : "category";
24746 if (!id) {
24747 return renderGenericPreview(file);
24748 }
24749 let data = null;
24750 try {
24751 data = await getJson(
24752 restUrl(`desktop-mode/v1/term-stats/${taxonomy}/${id}`)
24753 );
24754 } catch {
24755 return renderGenericPreview(file);
24756 }
24757 const wrap = articleShell();
24758 const h = document.createElement("h2");
24759 h.className = "desktop-mode-my-wordpress__article-title";
24760 h.textContent = data.profile.name || file.title || `#${id}`;
24761 wrap.appendChild(h);
24762 const meta = document.createElement("p");
24763 meta.className = "desktop-mode-my-wordpress__article-meta";
24764 meta.textContent = data.profile.taxonomyLabel || data.profile.taxonomy;
24765 wrap.appendChild(meta);
24766 if (data.profile.description) {
24767 const desc = document.createElement("div");
24768 desc.className = "desktop-mode-my-wordpress__article-content";
24769 desc.innerHTML = data.profile.description;
24770 wrap.appendChild(desc);
24771 }
24772 const cards = document.createElement("div");
24773 cards.className = "desktop-mode-my-wordpress__user-stats";
24774 cards.appendChild(
24775 statCard(
24776 data.counts.posts.total.toLocaleString(),
24777 __("Posts", "desktop-mode")
24778 )
24779 );
24780 cards.appendChild(
24781 statCard(
24782 data.counts.commentsReceived.toLocaleString(),
24783 __("Comments", "desktop-mode")
24784 )
24785 );
24786 cards.appendChild(
24787 statCard(
24788 data.counts.distinctAuthors.toLocaleString(),
24789 __("Authors", "desktop-mode")
24790 )
24791 );
24792 wrap.appendChild(cards);
24793 return wrap;
24794 }
24795 async function renderCommentSummary(ref, file) {
24796 const id = parseInt(ref, 10);
24797 if (!id) {
24798 return renderGenericPreview(file);
24799 }
24800 let data = null;
24801 try {
24802 data = await getJson(
24803 restUrl(`desktop-mode/v1/comment-stats/${id}`)
24804 );
24805 } catch {
24806 return renderGenericPreview(file);
24807 }
24808 const wrap = articleShell();
24809 const header = document.createElement("header");
24810 header.className = "desktop-mode-my-wordpress__user-header";
24811 if (data.author.avatarUrl) {
24812 const img = document.createElement("img");
24813 img.className = "desktop-mode-my-wordpress__user-avatar";
24814 img.src = data.author.avatarUrl;
24815 img.alt = "";
24816 header.appendChild(img);
24817 }
24818 const head = document.createElement("div");
24819 head.className = "desktop-mode-my-wordpress__user-headline";
24820 const h = document.createElement("h2");
24821 h.className = "desktop-mode-my-wordpress__article-title";
24822 h.textContent = data.author.name;
24823 head.appendChild(h);
24824 const sub = document.createElement("p");
24825 sub.className = "desktop-mode-my-wordpress__article-meta";
24826 sub.textContent = `${formatDate(data.comment.date)} · ${data.comment.status}`;
24827 head.appendChild(sub);
24828 header.appendChild(head);
24829 wrap.appendChild(header);
24830 const body = document.createElement("div");
24831 body.className = "desktop-mode-my-wordpress__article-content";
24832 body.innerHTML = data.comment.rendered;
24833 wrap.appendChild(body);
24834 if (data.post) {
24835 const card = document.createElement("div");
24836 card.className = "desktop-mode-my-wordpress__comment-post";
24837 const link = document.createElement("a");
24838 link.className = "desktop-mode-my-wordpress__comment-post-title";
24839 link.href = data.post.link;
24840 link.target = "_blank";
24841 link.rel = "noopener noreferrer";
24842 link.textContent = data.post.title;
24843 card.appendChild(link);
24844 wrap.appendChild(card);
24845 }
24846 return wrap;
24847 }
24848 async function renderAttachmentPreview(ref, file) {
24849 const id = parseInt(ref, 10);
24850 if (!id) {
24851 return renderGenericPreview(file);
24852 }
24853 let data = null;
24854 try {
24855 data = await getJson(
24856 restUrl(
24857 `wp/v2/media/${id}?_fields=id,title,source_url,mime_type,alt_text,media_details`
24858 )
24859 );
24860 } catch {
24861 return renderGenericPreview(file);
24862 }
24863 const wrap = articleShell();
24864 const h = document.createElement("h2");
24865 h.className = "desktop-mode-my-wordpress__article-title";
24866 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
24867 wrap.appendChild(h);
24868 const meta = document.createElement("p");
24869 meta.className = "desktop-mode-my-wordpress__article-meta";
24870 meta.textContent = data.mime_type;
24871 wrap.appendChild(meta);
24872 if (data.mime_type.startsWith("image/")) {
24873 const img = document.createElement("img");
24874 img.className = "desktop-mode-my-wordpress__article-hero";
24875 const sizes = data.media_details?.sizes;
24876 img.src = sizes?.large?.source_url ?? sizes?.medium?.source_url ?? data.source_url;
24877 img.alt = data.alt_text ?? "";
24878 wrap.appendChild(img);
24879 } else {
24880 const p = document.createElement("p");
24881 const a = document.createElement("a");
24882 a.href = data.source_url;
24883 a.textContent = data.source_url;
24884 a.target = "_blank";
24885 a.rel = "noopener noreferrer";
24886 p.appendChild(a);
24887 wrap.appendChild(p);
24888 }
24889 return wrap;
24890 }
24891 function renderFolderPreview(file) {
24892 const wrap = articleShell();
24893 const h = document.createElement("h2");
24894 h.className = "desktop-mode-my-wordpress__article-title";
24895 h.textContent = file.title || __("(folder)", "desktop-mode");
24896 wrap.appendChild(h);
24897 const meta = document.createElement("p");
24898 meta.className = "desktop-mode-my-wordpress__article-meta";
24899 meta.textContent = __("Double-click to open.", "desktop-mode");
24900 wrap.appendChild(meta);
24901 return wrap;
24902 }
24903 function renderShortcutPreview(file) {
24904 const wrap = articleShell();
24905 const h = document.createElement("h2");
24906 h.className = "desktop-mode-my-wordpress__article-title";
24907 h.textContent = file.title || __("Shortcut", "desktop-mode");
24908 wrap.appendChild(h);
24909 const meta = document.createElement("p");
24910 meta.className = "desktop-mode-my-wordpress__article-meta";
24911 meta.textContent = __("Plugin shortcut. Double-click to open.", "desktop-mode");
24912 wrap.appendChild(meta);
24913 return wrap;
24914 }
24915 function renderBookmarkPreview(file) {
24916 const wrap = articleShell();
24917 const h = document.createElement("h2");
24918 h.className = "desktop-mode-my-wordpress__article-title";
24919 h.textContent = file.title || __("Bookmark", "desktop-mode");
24920 wrap.appendChild(h);
24921 const url = typeof file.url === "string" ? file.url : "";
24922 if (url) {
24923 const a = document.createElement("a");
24924 a.href = url;
24925 a.textContent = url;
24926 a.target = "_blank";
24927 a.rel = "noopener noreferrer";
24928 wrap.appendChild(a);
24929 }
24930 return wrap;
24931 }
24932 function renderGenericPreview(file) {
24933 const wrap = articleShell();
24934 const h = document.createElement("h2");
24935 h.className = "desktop-mode-my-wordpress__article-title";
24936 h.textContent = file.title || file.type;
24937 wrap.appendChild(h);
24938 const meta = document.createElement("p");
24939 meta.className = "desktop-mode-my-wordpress__article-meta";
24940 meta.textContent = sprintf(
24941 // translators: %s is a file-type slug.
24942 __("Type: %s", "desktop-mode"),
24943 file.type
24944 );
24945 wrap.appendChild(meta);
24946 if (!file.exists) {
24947 const warn2 = document.createElement("p");
24948 warn2.className = "desktop-mode-my-wordpress__article-meta";
24949 warn2.textContent = __(
24950 "The underlying entity is no longer available.",
24951 "desktop-mode"
24952 );
24953 wrap.appendChild(warn2);
24954 }
24955 return wrap;
24956 }
24957 function articleShell(extraClass = "") {
24958 const article = document.createElement("article");
24959 article.className = "desktop-mode-my-wordpress__article" + (extraClass ? " " + extraClass : "");
24960 return article;
24961 }
24962 function statCard(value, label) {
24963 const card = document.createElement("div");
24964 card.className = "desktop-mode-my-wordpress__user-stat";
24965 const v = document.createElement("span");
24966 v.className = "desktop-mode-my-wordpress__user-stat-value";
24967 v.textContent = value;
24968 card.appendChild(v);
24969 const l = document.createElement("span");
24970 l.className = "desktop-mode-my-wordpress__user-stat-label";
24971 l.textContent = label;
24972 card.appendChild(l);
24973 return card;
24974 }
24975 function renderLoading() {
24976 const wrap = document.createElement("div");
24977 wrap.className = "desktop-mode-my-wordpress__preview-loading";
24978 const spinner = document.createElement("wpd-spinner");
24979 wrap.appendChild(spinner);
24980 return wrap;
24981 }
24982 function renderError(err) {
24983 const wrap = document.createElement("div");
24984 wrap.className = "desktop-mode-my-wordpress__error";
24985 wrap.textContent = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
24986 return wrap;
24987 }
24988 function stripTags(html2) {
24989 const div = document.createElement("div");
24990 div.innerHTML = html2;
24991 return (div.textContent ?? "").trim();
24992 }
24993 function formatDate(iso) {
24994 if (!iso) {
24995 return "";
24996 }
24997 try {
24998 return new Date(iso).toLocaleString();
24999 } catch {
25000 return iso;
25001 }
25002 }
25003 function renderPreviewEmpty() {
25004 const wrap = document.createElement("div");
25005 wrap.className = "desktop-mode-my-wordpress__preview-empty";
25006 wrap.textContent = __(
25007 "Select an item to preview it here.",
25008 "desktop-mode"
25009 );
25010 return wrap;
25011 }
25012 const ID_PREFIX = "desktop-mode-embed-";
25013 const DEFAULT_W = 800;
25014 const DEFAULT_H = 600;
25015 const MIN_W = 360;
25016 const MIN_H = 240;
25017 const PADDING = 16;
25018 const lastPersisted = /* @__PURE__ */ new Map();
25019 function openEmbedWindow(file, ctx) {
25020 const url = file.ref();
25021 if (!url) {
25022 return;
25023 }
25024 const wm = window.wp?.desktop?.windowManager;
25025 if (!wm) {
25026 return;
25027 }
25028 const placement = ctx?.placement;
25029 const meta = placement?.meta ?? null;
25030 const windowId = placement ? `${ID_PREFIX}${placement.id}` : `${ID_PREFIX}anon-${hash(url)}`;
25031 const customName = meta?.name?.trim() ?? "";
25032 const title = customName !== "" ? customName : file.title();
25033 const cfg = {
25034 id: windowId,
25035 baseId: windowId,
25036 url,
25037 title,
25038 icon: file.icon(),
25039 minWidth: MIN_W,
25040 minHeight: MIN_H
25041 };
25042 const saved = meta?.window;
25043 const area = document.getElementById("desktop-mode-area");
25044 const aw = area?.clientWidth ?? window.innerWidth;
25045 const ah = area?.clientHeight ?? window.innerHeight;
25046 if (saved && Number.isFinite(saved.width) && Number.isFinite(saved.height)) {
25047 const { x, y, width, height } = clampGeometry(saved, aw, ah);
25048 cfg.x = x;
25049 cfg.y = y;
25050 cfg.width = width;
25051 cfg.height = height;
25052 } else {
25053 cfg.width = Math.min(DEFAULT_W, Math.max(MIN_W, aw - PADDING * 2));
25054 cfg.height = Math.min(DEFAULT_H, Math.max(MIN_H, ah - PADDING * 2));
25055 }
25056 if (placement) {
25057 if (saved) {
25058 lastPersisted.set(windowId, { ...saved });
25059 }
25060 }
25061 wm.open(cfg);
25062 }
25063 let installed = false;
25064 function installEmbedPersistence() {
25065 if (installed) {
25066 return;
25067 }
25068 installed = true;
25069 const onChange = (payload) => {
25070 const p = payload;
25071 const id = p?.windowId;
25072 if (!id || !id.startsWith(ID_PREFIX)) {
25073 return;
25074 }
25075 const placementIdStr = id.slice(ID_PREFIX.length);
25076 const placementId = parseInt(placementIdStr, 10);
25077 if (!placementId) {
25078 return;
25079 }
25080 const wm = window.wp?.desktop?.windowManager;
25081 const win = wm?.getById?.(id);
25082 const el = win?.element;
25083 if (!el) {
25084 return;
25085 }
25086 const next = {
25087 x: el.offsetLeft,
25088 y: el.offsetTop,
25089 width: el.offsetWidth,
25090 height: el.offsetHeight
25091 };
25092 const prev = lastPersisted.get(id);
25093 if (prev && prev.x === next.x && prev.y === next.y && prev.width === next.width && prev.height === next.height) {
25094 return;
25095 }
25096 lastPersisted.set(id, next);
25097 void persist(placementId, next);
25098 };
25099 addAction(HOOKS.WINDOW_DRAG_END, "desktop-mode-embed-persist", onChange);
25100 addAction(HOOKS.WINDOW_RESIZE_END, "desktop-mode-embed-persist", onChange);
25101 }
25102 async function persist(placementId, geo) {
25103 try {
25104 const list2 = await listPlacements(0);
25105 const row = list2.placements.find((p) => p.id === placementId);
25106 const prevMeta = row?.meta ?? {};
25107 const nextMeta = {
25108 ...prevMeta,
25109 window: geo
25110 };
25111 await updatePlacement(placementId, { meta: nextMeta });
25112 } catch (err) {
25113 console.warn("[desktop-mode] embed window persist failed:", err);
25114 }
25115 }
25116 function clampGeometry(g, areaW, areaH) {
25117 const width = Math.max(MIN_W, Math.min(g.width, areaW - PADDING));
25118 const height = Math.max(MIN_H, Math.min(g.height, areaH - PADDING));
25119 const x = Math.max(0, Math.min(g.x, Math.max(0, areaW - width)));
25120 const y = Math.max(0, Math.min(g.y, Math.max(0, areaH - height)));
25121 return { x, y, width, height };
25122 }
25123 function hash(s) {
25124 let h = 0;
25125 for (let i = 0; i < s.length; i++) {
25126 h = (Math.imul(h, 31) + s.charCodeAt(i)) % 2147483647;
25127 }
25128 return Math.abs(h).toString(36);
25129 }
25130 function adminBase() {
25131 const cfg = window.wp?.desktop?.config;
25132 const url = cfg?.adminUrl ?? "/wp-admin/";
25133 return url.endsWith("/") ? url : `${url}/`;
25134 }
25135 function sanitizedWebUrl(file) {
25136 const url = typeof file.shape.url === "string" ? file.shape.url : "";
25137 if (!url) {
25138 return "";
25139 }
25140 try {
25141 const parsed = new URL(url, window.location.href);
25142 if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
25143 return "";
25144 }
25145 } catch {
25146 return "";
25147 }
25148 return url;
25149 }
25150 function registerBuiltInFileOpeners() {
25151 registerOpener({
25152 id: "wp-post-editor",
25153 label: "Block Editor",
25154 types: ["post"],
25155 isDefault: true,
25156 sort: 10,
25157 handler: {
25158 kind: "url",
25159 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
25160 }
25161 });
25162 registerOpener({
25163 id: "wp-media-editor",
25164 label: "Media editor",
25165 types: ["attachment"],
25166 isDefault: true,
25167 sort: 10,
25168 handler: {
25169 kind: "url",
25170 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
25171 }
25172 });
25173 registerOpener({
25174 id: "wp-user-profile",
25175 label: "User profile",
25176 types: ["user"],
25177 isDefault: true,
25178 sort: 10,
25179 handler: {
25180 kind: "url",
25181 url: (file) => `${adminBase()}user-edit.php?user_id=${encodeURIComponent(file.ref())}`
25182 }
25183 });
25184 registerOpener({
25185 id: "wp-term-editor",
25186 label: "Term editor",
25187 types: ["term"],
25188 isDefault: true,
25189 sort: 10,
25190 handler: {
25191 kind: "url",
25192 url: (file) => {
25193 const [taxonomy, termId] = file.ref().split(":");
25194 return `${adminBase()}term.php?taxonomy=${encodeURIComponent(taxonomy ?? "")}&tag_ID=${encodeURIComponent(termId ?? "")}`;
25195 }
25196 }
25197 });
25198 registerOpener({
25199 id: "wp-comment-editor",
25200 label: "Comment editor",
25201 types: ["comment"],
25202 isDefault: true,
25203 sort: 10,
25204 handler: {
25205 kind: "url",
25206 url: (file) => `${adminBase()}comment.php?action=editcomment&c=${encodeURIComponent(file.ref())}`
25207 }
25208 });
25209 registerOpener({
25210 id: "desktop-mode-folder-window",
25211 label: "Open folder",
25212 types: ["folder"],
25213 isDefault: true,
25214 sort: 10,
25215 handler: {
25216 kind: "js",
25217 open: (file) => {
25218 const folderId = parseInt(file.ref(), 10);
25219 if (!folderId) {
25220 return;
25221 }
25222 const wm = window.wp?.desktop?.windowManager;
25223 if (!wm) {
25224 return;
25225 }
25226 const id = `desktop-mode-folder-${folderId}`;
25227 const folderRow = store.getState().folders.get(folderId);
25228 const viewerId2 = Number(window.desktopModeConfig?.currentUserId ?? 0);
25229 const isRecipient = !!folderRow && folderRow.ownerId > 0 && folderRow.ownerId !== viewerId2;
25230 const baseTitle = file.title();
25231 const titleWithCue = isRecipient ? `${baseTitle} · Shared` : baseTitle;
25232 wm.open({
25233 id,
25234 baseId: id,
25235 url: `#folder-${folderId}`,
25236 title: titleWithCue,
25237 icon: file.icon(),
25238 native: true,
25239 render: (body) => {
25240 body.replaceChildren();
25241 body.classList.add("desktop-mode-folder-window");
25242 const routes = [
25243 { folderId, title: file.title() }
25244 ];
25245 let currentDispose = null;
25246 const breadcrumbsHost = document.createElement("header");
25247 body.appendChild(breadcrumbsHost);
25248 const bodyHost = document.createElement("div");
25249 bodyHost.style.cssText = "flex:1 1 auto;min-height:0;display:flex;flex-direction:column;";
25250 body.appendChild(bodyHost);
25251 const paintBreadcrumbs = () => {
25252 const segments = routes.map(
25253 (route, idx) => {
25254 const isCurrent = idx === routes.length - 1;
25255 if (isCurrent) {
25256 return { label: route.title };
25257 }
25258 return {
25259 label: route.title,
25260 onClick: () => {
25261 routes.length = idx + 1;
25262 mountCurrent();
25263 }
25264 };
25265 }
25266 );
25267 renderBreadcrumbs(breadcrumbsHost, segments, {
25268 onBack: () => {
25269 if (routes.length <= 1) {
25270 return;
25271 }
25272 routes.pop();
25273 mountCurrent();
25274 },
25275 backDisabled: routes.length <= 1
25276 });
25277 };
25278 const mountCurrent = () => {
25279 currentDispose?.();
25280 currentDispose = null;
25281 bodyHost.replaceChildren();
25282 const split = document.createElement("div");
25283 split.className = "desktop-mode-folder-window__split";
25284 bodyHost.appendChild(split);
25285 const layerHost = document.createElement("div");
25286 layerHost.className = "desktop-mode-folder-window__layer";
25287 split.appendChild(layerHost);
25288 const previewPane = document.createElement("div");
25289 previewPane.className = "desktop-mode-folder-window__preview";
25290 previewPane.appendChild(renderPreviewEmpty());
25291 split.appendChild(previewPane);
25292 const route = routes[routes.length - 1];
25293 const layer = mountFilesLayer(
25294 layerHost,
25295 route.folderId
25296 );
25297 const offSelection = layer.onSelectionChange(
25298 (placement) => {
25299 if (!placement) {
25300 previewPane.replaceChildren(
25301 renderPreviewEmpty()
25302 );
25303 return;
25304 }
25305 renderPlacementPreview(
25306 placement,
25307 previewPane
25308 );
25309 }
25310 );
25311 const dblClickHandler = (e) => {
25312 if (!(e.target instanceof Element)) {
25313 return;
25314 }
25315 const tile2 = e.target.closest(
25316 ".desktop-mode-file-tile"
25317 );
25318 if (!tile2) {
25319 return;
25320 }
25321 if (tile2.dataset.fileType !== "folder") {
25322 return;
25323 }
25324 const subId = parseInt(
25325 tile2.dataset.fileRef ?? "",
25326 10
25327 );
25328 if (!subId) {
25329 return;
25330 }
25331 e.preventDefault();
25332 e.stopPropagation();
25333 const subTitle = tile2.querySelector(
25334 ".desktop-mode-file-tile__label"
25335 )?.textContent ?? `#${subId}`;
25336 routes.push({
25337 folderId: subId,
25338 title: subTitle
25339 });
25340 mountCurrent();
25341 };
25342 layerHost.addEventListener(
25343 "dblclick",
25344 dblClickHandler,
25345 true
25346 );
25347 const menu = attachIconCanvasMenu(layerHost, {
25348 scope: `desktop-mode-folder:${route.folderId}`,
25349 onSort: (mode) => layer.sort(mode),
25350 extraItems: [
25351 {
25352 id: "new-folder",
25353 label: "New folder",
25354 icon: "dashicons-portfolio",
25355 sort: 5,
25356 onClick: () => {
25357 openCreateFolderDialog({
25358 onSubmit: async (name) => {
25359 const folder = await createFolder({
25360 name
25361 });
25362 const peers = store.getState().placementsByFolder.get(
25363 route.folderId
25364 ) ?? [];
25365 const occupied = buildOccupiedSet(peers);
25366 const cell = snapToEmptyCell(
25367 GRID_PADDING,
25368 GRID_PADDING,
25369 occupied,
25370 layerHost
25371 );
25372 const placement = await createPlacement({
25373 type: "folder",
25374 ref: String(folder.id),
25375 parentId: route.folderId,
25376 x: cell.x,
25377 y: cell.y
25378 });
25379 store.upsertFolder(folder);
25380 store.upsertPlacement(
25381 placement
25382 );
25383 }
25384 });
25385 }
25386 }
25387 ]
25388 });
25389 const status = mountFolderStatusBar(
25390 bodyHost,
25391 route.folderId
25392 );
25393 currentDispose = () => {
25394 offSelection();
25395 menu.dispose();
25396 status.dispose();
25397 layerHost.removeEventListener(
25398 "dblclick",
25399 dblClickHandler,
25400 true
25401 );
25402 layer.dispose();
25403 };
25404 paintBreadcrumbs();
25405 };
25406 mountCurrent();
25407 },
25408 width: 720,
25409 height: 480,
25410 minWidth: 360,
25411 minHeight: 240
25412 });
25413 }
25414 }
25415 });
25416 registerOpener({
25417 id: "desktop-mode-shortcut-opener",
25418 label: "Open shortcut",
25419 types: ["shortcut"],
25420 isDefault: true,
25421 sort: 10,
25422 handler: {
25423 kind: "js",
25424 open: (file) => {
25425 const extras = file.shape;
25426 const wp = window.wp?.desktop;
25427 if (!wp) {
25428 return;
25429 }
25430 if (extras.shortcutWindow && wp.openWindow) {
25431 wp.openWindow(extras.shortcutWindow);
25432 return;
25433 }
25434 if (extras.shortcutUrl && wp.windowManager) {
25435 try {
25436 const u = new URL(extras.shortcutUrl, window.location.origin);
25437 if (u.origin !== window.location.origin) {
25438 window.open(u.toString(), "_blank", "noopener,noreferrer");
25439 return;
25440 }
25441 const adminUrl = wp.config?.adminUrl;
25442 const id = adminUrl ? deriveWindowId(u.toString(), adminUrl) : `desktop-icon-${file.ref()}`;
25443 const entry = findMenuEntryForUrl(u.toString());
25444 wp.windowManager.open({
25445 id,
25446 baseId: id,
25447 url: u.toString(),
25448 parentUrl: entry?.url ?? u.toString(),
25449 title: file.title(),
25450 icon: file.icon(),
25451 submenu: entry?.submenu,
25452 multi: !!entry?.multi
25453 });
25454 } catch {
25455 }
25456 }
25457 }
25458 }
25459 });
25460 registerOpener({
25461 id: "browser-navigate",
25462 label: "Open in browser",
25463 types: ["bookmark"],
25464 isDefault: true,
25465 sort: 10,
25466 handler: {
25467 kind: "js",
25468 open: (file) => {
25469 const url = sanitizedWebUrl(file);
25470 if (!url) {
25471 return;
25472 }
25473 window.open(url, "_blank", "noopener,noreferrer");
25474 }
25475 }
25476 });
25477 registerOpener({
25478 id: "desktop-mode-link-opener",
25479 label: "Open in browser",
25480 types: ["link"],
25481 isDefault: true,
25482 sort: 10,
25483 handler: {
25484 kind: "js",
25485 open: (file) => {
25486 const url = sanitizedWebUrl(file);
25487 if (!url) {
25488 return;
25489 }
25490 window.open(url, "_blank", "noopener,noreferrer");
25491 }
25492 }
25493 });
25494 registerOpener({
25495 id: "desktop-mode-embed-opener",
25496 label: "Open as window",
25497 types: ["embed"],
25498 isDefault: true,
25499 sort: 10,
25500 handler: {
25501 kind: "js",
25502 open: (file, ctx) => {
25503 openEmbedWindow(file, ctx);
25504 }
25505 }
25506 });
25507 }
25508 const TAB_ID = "desktop-mode-file-associations";
25509 function registerFileAssociationsTab() {
25510 registerSettingsTab({
25511 id: TAB_ID,
25512 label: "File Associations",
25513 order: 50,
25514 render(body) {
25515 renderTab(body);
25516 }
25517 });
25518 }
25519 function renderTab(body) {
25520 body.replaceChildren();
25521 const types = getTypes();
25522 if (types.length === 0) {
25523 const empty = document.createElement("p");
25524 empty.className = "desktop-mode-file-associations__empty";
25525 empty.textContent = "No file types are registered.";
25526 body.appendChild(empty);
25527 return;
25528 }
25529 const intro = document.createElement("p");
25530 intro.className = "desktop-mode-file-associations__intro";
25531 intro.textContent = "Pick which app opens each kind of file when you double-click it on the desktop.";
25532 body.appendChild(intro);
25533 const associations = getUserAssociations();
25534 const list2 = document.createElement("div");
25535 list2.className = "desktop-mode-file-associations__list";
25536 list2.setAttribute("role", "list");
25537 for (const type of types) {
25538 list2.appendChild(buildRow(type.type, type.label, associations));
25539 }
25540 body.appendChild(list2);
25541 }
25542 function buildRow(typeSlug, typeLabel, associations) {
25543 const row = document.createElement("div");
25544 row.className = "desktop-mode-file-associations__row";
25545 row.setAttribute("role", "listitem");
25546 row.dataset.fileType = typeSlug;
25547 const label = document.createElement("label");
25548 label.className = "desktop-mode-file-associations__label";
25549 label.textContent = typeLabel;
25550 row.appendChild(label);
25551 const candidates = getOpenersForType(typeSlug);
25552 if (candidates.length === 0) {
25553 const empty = document.createElement("span");
25554 empty.className = "desktop-mode-file-associations__none";
25555 empty.textContent = "No app available";
25556 row.appendChild(empty);
25557 return row;
25558 }
25559 const resolved = resolveOpener(typeSlug);
25560 const currentId = associations[typeSlug] ?? resolved?.id ?? "";
25561 const select = document.createElement("wpd-select");
25562 select.setAttribute("value", currentId);
25563 select.setAttribute("aria-label", `Default app for ${typeLabel}`);
25564 select.className = "desktop-mode-file-associations__select";
25565 label.htmlFor = `assoc-${typeSlug}`;
25566 select.id = `assoc-${typeSlug}`;
25567 for (const o of candidates) {
25568 const opt = document.createElement("wpd-option");
25569 opt.setAttribute("value", o.id);
25570 opt.textContent = o.isDefault ? `${o.label} (default)` : o.label;
25571 select.appendChild(opt);
25572 }
25573 select.addEventListener("wpd-pick", (e) => {
25574 const next = e.detail?.value;
25575 if (!next) {
25576 return;
25577 }
25578 const merged = { ...getUserAssociations(), [typeSlug]: next };
25579 setUserAssociations(merged);
25580 void saveAssociations(merged).catch((err) => {
25581 console.error("[desktop-mode] saveAssociations failed:", err);
25582 });
25583 });
25584 row.appendChild(select);
25585 return row;
25586 }
25587 let _store$1 = null;
25588 function sharesStore() {
25589 if (!_store$1) {
25590 _store$1 = createSharedStore("desktop-files/shares", () => ({
25591 byFolder: /* @__PURE__ */ new Map(),
25592 pending: [],
25593 sharesVersion: 0,
25594 deniedFolders: /* @__PURE__ */ new Set()
25595 }));
25596 }
25597 return _store$1;
25598 }
25599 function setSharesForFolder(folderId, shares) {
25600 const s = sharesStore();
25601 s.state.byFolder.set(folderId, shares);
25602 s.notify();
25603 }
25604 function upsertShare(share) {
25605 if (!share || typeof share.folderId !== "number") {
25606 return;
25607 }
25608 const s = sharesStore();
25609 const existing = s.state.byFolder.get(share.folderId) ?? [];
25610 const next = existing.filter((r) => r.id !== share.id);
25611 next.push(share);
25612 s.state.byFolder.set(share.folderId, next);
25613 s.notify();
25614 }
25615 function removeShare(folderId, shareId) {
25616 const s = sharesStore();
25617 const existing = s.state.byFolder.get(folderId) ?? [];
25618 s.state.byFolder.set(
25619 folderId,
25620 existing.filter((r) => r.id !== shareId)
25621 );
25622 s.notify();
25623 }
25624 function inviteEquals(a, b) {
25625 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;
25626 }
25627 function ingestPendingInvites(invites) {
25628 const s = sharesStore();
25629 const existingById = new Map(s.state.pending.map((p) => [p.id, p]));
25630 let mutated = false;
25631 for (const inv of invites) {
25632 if (s.state.deniedFolders.has(inv.folderId)) {
25633 continue;
25634 }
25635 const existing = existingById.get(inv.id);
25636 if (existing) {
25637 if (inviteEquals(existing, inv)) {
25638 continue;
25639 }
25640 s.state.pending = s.state.pending.map((p) => p.id === inv.id ? inv : p);
25641 } else {
25642 s.state.pending.push(inv);
25643 }
25644 if (inv.invitedAtMs > s.state.sharesVersion) {
25645 s.state.sharesVersion = inv.invitedAtMs;
25646 }
25647 mutated = true;
25648 }
25649 if (mutated) {
25650 s.notify();
25651 }
25652 }
25653 function dropPending(shareId, opts = {}) {
25654 const s = sharesStore();
25655 s.state.pending = s.state.pending.filter((p) => p.id !== shareId);
25656 if (opts.denied && typeof opts.folderId === "number") {
25657 s.state.deniedFolders.add(opts.folderId);
25658 }
25659 s.notify();
25660 }
25661 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}`;
25662 const _WpdUserSearch = class _WpdUserSearch extends Component {
25663 constructor() {
25664 super(...arguments);
25665 this._timer = null;
25666 this._abort = null;
25667 this._results = [];
25668 this._query = "";
25669 this._open = false;
25670 this._phase = "idle";
25671 this._error = "";
25672 this._dropdownStyle = "";
25673 this._onScrollOrResize = () => void 0;
25674 this._onInput = (e) => {
25675 const value = e.target.value;
25676 this._query = value;
25677 this._scheduleSearch(value);
25678 };
25679 this._onFocus = () => {
25680 if (this._results.length === 0 && this._phase === "idle") {
25681 this._scheduleSearch(this._query);
25682 return;
25683 }
25684 this._open = true;
25685 this._positionDropdown();
25686 this.requestUpdate();
25687 };
25688 this._onBlur = () => {
25689 setTimeout(() => {
25690 this._open = false;
25691 this.requestUpdate();
25692 }, 150);
25693 };
25694 this._pick = (user) => {
25695 this.emit("wpd-user-pick", { user });
25696 this._results = [];
25697 this._open = false;
25698 this._phase = "idle";
25699 this._query = "";
25700 const input = this.shadowRoot?.querySelector(".input");
25701 if (input) {
25702 input.value = "";
25703 }
25704 this.requestUpdate();
25705 };
25706 }
25707 connectedCallback() {
25708 super.connectedCallback();
25709 this._onScrollOrResize = () => {
25710 if (this._open) {
25711 this._positionDropdown();
25712 this.requestUpdate();
25713 }
25714 };
25715 window.addEventListener("resize", this._onScrollOrResize);
25716 window.addEventListener("scroll", this._onScrollOrResize, true);
25717 }
25718 disconnectedCallback() {
25719 if (this._timer) {
25720 clearTimeout(this._timer);
25721 }
25722 if (this._abort) {
25723 this._abort.abort();
25724 }
25725 window.removeEventListener("resize", this._onScrollOrResize);
25726 window.removeEventListener("scroll", this._onScrollOrResize, true);
25727 }
25728 _endpoint() {
25729 const attr = this.getAttribute("endpoint");
25730 if (attr) {
25731 return attr;
25732 }
25733 return window.desktopModeConfig?.filesUsersSearchUrl || "";
25734 }
25735 _scheduleSearch(q) {
25736 if (this._timer) {
25737 clearTimeout(this._timer);
25738 }
25739 this._phase = "loading";
25740 this._open = true;
25741 this._positionDropdown();
25742 this.requestUpdate();
25743 this._timer = setTimeout(() => this._runSearch(q), 200);
25744 }
25745 async _runSearch(q) {
25746 const url = this._endpoint();
25747 if (!url) {
25748 this._phase = "error";
25749 this._error = "Search endpoint is not configured.";
25750 this._results = [];
25751 this._open = true;
25752 this.requestUpdate();
25753 return;
25754 }
25755 if (this._abort) {
25756 this._abort.abort();
25757 }
25758 const ctrl = new AbortController();
25759 this._abort = ctrl;
25760 const exclude = this.getAttribute("exclude") || "";
25761 const full = url + "?q=" + encodeURIComponent(q) + "&exclude=" + encodeURIComponent(exclude);
25762 try {
25763 const init2 = {
25764 signal: ctrl.signal,
25765 credentials: "same-origin"
25766 };
25767 const res = await trackedFetch$1(full, init2, {
25768 source: "desktop-mode/files-user-search",
25769 silent: true
25770 });
25771 if (!res.ok) {
25772 throw new Error(`HTTP ${res.status}`);
25773 }
25774 const json = await res.json();
25775 this._results = json && Array.isArray(json.users) ? json.users : [];
25776 this._phase = "ready";
25777 this._error = "";
25778 this._open = true;
25779 } catch (e) {
25780 if (e.name === "AbortError") {
25781 return;
25782 }
25783 this._results = [];
25784 this._phase = "error";
25785 this._error = e.message || "Search failed.";
25786 this._open = true;
25787 }
25788 this._positionDropdown();
25789 this.requestUpdate();
25790 }
25791 _positionDropdown() {
25792 const input = this.shadowRoot?.querySelector(".input");
25793 if (!input) {
25794 return;
25795 }
25796 const rect = input.getBoundingClientRect();
25797 const top = rect.bottom + 4;
25798 const left = rect.left;
25799 const width = rect.width;
25800 const viewportH = window.innerHeight;
25801 const spaceBelow = viewportH - rect.bottom;
25802 const spaceAbove = rect.top;
25803 const maxHeight = Math.max(120, Math.min(280, Math.max(spaceBelow, spaceAbove) - 16));
25804 if (spaceBelow < 200 && spaceAbove > spaceBelow) {
25805 this._dropdownStyle = [
25806 "position:fixed",
25807 `left:${left}px`,
25808 `top:${rect.top - 4 - maxHeight}px`,
25809 `width:${width}px`,
25810 `max-height:${maxHeight}px`
25811 ].join(";");
25812 } else {
25813 this._dropdownStyle = [
25814 "position:fixed",
25815 `left:${left}px`,
25816 `top:${top}px`,
25817 `width:${width}px`,
25818 `max-height:${maxHeight}px`
25819 ].join(";");
25820 }
25821 }
25822 _dropdownContent() {
25823 if (this._phase === "loading") {
25824 return html`<div class="empty">Searching…</div>`;
25825 }
25826 if (this._phase === "error") {
25827 return html`<div class="empty error">${this._error}</div>`;
25828 }
25829 if (this._results.length === 0) {
25830 const message = this._query ? "No matches." : "No users available.";
25831 return html`<div class="empty">${message}</div>`;
25832 }
25833 return this._results.map(
25834 (u) => html`
25835 <button
25836 type="button"
25837 class="item"
25838 role="option"
25839 @mousedown=${(e) => e.preventDefault()}
25840 @click=${() => this._pick(u)}
25841 >
25842 <img class="avatar" src=${u.avatarUrl} alt="" />
25843 <div>
25844 <div class="name">${u.name}</div>
25845 <div class="slug">${u.slug}</div>
25846 </div>
25847 </button>
25848 `
25849 );
25850 }
25851 render() {
25852 const placeholder = this.getAttribute("placeholder") || "Search users…";
25853 return html`
25854 <input
25855 class="input"
25856 type="search"
25857 placeholder=${placeholder}
25858 autocomplete="off"
25859 @input=${this._onInput}
25860 @focus=${this._onFocus}
25861 @blur=${this._onBlur}
25862 .value=${this._query}
25863 />
25864 ${this._open ? html`
25865 <div class="dropdown" role="listbox" style=${this._dropdownStyle}>
25866 ${this._dropdownContent()}
25867 </div>
25868 ` : html``}
25869 `;
25870 }
25871 };
25872 _WpdUserSearch.props = ["placeholder", "exclude", "endpoint"];
25873 _WpdUserSearch.styles = [userSearchStyles];
25874 _WpdUserSearch.help = {
25875 title: "User autocomplete",
25876 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.",
25877 status: "experimental",
25878 since: "0.8.5",
25879 props: [
25880 { name: "placeholder", type: "string", description: "Input placeholder text." },
25881 {
25882 name: "exclude",
25883 type: "csv user ids",
25884 description: "Already-picked user ids to suppress in results."
25885 },
25886 {
25887 name: "endpoint",
25888 type: "URL",
25889 description: "Override the search URL (defaults to desktopModeConfig.filesUsersSearchUrl)."
25890 }
25891 ],
25892 events: [
25893 { name: "wpd-user-pick", description: "Emitted on pick. Detail: `{ user: SearchUser }`." }
25894 ]
25895 };
25896 let WpdUserSearch = _WpdUserSearch;
25897 defineComponent("wpd-user-search", WpdUserSearch);
25898 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}`;
25899 const _WpdRolePicker = class _WpdRolePicker extends Component {
25900 constructor() {
25901 super(...arguments);
25902 this._onToggle = (slug) => {
25903 const selected = !this._selectedSet().has(slug);
25904 this.emit("wpd-role-toggle", { slug, selected });
25905 };
25906 }
25907 _selectedSet() {
25908 const raw = this.getAttribute("selected") || "";
25909 return new Set(
25910 raw.split(",").map((s) => s.trim()).filter((s) => s !== "")
25911 );
25912 }
25913 _roles() {
25914 const attr = this.getAttribute("roles");
25915 if (attr) {
25916 try {
25917 const parsed = JSON.parse(attr);
25918 if (Array.isArray(parsed)) {
25919 return parsed;
25920 }
25921 } catch (e) {
25922 }
25923 }
25924 return window.desktopModeConfig?.shareEligibleRoles || [];
25925 }
25926 render() {
25927 const roles = this._roles();
25928 if (roles.length === 0) {
25929 return html`<span class="empty">No eligible roles.</span>`;
25930 }
25931 const set = this._selectedSet();
25932 return html`
25933 ${roles.map((r) => {
25934 const isSelected = set.has(r.slug);
25935 return html`
25936 <button
25937 type="button"
25938 class="chip"
25939 aria-pressed=${isSelected ? "true" : "false"}
25940 @click=${() => this._onToggle(r.slug)}
25941 >${r.name}</button>
25942 `;
25943 })}
25944 `;
25945 }
25946 };
25947 _WpdRolePicker.props = ["selected", "roles"];
25948 _WpdRolePicker.styles = [rolePickerStyles];
25949 _WpdRolePicker.help = {
25950 title: "Role picker",
25951 summary: "Chip multi-select for WordPress roles. Reads eligible roles from desktopModeConfig.shareEligibleRoles; emits wpd-role-toggle { slug, selected } on every change.",
25952 status: "experimental",
25953 since: "0.8.5",
25954 props: [
25955 {
25956 name: "selected",
25957 type: "csv role slugs",
25958 description: "Comma-separated role slugs that are currently selected."
25959 },
25960 {
25961 name: "roles",
25962 type: "JSON",
25963 description: "Override the source of eligible roles (defaults to the global config)."
25964 }
25965 ],
25966 events: [
25967 {
25968 name: "wpd-role-toggle",
25969 description: "Emitted on every click. Detail: `{ slug, selected }`."
25970 }
25971 ]
25972 };
25973 let WpdRolePicker = _WpdRolePicker;
25974 defineComponent("wpd-role-picker", WpdRolePicker);
25975 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}`;
25976 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}`;
25977 const _WpdSegment = class _WpdSegment extends Component {
25978 render() {
25979 this.setAttribute("role", "radio");
25980 return html`
25981 <button type="button" @click=${() => this._onPick()}>
25982 <slot></slot>
25983 </button>
25984 `;
25985 }
25986 _onPick() {
25987 this.emit("wpd-segment-pick", {
25988 value: this.value
25989 });
25990 }
25991 };
25992 _WpdSegment.props = ["value"];
25993 _WpdSegment.styles = [segmentStyles];
25994 _WpdSegment.help = {
25995 title: "Segment",
25996 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
25997 status: "stable",
25998 since: "0.9.0",
25999 props: [
26000 {
26001 name: "value",
26002 type: "string",
26003 description: "Identifier this segment contributes to the parent group selection."
26004 }
26005 ],
26006 slots: [
26007 { name: "(default)", description: "Visible segment label." }
26008 ],
26009 events: [
26010 {
26011 name: "wpd-segment-pick",
26012 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
26013 detail: "{ value: string }"
26014 }
26015 ]
26016 };
26017 let WpdSegment = _WpdSegment;
26018 defineComponent("wpd-segment", WpdSegment);
26019 const _WpdSegmented = class _WpdSegmented extends Component {
26020 connectedCallback() {
26021 super.connectedCallback();
26022 this.addEventListener("wpd-segment-pick", (e) => {
26023 const detail = e.detail;
26024 e.stopPropagation();
26025 this.value = detail.value;
26026 this.emit("wpd-pick", { value: detail.value });
26027 });
26028 }
26029 /**
26030 * Declarative item-list setter. Replaces the existing
26031 * `<wpd-segment>` children with a fresh set built from a
26032 * `{ value, label }` array; preserves the current selection
26033 * when the value still matches an entry, otherwise falls back
26034 * to the first item.
26035 *
26036 * Collapses the pre-0.11 imperative dance (clear children,
26037 * `createElement`, set `textContent`, `appendChild`, then
26038 * `setAttribute('value', …)` on the group — order matters) to
26039 * a single assignment:
26040 *
26041 * ```js
26042 * segmented.items = [
26043 * { value: 'm', label: 'm' },
26044 * { value: 'km', label: 'km' },
26045 * ];
26046 * ```
26047 *
26048 * @since 0.5.0
26049 */
26050 set items(list2) {
26051 const existing = this.querySelectorAll(":scope > wpd-segment");
26052 for (const el of Array.from(existing)) {
26053 el.remove();
26054 }
26055 for (const item of list2) {
26056 const seg = document.createElement("wpd-segment");
26057 seg.setAttribute("value", item.value);
26058 seg.textContent = item.label;
26059 this.appendChild(seg);
26060 }
26061 const current = this.value;
26062 const stillValid = current !== null && list2.some((i) => i.value === current);
26063 if (!stillValid && list2.length > 0) {
26064 this.value = list2[0].value;
26065 } else {
26066 this.requestUpdate();
26067 }
26068 }
26069 render() {
26070 const label = this.label || "";
26071 if (label) {
26072 this.setAttribute("aria-label", label);
26073 }
26074 this.setAttribute("role", "radiogroup");
26075 const current = this.value;
26076 queueMicrotask(() => {
26077 const segs = this.querySelectorAll("wpd-segment");
26078 for (const seg of Array.from(segs)) {
26079 const v = seg.getAttribute("value");
26080 seg.setAttribute(
26081 "aria-checked",
26082 v === current ? "true" : "false"
26083 );
26084 }
26085 });
26086 return html`<slot></slot>`;
26087 }
26088 };
26089 _WpdSegmented.props = ["value", "label"];
26090 _WpdSegmented.styles = [segmentedStyles];
26091 _WpdSegmented.help = {
26092 title: "Segmented",
26093 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
26094 status: "stable",
26095 since: "0.9.0",
26096 props: [
26097 {
26098 name: "value",
26099 type: "string",
26100 description: "Currently selected segment value. Mirrored onto child aria-checked."
26101 },
26102 {
26103 name: "label",
26104 type: "string",
26105 description: "aria-label for the radiogroup."
26106 }
26107 ],
26108 slots: [
26109 { name: "(default)", description: '<wpd-segment value="…"> children.' }
26110 ],
26111 events: [
26112 {
26113 name: "wpd-pick",
26114 description: "Fires when the selected segment changes.",
26115 detail: "{ value: string }"
26116 }
26117 ],
26118 cssProps: [
26119 { name: "--desktop-mode-window-bg", description: "Pill background." },
26120 { name: "--desktop-mode-text", description: "Active label colour." },
26121 { name: "--desktop-mode-muted", description: "Inactive label colour." }
26122 ],
26123 example: html`
26124 <wpd-segmented value="md" label="Dock size">
26125 <wpd-segment value="sm">Small</wpd-segment>
26126 <wpd-segment value="md">Medium</wpd-segment>
26127 <wpd-segment value="lg">Large</wpd-segment>
26128 </wpd-segmented>
26129 `
26130 };
26131 let WpdSegmented = _WpdSegmented;
26132 defineComponent("wpd-segmented", WpdSegmented);
26133 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}`;
26134 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:10px;border:1px solid rgba( 255,255,255,0.12 );box-shadow:0 10px 30px rgba( 0,0,0,0.4 ),0 2px 6px rgba( 0,0,0,0.18 ),inset 0 0 0 1px rgba( 255,255,255,0.04 );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}.wpd-toast__close{display:inline-flex;align-items:center;justify-content:center;padding:4px;border-radius:6px;background:transparent;color:rgba( 255,255,255,0.7 )}.wpd-toast__close:hover{background:rgba( 255,255,255,0.14 );color:#fff}@media ( prefers-reduced-motion:reduce ){:host{transition-duration:0.01ms}}`;
26135 const _WpdToastContainer = class _WpdToastContainer extends Component {
26136 connectedCallback() {
26137 super.connectedCallback();
26138 this.setAttribute("aria-live", "polite");
26139 }
26140 render() {
26141 return html`<slot></slot>`;
26142 }
26143 };
26144 _WpdToastContainer.styles = [containerStyles];
26145 _WpdToastContainer.help = {
26146 title: "Toast container",
26147 summary: "Singleton stack beneath <body> that hosts transient <wpd-toast> notifications in the top-right. Created lazily by showToast(); authors rarely place one themselves.",
26148 status: "stable",
26149 since: "0.9.0",
26150 slots: [
26151 { name: "(default)", description: "<wpd-toast> children, stacked vertically." }
26152 ],
26153 cssProps: [
26154 { name: "--desktop-mode-z-fullscreen", description: "z-index base — toasts sit above fullscreen windows." }
26155 ],
26156 example: html`
26157 <wpd-toast-container>
26158 <wpd-toast state="in">Settings saved.</wpd-toast>
26159 <wpd-toast state="in" action="Undo">Theme changed.</wpd-toast>
26160 </wpd-toast-container>
26161 `
26162 };
26163 let WpdToastContainer = _WpdToastContainer;
26164 defineComponent("wpd-toast-container", WpdToastContainer);
26165 const _WpdToast = class _WpdToast extends Component {
26166 connectedCallback() {
26167 super.connectedCallback();
26168 if (!this.hasAttribute("role")) {
26169 this.setAttribute("role", "status");
26170 }
26171 }
26172 render() {
26173 const action = this.action || "";
26174 const dismissible = this.hasAttribute("dismissible");
26175 return html`
26176 <span class="wpd-toast__label"><slot></slot></span>
26177 <button
26178 type="button"
26179 ?hidden=${!action}
26180 @click=${(e) => this._onAction(e)}
26181 >
26182 ${action}
26183 </button>
26184 <button
26185 type="button"
26186 class="wpd-toast__close"
26187 aria-label=${__("Dismiss")}
26188 ?hidden=${!dismissible}
26189 @click=${(e) => this._onDismiss(e)}
26190 >
26191 <svg viewBox="0 0 14 14" width="12" height="12" aria-hidden="true" focusable="false">
26192 <path
26193 d="M3 3 L11 11 M11 3 L3 11"
26194 stroke="currentColor"
26195 stroke-width="1.7"
26196 stroke-linecap="round"
26197 fill="none"
26198 ></path>
26199 </svg>
26200 </button>
26201 `;
26202 }
26203 _onAction(e) {
26204 e.preventDefault();
26205 e.stopPropagation();
26206 this.emit("wpd-toast-action", {});
26207 }
26208 _onDismiss(e) {
26209 e.preventDefault();
26210 e.stopPropagation();
26211 this.emit("wpd-toast-dismiss", {});
26212 }
26213 };
26214 _WpdToast.props = ["action", "state", "dismissible"];
26215 _WpdToast.styles = [toastStyles];
26216 _WpdToast.help = {
26217 title: "Toast",
26218 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.',
26219 status: "stable",
26220 since: "0.9.0",
26221 props: [
26222 {
26223 name: "action",
26224 type: "string",
26225 description: "Optional action button label. When set, a button renders on the right and emits wpd-toast-action on click."
26226 },
26227 {
26228 name: "state",
26229 type: "'in' | 'out'",
26230 description: 'Drives the CSS fade transition. Set to "in" when rendered, flip to "out" before removal.'
26231 },
26232 {
26233 name: "dismissible",
26234 type: "boolean",
26235 description: "When set, a close (×) button renders on the right and emits wpd-toast-dismiss on click. Use for persistent toasts the user must be able to close."
26236 }
26237 ],
26238 slots: [
26239 { name: "(default)", description: "Message text." }
26240 ],
26241 events: [
26242 {
26243 name: "wpd-toast-action",
26244 description: "Fires when the action button is clicked.",
26245 detail: "{}"
26246 },
26247 {
26248 name: "wpd-toast-dismiss",
26249 description: "Fires when the close (×) button is clicked.",
26250 detail: "{}"
26251 }
26252 ],
26253 example: html`
26254 <wpd-toast state="in" action="Undo">Post moved to trash.</wpd-toast>
26255 `
26256 };
26257 let WpdToast = _WpdToast;
26258 defineComponent("wpd-toast", WpdToast);
26259 function buildCapSegmented(initial, onChange) {
26260 const segmented = document.createElement("wpd-segmented");
26261 segmented.setAttribute("value", initial);
26262 segmented.setAttribute("label", "Capability");
26263 segmented.style.setProperty("--wpd-segmented-bg", "rgba(255,255,255,0.06)");
26264 segmented.style.setProperty(
26265 "--desktop-mode-window-bg",
26266 "var(--wp-admin-theme-color, #2271b1)"
26267 );
26268 segmented.style.setProperty("--desktop-mode-text", "#fff");
26269 segmented.style.setProperty("--desktop-mode-muted", "rgba(255,255,255,0.65)");
26270 const segRead = document.createElement("wpd-segment");
26271 segRead.setAttribute("value", "read");
26272 segRead.textContent = "Read";
26273 segmented.appendChild(segRead);
26274 const segWrite = document.createElement("wpd-segment");
26275 segWrite.setAttribute("value", "write");
26276 segWrite.textContent = "Read + Write";
26277 segmented.appendChild(segWrite);
26278 segmented.addEventListener("wpd-pick", (e) => {
26279 const detail = e.detail;
26280 onChange(detail.value);
26281 });
26282 return segmented;
26283 }
26284 function buildIconButton(label, onClick, opts = {}) {
26285 const btn = document.createElement("wpd-button");
26286 btn.setAttribute("variant", "ghost");
26287 btn.setAttribute("aria-label", opts.danger ? "Remove" : "Dismiss");
26288 btn.textContent = label;
26289 const fg = opts.danger ? "#ff8080" : "rgba(255,255,255,0.75)";
26290 const border = opts.danger ? "1px solid rgba(255,128,128,0.45)" : "1px solid rgba(255,255,255,0.18)";
26291 btn.style.setProperty("--wpd-button-fg", fg);
26292 btn.style.setProperty("--wpd-button-border", border);
26293 btn.style.setProperty("--wpd-button-padding", "6px 12px");
26294 btn.style.setProperty("--wpd-button-border-radius", "7px");
26295 btn.style.setProperty("--wpd-button-min-height", "34px");
26296 btn.style.minWidth = "34px";
26297 btn.style.fontSize = "18px";
26298 btn.style.lineHeight = "1";
26299 btn.addEventListener("click", onClick);
26300 return btn;
26301 }
26302 async function openShareSettingsModal(opts) {
26303 const modal = document.createElement("wpd-modal");
26304 modal.setAttribute("open", "");
26305 modal.setAttribute("size", "lg");
26306 modal.setAttribute("title", `Share "${opts.folderName}"`);
26307 document.body.appendChild(modal);
26308 let shares = [];
26309 let pendingPicks = [];
26310 const renderBody = () => {
26311 modal.innerHTML = "";
26312 const owner = document.createElement("div");
26313 owner.style.cssText = "opacity:0.7;margin-bottom:14px;font-size:12px;";
26314 owner.textContent = opts.ownerName ? `Owner: ${opts.ownerName} — cannot be changed` : "Owner cannot be changed";
26315 modal.appendChild(owner);
26316 const addPeople = document.createElement("div");
26317 addPeople.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
26318 const addPeopleLabel = document.createElement("div");
26319 addPeopleLabel.textContent = "Add people";
26320 addPeopleLabel.style.cssText = "font-weight:600;";
26321 addPeople.appendChild(addPeopleLabel);
26322 const userSearch = document.createElement("wpd-user-search");
26323 const excludedUserIds = shares.filter((s) => s.principalType === "user").map((s) => s.principalRef).concat(pendingPicks.filter((p) => p.kind === "user").map((p) => p.ref));
26324 userSearch.setAttribute("exclude", excludedUserIds.join(","));
26325 userSearch.setAttribute("placeholder", "Search users…");
26326 userSearch.addEventListener("wpd-user-pick", (e) => {
26327 const detail = e.detail;
26328 pendingPicks.push({
26329 kind: "user",
26330 ref: String(detail.user.id),
26331 label: detail.user.name,
26332 cap: "read"
26333 });
26334 renderBody();
26335 });
26336 addPeople.appendChild(userSearch);
26337 modal.appendChild(addPeople);
26338 const addRoles = document.createElement("div");
26339 addRoles.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
26340 const addRolesLabel = document.createElement("div");
26341 addRolesLabel.textContent = "Add roles";
26342 addRolesLabel.style.cssText = "font-weight:600;";
26343 addRoles.appendChild(addRolesLabel);
26344 const rolePicker = document.createElement("wpd-role-picker");
26345 const grantedRoles = shares.filter((s) => s.principalType === "role").map((s) => s.principalRef);
26346 const pickedRoles = pendingPicks.filter((p) => p.kind === "role").map((p) => p.ref);
26347 rolePicker.setAttribute("selected", [...grantedRoles, ...pickedRoles].join(","));
26348 rolePicker.addEventListener("wpd-role-toggle", (e) => {
26349 const detail = e.detail;
26350 const existing = shares.find(
26351 (s) => s.principalType === "role" && s.principalRef === detail.slug
26352 );
26353 if (existing) {
26354 if (!detail.selected) {
26355 void revoke(existing);
26356 }
26357 return;
26358 }
26359 if (detail.selected) {
26360 const eligible = (window.desktopModeConfig?.shareEligibleRoles ?? []).find(
26361 (r) => r.slug === detail.slug
26362 );
26363 pendingPicks.push({
26364 kind: "role",
26365 ref: detail.slug,
26366 label: eligible ? eligible.name : detail.slug,
26367 cap: "read"
26368 });
26369 } else {
26370 pendingPicks = pendingPicks.filter(
26371 (p) => !(p.kind === "role" && p.ref === detail.slug)
26372 );
26373 }
26374 renderBody();
26375 });
26376 addRoles.appendChild(rolePicker);
26377 modal.appendChild(addRoles);
26378 if (pendingPicks.length > 0) {
26379 const pendingBlock = document.createElement("div");
26380 pendingBlock.style.cssText = "border:1px dashed rgba(255,255,255,0.18);border-radius:8px;padding:10px;margin-bottom:14px;";
26381 const pendingTitle = document.createElement("div");
26382 pendingTitle.textContent = "New invites (not sent yet)";
26383 pendingTitle.style.cssText = "font-weight:600;margin-bottom:6px;font-size:12px;";
26384 pendingBlock.appendChild(pendingTitle);
26385 for (const pick of pendingPicks) {
26386 const row = document.createElement("div");
26387 row.style.cssText = "display:flex;align-items:center;gap:8px;padding:4px 0;font-size:13px;";
26388 const tag = document.createElement("span");
26389 tag.textContent = pick.kind === "role" ? `Role: ${pick.label}` : pick.label;
26390 tag.style.flex = "1";
26391 row.appendChild(tag);
26392 const capSeg = buildCapSegmented(pick.cap, (next) => {
26393 pick.cap = next;
26394 });
26395 row.appendChild(capSeg);
26396 const removeBtn = buildIconButton("×", () => {
26397 pendingPicks = pendingPicks.filter(
26398 (p) => !(p.kind === pick.kind && p.ref === pick.ref)
26399 );
26400 renderBody();
26401 });
26402 row.appendChild(removeBtn);
26403 pendingBlock.appendChild(row);
26404 }
26405 const sendBtn = document.createElement("wpd-button");
26406 sendBtn.setAttribute("variant", "primary");
26407 sendBtn.textContent = `Send ${pendingPicks.length} invite${pendingPicks.length === 1 ? "" : "s"}`;
26408 sendBtn.style.marginTop = "8px";
26409 sendBtn.addEventListener("click", async () => {
26410 if (pendingPicks.length === 0) {
26411 return;
26412 }
26413 sendBtn.setAttribute("busy", "");
26414 sendBtn.setAttribute("disabled", "");
26415 const snapshot = pendingPicks.slice();
26416 let succeeded = 0;
26417 let firstError = null;
26418 for (const pick of snapshot) {
26419 try {
26420 await inviteShare(opts.folderId, {
26421 principalType: pick.kind,
26422 principalRef: pick.ref,
26423 capability: pick.cap
26424 });
26425 succeeded++;
26426 } catch (err) {
26427 firstError = err;
26428 break;
26429 }
26430 }
26431 if (succeeded > 0) {
26432 pendingPicks = pendingPicks.slice(succeeded);
26433 }
26434 try {
26435 await refresh();
26436 } catch (_e) {
26437 }
26438 if (firstError) {
26439 showToast({
26440 message: `Could not send invites: ${firstError.message}`
26441 });
26442 } else {
26443 showToast({
26444 message: 1 === succeeded ? "Invite sent." : `${succeeded} invites sent.`
26445 });
26446 }
26447 sendBtn.removeAttribute("busy");
26448 sendBtn.removeAttribute("disabled");
26449 renderBody();
26450 });
26451 pendingBlock.appendChild(sendBtn);
26452 modal.appendChild(pendingBlock);
26453 }
26454 const listTitle = document.createElement("div");
26455 listTitle.textContent = "Who has access";
26456 listTitle.style.cssText = "font-weight:600;margin:8px 0 6px;";
26457 modal.appendChild(listTitle);
26458 if (shares.length === 0) {
26459 const empty = document.createElement("div");
26460 empty.textContent = "Only you can see this folder.";
26461 empty.style.cssText = "opacity:0.6;font-size:12px;";
26462 modal.appendChild(empty);
26463 } else {
26464 for (const s of shares) {
26465 const row = document.createElement("div");
26466 row.style.cssText = "display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.04);";
26467 const label = document.createElement("div");
26468 label.style.flex = "1";
26469 label.textContent = s.principalType === "role" ? `Role: ${s.displayName}` : s.displayName;
26470 if (s.state === "pending") {
26471 const tag = document.createElement("span");
26472 tag.textContent = " · pending";
26473 tag.style.cssText = "opacity:0.6;font-size:12px;";
26474 label.appendChild(tag);
26475 } else if (s.state === "denied") {
26476 const tag = document.createElement("span");
26477 tag.textContent = " · denied";
26478 tag.style.cssText = "color:#d63638;font-size:12px;";
26479 label.appendChild(tag);
26480 }
26481 row.appendChild(label);
26482 const cap = s.capability === "write" ? "write" : "read";
26483 const capSeg = buildCapSegmented(cap, (next) => {
26484 void changeCap(s, next);
26485 });
26486 row.appendChild(capSeg);
26487 const removeBtn = buildIconButton(
26488 "×",
26489 () => {
26490 void revoke(s);
26491 },
26492 { danger: true }
26493 );
26494 row.appendChild(removeBtn);
26495 modal.appendChild(row);
26496 }
26497 }
26498 const footer = document.createElement("div");
26499 footer.setAttribute("slot", "footer");
26500 footer.style.display = "flex";
26501 footer.style.justifyContent = "flex-end";
26502 footer.style.gap = "10px";
26503 footer.style.flexWrap = "wrap";
26504 const doneBtn = document.createElement("wpd-button");
26505 doneBtn.setAttribute("variant", "secondary");
26506 doneBtn.textContent = "Done";
26507 doneBtn.addEventListener("click", () => modal.remove());
26508 footer.appendChild(doneBtn);
26509 modal.appendChild(footer);
26510 };
26511 const refresh = async () => {
26512 try {
26513 const res = await listShares(opts.folderId);
26514 shares = res.shares;
26515 setSharesForFolder(opts.folderId, shares);
26516 } catch (err) {
26517 showToast({
26518 message: `Could not load shares: ${err.message}`
26519 });
26520 }
26521 renderBody();
26522 };
26523 const revoke = async (s) => {
26524 try {
26525 await revokeShare(opts.folderId, s.id);
26526 removeShare(opts.folderId, s.id);
26527 await refresh();
26528 showToast({ message: "Access revoked." });
26529 } catch (err) {
26530 showToast({
26531 message: `Could not revoke: ${err.message}`
26532 });
26533 }
26534 };
26535 const changeCap = async (s, cap) => {
26536 try {
26537 const next = await updateShareCapability(opts.folderId, s.id, cap);
26538 upsertShare(next);
26539 await refresh();
26540 } catch (err) {
26541 showToast({
26542 message: `Could not update capability: ${err.message}`
26543 });
26544 }
26545 };
26546 modal.addEventListener("wpd-modal-cancel", () => modal.remove());
26547 renderBody();
26548 await refresh();
26549 }
26550 function openPendingInviteModal(invite) {
26551 return new Promise((resolve2) => {
26552 const modal = document.createElement("wpd-modal");
26553 modal.setAttribute("open", "");
26554 modal.setAttribute("title", invite.folderName ? `${invite.ownerName ?? "Someone"} shared "${invite.folderName}" with you` : "Folder shared with you");
26555 const body = document.createElement("div");
26556 const capLabel = invite.capability === "write" ? "Read + Write" : "Read";
26557 body.innerHTML = `
26558 <p style="margin: 0 0 12px;">Accept the invite to add this folder to your desktop.</p>
26559 <p style="margin: 0; opacity: 0.75;">Access level: <strong>${capLabel}</strong></p>
26560 `;
26561 modal.appendChild(body);
26562 const footer = document.createElement("div");
26563 footer.setAttribute("slot", "footer");
26564 footer.style.display = "flex";
26565 footer.style.justifyContent = "flex-end";
26566 footer.style.gap = "10px";
26567 footer.style.flexWrap = "wrap";
26568 const laterBtn = document.createElement("wpd-button");
26569 laterBtn.setAttribute("variant", "secondary");
26570 laterBtn.textContent = "Decide later";
26571 laterBtn.addEventListener("click", () => {
26572 modal.remove();
26573 resolve2("dismissed");
26574 });
26575 const denyBtn = document.createElement("wpd-button");
26576 denyBtn.setAttribute("variant", "danger");
26577 denyBtn.textContent = "Deny";
26578 denyBtn.addEventListener("click", async () => {
26579 denyBtn.setAttribute("busy", "");
26580 denyBtn.setAttribute("disabled", "");
26581 try {
26582 await denyShare(invite.folderId, invite.id);
26583 sharesStore().state.deniedFolders.add(invite.folderId);
26584 sharesStore().notify();
26585 modal.remove();
26586 resolve2("denied");
26587 } catch (err) {
26588 showToast({
26589 message: `Could not deny: ${err.message}`
26590 });
26591 denyBtn.removeAttribute("busy");
26592 denyBtn.removeAttribute("disabled");
26593 }
26594 });
26595 const acceptBtn = document.createElement("wpd-button");
26596 acceptBtn.setAttribute("variant", "primary");
26597 acceptBtn.textContent = "Accept";
26598 acceptBtn.addEventListener("click", async () => {
26599 acceptBtn.setAttribute("busy", "");
26600 acceptBtn.setAttribute("disabled", "");
26601 try {
26602 await acceptShare(invite.folderId, invite.id);
26603 try {
26604 const res = await listPlacements(0);
26605 setFolderPlacements(0, res.placements);
26606 } catch (_e) {
26607 }
26608 modal.remove();
26609 resolve2("accepted");
26610 } catch (err) {
26611 showToast({
26612 message: `Could not accept: ${err.message}`
26613 });
26614 acceptBtn.removeAttribute("busy");
26615 acceptBtn.removeAttribute("disabled");
26616 }
26617 });
26618 footer.appendChild(laterBtn);
26619 footer.appendChild(denyBtn);
26620 footer.appendChild(acceptBtn);
26621 modal.appendChild(footer);
26622 modal.addEventListener("wpd-modal-cancel", () => {
26623 modal.remove();
26624 resolve2("dismissed");
26625 });
26626 document.body.appendChild(modal);
26627 });
26628 }
26629 function viewerId() {
26630 return Number(window.desktopModeConfig?.currentUserId ?? 0);
26631 }
26632 function sharingEnabled$1() {
26633 const settings = window.wp?.desktop?.getOsSettings?.();
26634 if (!settings) {
26635 return true;
26636 }
26637 return settings.foldersSharingEnabled !== false;
26638 }
26639 function folderOwnerId(folderId) {
26640 const folder = getFilesState().folders.get(folderId);
26641 return folder ? Number(folder.ownerId) : 0;
26642 }
26643 function folderIdFromBaseId(baseId) {
26644 if (typeof baseId !== "string") {
26645 return null;
26646 }
26647 const m = /^desktop-mode-folder-(\d+)$/.exec(baseId);
26648 return m ? Number(m[1]) : null;
26649 }
26650 function placementFolderId(placement) {
26651 if (placement.file.type !== "folder") {
26652 return null;
26653 }
26654 const ref = Number(placement.file.ref);
26655 if (!Number.isFinite(ref) || ref <= 0) {
26656 return null;
26657 }
26658 return ref;
26659 }
26660 function placementOwnerId(placement) {
26661 return Number(placement.file.ownerId ?? 0);
26662 }
26663 function installShareMenuItems() {
26664 addFilter(
26665 "desktop-mode.files.tile-menu",
26666 "desktop-mode/folder-share",
26667 (items, placement) => {
26668 if (!sharingEnabled$1()) {
26669 return items;
26670 }
26671 const folderId = placementFolderId(placement);
26672 if (folderId === null) {
26673 return items;
26674 }
26675 const ownerId = folderOwnerId(folderId) || placementOwnerId(placement);
26676 const viewer = viewerId();
26677 if (ownerId === viewer) {
26678 const shared = !!placement.file.shareSummary?.shared;
26679 const label = shared ? "Manage sharing…" : "Share folder…";
26680 items.push({
26681 id: "desktop-mode/folder-share",
26682 label,
26683 icon: "dashicons-share",
26684 sort: 30,
26685 onClick: () => {
26686 void openShareSettingsModal({
26687 folderId,
26688 folderName: placement.file.title || `Folder ${folderId}`
26689 });
26690 }
26691 });
26692 } else if (ownerId > 0) {
26693 items.push({
26694 id: "desktop-mode/folder-leave",
26695 label: "Leave shared folder",
26696 icon: "dashicons-exit",
26697 sort: 80,
26698 danger: true,
26699 onClick: async () => {
26700 const ok = await wpdConfirm$1({
26701 title: "Leave this folder?",
26702 message: "The folder will be removed from your desktop. The original and its contents are not deleted; the owner keeps them.",
26703 confirmLabel: "Leave",
26704 danger: true
26705 });
26706 if (!ok) {
26707 return;
26708 }
26709 try {
26710 await leaveShare(folderId);
26711 removePlacement(placement.id);
26712 try {
26713 const res = await listPlacements(0);
26714 setFolderPlacements(0, res.placements);
26715 } catch (_e) {
26716 }
26717 const winId = `desktop-mode-folder-${folderId}`;
26718 const mgr = window.desktopMode?.windowManager;
26719 mgr?.close?.(winId);
26720 showToast({ message: "You left the shared folder." });
26721 } catch (err) {
26722 showToast({
26723 message: `Could not leave: ${err.message}`
26724 });
26725 }
26726 }
26727 });
26728 }
26729 return items;
26730 }
26731 );
26732 registerTitleBarButton({
26733 id: "desktop-mode/folder-share",
26734 label: "Share folder",
26735 icon: "dashicons-share",
26736 placement: "right",
26737 order: 50,
26738 match: (w) => {
26739 if (!sharingEnabled$1()) {
26740 return false;
26741 }
26742 const base = w.config.baseId ?? w.id;
26743 const folderId = folderIdFromBaseId(base);
26744 if (folderId === null) {
26745 return false;
26746 }
26747 return folderOwnerId(folderId) === viewerId();
26748 },
26749 onClick: (w) => {
26750 const base = w.config.baseId ?? w.id;
26751 const folderId = folderIdFromBaseId(base);
26752 if (folderId === null) {
26753 return;
26754 }
26755 void openShareSettingsModal({
26756 folderId,
26757 folderName: w.config.title || `Folder ${folderId}`
26758 });
26759 }
26760 });
26761 addAction(
26762 "desktop-mode.files.tile-rendered",
26763 "desktop-mode/folder-share",
26764 (payload) => {
26765 const { tile: tile2, placement } = payload;
26766 if (placement.file.type !== "folder") {
26767 return;
26768 }
26769 const summary = placement.file.shareSummary;
26770 if (!summary?.shared) {
26771 return;
26772 }
26773 if (tile2.querySelector(".desktop-mode-file-tile__share-badge")) {
26774 return;
26775 }
26776 const badge = document.createElement("span");
26777 badge.className = "desktop-mode-file-tile__share-badge dashicons dashicons-share";
26778 badge.setAttribute("aria-label", "Shared folder");
26779 badge.title = "Shared folder";
26780 badge.style.cssText = [
26781 "position:absolute",
26782 "top:6px",
26783 "inset-inline-end:6px",
26784 "background:rgba(0,0,0,0.55)",
26785 "color:#fff",
26786 "border-radius:50%",
26787 "width:18px",
26788 "height:18px",
26789 "font-size:12px",
26790 "line-height:18px",
26791 "text-align:center",
26792 "pointer-events:none"
26793 ].join(";");
26794 tile2.appendChild(badge);
26795 }
26796 );
26797 }
26798 const prompted = /* @__PURE__ */ new Set();
26799 function sharingEnabled() {
26800 const settings = window.wp?.desktop?.getOsSettings?.();
26801 if (!settings) {
26802 return true;
26803 }
26804 return settings.foldersSharingEnabled !== false;
26805 }
26806 function installShareInviteBanner() {
26807 const store2 = sharesStore();
26808 const handle = (state2) => {
26809 if (!sharingEnabled()) {
26810 return;
26811 }
26812 for (const invite of state2.pending) {
26813 if (prompted.has(invite.id)) {
26814 continue;
26815 }
26816 prompted.add(invite.id);
26817 void openPendingInviteModal({
26818 id: invite.id,
26819 folderId: invite.folderId,
26820 folderName: invite.folderName,
26821 ownerName: invite.ownerName,
26822 capability: invite.capability
26823 }).then((decision) => {
26824 if (decision === "accepted") {
26825 dropPending(invite.id);
26826 } else if (decision === "denied") {
26827 dropPending(invite.id, { denied: true, folderId: invite.folderId });
26828 }
26829 });
26830 }
26831 };
26832 store2.subscribe(handle);
26833 handle(store2.state);
26834 }
26835 registerBuiltInFileTypes();
26836 registerBuiltInFileOpeners();
26837 installEmbedPersistence();
26838 registerFileAssociationsTab();
26839 installShareMenuItems();
26840 const seededPending = window.desktopModeConfig?.serverPendingShares;
26841 if (Array.isArray(seededPending) && seededPending.length > 0) {
26842 ingestPendingInvites(seededPending);
26843 }
26844 installShareInviteBanner();
26845 const filesApi = {
26846 DesktopFile,
26847 registerType,
26848 unregisterType,
26849 getType,
26850 getTypes,
26851 resolve,
26852 subscribe,
26853 registerOpener,
26854 unregisterOpener,
26855 getOpener,
26856 getOpeners,
26857 getOpenersForType,
26858 resolveOpener,
26859 subscribeOpeners,
26860 getUserAssociations,
26861 open: openFile,
26862 rest: filesRest,
26863 store: {
26864 get: getFilesStore,
26865 getState: getFilesState,
26866 subscribe: subscribeFilesStore,
26867 setFolderPlacements,
26868 upsertPlacement,
26869 removePlacement,
26870 setFolders,
26871 upsertFolder,
26872 removeFolder
26873 }
26874 };
26875 const SYNTH_META_KEY = "__synthFromDockItem";
26876 function hashToNegativeId(s) {
26877 let h = 0;
26878 for (let i = 0; i < s.length; i++) {
26879 h = (h * 31 + s.charCodeAt(i)) % 2147483647;
26880 }
26881 return -(h + 1);
26882 }
26883 function buildSyntheticPlacement(item, persistedPositions) {
26884 const saved = persistedPositions[item.id];
26885 return {
26886 id: hashToNegativeId(item.id),
26887 parentId: 0,
26888 x: saved ? saved.x : 0,
26889 y: saved ? saved.y : 0,
26890 sortOrder: 9999,
26891 updatedAtMs: Date.now(),
26892 meta: { [SYNTH_META_KEY]: item.id },
26893 file: {
26894 type: "shortcut",
26895 ref: `dock-promoted:${item.id}`,
26896 title: item.title,
26897 icon: item.icon,
26898 previewUrl: "",
26899 exists: true,
26900 // The shortcut opener (built-in-openers.ts) reads these
26901 // off the file shape — `shortcutUrl` is what a dock-item
26902 // promotion naturally has.
26903 shortcutUrl: item.url
26904 }
26905 };
26906 }
26907 function readDockItems() {
26908 const api = window.wp?.desktop;
26909 if (api?.getMenuItems) {
26910 const items = api.getMenuItems();
26911 return items.map((i) => ({
26912 id: i.id,
26913 title: i.title,
26914 icon: i.icon,
26915 url: i.url,
26916 badge: i.badge ?? 0,
26917 submenu: i.submenu ?? [],
26918 isCore: i.isCore
26919 }));
26920 }
26921 const cfg = window.desktopModeConfig;
26922 return cfg?.dockItems ?? [];
26923 }
26924 function readServerIcons() {
26925 const cfg = window.desktopModeConfig;
26926 return cfg?.desktopIcons ?? [];
26927 }
26928 let reentrant = false;
26929 const removedServerPlacementsByRef = /* @__PURE__ */ new Map();
26930 function prunePromotedPositions(ids) {
26931 const api = window.wp?.desktop;
26932 if (!api?.getOsSettings || !api?.updateOsSettings) {
26933 return;
26934 }
26935 const current = api.getOsSettings().dockPromotedPositions ?? {};
26936 const next = { ...current };
26937 let changed = false;
26938 for (const id of ids) {
26939 if (id in next) {
26940 delete next[id];
26941 changed = true;
26942 }
26943 }
26944 if (changed) {
26945 api.updateOsSettings({ dockPromotedPositions: next });
26946 }
26947 }
26948 function syncShortcutsWithVisibility(visibility, positions = {}, layout) {
26949 if (reentrant) {
26950 return;
26951 }
26952 reentrant = true;
26953 try {
26954 const dockItems = readDockItems();
26955 const serverIcons = readServerIcons();
26956 const dockItemsById = new Map(
26957 dockItems.map((item) => [item.id, item])
26958 );
26959 const state2 = filesApi.store.getState();
26960 const root = state2.placementsByFolder.get(0) ?? [];
26961 const currentSynth = /* @__PURE__ */ new Map();
26962 for (const p of root) {
26963 const sourceId = (p.meta ?? null) && typeof p.meta === "object" ? p.meta[SYNTH_META_KEY] : null;
26964 if (typeof sourceId === "string") {
26965 currentSynth.set(sourceId, p);
26966 }
26967 }
26968 const realByRef = /* @__PURE__ */ new Map();
26969 const registeredIconIds = new Set(
26970 serverIcons.map((i) => i.id)
26971 );
26972 for (const p of root) {
26973 const ref = p?.file?.ref;
26974 if (typeof ref === "string" && registeredIconIds.has(ref)) {
26975 realByRef.set(ref, p);
26976 }
26977 }
26978 const desiredSynth = /* @__PURE__ */ new Set();
26979 for (const item of dockItems) {
26980 const resolved = resolvePlacement(item.id, "dock", visibility);
26981 const explicitlyPromoted = resolved === "desktop" || resolved === "both";
26982 const spatialCore = layout === "spatial" && Boolean(item.isCore) && (resolved === "dock" || resolved === "both");
26983 if (explicitlyPromoted || spatialCore) {
26984 desiredSynth.add(item.id);
26985 if (!currentSynth.has(item.id)) {
26986 filesApi.store.upsertPlacement(
26987 buildSyntheticPlacement(item, positions)
26988 );
26989 }
26990 }
26991 }
26992 const positionsToPrune = [];
26993 for (const [sourceId, p] of currentSynth) {
26994 if (desiredSynth.has(sourceId)) {
26995 continue;
26996 }
26997 filesApi.store.removePlacement(p.id);
26998 const sourceItem = dockItemsById.get(sourceId);
26999 const wasOnlySpatialCore = Boolean(sourceItem?.isCore) && visibility[sourceId] === void 0;
27000 if (positions[sourceId] && !wasOnlySpatialCore) {
27001 positionsToPrune.push(sourceId);
27002 }
27003 }
27004 if (positionsToPrune.length > 0) {
27005 prunePromotedPositions(positionsToPrune);
27006 }
27007 for (const icon of serverIcons) {
27008 const placement = visibility[icon.id];
27009 const inStore = realByRef.get(icon.id);
27010 if (placement === "dock" || placement === "hidden") {
27011 if (inStore) {
27012 removedServerPlacementsByRef.set(icon.id, inStore);
27013 filesApi.store.removePlacement(inStore.id);
27014 }
27015 continue;
27016 }
27017 if (!inStore) {
27018 const cached = removedServerPlacementsByRef.get(icon.id);
27019 if (cached) {
27020 filesApi.store.upsertPlacement(cached);
27021 removedServerPlacementsByRef.delete(icon.id);
27022 }
27023 }
27024 }
27025 } finally {
27026 reentrant = false;
27027 }
27028 }
27029 function installShortcutsSync(getVisibility, getPositions = () => ({}), getLayout = () => void 0) {
27030 queueMicrotask(
27031 () => syncShortcutsWithVisibility(
27032 getVisibility(),
27033 getPositions(),
27034 getLayout()
27035 )
27036 );
27037 const off = filesApi.store.subscribe(() => {
27038 syncShortcutsWithVisibility(
27039 getVisibility(),
27040 getPositions(),
27041 getLayout()
27042 );
27043 });
27044 return off;
27045 }
27046 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%}`;
27047 const DEFAULT_EVENT = "desktop-mode-os-settings-save-lifecycle";
27048 const DEFAULT_AUTO_CLEAR_SAVED_MS = 2200;
27049 const DEFAULT_AUTO_CLEAR_FAILED_MS = 6e3;
27050 const _WpdSaveStatus = class _WpdSaveStatus extends Component {
27051 constructor() {
27052 super(...arguments);
27053 this._autoTimer = null;
27054 this._docListener = null;
27055 }
27056 connectedCallback() {
27057 super.connectedCallback();
27058 if (this.auto !== null) {
27059 this._installAutoListener();
27060 }
27061 }
27062 disconnectedCallback() {
27063 this._removeAutoListener();
27064 if (this._autoTimer !== null) {
27065 window.clearTimeout(this._autoTimer);
27066 this._autoTimer = null;
27067 }
27068 }
27069 attributeChangedCallback(name, oldValue, newValue) {
27070 super.attributeChangedCallback(name, oldValue, newValue);
27071 if (name === "auto" || name === "event") {
27072 this._removeAutoListener();
27073 if (this.auto !== null) {
27074 this._installAutoListener();
27075 }
27076 }
27077 if (name === "phase") {
27078 this._scheduleAutoClear();
27079 const detail = {
27080 phase: this.phase ?? "idle",
27081 error: this.error ?? void 0
27082 };
27083 this.emit("wpd-save-status-change", detail);
27084 }
27085 }
27086 render() {
27087 const phase = this.phase ?? "idle";
27088 const mode = this.mode ?? "dot";
27089 const error = this.error ?? "";
27090 const title = error || this._labelForPhase(phase);
27091 if (title) {
27092 this.setAttribute("title", title);
27093 } else {
27094 this.removeAttribute("title");
27095 }
27096 this.setAttribute("aria-live", phase === "failed" ? "assertive" : "polite");
27097 this.setAttribute("role", phase === "failed" ? "alert" : "status");
27098 return html`
27099 <span class="wpd-save-status">
27100 <span class="wpd-save-status__indicator" aria-hidden="true">
27101 <span class="wpd-save-status__glyph">${this._renderGlyph(phase)}</span>
27102 </span>
27103 ${mode === "pill" ? html`<span class="wpd-save-status__label"
27104 >${this._labelForPhase(phase)}</span
27105 >` : html``}
27106 </span>
27107 `;
27108 }
27109 _renderGlyph(phase) {
27110 if (phase === "saved") {
27111 return _iconCheck();
27112 }
27113 if (phase === "failed") {
27114 return _iconBang();
27115 }
27116 return "";
27117 }
27118 _labelForPhase(phase) {
27119 switch (phase) {
27120 case "pending":
27121 case "saving":
27122 return this["saving-label"] ?? "Saving…";
27123 case "saved":
27124 return this["saved-label"] ?? "Saved";
27125 case "failed": {
27126 const err = this.error ?? "";
27127 return err || "Couldn’t save";
27128 }
27129 default:
27130 return this["idle-label"] ?? "";
27131 }
27132 }
27133 _installAutoListener() {
27134 const eventName = this.event || DEFAULT_EVENT;
27135 this._docListener = (e) => {
27136 const detail = e.detail;
27137 if (!detail || typeof detail.phase !== "string") {
27138 return;
27139 }
27140 this.phase = detail.phase;
27141 if (detail.error) {
27142 this.error = detail.error;
27143 } else if (detail.phase !== "failed" && this.error) {
27144 this.removeAttribute("error");
27145 }
27146 };
27147 document.addEventListener(eventName, this._docListener);
27148 }
27149 _removeAutoListener() {
27150 if (!this._docListener) {
27151 return;
27152 }
27153 const eventName = this.event || DEFAULT_EVENT;
27154 document.removeEventListener(eventName, this._docListener);
27155 this._docListener = null;
27156 }
27157 _scheduleAutoClear() {
27158 if (this._autoTimer !== null) {
27159 window.clearTimeout(this._autoTimer);
27160 this._autoTimer = null;
27161 }
27162 const phase = this.phase ?? "idle";
27163 const ms = this._autoClearMsFor(phase);
27164 if (ms <= 0) {
27165 return;
27166 }
27167 this._autoTimer = window.setTimeout(() => {
27168 this._autoTimer = null;
27169 this.phase = "idle";
27170 }, ms);
27171 }
27172 _autoClearMsFor(phase) {
27173 if (phase === "saved") {
27174 const raw = this["auto-clear-saved-ms"];
27175 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_SAVED_MS;
27176 }
27177 if (phase === "failed") {
27178 const raw = this["auto-clear-failed-ms"];
27179 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_FAILED_MS;
27180 }
27181 return 0;
27182 }
27183 };
27184 _WpdSaveStatus.props = [
27185 "phase",
27186 "mode",
27187 "animation",
27188 "auto",
27189 "event",
27190 "error",
27191 "saving-label",
27192 "saved-label",
27193 "idle-label",
27194 "auto-clear-saved-ms",
27195 "auto-clear-failed-ms"
27196 ];
27197 _WpdSaveStatus.styles = [styles$2];
27198 _WpdSaveStatus.help = {
27199 title: "Save status",
27200 summary: 'Tiny status indicator for "is this change saved yet?" affordances. Three layouts (dot / icon / pill), five phases, optional auto-listen to a save-lifecycle CustomEvent so every input in the panel inherits feedback for free.',
27201 status: "experimental",
27202 since: "0.8.0",
27203 props: [
27204 {
27205 name: "phase",
27206 type: "'idle' | 'pending' | 'saving' | 'saved' | 'failed'",
27207 default: "idle",
27208 description: "Current lifecycle phase. Set manually for one-off integrations, or rely on `auto` to populate it from a CustomEvent."
27209 },
27210 {
27211 name: "mode",
27212 type: "'dot' | 'icon' | 'pill'",
27213 default: "dot",
27214 description: "Layout. `dot` is the smallest (10×10 colored dot); `icon` adds a glyph inside on saved/failed; `pill` adds an inline label."
27215 },
27216 {
27217 name: "animation",
27218 type: "'pulse' | 'modem'",
27219 default: "pulse",
27220 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."
27221 },
27222 {
27223 name: "auto",
27224 type: "boolean attribute",
27225 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="…"`.'
27226 },
27227 {
27228 name: "event",
27229 type: "string",
27230 default: "desktop-mode-os-settings-save-lifecycle",
27231 description: "CustomEvent name to listen on when `auto` is set."
27232 },
27233 {
27234 name: "error",
27235 type: "string",
27236 description: "Error message shown in `pill` mode and exposed as the host title attribute (so dot/icon modes still surface the message via tooltip)."
27237 },
27238 {
27239 name: "saving-label",
27240 type: "string",
27241 default: "Saving…",
27242 description: "Pill-mode label shown during `pending` / `saving`."
27243 },
27244 {
27245 name: "saved-label",
27246 type: "string",
27247 default: "Saved",
27248 description: "Pill-mode label shown during `saved`."
27249 },
27250 {
27251 name: "idle-label",
27252 type: "string",
27253 description: 'Optional pill-mode label shown during `idle` (e.g. "All changes saved"). When unset, the pill collapses to invisible while idle.'
27254 },
27255 {
27256 name: "auto-clear-saved-ms",
27257 type: "integer",
27258 default: "2200",
27259 description: "How long the `saved` phase stays visible before auto-fading back to `idle`."
27260 },
27261 {
27262 name: "auto-clear-failed-ms",
27263 type: "integer",
27264 default: "6000",
27265 description: "How long the `failed` phase stays visible before auto-fading back to `idle`."
27266 }
27267 ],
27268 events: [
27269 {
27270 name: "wpd-save-status-change",
27271 description: "Fires when the phase changes (manually or via auto-listen).",
27272 detail: "{ phase, error }"
27273 }
27274 ],
27275 cssProps: [
27276 {
27277 name: "--wpd-save-status-bg",
27278 description: "Indicator background color (saving/pending phase)."
27279 },
27280 {
27281 name: "--wpd-save-status-saved-bg",
27282 description: "Indicator background on saved."
27283 },
27284 {
27285 name: "--wpd-save-status-failed-bg",
27286 description: "Indicator background on failed."
27287 },
27288 {
27289 name: "--wpd-save-status-pill-bg",
27290 description: "Pill background (mode=pill)."
27291 },
27292 {
27293 name: "--wpd-save-status-pill-fg",
27294 description: "Pill foreground (mode=pill)."
27295 }
27296 ],
27297 example: html`
27298 <wpd-cluster gap="12">
27299 <wpd-save-status phase="pending"></wpd-save-status>
27300 <wpd-save-status phase="saving"></wpd-save-status>
27301 <wpd-save-status phase="saved"></wpd-save-status>
27302 <wpd-save-status phase="failed"></wpd-save-status>
27303 <wpd-save-status mode="pill" phase="saving"></wpd-save-status>
27304 <wpd-save-status mode="pill" phase="saved"></wpd-save-status>
27305 <wpd-save-status mode="pill" phase="failed" error="Network error."></wpd-save-status>
27306 </wpd-cluster>
27307 `
27308 };
27309 let WpdSaveStatus = _WpdSaveStatus;
27310 defineComponent("wpd-save-status", WpdSaveStatus);
27311 function _iconCheck() {
27312 return html`
27313 <svg
27314 viewBox="0 0 12 12"
27315 aria-hidden="true"
27316 focusable="false"
27317 fill="none"
27318 stroke="currentColor"
27319 stroke-width="2"
27320 stroke-linecap="round"
27321 stroke-linejoin="round"
27322 >
27323 <path d="M2.5 6 L5 8.5 L9.5 4" />
27324 </svg>
27325 `;
27326 }
27327 function _iconBang() {
27328 return html`
27329 <svg
27330 viewBox="0 0 12 12"
27331 aria-hidden="true"
27332 focusable="false"
27333 fill="currentColor"
27334 >
27335 <path
27336 d="M5 2 H7 V7 H5 z M5 8.5 H7 V10.5 H5 z"
27337 />
27338 </svg>
27339 `;
27340 }
27341 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}`;
27342 const _WpdTextarea = class _WpdTextarea extends Component {
27343 constructor() {
27344 super(...arguments);
27345 this._textareaEl = null;
27346 }
27347 connectedCallback() {
27348 super.connectedCallback();
27349 ensureAutoId(this);
27350 }
27351 render() {
27352 const label = this._attr("label") || "";
27353 const value = this._attr("value") ?? "";
27354 const placeholder = this._attr("placeholder") || "";
27355 const disabled = this._boolAttr("disabled");
27356 const readonly = this._boolAttr("readonly");
27357 const ariaLabel = this._attr("aria-label") || label;
27358 const name = this._attr("name") || "";
27359 const rows = Number(this._attr("rows")) || 3;
27360 const maxLength = this._attr("maxlength");
27361 const minLength = this._attr("minlength");
27362 const invalid = this._boolAttr("invalid");
27363 const hostId = this.id || "wpd-unnamed";
27364 const fieldId = `${hostId}__field`;
27365 return html`
27366 ${label ? html`<label class="wpd-textarea__label" for=${fieldId}>${label}</label>` : html``}
27367 <textarea
27368 id=${fieldId}
27369 part="textarea"
27370 .value=${value}
27371 placeholder=${placeholder}
27372 ?disabled=${disabled}
27373 ?readonly=${readonly}
27374 rows=${rows}
27375 maxlength=${maxLength ?? ""}
27376 minlength=${minLength ?? ""}
27377 name=${name}
27378 aria-invalid=${invalid ? "true" : "false"}
27379 aria-label=${ariaLabel || ""}
27380 @input=${(e) => this._onInput(e)}
27381 @change=${(e) => this._onChange(e)}
27382 @keydown=${(e) => this._onKeyDown(e)}
27383 ></textarea>
27384 `;
27385 }
27386 _attr(name) {
27387 return this.getAttribute(name);
27388 }
27389 _boolAttr(name) {
27390 return this.getAttribute(name) !== null;
27391 }
27392 _onInput(e) {
27393 const ta = e.target;
27394 this._textareaEl = ta;
27395 this.setAttribute("value", ta.value);
27396 this.emit("wpd-input-change", { value: ta.value });
27397 if (this._boolAttr("auto-grow")) {
27398 this._autosize(ta);
27399 }
27400 }
27401 _onChange(e) {
27402 const ta = e.target;
27403 this.emit("wpd-input-commit", { value: ta.value });
27404 }
27405 _onKeyDown(e) {
27406 if (!this._boolAttr("submit-on-enter")) {
27407 return;
27408 }
27409 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
27410 e.preventDefault();
27411 const ta = e.target;
27412 this.emit("wpd-submit", { value: ta.value });
27413 }
27414 }
27415 /**
27416 * Grow the textarea height to fit content, capped at `max-rows`.
27417 * Resets to scroll-height each input then clamps; cheap because
27418 * the browser caches layout.
27419 */
27420 _autosize(ta) {
27421 const maxRows = Number(this._attr("max-rows")) || 8;
27422 const cs = window.getComputedStyle(ta);
27423 const fontSize = parseFloat(cs.fontSize) || 13;
27424 const lineHeightRaw = cs.lineHeight;
27425 const lineHeight = lineHeightRaw === "normal" ? fontSize * 1.45 : parseFloat(lineHeightRaw) || fontSize * 1.45;
27426 const paddingTop = parseFloat(cs.paddingTop) || 0;
27427 const paddingBottom = parseFloat(cs.paddingBottom) || 0;
27428 const max = lineHeight * maxRows + paddingTop + paddingBottom;
27429 ta.style.height = "auto";
27430 const next = Math.min(ta.scrollHeight, max);
27431 ta.style.height = `${next}px`;
27432 }
27433 /** Public helper for callers that programmatically set `.value` and want autosize to re-run. */
27434 refreshAutosize() {
27435 if (this._textareaEl && this._boolAttr("auto-grow")) {
27436 this._autosize(this._textareaEl);
27437 }
27438 }
27439 /** Imperatively focus the underlying textarea. */
27440 focusInput() {
27441 const root = this.shadowRoot ?? this;
27442 const ta = root.querySelector("textarea");
27443 ta?.focus();
27444 }
27445 /** Imperatively clear the value. */
27446 clear() {
27447 this.setAttribute("value", "");
27448 const root = this.shadowRoot ?? this;
27449 const ta = root.querySelector("textarea");
27450 if (ta) {
27451 ta.value = "";
27452 if (this._boolAttr("auto-grow")) {
27453 this._autosize(ta);
27454 }
27455 }
27456 }
27457 };
27458 _WpdTextarea.props = [
27459 "label",
27460 "value",
27461 "placeholder",
27462 "disabled",
27463 "readonly",
27464 "ariaLabel",
27465 "name",
27466 "rows",
27467 "maxlength",
27468 "minlength",
27469 "invalid",
27470 "autoGrow",
27471 "maxRows",
27472 "submitOnEnter"
27473 ];
27474 _WpdTextarea.styles = [textareaStyles];
27475 _WpdTextarea.help = {
27476 title: "Textarea",
27477 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).",
27478 status: "stable",
27479 since: "0.6.0",
27480 props: [
27481 { name: "label", type: "string", description: "Visible label above the textarea." },
27482 { name: "value", type: "string", description: "Current value; reflected two-way." },
27483 { name: "placeholder", type: "string", description: "Native placeholder." },
27484 { name: "disabled", type: "boolean attribute" },
27485 { name: "readonly", type: "boolean attribute" },
27486 { name: "aria-label", type: "string", description: "Accessible label when no visible label is rendered." },
27487 { name: "name", type: "string", description: "Forwarded to native textarea for form submission." },
27488 { name: "rows", type: "integer (string)", default: "3", description: "Initial visible row count." },
27489 { name: "maxlength", type: "integer (string)" },
27490 { name: "minlength", type: "integer (string)" },
27491 { name: "invalid", type: "boolean attribute", description: "Sets aria-invalid + error styling." },
27492 { name: "auto-grow", type: "boolean attribute", description: "Grows up to max-rows as the user types." },
27493 { name: "max-rows", type: "integer (string)", default: "8" },
27494 {
27495 name: "submit-on-enter",
27496 type: "boolean attribute",
27497 description: "Enter fires wpd-submit; Shift+Enter inserts a newline."
27498 }
27499 ],
27500 events: [
27501 { name: "wpd-input-change", description: "Fires on every keystroke.", detail: "{ value: string }" },
27502 { name: "wpd-input-commit", description: "Fires on blur / native change.", detail: "{ value: string }" },
27503 {
27504 name: "wpd-submit",
27505 description: "Fires on Enter (without Shift) when submit-on-enter is set.",
27506 detail: "{ value: string }"
27507 }
27508 ],
27509 example: html`
27510 <wpd-textarea label="Message" rows="3" auto-grow max-rows="8" submit-on-enter></wpd-textarea>
27511 `
27512 };
27513 let WpdTextarea = _WpdTextarea;
27514 defineComponent("wpd-textarea", WpdTextarea);
27515 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}`;
27516 const ICONS = {
27517 minimize: '<path d="M3 6h6" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>',
27518 maximize: '<rect x="3" y="3" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.25" fill="none"/>',
27519 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"/>',
27520 "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"/>',
27521 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"/>',
27522 reload: (
27523 // Filled icon scaled from a 512×512 source into the 12×12 viewBox
27524 // shared with the other title-bar glyphs. The wrapping `<g>` does
27525 // the math; the inner path is dropped in unmodified so its
27526 // authoring tool can be re-edited and copy-pasted again.
27527 // `scale(0.021)` ≈ 90% of full fit, with `translate(0.6)` to
27528 // keep the result centered inside the 12×12 viewBox so the
27529 // glyph reads slightly smaller than min/max/close — closer to
27530 // the visual weight of the other title-bar buttons.
27531 '<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>'
27532 ),
27533 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"/>',
27534 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"/>'
27535 };
27536 const _WpdWindowButton = class _WpdWindowButton extends Component {
27537 constructor() {
27538 super(...arguments);
27539 this._activateWired = false;
27540 }
27541 render() {
27542 const iconKey = this.icon || "";
27543 const svgInner = ICONS[iconKey] || "";
27544 return html`
27545 <button type="button">
27546 <svg
27547 width="14"
27548 height="14"
27549 viewBox="0 0 12 12"
27550 aria-hidden="true"
27551 focusable="false"
27552 ></svg>
27553 <slot></slot>
27554 </button>
27555 <span data-svg-buffer style="display:none">${svgInner}</span>
27556 `;
27557 }
27558 /**
27559 * After each render, copy the raw SVG markup into the actual
27560 * `<svg>` element. The templater only writes text into slots,
27561 * so we stash the intended markup in a hidden buffer and
27562 * `innerHTML = ` the svg once here — a one-shot post-render
27563 * hook that keeps the declarative template honest.
27564 *
27565 * Also wires up the `wpd-button-activate` CustomEvent that
27566 * fires exactly once per gesture — the canonical contract
27567 * for plugin-registered title-bar buttons. Plugin authors who
27568 * use `addEventListener( 'click', cb )` directly still get
27569 * what they expect (the title bar's drag-handler now excludes
27570 * chrome buttons by class so static clicks land normally),
27571 * but `wpd-button-activate` is the documented surface that
27572 * documents the once-per-gesture contract explicitly. See
27573 * the class-level docblock for rationale.
27574 */
27575 connectedCallback() {
27576 super.connectedCallback();
27577 queueMicrotask(() => this._paintSvg());
27578 queueMicrotask(() => this._wireActivateEvent());
27579 }
27580 attributeChangedCallback(name, oldValue, newValue) {
27581 super.attributeChangedCallback(name, oldValue, newValue);
27582 queueMicrotask(() => this._paintSvg());
27583 }
27584 _paintSvg() {
27585 const root = this.shadowRoot;
27586 if (!root) {
27587 return;
27588 }
27589 const svg = root.querySelector("svg");
27590 const buffer = root.querySelector("[data-svg-buffer]");
27591 if (svg && buffer) {
27592 const markup = buffer.textContent || "";
27593 if (svg.innerHTML !== markup) {
27594 svg.innerHTML = markup;
27595 }
27596 }
27597 }
27598 _wireActivateEvent() {
27599 if (this._activateWired) {
27600 return;
27601 }
27602 const root = this.shadowRoot;
27603 if (!root) {
27604 return;
27605 }
27606 const button = root.querySelector("button");
27607 if (!button) {
27608 return;
27609 }
27610 this._activateWired = true;
27611 button.addEventListener("click", () => {
27612 this.dispatchEvent(
27613 new CustomEvent("wpd-button-activate", {
27614 bubbles: true,
27615 composed: true,
27616 cancelable: true
27617 })
27618 );
27619 });
27620 }
27621 };
27622 _WpdWindowButton.props = ["icon", "active", "danger"];
27623 _WpdWindowButton.styles = [styles$1];
27624 _WpdWindowButton.help = {
27625 title: "Window button",
27626 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.",
27627 status: "stable",
27628 since: "0.9.0",
27629 props: [
27630 {
27631 name: "icon",
27632 type: "'minimize' | 'maximize' | 'fullscreen' | 'fullscreen-exit' | 'detach' | 'reload' | 'close' | 'menu'",
27633 description: "Which built-in inline SVG to paint. Omit to supply your own via the slot."
27634 },
27635 {
27636 name: "active",
27637 type: "boolean attribute",
27638 description: "Applies the pressed-down look (used e.g. while a menu it triggers is open)."
27639 },
27640 {
27641 name: "danger",
27642 type: "boolean attribute",
27643 description: "Swaps the hover wash to red — used by the close button."
27644 }
27645 ],
27646 slots: [
27647 { name: "(default)", description: "Optional custom icon markup (inline SVG) when `icon` is omitted." }
27648 ],
27649 cssProps: [
27650 { name: "--wpd-btn-color", description: "Resting foreground." },
27651 { name: "--wpd-btn-color-hover", description: "Hover foreground." },
27652 { name: "--wpd-btn-bg-hover", description: "Hover background wash." },
27653 { name: "--wpd-btn-bg-active", description: "Pressed background." },
27654 { name: "--wpd-btn-danger-hover", description: "Hover background for danger variant." },
27655 { name: "--wpd-btn-outline", description: "Focus outline colour." }
27656 ],
27657 example: html`
27658 <wpd-cluster gap="2">
27659 <wpd-window-button icon="minimize"></wpd-window-button>
27660 <wpd-window-button icon="maximize"></wpd-window-button>
27661 <wpd-window-button icon="menu"></wpd-window-button>
27662 <wpd-window-button icon="close" danger></wpd-window-button>
27663 </wpd-cluster>
27664 `
27665 };
27666 let WpdWindowButton = _WpdWindowButton;
27667 defineComponent("wpd-window-button", WpdWindowButton);
27668 const DEFAULT_STICKY_TITLE = "Sticky Note";
27669 const LEGACY_METADATA_PREFIX = "<!-- wpworkspace-sticky:";
27670 const LEGACY_METADATA_SUFFIX = "-->";
27671 const TITLE_MAX = 64;
27672 const GENERATED_TITLE_MAX = 48;
27673 const EXCERPT_MAX = 180;
27674 function noteFromGuideline(guideline) {
27675 const title = titleField(guideline.title);
27676 const content = removeLegacyMetadataComment(
27677 textFieldValue(guideline.content, { stripHtmlForRendered: true })
27678 );
27679 const modifiedMs = modifiedTimeMs(guideline);
27680 return {
27681 localId: `guideline:${guideline.id}`,
27682 guidelineId: guideline.id,
27683 title,
27684 body: editorBody(title, content),
27685 modified: guideline.modified,
27686 ...modifiedMs > 0 ? { modifiedMs } : {},
27687 link: guideline.link,
27688 termIds: Array.isArray(guideline.wp_guideline_type) ? guideline.wp_guideline_type.filter(isFiniteNumber) : []
27689 };
27690 }
27691 function titleField(field) {
27692 const candidates = [];
27693 if (typeof field === "string") {
27694 candidates.push(field);
27695 } else if (field && typeof field === "object") {
27696 if (typeof field.raw === "string") {
27697 candidates.push(field.raw);
27698 }
27699 if (typeof field.rendered === "string") {
27700 candidates.push(stripHtml(field.rendered));
27701 }
27702 }
27703 for (const candidate of candidates) {
27704 const trimmed = stripHtml(candidate).trim();
27705 if (trimmed) {
27706 return trimmed;
27707 }
27708 }
27709 return DEFAULT_STICKY_TITLE;
27710 }
27711 function textFieldValue(field, options = {}) {
27712 if (typeof field === "string") {
27713 return field;
27714 }
27715 if (!field || typeof field !== "object") {
27716 return "";
27717 }
27718 if (typeof field.raw === "string" && field.raw.length > 0) {
27719 return field.raw;
27720 }
27721 if (typeof field.rendered === "string") {
27722 return options.stripHtmlForRendered ? stripHtml(field.rendered) : field.rendered;
27723 }
27724 return "";
27725 }
27726 function titleForBody(body) {
27727 const line = body.split(/\r?\n/).find((item) => item.trim().length > 0)?.trim();
27728 const title = line && line.length > 0 ? line : DEFAULT_STICKY_TITLE;
27729 return truncate(title, TITLE_MAX);
27730 }
27731 function generatedTitle(body) {
27732 const collapsed = body.replace(/\s+/g, " ").trim();
27733 const title = collapsed || DEFAULT_STICKY_TITLE;
27734 return truncate(title, GENERATED_TITLE_MAX);
27735 }
27736 function editorBody(title, content) {
27737 const trimmedTitle = title.trim();
27738 if (!trimmedTitle) {
27739 return content;
27740 }
27741 const firstLine = content.split(/\r?\n/)[0]?.trim();
27742 if (firstLine === trimmedTitle) {
27743 return content;
27744 }
27745 if (!content) {
27746 return trimmedTitle;
27747 }
27748 return `${trimmedTitle}
27749 ${content}`;
27750 }
27751 function noteComponentsForBody(editorValue, fallbackTitle = DEFAULT_STICKY_TITLE) {
27752 const fallback = fallbackTitle.trim() || DEFAULT_STICKY_TITLE;
27753 const title = titleForBody(editorValue);
27754 const firstNewline = editorValue.search(/\r?\n/);
27755 if (firstNewline === -1) {
27756 const resolvedTitle = title === DEFAULT_STICKY_TITLE ? fallback : title;
27757 return {
27758 title: resolvedTitle,
27759 content: "",
27760 excerpt: excerptFor(resolvedTitle)
27761 };
27762 }
27763 let content = editorValue.slice(firstNewline);
27764 content = content.replace(/^\r?\n/, "");
27765 if (content.startsWith("\n")) {
27766 content = content.slice(1);
27767 }
27768 return {
27769 title,
27770 content,
27771 excerpt: excerptFor(content.trim() ? content : title)
27772 };
27773 }
27774 function excerptFor(body) {
27775 const collapsed = body.replace(/[\n\t]+/g, " ").trim();
27776 return truncate(collapsed, EXCERPT_MAX);
27777 }
27778 function removeLegacyMetadataComment(content) {
27779 if (!content.startsWith(LEGACY_METADATA_PREFIX) || !content.includes(LEGACY_METADATA_SUFFIX)) {
27780 return content;
27781 }
27782 const end = content.indexOf(LEGACY_METADATA_SUFFIX);
27783 let body = content.slice(end + LEGACY_METADATA_SUFFIX.length);
27784 if (body.startsWith("\r\n")) {
27785 body = body.slice(2);
27786 } else if (body.startsWith("\n")) {
27787 body = body.slice(1);
27788 }
27789 return body;
27790 }
27791 function stripHtml(value) {
27792 if (typeof document !== "undefined") {
27793 const template = document.createElement("template");
27794 template.innerHTML = value;
27795 return (template.content.textContent ?? "").trim();
27796 }
27797 return value.replace(/<[^>]*>/g, "").trim();
27798 }
27799 function truncate(value, max) {
27800 return value.length > max ? `${value.slice(0, max)}...` : value;
27801 }
27802 function modifiedTimeMs(guideline) {
27803 if (typeof guideline.desktop_mode_modified_ms === "number" && Number.isFinite(guideline.desktop_mode_modified_ms)) {
27804 return guideline.desktop_mode_modified_ms;
27805 }
27806 if (!guideline.modified) {
27807 return 0;
27808 }
27809 const parsed = Date.parse(guideline.modified);
27810 return Number.isFinite(parsed) ? parsed : 0;
27811 }
27812 function isFiniteNumber(value) {
27813 return typeof value === "number" && Number.isFinite(value);
27814 }
27815 class StickyNotesRestError extends Error {
27816 constructor(message, status) {
27817 super(message);
27818 this.name = "StickyNotesRestError";
27819 this.status = status;
27820 }
27821 }
27822 async function resolveStickyTerms(config) {
27823 const terms = await fetchStickyTermCandidates(config);
27824 const picked = pickStickyTerms(
27825 [...terms.artifactTerms, ...terms.artifactsTerms],
27826 terms.noteTerms,
27827 terms.stickyTerms
27828 );
27829 if (picked) {
27830 return picked;
27831 }
27832 const artifact = await ensureTerm(config, {
27833 slug: "artifact",
27834 name: "Artifact",
27835 parent: 0
27836 });
27837 const note = await ensureTerm(config, {
27838 slug: "note",
27839 name: "Note",
27840 parent: artifact.id
27841 });
27842 const sticky = await ensureTerm(config, {
27843 slug: "sticky",
27844 name: "Sticky",
27845 parent: artifact.id
27846 });
27847 return {
27848 stickyTermId: sticky.id,
27849 termIds: uniqueNumbers([artifact.id, note.id, sticky.id])
27850 };
27851 }
27852 async function fetchStickyTermCandidates(config) {
27853 const [artifactTerms, artifactsTerms, noteTerms, stickyTerms] = await Promise.all([
27854 fetchTermsBySlug(config, "artifact"),
27855 fetchTermsBySlug(config, "artifacts"),
27856 fetchTermsBySlug(config, "note"),
27857 fetchTermsBySlug(config, "sticky")
27858 ]);
27859 return {
27860 artifactTerms,
27861 artifactsTerms,
27862 noteTerms,
27863 stickyTerms
27864 };
27865 }
27866 function pickStickyTerms(artifactTerms, noteTerms, stickyTerms) {
27867 if (stickyTerms.length === 0) {
27868 return null;
27869 }
27870 const artifact = artifactTerms.find(
27871 (term) => ["artifact", "artifacts"].includes(term.slug)
27872 ) ?? artifactTerms[0] ?? null;
27873 const sticky = artifact ? stickyTerms.find((term) => Number(term.parent) === artifact.id) ?? stickyTerms[0] : stickyTerms[0];
27874 if (!sticky) {
27875 return null;
27876 }
27877 const note = artifact ? noteTerms.find((term) => Number(term.parent) === artifact.id) ?? null : null;
27878 return {
27879 stickyTermId: sticky.id,
27880 termIds: uniqueNumbers([
27881 artifact?.id,
27882 note?.id,
27883 sticky.id
27884 ])
27885 };
27886 }
27887 async function fetchStickyNotes(config, stickyTermId) {
27888 const guidelines = await requestJson(
27889 config,
27890 pathWithQuery("wp/v2/guidelines", {
27891 context: "edit",
27892 status: "private",
27893 per_page: "100",
27894 orderby: "modified",
27895 order: "desc",
27896 wp_guideline_type: String(stickyTermId)
27897 }),
27898 void 0,
27899 true
27900 );
27901 return guidelines.filter(
27902 (guideline) => Array.isArray(guideline.wp_guideline_type) ? guideline.wp_guideline_type.includes(stickyTermId) : true
27903 ).map(noteFromGuideline);
27904 }
27905 async function saveStickyNote(config, note, terms) {
27906 const components = noteComponentsForBody(note.body, note.title);
27907 const payload = {
27908 status: "private",
27909 title: components.title,
27910 content: components.content,
27911 excerpt: components.excerpt
27912 };
27913 if (note.guidelineId === null) {
27914 payload.wp_guideline_type = terms.termIds;
27915 }
27916 const path = note.guidelineId === null ? "wp/v2/guidelines" : `wp/v2/guidelines/${note.guidelineId}`;
27917 const guideline = await requestJson(
27918 config,
27919 path,
27920 {
27921 method: "POST",
27922 headers: {
27923 "Content-Type": "application/json"
27924 },
27925 body: JSON.stringify(payload)
27926 },
27927 false
27928 );
27929 return noteFromGuideline(guideline);
27930 }
27931 function buildGuidelineEditUrl(adminUrl, guidelineId) {
27932 const url = new URL("post.php", adminUrl);
27933 url.searchParams.set("post", String(guidelineId));
27934 url.searchParams.set("action", "edit");
27935 return url.toString();
27936 }
27937 async function fetchTermsBySlug(config, slug) {
27938 try {
27939 return await requestJson(
27940 config,
27941 pathWithQuery("wp/v2/wp_guideline_type", {
27942 context: "edit",
27943 slug,
27944 per_page: "100"
27945 }),
27946 void 0,
27947 true
27948 );
27949 } catch (error) {
27950 if (error instanceof StickyNotesRestError && (error.status === 404 || error.status === 400)) {
27951 return [];
27952 }
27953 throw error;
27954 }
27955 }
27956 async function ensureTerm(config, term) {
27957 const existing = await fetchTermsBySlug(config, term.slug);
27958 const byParent = existing.find(
27959 (item) => Number(item.parent ?? 0) === term.parent
27960 );
27961 if (byParent) {
27962 return byParent;
27963 }
27964 if (existing[0]) {
27965 return existing[0];
27966 }
27967 try {
27968 return await requestJson(
27969 config,
27970 "wp/v2/wp_guideline_type",
27971 {
27972 method: "POST",
27973 headers: {
27974 "Content-Type": "application/json"
27975 },
27976 body: JSON.stringify(term)
27977 },
27978 true
27979 );
27980 } catch (error) {
27981 const fallback = await fetchTermsBySlug(config, term.slug);
27982 if (fallback[0]) {
27983 return fallback[0];
27984 }
27985 throw error;
27986 }
27987 }
27988 async function requestJson(config, path, init2, silent = true) {
27989 const response = await trackedFetch$1(
27990 joinRestUrl(restRoot(config), path),
27991 init2,
27992 {
27993 source: "desktop-mode/sticky-notes",
27994 silent
27995 }
27996 );
27997 if (!response.ok) {
27998 throw new StickyNotesRestError(
27999 response.statusText || `${DEFAULT_STICKY_TITLE} request failed`,
28000 response.status
28001 );
28002 }
28003 return await response.json();
28004 }
28005 function restRoot(config) {
28006 if (config.restUrl) {
28007 return config.restUrl;
28008 }
28009 return `${window.location.origin}/wp-json/`;
28010 }
28011 function pathWithQuery(path, query) {
28012 const params = new URLSearchParams();
28013 Object.entries(query).forEach(([key, value]) => {
28014 params.set(key, value);
28015 });
28016 return `${path}?${params.toString()}`;
28017 }
28018 function uniqueNumbers(values) {
28019 const out = [];
28020 values.forEach((value) => {
28021 if (typeof value === "number" && Number.isFinite(value) && !out.includes(value)) {
28022 out.push(value);
28023 }
28024 });
28025 return out;
28026 }
28027 const SUBSCRIBE_FIELD = "desktop_mode_sticky_notes_subscribe";
28028 const RESPONSE_FIELD = "desktop_mode_sticky_notes";
28029 let started$3 = false;
28030 let target = null;
28031 function startStickyNotesHeartbeat(nextTarget) {
28032 target = nextTarget;
28033 if (started$3) {
28034 return;
28035 }
28036 started$3 = true;
28037 heartbeat.contribute(
28038 SUBSCRIBE_FIELD,
28039 () => target?.getHeartbeatSubscription()
28040 );
28041 heartbeat.subscribe(
28042 RESPONSE_FIELD,
28043 (payload) => {
28044 target?.applyHeartbeatPayload(payload);
28045 }
28046 );
28047 }
28048 const GEOMETRY_KEY = "desktop-mode-sticky-notes-geometry";
28049 const DEFAULT_WIDTH = 264;
28050 const DEFAULT_HEIGHT = 176;
28051 const MIN_WIDTH = 180;
28052 const MIN_HEIGHT = 128;
28053 const EDGE_PADDING = 16;
28054 const SAVE_DEBOUNCE_MS = 1e3;
28055 class StickyNotesLayer {
28056 constructor(options) {
28057 this.root = null;
28058 this.terms = null;
28059 this.controllers = /* @__PURE__ */ new Map();
28060 this.contextMenuInstalled = false;
28061 this.desktopHooksInstalled = false;
28062 this.highWaterMs = 0;
28063 this.zIndexCounter = 0;
28064 this.host = options.host;
28065 this.config = options.config;
28066 this.available = options.available ?? true;
28067 this.openArtifact = options.openArtifact;
28068 this.getActiveDesktopId = options.getActiveDesktopId ?? (() => "desktop-1");
28069 this.onError = options.onError;
28070 }
28071 async boot() {
28072 if (!this.available) {
28073 return;
28074 }
28075 try {
28076 this.terms = await resolveStickyTerms(this.config);
28077 if (!this.terms) {
28078 return;
28079 }
28080 this.installContextMenu();
28081 this.installDesktopHooks();
28082 const notes = await fetchStickyNotes(
28083 this.config,
28084 this.terms.stickyTermId
28085 );
28086 this.bumpHighWaterFromNotes(notes);
28087 startStickyNotesHeartbeat(this);
28088 if (notes.length === 0) {
28089 return;
28090 }
28091 this.ensureRoot();
28092 sortNotesByModified(notes).forEach(
28093 (note, index2) => this.upsert(note, index2)
28094 );
28095 } catch (error) {
28096 if (error instanceof Error) {
28097 console.debug("[desktop-mode] Sticky notes unavailable:", error.message);
28098 }
28099 }
28100 }
28101 createNote(body = "") {
28102 if (!this.terms) {
28103 return;
28104 }
28105 const note = {
28106 localId: `local:${Date.now()}:${Math.random().toString(36).slice(2)}`,
28107 guidelineId: null,
28108 title: body.trim() ? generatedTitle(body) : DEFAULT_STICKY_TITLE,
28109 body,
28110 termIds: this.terms.termIds
28111 };
28112 const controller = this.upsert(note, this.controllers.size, {
28113 activate: true
28114 });
28115 controller.focus();
28116 }
28117 upsert(note, index2, options = {}) {
28118 this.ensureRoot();
28119 const key = noteKey(note);
28120 const existing = this.controllers.get(key);
28121 if (existing) {
28122 existing.replace(note);
28123 if (options.activate) {
28124 this.bringToFront(existing);
28125 }
28126 return existing;
28127 }
28128 const controller = new StickyNoteController({
28129 layer: this,
28130 note,
28131 index: index2
28132 });
28133 this.controllers.set(key, controller);
28134 this.root?.appendChild(controller.element);
28135 this.assignZIndex(controller);
28136 this.applyDesktopVisibility(controller);
28137 if (options.activate) {
28138 this.bringToFront(controller);
28139 }
28140 return controller;
28141 }
28142 ensureRoot() {
28143 if (this.root) {
28144 return this.root;
28145 }
28146 const root = document.createElement("section");
28147 root.className = "desktop-mode-sticky-notes";
28148 root.setAttribute("aria-label", __("Sticky notes"));
28149 this.host.appendChild(root);
28150 this.root = root;
28151 return root;
28152 }
28153 installContextMenu() {
28154 if (this.contextMenuInstalled) {
28155 return;
28156 }
28157 this.contextMenuInstalled = true;
28158 addFilter(
28159 "desktop-mode.wallpaper-context-menu",
28160 "desktop-mode/sticky-notes",
28161 (items) => {
28162 if (!Array.isArray(items) || !this.terms) {
28163 return items;
28164 }
28165 if (items.some(
28166 (item) => item.id === "new-sticky-note"
28167 )) {
28168 return items;
28169 }
28170 return [
28171 ...items,
28172 {
28173 id: "new-sticky-note",
28174 label: __("New sticky note"),
28175 icon: "dashicons-edit-page",
28176 sort: 14,
28177 onClick: () => this.createNote()
28178 }
28179 ];
28180 }
28181 );
28182 }
28183 installDesktopHooks() {
28184 if (this.desktopHooksInstalled) {
28185 return;
28186 }
28187 this.desktopHooksInstalled = true;
28188 addAction(
28189 HOOKS.DESKTOP_SWITCHED,
28190 "desktop-mode/sticky-notes",
28191 () => this.refreshDesktopVisibility()
28192 );
28193 addAction(
28194 HOOKS.DESKTOP_CLOSED,
28195 "desktop-mode/sticky-notes",
28196 (detail) => {
28197 this.migrateDesktopAssignments(detail?.desktopId, detail?.migratedTo);
28198 this.refreshDesktopVisibility();
28199 }
28200 );
28201 }
28202 save(note) {
28203 if (!this.terms) {
28204 return Promise.reject(new Error(__("Sticky term is unavailable.")));
28205 }
28206 return saveStickyNote(this.config, note, this.terms);
28207 }
28208 getHeartbeatSubscription() {
28209 if (!this.terms) {
28210 return void 0;
28211 }
28212 return {
28213 stickyTermId: this.terms.stickyTermId,
28214 knownIds: this.knownGuidelineIds(),
28215 version: this.highWaterMs
28216 };
28217 }
28218 applyHeartbeatPayload(payload) {
28219 for (const guideline of payload.notes ?? []) {
28220 const note = noteFromGuideline(guideline);
28221 this.upsertRemote(note);
28222 }
28223 for (const id of payload.removed ?? []) {
28224 this.forgetGuidelineId(id);
28225 }
28226 if (typeof payload.serverTimeMs === "number" && Number.isFinite(payload.serverTimeMs) && payload.serverTimeMs > this.highWaterMs) {
28227 this.highWaterMs = payload.serverTimeMs;
28228 }
28229 if (payload.truncated) {
28230 void this.reloadFromServer();
28231 }
28232 }
28233 openNoteArtifact(note) {
28234 if (note.guidelineId === null) {
28235 return;
28236 }
28237 this.openArtifact(
28238 buildGuidelineEditUrl(this.config.adminUrl, note.guidelineId),
28239 note.title,
28240 note.guidelineId
28241 );
28242 }
28243 notifyError(message) {
28244 this.onError?.(message);
28245 }
28246 hostSize() {
28247 return {
28248 width: Math.max(1, this.host.clientWidth),
28249 height: Math.max(1, this.host.clientHeight)
28250 };
28251 }
28252 defaultGeometry(index2) {
28253 const { width: hostWidth, height: hostHeight } = this.hostSize();
28254 const width = Math.min(
28255 DEFAULT_WIDTH,
28256 Math.max(MIN_WIDTH, hostWidth - EDGE_PADDING * 2)
28257 );
28258 const height = Math.min(
28259 DEFAULT_HEIGHT,
28260 Math.max(MIN_HEIGHT, hostHeight - EDGE_PADDING * 2)
28261 );
28262 const offset = index2 % 8 * 28;
28263 const left = clamp(
28264 hostWidth - width - 32 - offset,
28265 EDGE_PADDING,
28266 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
28267 );
28268 const top = clamp(
28269 32 + offset,
28270 EDGE_PADDING,
28271 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
28272 );
28273 return {
28274 x: left / hostWidth,
28275 y: top / hostHeight,
28276 width,
28277 height
28278 };
28279 }
28280 forget(controller) {
28281 this.controllers.delete(noteKey(controller.note));
28282 controller.dispose();
28283 controller.element.remove();
28284 if (this.controllers.size === 0) {
28285 this.root?.remove();
28286 this.root = null;
28287 }
28288 }
28289 replaceControllerKey(oldKey, controller) {
28290 const newKey = noteKey(controller.note);
28291 this.controllers.delete(oldKey);
28292 this.controllers.set(newKey, controller);
28293 moveStoredGeometry(oldKey, newKey);
28294 this.applyDesktopVisibility(controller);
28295 }
28296 bumpHighWaterFromNote(note) {
28297 const modifiedMs = noteModifiedMs(note);
28298 if (modifiedMs > this.highWaterMs) {
28299 this.highWaterMs = modifiedMs;
28300 }
28301 }
28302 bringToFront(controller) {
28303 controller.setZIndex(this.nextZIndex());
28304 }
28305 geometryForNote(note, index2) {
28306 const key = noteKey(note);
28307 const loaded = loadGeometry(key);
28308 const desktopId = this.normalizeDesktopId(loaded?.desktopId);
28309 const geometry = loaded ? { ...loaded, desktopId } : { ...this.defaultGeometry(index2), desktopId };
28310 if (!loaded || loaded.desktopId !== geometry.desktopId) {
28311 saveGeometry(key, geometry);
28312 }
28313 return geometry;
28314 }
28315 upsertRemote(note) {
28316 const key = noteKey(note);
28317 const existing = this.controllers.get(key);
28318 if (existing) {
28319 if (!existing.shouldReplaceFromRemote(note)) {
28320 this.bumpHighWaterFromNote(note);
28321 return existing;
28322 }
28323 existing.replace(note);
28324 this.bumpHighWaterFromNote(note);
28325 return existing;
28326 }
28327 const controller = this.upsert(note, this.controllers.size);
28328 this.bumpHighWaterFromNote(note);
28329 return controller;
28330 }
28331 forgetGuidelineId(guidelineId) {
28332 for (const controller of this.controllers.values()) {
28333 if (controller.note.guidelineId === guidelineId) {
28334 this.forget(controller);
28335 return;
28336 }
28337 }
28338 }
28339 knownGuidelineIds() {
28340 const ids = [];
28341 for (const controller of this.controllers.values()) {
28342 if (controller.note.guidelineId !== null) {
28343 ids.push(controller.note.guidelineId);
28344 }
28345 }
28346 return ids;
28347 }
28348 bumpHighWaterFromNotes(notes) {
28349 notes.forEach((note) => this.bumpHighWaterFromNote(note));
28350 }
28351 assignZIndex(controller) {
28352 controller.setZIndex(this.nextZIndex());
28353 }
28354 nextZIndex() {
28355 this.zIndexCounter += 1;
28356 return this.zIndexCounter;
28357 }
28358 applyDesktopVisibility(controller) {
28359 controller.setVisible(this.isNoteOnActiveDesktop(controller.note));
28360 }
28361 refreshDesktopVisibility() {
28362 for (const controller of this.controllers.values()) {
28363 this.applyDesktopVisibility(controller);
28364 }
28365 }
28366 isNoteOnActiveDesktop(note) {
28367 const key = noteKey(note);
28368 const geometry = loadGeometry(key);
28369 const desktopId = this.normalizeDesktopId(geometry?.desktopId);
28370 if (geometry && geometry.desktopId !== desktopId) {
28371 saveGeometry(key, { ...geometry, desktopId });
28372 }
28373 return desktopId === this.activeDesktopId();
28374 }
28375 migrateDesktopAssignments(desktopId, migratedTo) {
28376 if (!desktopId || !migratedTo || desktopId === migratedTo) {
28377 return;
28378 }
28379 const map = readGeometryMap();
28380 let changed = false;
28381 Object.entries(map).forEach(([key, geometry]) => {
28382 if (geometry.desktopId === desktopId) {
28383 map[key] = {
28384 ...geometry,
28385 desktopId: this.normalizeDesktopId(migratedTo)
28386 };
28387 changed = true;
28388 }
28389 });
28390 if (changed) {
28391 writeGeometryMap(map);
28392 }
28393 }
28394 activeDesktopId() {
28395 try {
28396 const id = this.getActiveDesktopId();
28397 return typeof id === "string" && id ? id : "desktop-1";
28398 } catch {
28399 return "desktop-1";
28400 }
28401 }
28402 normalizeDesktopId(desktopId) {
28403 if (!desktopId) {
28404 return this.activeDesktopId();
28405 }
28406 return desktopId;
28407 }
28408 async reloadFromServer() {
28409 if (!this.terms) {
28410 return;
28411 }
28412 try {
28413 const notes = await fetchStickyNotes(
28414 this.config,
28415 this.terms.stickyTermId
28416 );
28417 const ids = /* @__PURE__ */ new Set();
28418 sortNotesByModified(notes).forEach((note) => {
28419 if (note.guidelineId !== null) {
28420 ids.add(note.guidelineId);
28421 }
28422 this.upsertRemote(note);
28423 });
28424 this.knownGuidelineIds().forEach((id) => {
28425 if (!ids.has(id)) {
28426 this.forgetGuidelineId(id);
28427 }
28428 });
28429 } catch {
28430 }
28431 }
28432 }
28433 class StickyNoteController {
28434 constructor(options) {
28435 this.saveTimer = null;
28436 this.geometryTimer = null;
28437 this.saving = false;
28438 this.saveAgain = false;
28439 this.resizeObserver = null;
28440 this.disposed = false;
28441 this.layer = options.layer;
28442 this.note = options.note;
28443 this.index = options.index;
28444 this.element = document.createElement("article");
28445 this.element.className = "desktop-mode-sticky-note";
28446 this.element.dataset.stickyNoteId = noteKey(this.note);
28447 this.titleEl = document.createElement("span");
28448 this.editor = document.createElement("wpd-textarea");
28449 this.statusEl = document.createElement("wpd-save-status");
28450 this.openButton = document.createElement("wpd-window-button");
28451 this.paint();
28452 this.applyGeometry(this.layer.geometryForNote(this.note, this.index));
28453 this.element.addEventListener(
28454 "pointerdown",
28455 () => this.layer.bringToFront(this),
28456 { capture: true }
28457 );
28458 this.element.addEventListener("focusin", () => this.layer.bringToFront(this));
28459 this.watchResize();
28460 }
28461 focus() {
28462 window.setTimeout(() => this.editor.focusInput?.(), 0);
28463 }
28464 replace(note) {
28465 this.note = note;
28466 this.element.dataset.stickyNoteId = noteKey(this.note);
28467 this.titleEl.textContent = this.note.title;
28468 this.editor.setAttribute("value", this.note.body);
28469 this.refreshOpenButton();
28470 }
28471 shouldReplaceFromRemote(note) {
28472 if (this.hasLocalChanges()) {
28473 return false;
28474 }
28475 const currentMs = noteModifiedMs(this.note);
28476 const incomingMs = noteModifiedMs(note);
28477 if (currentMs > 0 && incomingMs > 0 && incomingMs <= currentMs && this.note.title === note.title && this.note.body === note.body) {
28478 return false;
28479 }
28480 return true;
28481 }
28482 setZIndex(zIndex) {
28483 this.element.style.zIndex = String(zIndex);
28484 }
28485 setVisible(visible) {
28486 this.element.style.display = visible ? "" : "none";
28487 }
28488 dispose() {
28489 this.disposed = true;
28490 if (this.saveTimer !== null) {
28491 window.clearTimeout(this.saveTimer);
28492 this.saveTimer = null;
28493 }
28494 if (this.geometryTimer !== null) {
28495 window.clearTimeout(this.geometryTimer);
28496 this.geometryTimer = null;
28497 }
28498 this.resizeObserver?.disconnect();
28499 this.resizeObserver = null;
28500 }
28501 paint() {
28502 this.element.innerHTML = "";
28503 this.element.style.minWidth = `${MIN_WIDTH}px`;
28504 this.element.style.minHeight = `${MIN_HEIGHT}px`;
28505 const header = document.createElement("div");
28506 header.className = "desktop-mode-sticky-note__header";
28507 const grip = document.createElement("span");
28508 grip.className = "desktop-mode-sticky-note__grip";
28509 grip.setAttribute("aria-hidden", "true");
28510 this.titleEl.className = "desktop-mode-sticky-note__title";
28511 this.titleEl.textContent = this.note.title;
28512 this.statusEl.setAttribute("mode", "icon");
28513 this.statusEl.setAttribute("phase", "idle");
28514 this.statusEl.className = "desktop-mode-sticky-note__status";
28515 this.openButton.setAttribute("icon", "detach");
28516 this.openButton.setAttribute("title", __("Open artifact"));
28517 this.openButton.className = "desktop-mode-sticky-note__open";
28518 this.openButton.addEventListener("wpd-button-activate", () => {
28519 this.layer.openNoteArtifact(this.note);
28520 });
28521 const close = document.createElement("wpd-window-button");
28522 close.setAttribute("icon", "close");
28523 close.setAttribute("danger", "");
28524 close.setAttribute("title", __("Hide sticky note"));
28525 close.className = "desktop-mode-sticky-note__close";
28526 close.addEventListener("wpd-button-activate", () => this.close());
28527 header.append(grip, this.titleEl, this.statusEl, this.openButton, close);
28528 header.addEventListener("pointerdown", (event) => this.startDrag(event));
28529 this.editor.className = "desktop-mode-sticky-note__editor";
28530 this.editor.setAttribute("aria-label", __("Sticky note text"));
28531 this.editor.setAttribute("rows", "8");
28532 this.editor.setAttribute("value", this.note.body);
28533 this.installEditorKeyboardGuard();
28534 this.editor.addEventListener("wpd-input-change", (event) => {
28535 const detail = event.detail;
28536 this.note.body = detail.value;
28537 this.note.title = titleForBody(detail.value);
28538 this.titleEl.textContent = this.note.title;
28539 this.setPhase("pending");
28540 this.scheduleSave();
28541 });
28542 this.editor.addEventListener("wpd-input-commit", () => this.flushSave());
28543 this.element.append(header, this.editor);
28544 this.refreshOpenButton();
28545 }
28546 installEditorKeyboardGuard() {
28547 ["keydown", "keypress", "keyup"].forEach((eventName) => {
28548 this.editor.addEventListener(eventName, (event) => {
28549 event.stopPropagation();
28550 });
28551 });
28552 }
28553 refreshOpenButton() {
28554 const disabled = this.note.guidelineId === null;
28555 this.openButton.classList.toggle("is-disabled", disabled);
28556 this.openButton.setAttribute("aria-disabled", disabled ? "true" : "false");
28557 }
28558 close() {
28559 if (this.note.guidelineId === null && this.note.body.trim().length === 0) {
28560 this.layer.forget(this);
28561 return;
28562 }
28563 this.flushSave();
28564 this.layer.forget(this);
28565 }
28566 scheduleSave() {
28567 if (this.note.guidelineId === null && this.note.body.trim().length === 0) {
28568 this.setPhase("idle");
28569 return;
28570 }
28571 if (this.saveTimer !== null) {
28572 window.clearTimeout(this.saveTimer);
28573 }
28574 this.saveTimer = window.setTimeout(() => {
28575 this.saveTimer = null;
28576 void this.save();
28577 }, SAVE_DEBOUNCE_MS);
28578 }
28579 flushSave() {
28580 if (this.saveTimer !== null) {
28581 window.clearTimeout(this.saveTimer);
28582 this.saveTimer = null;
28583 }
28584 if (this.note.guidelineId !== null || this.note.body.trim().length > 0) {
28585 void this.save();
28586 }
28587 }
28588 async save() {
28589 if (this.saving) {
28590 this.saveAgain = true;
28591 this.setPhase("pending");
28592 return;
28593 }
28594 this.saving = true;
28595 this.setPhase("saving");
28596 const bodyAtSave = this.note.body;
28597 try {
28598 const saved = await this.layer.save({
28599 ...this.note,
28600 body: bodyAtSave
28601 });
28602 if (this.disposed) {
28603 return;
28604 }
28605 const oldKey = noteKey(this.note);
28606 this.note.guidelineId = saved.guidelineId;
28607 this.note.modified = saved.modified;
28608 this.note.link = saved.link;
28609 this.note.termIds = saved.termIds.length > 0 ? saved.termIds : this.note.termIds;
28610 if (this.note.body === bodyAtSave) {
28611 this.note.title = saved.title;
28612 this.titleEl.textContent = saved.title;
28613 }
28614 if (oldKey !== noteKey(this.note)) {
28615 this.element.dataset.stickyNoteId = noteKey(this.note);
28616 this.layer.replaceControllerKey(oldKey, this);
28617 }
28618 this.layer.bumpHighWaterFromNote(this.note);
28619 this.refreshOpenButton();
28620 this.setPhase("saved");
28621 } catch (error) {
28622 if (this.disposed) {
28623 return;
28624 }
28625 const message = error instanceof Error ? error.message : __("Could not save sticky note.");
28626 this.setPhase("failed", message);
28627 this.layer.notifyError(message);
28628 } finally {
28629 this.saving = false;
28630 if (!this.disposed && this.saveAgain) {
28631 this.saveAgain = false;
28632 this.scheduleSave();
28633 }
28634 }
28635 }
28636 setPhase(phase, error) {
28637 this.statusEl.setAttribute("phase", phase);
28638 if (error) {
28639 this.statusEl.setAttribute("error", error);
28640 this.statusEl.setAttribute("title", error);
28641 } else {
28642 this.statusEl.removeAttribute("error");
28643 this.statusEl.removeAttribute("title");
28644 }
28645 }
28646 hasLocalChanges() {
28647 const phase = this.statusEl.getAttribute("phase");
28648 return this.saveTimer !== null || this.saving || this.saveAgain || phase === "pending" || phase === "failed";
28649 }
28650 startDrag(event) {
28651 if (event.button !== 0) {
28652 return;
28653 }
28654 const target2 = event.target;
28655 if (target2?.closest("wpd-window-button, wpd-save-status")) {
28656 return;
28657 }
28658 event.preventDefault();
28659 const startRect = this.element.getBoundingClientRect();
28660 const hostRect = this.layerHostRect();
28661 const startLeft = startRect.left - hostRect.left;
28662 const startTop = startRect.top - hostRect.top;
28663 const startX = event.clientX;
28664 const startY = event.clientY;
28665 this.element.classList.add("desktop-mode-sticky-note--dragging");
28666 this.element.setPointerCapture?.(event.pointerId);
28667 const move = (moveEvent) => {
28668 const width = this.element.offsetWidth;
28669 const height = this.element.offsetHeight;
28670 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
28671 const left = clamp(
28672 startLeft + moveEvent.clientX - startX,
28673 EDGE_PADDING,
28674 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
28675 );
28676 const top = clamp(
28677 startTop + moveEvent.clientY - startY,
28678 EDGE_PADDING,
28679 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
28680 );
28681 this.element.style.left = `${left}px`;
28682 this.element.style.top = `${top}px`;
28683 };
28684 const up = (upEvent) => {
28685 this.element.classList.remove("desktop-mode-sticky-note--dragging");
28686 this.element.releasePointerCapture?.(upEvent.pointerId);
28687 document.removeEventListener("pointermove", move);
28688 document.removeEventListener("pointerup", up);
28689 this.persistGeometry();
28690 };
28691 document.addEventListener("pointermove", move);
28692 document.addEventListener("pointerup", up);
28693 }
28694 applyGeometry(geometry) {
28695 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
28696 const width = clamp(geometry.width, MIN_WIDTH, hostWidth - EDGE_PADDING * 2);
28697 const height = clamp(geometry.height, MIN_HEIGHT, hostHeight - EDGE_PADDING * 2);
28698 const left = clamp(
28699 geometry.x * hostWidth,
28700 EDGE_PADDING,
28701 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
28702 );
28703 const top = clamp(
28704 geometry.y * hostHeight,
28705 EDGE_PADDING,
28706 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
28707 );
28708 this.element.style.left = `${left}px`;
28709 this.element.style.top = `${top}px`;
28710 this.element.style.width = `${width}px`;
28711 this.element.style.height = `${height}px`;
28712 }
28713 watchResize() {
28714 if (typeof ResizeObserver === "undefined") {
28715 return;
28716 }
28717 this.resizeObserver = new ResizeObserver(() => {
28718 if (this.geometryTimer !== null) {
28719 window.clearTimeout(this.geometryTimer);
28720 }
28721 this.geometryTimer = window.setTimeout(() => {
28722 this.geometryTimer = null;
28723 this.persistGeometry();
28724 }, 150);
28725 });
28726 this.resizeObserver.observe(this.element);
28727 }
28728 persistGeometry() {
28729 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
28730 const left = parseFloat(this.element.style.left) || 0;
28731 const top = parseFloat(this.element.style.top) || 0;
28732 const existing = loadGeometry(noteKey(this.note));
28733 saveGeometry(noteKey(this.note), {
28734 ...existing ?? {},
28735 x: clamp(left / hostWidth, 0, 1),
28736 y: clamp(top / hostHeight, 0, 1),
28737 width: this.element.offsetWidth,
28738 height: this.element.offsetHeight
28739 });
28740 }
28741 layerHostRect() {
28742 const parent = this.element.parentElement?.parentElement;
28743 return (parent ?? document.body).getBoundingClientRect();
28744 }
28745 }
28746 function bootStickyNotes(options) {
28747 const layer = new StickyNotesLayer(options);
28748 void layer.boot();
28749 return layer;
28750 }
28751 function noteKey(note) {
28752 return note.guidelineId === null ? note.localId : `guideline:${note.guidelineId}`;
28753 }
28754 function noteModifiedMs(note) {
28755 if (typeof note.modifiedMs === "number" && Number.isFinite(note.modifiedMs)) {
28756 return note.modifiedMs;
28757 }
28758 if (!note.modified) {
28759 return 0;
28760 }
28761 const parsed = Date.parse(note.modified);
28762 return Number.isFinite(parsed) ? parsed : 0;
28763 }
28764 function sortNotesByModified(notes) {
28765 return [...notes].sort((a, b) => noteModifiedMs(a) - noteModifiedMs(b));
28766 }
28767 function loadGeometry(key) {
28768 const map = readGeometryMap();
28769 const value = map[key];
28770 if (!value || !Number.isFinite(value.x) || !Number.isFinite(value.y) || !Number.isFinite(value.width) || !Number.isFinite(value.height)) {
28771 return null;
28772 }
28773 return value;
28774 }
28775 function saveGeometry(key, geometry) {
28776 const map = readGeometryMap();
28777 map[key] = geometry;
28778 writeGeometryMap(map);
28779 }
28780 function moveStoredGeometry(oldKey, newKey) {
28781 if (oldKey === newKey) {
28782 return;
28783 }
28784 const map = readGeometryMap();
28785 if (map[oldKey]) {
28786 map[newKey] = map[oldKey];
28787 delete map[oldKey];
28788 writeGeometryMap(map);
28789 }
28790 }
28791 function readGeometryMap() {
28792 try {
28793 const raw = window.localStorage.getItem(GEOMETRY_KEY);
28794 return raw ? JSON.parse(raw) : {};
28795 } catch {
28796 return {};
28797 }
28798 }
28799 function writeGeometryMap(map) {
28800 try {
28801 window.localStorage.setItem(GEOMETRY_KEY, JSON.stringify(map));
28802 } catch {
28803 }
28804 }
28805 function clamp(value, min, max) {
28806 if (max < min) {
28807 return min;
28808 }
28809 return Math.min(max, Math.max(min, value));
28810 }
28811 const clock = {
28812 id: "clock",
28813 // Labels/descriptions on built-in defs stay string-literal at
28814 // module-eval time so the extract-pot pass picks them up. The
28815 // values are wrapped in `__()` so they translate at runtime.
28816 get label() {
28817 return __("Clock");
28818 },
28819 get description() {
28820 return __("Local time and date, refreshed every second.");
28821 },
28822 icon: "dashicons-clock",
28823 mount: (container) => {
28824 container.classList.add("desktop-mode-widget-clock");
28825 const time = document.createElement("div");
28826 time.className = "desktop-mode-widget-clock__time";
28827 container.appendChild(time);
28828 const date = document.createElement("div");
28829 date.className = "desktop-mode-widget-clock__date";
28830 container.appendChild(date);
28831 const render2 = () => {
28832 const now = /* @__PURE__ */ new Date();
28833 time.textContent = now.toLocaleTimeString(void 0, {
28834 hour: "2-digit",
28835 minute: "2-digit"
28836 });
28837 date.textContent = now.toLocaleDateString(void 0, {
28838 weekday: "long",
28839 month: "short",
28840 day: "numeric"
28841 });
28842 };
28843 render2();
28844 const msUntilNextSecond = 1e3 - Date.now() % 1e3;
28845 let interval = null;
28846 const kickoff = window.setTimeout(() => {
28847 render2();
28848 interval = window.setInterval(render2, 1e3);
28849 }, msUntilNextSecond);
28850 return () => {
28851 window.clearTimeout(kickoff);
28852 if (interval !== null) {
28853 window.clearInterval(interval);
28854 }
28855 };
28856 }
28857 };
28858 function registerBuiltInWidgets() {
28859 register(clock);
28860 }
28861 const STYLE_ID = "desktop-mode-release-card-styles";
28862 const HOST_CLASS = "desktop-mode-release-host";
28863 const WP_LOGO = '<svg viewBox="0 0 122.52 122.523" aria-hidden="true"><path fill="currentColor" d="M8.708 61.26c0 20.802 12.089 38.779 29.619 47.298L13.258 39.872a52.352 52.352 0 0 0-4.55 21.388zm87.892-2.652c0-6.495-2.333-10.993-4.334-14.494-2.664-4.329-5.161-7.995-5.161-12.324 0-4.831 3.664-9.328 8.825-9.328.233 0 .454.029.681.042-9.35-8.566-21.807-13.796-35.489-13.796-18.36 0-34.513 9.42-43.91 23.688 1.233.037 2.395.063 3.382.063 5.497 0 14.006-.667 14.006-.667 2.833-.167 3.167 3.994.337 4.329 0 0-2.847.335-6.015.501l19.138 56.925 11.502-34.493-8.187-22.432c-2.831-.166-5.51-.501-5.51-.501-2.831-.167-2.499-4.496.332-4.329 0 0 8.679.667 13.843.667 5.496 0 14.006-.667 14.006-.667 2.835-.167 3.168 3.994.337 4.329 0 0-2.852.335-6.015.501l18.992 56.494 5.242-17.517c2.272-7.269 4.001-12.49 4.001-16.989zm-34.404 7.223l-15.768 45.819a52.552 52.552 0 0 0 14.807 2.136c6.309 0 12.36-1.091 17.996-3.075a4.617 4.617 0 0 1-.374-.724L62.196 65.831zm45.192-29.81c.226 1.674.354 3.471.354 5.404 0 5.333-.996 11.328-3.996 18.824l-16.053 46.413c15.624-9.111 26.133-26.038 26.133-45.426.001-9.137-2.333-17.729-6.438-25.215zM61.262 0C27.483 0 0 27.481 0 61.26c0 33.783 27.483 61.263 61.262 61.263 33.778 0 61.265-27.48 61.265-61.263C122.526 27.481 95.04 0 61.262 0zm0 119.715c-32.23 0-58.453-26.223-58.453-58.455 0-32.23 26.222-58.451 58.453-58.451 32.229 0 58.45 26.221 58.45 58.451 0 32.232-26.221 58.455-58.45 58.455z"/></svg>';
28864 const CLOSE_ICON = '<svg viewBox="0 0 14 14" aria-hidden="true"><path d="M3 3 L11 11 M11 3 L3 11" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" fill="none"></path></svg>';
28865 const STYLES = `
28866 .dm-release-card {
28867 position: relative; box-sizing: border-box; width: 268px; padding: 11px;
28868 border-radius: 14px; color: #fff;
28869 font-family: var( --desktop-mode-font, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif );
28870 background: #34373f; border: 1px solid rgba( 255, 255, 255, 0.14 );
28871 box-shadow: 0 16px 40px rgba( 0, 0, 0, 0.55 ), 0 3px 8px rgba( 0, 0, 0, 0.3 ), inset 0 0 0 1px rgba( 255, 255, 255, 0.04 );
28872 --accent: #2271b1; --accent-ink: #ffffff;
28873 animation: dmRcCardIn 0.5s cubic-bezier( 0.2, 1.2, 0.35, 1 ) both;
28874 }
28875 .dm-release-card, .dm-release-card * { box-sizing: border-box; }
28876 @keyframes dmRcCardIn {
28877 from { opacity: 0; transform: translateY( -16px ) scale( 0.96 ); }
28878 to { opacity: 1; transform: none; }
28879 }
28880 .dm-rc__close {
28881 position: absolute; top: 9px; right: 9px; z-index: 10;
28882 width: 22px; height: 22px; padding: 0; border: none; border-radius: 50%;
28883 display: inline-flex; align-items: center; justify-content: center;
28884 background: rgba( 0, 0, 0, 0.5 ); color: #fff; opacity: 0.72; cursor: pointer;
28885 transition: opacity 0.12s ease, background-color 0.12s ease;
28886 }
28887 .dm-rc__close:hover { opacity: 1; background: rgba( 0, 0, 0, 0.7 ); }
28888 .dm-rc__close:focus-visible { opacity: 1; outline: 2px solid #fff; outline-offset: 2px; }
28889 .dm-rc__close svg { width: 11px; height: 11px; }
28890 .dm-rc__art { position: relative; height: 150px; }
28891 .dm-rc__cover {
28892 position: absolute; left: 2px; top: 0; width: 150px; height: 150px;
28893 border-radius: 2px; overflow: hidden; z-index: 3;
28894 box-shadow: 0 8px 20px rgba( 0, 0, 0, 0.5 ), inset 0 0 0 1px rgba( 255, 255, 255, 0.08 );
28895 }
28896 .dm-rc__canvas { width: 100%; height: 100%; display: block; }
28897 .dm-rc__disc-wrap {
28898 position: absolute; left: 94px; top: 2px; width: 148px; height: 148px; z-index: 2;
28899 border-radius: 50%; box-shadow: 0 14px 26px rgba( 0, 0, 0, 0.6 );
28900 animation: dmRcEmerge 0.8s cubic-bezier( 0.2, 1, 0.28, 1 ) 0.45s both;
28901 }
28902 @keyframes dmRcEmerge {
28903 from { transform: translateX( -84px ); }
28904 to { transform: translateX( 0 ); }
28905 }
28906 .dm-rc__disc {
28907 position: absolute; inset: 0; border-radius: 50%;
28908 background:
28909 repeating-radial-gradient( circle at 50% 50%, rgba( 255, 255, 255, 0.05 ) 0 1px, rgba( 0, 0, 0, 0 ) 1px 2.4px ),
28910 radial-gradient( circle at 50% 50%, #1a1a1e 0 11%, #0a0a0c 12% 62%, #050506 100% );
28911 box-shadow: inset 0 0 26px rgba( 0, 0, 0, 0.9 ), inset 0 0 0 1px rgba( 255, 255, 255, 0.05 );
28912 animation: dmRcSettle 2.5s cubic-bezier( 0.12, 0.72, 0.16, 1 ) 0.45s both;
28913 }
28914 @keyframes dmRcSettle {
28915 from { transform: rotate( 0 ); }
28916 to { transform: rotate( 720deg ); }
28917 }
28918 .dm-rc__label {
28919 position: absolute; inset: 34%; border-radius: 50%; display: grid; place-items: center;
28920 background: var( --accent ); color: var( --accent-ink );
28921 box-shadow: inset 0 0 0 2px rgba( 0, 0, 0, 0.18 ), 0 1px 2px rgba( 0, 0, 0, 0.4 );
28922 }
28923 .dm-rc__label svg { width: 59%; height: 59%; display: block; }
28924 .dm-rc__sheen {
28925 position: absolute; inset: 0; border-radius: 50%; pointer-events: none; z-index: 3;
28926 background: linear-gradient( 118deg, rgba( 255, 255, 255, 0.18 ) 0%, transparent 24%, transparent 74%, rgba( 255, 255, 255, 0.1 ) 100% );
28927 mix-blend-mode: screen;
28928 }
28929 .dm-rc__meta {
28930 display: flex; align-items: center; gap: 10px; margin-top: 11px;
28931 opacity: 0; animation: dmRcFade 0.5s ease 1.05s forwards;
28932 }
28933 @keyframes dmRcFade { to { opacity: 1; } }
28934 .dm-rc__text { flex: 1; font-size: 13px; line-height: 1.35; color: #fff; }
28935 .dm-rc__text b { font-weight: 650; }
28936 .dm-rc__btn {
28937 flex-shrink: 0; padding: 7px 12px; border: none; border-radius: 7px;
28938 color: var( --accent-ink ); background: var( --accent ); font: inherit; font-size: 12px; font-weight: 600;
28939 cursor: pointer; box-shadow: 0 2px 8px rgba( 0, 0, 0, 0.3 ); transition: filter 0.12s;
28940 }
28941 .dm-rc__btn:hover { filter: brightness( 1.12 ); }
28942 .dm-rc__btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
28943 @media ( prefers-reduced-motion: reduce ) {
28944 .dm-release-card, .dm-rc__disc-wrap, .dm-rc__disc, .dm-rc__meta { animation: none !important; }
28945 .dm-rc__disc-wrap { transform: translateX( 0 ); }
28946 .dm-rc__meta { opacity: 1; }
28947 }
28948 `;
28949 function ensureStyles() {
28950 if (document.getElementById(STYLE_ID)) {
28951 return;
28952 }
28953 const el = document.createElement("style");
28954 el.id = STYLE_ID;
28955 el.textContent = STYLES;
28956 document.head.appendChild(el);
28957 }
28958 function ensureHost() {
28959 const existing = document.querySelector("." + HOST_CLASS);
28960 if (existing) {
28961 return existing;
28962 }
28963 const el = document.createElement("div");
28964 el.className = HOST_CLASS;
28965 el.style.cssText = "position:fixed;top:calc(var(--wp-admin--admin-bar--height,32px) + 16px);inset-inline-end:16px;z-index:calc(var(--desktop-mode-z-fullscreen,99999) + 10);pointer-events:none;";
28966 document.body.appendChild(el);
28967 return el;
28968 }
28969 function paintSleeve(root, canvas, artUrl, hasExplicitAccent) {
28970 const img = new Image();
28971 img.crossOrigin = "anonymous";
28972 img.addEventListener(
28973 "load",
28974 () => {
28975 const w = img.naturalWidth || 0;
28976 const h = img.naturalHeight || 0;
28977 if (!w || !h) {
28978 return;
28979 }
28980 const size = 320;
28981 canvas.width = size;
28982 canvas.height = size;
28983 const ctx = canvas.getContext("2d");
28984 if (!ctx) {
28985 return;
28986 }
28987 const baseSide = Math.min(w, h);
28988 ctx.drawImage(img, 0, 0, baseSide, baseSide, 0, 0, size, size);
28989 try {
28990 const work = document.createElement("canvas");
28991 work.width = w;
28992 work.height = h;
28993 const wctx = work.getContext("2d");
28994 if (!wctx) {
28995 return;
28996 }
28997 wctx.drawImage(img, 0, 0);
28998 const data = wctx.getImageData(0, 0, w, h).data;
28999 const isWhite = (x, y) => {
29000 const i = (y * w + x) * 4;
29001 return data[i] > 248 && data[i + 1] > 248 && data[i + 2] > 248 && data[i + 3] > 200;
29002 };
29003 const rowWhite = (y) => {
29004 for (let x = 0; x < w; x += 2) {
29005 if (!isWhite(x, y)) {
29006 return false;
29007 }
29008 }
29009 return true;
29010 };
29011 const colWhite = (x) => {
29012 for (let y = 0; y < h; y += 2) {
29013 if (!isWhite(x, y)) {
29014 return false;
29015 }
29016 }
29017 return true;
29018 };
29019 let top = 0;
29020 while (top < h - 1 && rowWhite(top)) {
29021 top++;
29022 }
29023 let bottom = h - 1;
29024 while (bottom > top && rowWhite(bottom)) {
29025 bottom--;
29026 }
29027 let left = 0;
29028 while (left < w - 1 && colWhite(left)) {
29029 left++;
29030 }
29031 let right = w - 1;
29032 while (right > left && colWhite(right)) {
29033 right--;
29034 }
29035 const side = Math.max(1, Math.min(right - left + 1, bottom - top + 1));
29036 ctx.clearRect(0, 0, size, size);
29037 ctx.drawImage(img, left, top, side, side, 0, 0, size, size);
29038 if (!hasExplicitAccent) {
29039 extractAccent(root, ctx, size);
29040 }
29041 } catch {
29042 }
29043 },
29044 { once: true }
29045 );
29046 img.src = artUrl;
29047 }
29048 function extractAccent(root, ctx, size) {
29049 const { data } = ctx.getImageData(0, 0, size, size);
29050 const buckets = /* @__PURE__ */ new Map();
29051 let best = null;
29052 let bestScore = -1;
29053 for (let i = 0; i < data.length; i += 4) {
29054 const r2 = data[i];
29055 const g2 = data[i + 1];
29056 const b2 = data[i + 2];
29057 if (data[i + 3] < 200) {
29058 continue;
29059 }
29060 const max = Math.max(r2, g2, b2);
29061 const min = Math.min(r2, g2, b2);
29062 const v = max / 255;
29063 const s = max === 0 ? 0 : (max - min) / max;
29064 if (v < 0.2 || s < 0.25) {
29065 continue;
29066 }
29067 const key = `${Math.floor(r2 / 16)},${Math.floor(g2 / 16)},${Math.floor(b2 / 16)}`;
29068 let bucket2 = buckets.get(key);
29069 if (!bucket2) {
29070 bucket2 = { r: 0, g: 0, b: 0, n: 0, score: 0 };
29071 buckets.set(key, bucket2);
29072 }
29073 bucket2.r += r2;
29074 bucket2.g += g2;
29075 bucket2.b += b2;
29076 bucket2.n += 1;
29077 bucket2.score += s * v;
29078 if (bucket2.score > bestScore) {
29079 bestScore = bucket2.score;
29080 best = bucket2;
29081 }
29082 }
29083 if (!best) {
29084 return;
29085 }
29086 const r = Math.round(best.r / best.n);
29087 const g = Math.round(best.g / best.n);
29088 const b = Math.round(best.b / best.n);
29089 const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
29090 root.style.setProperty("--accent", `rgb(${r}, ${g}, ${b})`);
29091 root.style.setProperty("--accent-ink", lum > 0.6 ? "#1a1a1a" : "#ffffff");
29092 }
29093 function showReleaseCard(opts) {
29094 ensureStyles();
29095 const host = ensureHost();
29096 host.textContent = "";
29097 const root = document.createElement("div");
29098 root.className = "dm-release-card";
29099 root.setAttribute("role", "status");
29100 root.style.pointerEvents = "auto";
29101 if (opts.accent) {
29102 root.style.setProperty("--accent", opts.accent);
29103 }
29104 if (opts.accentInk) {
29105 root.style.setProperty("--accent-ink", opts.accentInk);
29106 }
29107 root.innerHTML = `<button type="button" class="dm-rc__close">${CLOSE_ICON}</button><div class="dm-rc__art"><div class="dm-rc__disc-wrap"><div class="dm-rc__disc"><div class="dm-rc__label">${WP_LOGO}</div></div><div class="dm-rc__sheen"></div></div><div class="dm-rc__cover"><canvas class="dm-rc__canvas"></canvas></div></div><div class="dm-rc__meta"><span class="dm-rc__text"></span><button type="button" class="dm-rc__btn"></button></div>`;
29108 root.querySelector(".dm-rc__text").textContent = opts.message;
29109 const closeBtn = root.querySelector(".dm-rc__close");
29110 closeBtn.setAttribute("aria-label", __("Dismiss"));
29111 const updateBtn = root.querySelector(".dm-rc__btn");
29112 updateBtn.textContent = __("Update now");
29113 host.appendChild(root);
29114 paintSleeve(
29115 root,
29116 root.querySelector(".dm-rc__canvas"),
29117 opts.artUrl,
29118 !!opts.accent
29119 );
29120 let done = false;
29121 let timer = null;
29122 const removeNow = () => {
29123 done = true;
29124 if (timer !== null) {
29125 clearTimeout(timer);
29126 timer = null;
29127 }
29128 root.remove();
29129 };
29130 closeBtn.addEventListener(
29131 "click",
29132 (e) => {
29133 e.preventDefault();
29134 e.stopPropagation();
29135 if (done) {
29136 return;
29137 }
29138 markNoticeDismissed(opts.dismissKey);
29139 const reduce = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
29140 if (reduce) {
29141 removeNow();
29142 return;
29143 }
29144 done = true;
29145 root.style.animation = "none";
29146 root.style.transition = "opacity 0.2s ease";
29147 requestAnimationFrame(() => {
29148 root.style.opacity = "0";
29149 });
29150 timer = window.setTimeout(() => root.remove(), 240);
29151 }
29152 );
29153 updateBtn.addEventListener("click", (e) => {
29154 e.preventDefault();
29155 e.stopPropagation();
29156 opts.onUpdate();
29157 removeNow();
29158 });
29159 return removeNow;
29160 }
29161 const CACHE_PREFIX = "desktop-mode/release-art:v1:";
29162 const MISS_TTL_MS = 6 * 60 * 60 * 1e3;
29163 function str(v) {
29164 return typeof v === "string" ? v : "";
29165 }
29166 function prop(o, key) {
29167 return o && typeof o === "object" ? o[key] : void 0;
29168 }
29169 function decodeEntities(s) {
29170 const el = document.createElement("textarea");
29171 el.innerHTML = s;
29172 return el.value;
29173 }
29174 function pickMedia(post) {
29175 const media = prop(prop(post, "_embedded"), "wp:featuredmedia");
29176 const first = Array.isArray(media) ? media[0] : void 0;
29177 const sizes = prop(prop(first, "media_details"), "sizes");
29178 for (const key of ["medium_large", "large", "1536x1536", "medium"]) {
29179 const url = str(prop(prop(sizes, key), "source_url"));
29180 if (url) {
29181 return url;
29182 }
29183 }
29184 return str(prop(first, "source_url"));
29185 }
29186 function parseReleaseArt(posts, branch) {
29187 if (!Array.isArray(posts)) {
29188 return null;
29189 }
29190 const escaped = branch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29191 const re = new RegExp(
29192 "^WordPress " + escaped + '\\s*[“"]([^”"]+)[”"]'
29193 );
29194 for (const post of posts) {
29195 const title = decodeEntities(str(prop(prop(post, "title"), "rendered")));
29196 const m = re.exec(title);
29197 if (!m) {
29198 continue;
29199 }
29200 const artUrl = pickMedia(post);
29201 if (artUrl) {
29202 return { name: m[1].trim(), artUrl };
29203 }
29204 }
29205 return null;
29206 }
29207 function readCache(branch) {
29208 try {
29209 const raw = localStorage.getItem(CACHE_PREFIX + branch);
29210 if (!raw) {
29211 return null;
29212 }
29213 const v = JSON.parse(raw);
29214 if (v.ok === true && str(v.name) && str(v.artUrl)) {
29215 return { name: str(v.name), artUrl: str(v.artUrl) };
29216 }
29217 if (v.ok === false && typeof v.ts === "number" && Date.now() - v.ts < MISS_TTL_MS) {
29218 return "miss";
29219 }
29220 return null;
29221 } catch {
29222 return null;
29223 }
29224 }
29225 function writeCache(branch, value) {
29226 try {
29227 localStorage.setItem(CACHE_PREFIX + branch, JSON.stringify(value));
29228 } catch {
29229 }
29230 }
29231 async function resolveReleaseArt(branch) {
29232 if (!branch) {
29233 return null;
29234 }
29235 const cached = readCache(branch);
29236 if (cached === "miss") {
29237 return null;
29238 }
29239 if (cached) {
29240 return cached;
29241 }
29242 try {
29243 const url = "https://wordpress.org/news/wp-json/wp/v2/posts?search=" + encodeURIComponent(branch) + "&per_page=100&_fields=title,_links,_embedded&_embed=wp:featuredmedia";
29244 const res = await trackedFetch$1(
29245 url,
29246 { credentials: "omit" },
29247 { silent: true, source: "desktop-mode/release-art" }
29248 );
29249 if (!res.ok) {
29250 writeCache(branch, { ok: false, ts: Date.now() });
29251 return null;
29252 }
29253 const art = parseReleaseArt(await res.json(), branch);
29254 if (art) {
29255 writeCache(branch, { ok: true, name: art.name, artUrl: art.artUrl });
29256 return art;
29257 }
29258 writeCache(branch, { ok: false, ts: Date.now() });
29259 return null;
29260 } catch {
29261 writeCache(branch, { ok: false, ts: Date.now() });
29262 return null;
29263 }
29264 }
29265 function preloadImage(url, timeoutMs = 5e3) {
29266 return new Promise((resolve2) => {
29267 const img = new Image();
29268 img.crossOrigin = "anonymous";
29269 let done = false;
29270 const finish = (ok) => {
29271 if (done) {
29272 return;
29273 }
29274 done = true;
29275 resolve2(ok);
29276 };
29277 img.addEventListener("load", () => finish(true), { once: true });
29278 img.addEventListener("error", () => finish(false), { once: true });
29279 window.setTimeout(() => finish(false), timeoutMs);
29280 img.src = url;
29281 });
29282 }
29283 function updateMessage(version, name) {
29284 if (name) {
29285 const withName = __('WordPress %1$s "%2$s" is available.');
29286 return sprintf(withName, version, name);
29287 }
29288 const versionOnly = __("WordPress %s is available.");
29289 return sprintf(versionOnly, version);
29290 }
29291 async function maybeShowUpdate(deps2) {
29292 const { update, openUrl } = deps2;
29293 if (!update || typeof update.version !== "string" || !update.version || typeof update.url !== "string" || !update.url) {
29294 return;
29295 }
29296 const version = update.version;
29297 const branch = typeof update.branch === "string" && update.branch ? update.branch : version;
29298 const crossing = update.crossing === true;
29299 const exact = typeof update.available === "string" && update.available ? update.available : version;
29300 const dismissKey = `desktop-mode/core-update:${exact}`;
29301 if (isNoticeDismissed(dismissKey)) {
29302 return;
29303 }
29304 const openUpdateScreen = () => openUrl({ url: update.url, title: __("WordPress Updates") });
29305 const resolveArt = deps2.resolveArt ?? resolveReleaseArt;
29306 const load = deps2.loadImage ?? preloadImage;
29307 const art = await resolveArt(branch);
29308 if (art && art.artUrl && await load(art.artUrl)) {
29309 showReleaseCard({
29310 message: updateMessage(version, crossing ? art.name : ""),
29311 artUrl: art.artUrl,
29312 dismissKey,
29313 onUpdate: openUpdateScreen
29314 });
29315 return;
29316 }
29317 showToast({
29318 message: updateMessage(version, ""),
29319 persistent: true,
29320 dismissible: true,
29321 onDismiss: () => markNoticeDismissed(dismissKey),
29322 action: {
29323 label: __("Update now"),
29324 onClick: openUpdateScreen
29325 }
29326 });
29327 }
29328 const STARTER_WIDGET_ID = "desktop-mode/starter";
29329 const FILTER_NAMESPACE = "desktop-mode/dev-mode-gate";
29330 let _started = false;
29331 function setupDevModeWidgetGate({ osSettings, layer }) {
29332 if (_started) {
29333 return;
29334 }
29335 _started = true;
29336 addFilter(
29337 HOOKS.WIDGETS,
29338 FILTER_NAMESPACE,
29339 (defs) => {
29340 if (osSettings.getOsSettingsSnapshot().developerModeEnabled) {
29341 return defs;
29342 }
29343 return defs.filter((def) => def.id !== STARTER_WIDGET_ID);
29344 }
29345 );
29346 let developerModeEnabled = osSettings.getOsSettingsSnapshot().developerModeEnabled;
29347 osSettings.subscribeOsSettings((snapshot) => {
29348 if (snapshot.developerModeEnabled === developerModeEnabled) {
29349 return;
29350 }
29351 developerModeEnabled = snapshot.developerModeEnabled;
29352 if (developerModeEnabled) {
29353 layer.mountIfEnabled(STARTER_WIDGET_ID);
29354 } else {
29355 layer.unmount(STARTER_WIDGET_ID);
29356 }
29357 refreshWidgetPicker();
29358 });
29359 }
29360 function createWidgetRegistrySync(deps2) {
29361 const { layer } = deps2;
29362 const registered = /* @__PURE__ */ new Set();
29363 const loadedScripts = /* @__PURE__ */ new Set();
29364 const ensureScript = async (entry) => {
29365 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
29366 return;
29367 }
29368 try {
29369 await loadVendorScript(entry.scriptUrl, {
29370 translations: entry.scriptTranslations,
29371 l10n: entry.scriptL10n,
29372 before: entry.scriptBefore,
29373 after: entry.scriptAfter
29374 });
29375 } catch (err) {
29376 doAction(HOOKS.SHELL_ERROR, {
29377 scope: "widget-script-load",
29378 id: entry.id,
29379 error: err
29380 });
29381 return;
29382 }
29383 loadedScripts.add(entry.scriptUrl);
29384 };
29385 const buildDefFromEntry = (entry) => {
29386 const globals = window.desktopModeWidgets || {};
29387 const mount = globals[entry.id];
29388 if (!mount) {
29389 doAction(HOOKS.SHELL_ERROR, {
29390 scope: "widget-missing-mount",
29391 id: entry.id,
29392 error: new Error(
29393 `[desktop-mode] No mount callback on window.desktopModeWidgets["${entry.id}"]. Plugin script loaded but didn't register. Check the plugin's enqueue + global assignment.`
29394 )
29395 });
29396 return null;
29397 }
29398 return {
29399 id: entry.id,
29400 label: entry.label,
29401 description: entry.description,
29402 icon: entry.icon,
29403 movable: entry.movable,
29404 resizable: entry.resizable,
29405 minWidth: entry.minWidth || void 0,
29406 minHeight: entry.minHeight || void 0,
29407 maxWidth: entry.maxWidth || void 0,
29408 maxHeight: entry.maxHeight || void 0,
29409 defaultWidth: entry.defaultWidth || void 0,
29410 defaultHeight: entry.defaultHeight || void 0,
29411 mount
29412 };
29413 };
29414 const registerEntry = async (entry) => {
29415 if (registered.has(entry.id)) {
29416 return;
29417 }
29418 await ensureScript(entry);
29419 const def = buildDefFromEntry(entry);
29420 if (!def) {
29421 return;
29422 }
29423 try {
29424 register(def);
29425 } catch (err) {
29426 doAction(HOOKS.SHELL_ERROR, {
29427 scope: "widget-register",
29428 id: entry.id,
29429 error: err
29430 });
29431 return;
29432 }
29433 registered.add(entry.id);
29434 refreshWidgetPicker();
29435 if (layer) {
29436 layer.mountIfEnabled(entry.id);
29437 }
29438 };
29439 const unregisterEntry = (id) => {
29440 if (!registered.has(id)) {
29441 return;
29442 }
29443 layer?.unmount(id);
29444 unregister(id);
29445 registered.delete(id);
29446 refreshWidgetPicker();
29447 };
29448 return async (list2) => {
29449 const incoming = /* @__PURE__ */ new Set();
29450 for (const entry of list2) {
29451 incoming.add(entry.id);
29452 }
29453 for (const id of Array.from(registered)) {
29454 if (!incoming.has(id)) {
29455 unregisterEntry(id);
29456 }
29457 }
29458 for (const entry of list2) {
29459 if (!registered.has(entry.id)) {
29460 await registerEntry(entry);
29461 }
29462 }
29463 };
29464 }
29465 const WPD_COMPONENT_TAGS = [
29466 "wpd-section",
29467 "wpd-button",
29468 "wpd-swatch",
29469 "wpd-swatch-grid",
29470 "wpd-segmented",
29471 "wpd-segment",
29472 "wpd-select",
29473 "wpd-option",
29474 "wpd-multiselect",
29475 "wpd-color-field",
29476 "wpd-range-field",
29477 "wpd-text-field",
29478 "wpd-number-field",
29479 "wpd-checkbox",
29480 "wpd-checkbox-label",
29481 "wpd-toast",
29482 "wpd-toast-container",
29483 "wpd-tabs",
29484 "wpd-tab",
29485 "wpd-tabpanel",
29486 "wpd-window-button",
29487 "wpd-menu",
29488 "wpd-menu-item",
29489 "wpd-context-menu",
29490 "wpd-context-menu-option",
29491 "wpd-confirm-dialog",
29492 "wpd-modal",
29493 "wpd-user-search",
29494 "wpd-role-picker",
29495 "wpd-flyout",
29496 "wpd-tab-chip",
29497 "wpd-stack",
29498 "wpd-cluster",
29499 "wpd-icon",
29500 "wpd-body",
29501 "wpd-panel",
29502 "wpd-row",
29503 "wpd-grid",
29504 "wpd-display",
29505 "wpd-empty-state",
29506 "wpd-key",
29507 "wpd-code",
29508 "wpd-badge",
29509 "wpd-ribbon",
29510 "wpd-tile",
29511 "wpd-log",
29512 "wpd-steps",
29513 "wpd-step",
29514 "wpd-table",
29515 "wpd-spinner",
29516 "wpd-relative-time",
29517 "wpd-avatar",
29518 "wpd-textarea",
29519 "wpd-chip",
29520 "wpd-tag-input",
29521 "wpd-form",
29522 "wpd-save-status",
29523 "wpd-category-picker",
29524 "wpd-crumb-chain",
29525 "wpd-card",
29526 "wpd-rating-summary",
29527 "wpd-notice",
29528 "wpd-progress-bar"
29529 ];
29530 const KNOWN = new Set(WPD_COMPONENT_TAGS);
29531 const WARN_GRACE_MS = 2e3;
29532 const warnedTags = /* @__PURE__ */ new Set();
29533 const observedRoots = /* @__PURE__ */ new WeakSet();
29534 let started$2 = false;
29535 function distance(a, b) {
29536 const m = a.length;
29537 const n = b.length;
29538 if (m === 0) {
29539 return n;
29540 }
29541 if (n === 0) {
29542 return m;
29543 }
29544 const dp = new Array(n + 1);
29545 for (let j = 0; j <= n; j++) {
29546 dp[j] = j;
29547 }
29548 for (let i = 1; i <= m; i++) {
29549 let prev = dp[0];
29550 dp[0] = i;
29551 for (let j = 1; j <= n; j++) {
29552 const tmp = dp[j];
29553 dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
29554 prev = tmp;
29555 }
29556 }
29557 return dp[n];
29558 }
29559 function suggest(tag) {
29560 let best = null;
29561 let bestD = Infinity;
29562 for (const known of KNOWN) {
29563 const d = distance(tag, known);
29564 if (d < bestD) {
29565 bestD = d;
29566 best = known;
29567 }
29568 }
29569 return bestD > 0 && bestD <= 3 ? best : null;
29570 }
29571 function folderFor(tag) {
29572 return tag.startsWith("wpd-") ? tag.slice(4) : tag;
29573 }
29574 function warnFor(tag, sample) {
29575 if (warnedTags.has(tag)) {
29576 return;
29577 }
29578 warnedTags.add(tag);
29579 const isKnown = KNOWN.has(tag);
29580 if (isKnown) {
29581 const folder = folderFor(tag);
29582 console.error(
29583 `[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.
29584
29585 Fix — side-effect-import the component module from wherever you render it:
29586
29587 import '<rel>/ui/components/${folder}/${folder}';
29588
29589 Or pull every wpd-* component in one go (heavier — only do this from an entry bundle):
29590
29591 import '<rel>/ui/components';
29592
29593 See docs/components-reference.md for the full list.`,
29594 "\nFirst offending element:",
29595 sample
29596 );
29597 return;
29598 }
29599 const guess = suggest(tag);
29600 if (guess) {
29601 console.error(
29602 `[wp.desktop] <${tag}> is not a registered wpd-* component. Did you mean <${guess}>?
29603
29604 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'.`,
29605 "\nFirst offending element:",
29606 sample
29607 );
29608 return;
29609 }
29610 console.error(
29611 `[wp.desktop] <${tag}> looks like a wpd-* tag but no component by that name exists.
29612
29613 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.`,
29614 "\nFirst offending element:",
29615 sample
29616 );
29617 }
29618 function checkElement(el) {
29619 const tag = el.tagName.toLowerCase();
29620 if (!tag.startsWith("wpd-")) {
29621 return;
29622 }
29623 if (warnedTags.has(tag)) {
29624 return;
29625 }
29626 if (customElements.get(tag)) {
29627 return;
29628 }
29629 let settled = false;
29630 customElements.whenDefined(tag).then(() => {
29631 settled = true;
29632 });
29633 setTimeout(() => {
29634 if (settled) {
29635 return;
29636 }
29637 if (customElements.get(tag)) {
29638 return;
29639 }
29640 warnFor(tag, el);
29641 }, WARN_GRACE_MS);
29642 }
29643 function walk(root) {
29644 if (root instanceof Element) {
29645 checkElement(root);
29646 if (root.shadowRoot) {
29647 observeRoot(root.shadowRoot);
29648 }
29649 }
29650 const all2 = root.querySelectorAll("*");
29651 for (let i = 0; i < all2.length; i++) {
29652 const el = all2[i];
29653 checkElement(el);
29654 if (el.shadowRoot) {
29655 observeRoot(el.shadowRoot);
29656 }
29657 }
29658 }
29659 function observeRoot(root) {
29660 if (observedRoots.has(root)) {
29661 return;
29662 }
29663 observedRoots.add(root);
29664 walk(root);
29665 const mo = new MutationObserver((records) => {
29666 for (let i = 0; i < records.length; i++) {
29667 const added = records[i].addedNodes;
29668 for (let j = 0; j < added.length; j++) {
29669 const node = added[j];
29670 if (node.nodeType === 1) {
29671 walk(node);
29672 }
29673 }
29674 }
29675 });
29676 mo.observe(root, { childList: true, subtree: true });
29677 }
29678 function patchAttachShadow() {
29679 const proto = Element.prototype;
29680 const original = proto.attachShadow;
29681 if (original.__wpdPatched) {
29682 return;
29683 }
29684 const patched = function(init2) {
29685 const root = original.call(this, init2);
29686 if (root.mode === "open") {
29687 observeRoot(root);
29688 }
29689 return root;
29690 };
29691 patched.__wpdPatched = true;
29692 proto.attachShadow = patched;
29693 }
29694 function startMissingImportWarner() {
29695 if (started$2) {
29696 return;
29697 }
29698 if (typeof document === "undefined") {
29699 return;
29700 }
29701 started$2 = true;
29702 patchAttachShadow();
29703 observeRoot(document);
29704 }
29705 const TRASHABLE_SHORTCUT_KINDS = /* @__PURE__ */ new Set(["post"]);
29706 function getMyWordpressTrashApi() {
29707 const api = window.wp?.desktop?.myWordpress;
29708 return api && typeof api.trashEntity === "function" ? api : null;
29709 }
29710 const TRASH_DROP_ACTIVE_ATTR = "data-desktop-mode-trash-drop-active";
29711 const RECYCLE_BIN_WINDOW_ID = "desktop-mode-recycle-bin";
29712 const BIN_TILE_SELECTORS = [
29713 `.desktop-mode-file-tile[data-file-ref="${RECYCLE_BIN_WINDOW_ID}"]`,
29714 `[data-icon-id="${RECYCLE_BIN_WINDOW_ID}"]`,
29715 `[data-system-id="${RECYCLE_BIN_WINDOW_ID}"]`
29716 ];
29717 function findBinTile() {
29718 for (const sel of BIN_TILE_SELECTORS) {
29719 const el = document.querySelector(sel);
29720 if (el instanceof HTMLElement) {
29721 return el;
29722 }
29723 }
29724 return null;
29725 }
29726 let _installed = false;
29727 let _dockDeregister = null;
29728 let _windowDeregister = null;
29729 let _binMutationObserver = null;
29730 function isDesktopFilePayload(session) {
29731 return session.payload.type === "desktop-file";
29732 }
29733 function isShortcutPayload(session) {
29734 return session.payload.type === "shortcut";
29735 }
29736 function isTrashableShortcut(data) {
29737 if (!data.kind || !data.ref || !data.entityId) {
29738 return false;
29739 }
29740 if (!TRASHABLE_SHORTCUT_KINDS.has(data.kind)) {
29741 return false;
29742 }
29743 const numericRef = Number.parseInt(data.ref, 10);
29744 if (!Number.isFinite(numericRef) || numericRef <= 0) {
29745 return false;
29746 }
29747 return getMyWordpressTrashApi() !== null;
29748 }
29749 function registerOn(dragManager, id, el) {
29750 return dragManager.registerDropTarget({
29751 id,
29752 element: el,
29753 // Override the ghost-chip label: while the cursor is over
29754 // the bin the user is trashing, not creating a shortcut /
29755 // moving the placement. The DragManager swaps this in for
29756 // the payload-default "Drop here to create shortcut" /
29757 // "Drop here to move" chip text whenever this target is the
29758 // current accept-mode target.
29759 acceptLabel: __("Move to Trash", "desktop-mode"),
29760 // Reject the drop UP FRONT when the viewer can't trash the
29761 // payload's placement (e.g. an item inside a read-only
29762 // shared folder, or someone else's tile in a shared
29763 // namespace). `accept` flipping to `false` means the
29764 // drop-active highlight never lights up + onDrop never
29765 // fires + the drag manager surfaces a `rejected` outcome.
29766 // The user sees the icon snap back instead of attempting a
29767 // REST call that would 403 and only log to the console.
29768 accept: (payload) => {
29769 if (payload.type === "desktop-file") {
29770 const data = payload.data;
29771 const placement = data?.placement;
29772 if (!placement) {
29773 return false;
29774 }
29775 if (placement.file?.ref === RECYCLE_BIN_WINDOW_ID) {
29776 return false;
29777 }
29778 return placement.canTrash !== false;
29779 }
29780 if (payload.type === "shortcut") {
29781 const data = payload.data;
29782 return isTrashableShortcut(data);
29783 }
29784 return false;
29785 },
29786 onEnter: () => {
29787 el.setAttribute(TRASH_DROP_ACTIVE_ATTR, "");
29788 },
29789 onLeave: () => {
29790 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
29791 },
29792 onDrop: (session) => {
29793 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
29794 if (isDesktopFilePayload(session)) {
29795 const placement = session.payload.data.placement;
29796 void trashByFileType(placement);
29797 return;
29798 }
29799 if (isShortcutPayload(session)) {
29800 const data = session.payload.data;
29801 const api = getMyWordpressTrashApi();
29802 if (!api?.trashEntity || !data.entityId) {
29803 return;
29804 }
29805 const numericRef = Number.parseInt(data.ref, 10);
29806 if (!Number.isFinite(numericRef) || numericRef <= 0) {
29807 return;
29808 }
29809 void api.trashEntity(data.entityId, numericRef).catch(
29810 (err) => {
29811 console.error(
29812 "[desktop-mode] recycle-bin: shortcut trash failed:",
29813 err
29814 );
29815 }
29816 );
29817 }
29818 }
29819 });
29820 }
29821 function installRecycleBinDropTargets(dragManager) {
29822 if (_installed) {
29823 return;
29824 }
29825 _installed = true;
29826 const reprobeTile = () => {
29827 const el = findBinTile();
29828 if (!el) {
29829 _dockDeregister?.();
29830 _dockDeregister = null;
29831 return;
29832 }
29833 if (_dockDeregister && getRegisteredElementId(dragManager) === el) {
29834 return;
29835 }
29836 _dockDeregister?.();
29837 _dockDeregister = registerOn(dragManager, "recycle-bin-dock", el);
29838 };
29839 reprobeTile();
29840 document.addEventListener("desktop-mode-files-changed", reprobeTile);
29841 addAction(
29842 HOOKS.DESKTOP_ICONS_RENDERED,
29843 "desktop-mode/files/recycle-bin-icons-target",
29844 reprobeTile
29845 );
29846 addAction(
29847 HOOKS.DOCK_AFTER_RENDER,
29848 "desktop-mode/files/recycle-bin-dock-target",
29849 reprobeTile
29850 );
29851 if (typeof MutationObserver !== "undefined") {
29852 _binMutationObserver = new MutationObserver(() => {
29853 reprobeTile();
29854 });
29855 const desktopArea = document.getElementById("desktop-mode-area") ?? document.body;
29856 _binMutationObserver.observe(desktopArea, {
29857 childList: true,
29858 subtree: true
29859 });
29860 }
29861 addAction(
29862 HOOKS.WINDOW_OPENED,
29863 "desktop-mode/files/recycle-bin-window-target",
29864 (detail) => {
29865 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
29866 return;
29867 }
29868 _windowDeregister?.();
29869 _windowDeregister = null;
29870 const el = document.querySelector(
29871 "[data-desktop-mode-recycle-bin-root]"
29872 );
29873 if (el instanceof HTMLElement) {
29874 _windowDeregister = registerOn(
29875 dragManager,
29876 "recycle-bin-window",
29877 el
29878 );
29879 }
29880 }
29881 );
29882 addAction(
29883 HOOKS.WINDOW_CLOSED,
29884 "desktop-mode/files/recycle-bin-window-cleanup",
29885 (detail) => {
29886 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
29887 return;
29888 }
29889 _windowDeregister?.();
29890 _windowDeregister = null;
29891 }
29892 );
29893 }
29894 function getRegisteredElementId(dragManager) {
29895 const t = dragManager.debug().listTargets().find((target2) => target2.id === "recycle-bin-dock");
29896 return t ? t.element : null;
29897 }
29898 let started$1 = false;
29899 let highWaterMs = 0;
29900 function startFilesHeartbeat() {
29901 if (started$1) {
29902 return;
29903 }
29904 started$1 = true;
29905 heartbeat.contribute("desktop_mode_files_subscribe", () => {
29906 const state2 = getFilesState();
29907 const folderVersions = {};
29908 for (const [id, folder] of state2.folders) {
29909 folderVersions[String(id)] = folder.updatedAtMs;
29910 }
29911 return {
29912 folderVersions,
29913 placementsVersion: highWaterMs,
29914 sharesVersion: sharesStore().state.sharesVersion
29915 };
29916 });
29917 heartbeat.subscribe("desktop_mode_files", (payload) => {
29918 applyDelta(payload);
29919 });
29920 }
29921 function applyDelta(payload) {
29922 const folders = payload.folders ?? [];
29923 for (const folder of folders) {
29924 upsertFolder(folder, "remote");
29925 if (folder.updatedAtMs > highWaterMs) {
29926 highWaterMs = folder.updatedAtMs;
29927 }
29928 }
29929 const placements = payload.placements ?? [];
29930 for (const placement of placements) {
29931 upsertPlacement(placement, "remote");
29932 if (placement.updatedAtMs > highWaterMs) {
29933 highWaterMs = placement.updatedAtMs;
29934 }
29935 }
29936 const removed = payload.removed ?? {};
29937 for (const id of removed.folders ?? []) {
29938 removeFolder(id, "remote");
29939 }
29940 for (const id of removed.placements ?? []) {
29941 removePlacement(id, "remote");
29942 }
29943 if (typeof payload.serverTimeMs === "number" && payload.serverTimeMs > highWaterMs) {
29944 highWaterMs = payload.serverTimeMs;
29945 }
29946 const pending2 = payload.shares?.pending;
29947 if (Array.isArray(pending2) && pending2.length > 0) {
29948 ingestPendingInvites(pending2);
29949 }
29950 if (payload.truncated) {
29951 const hydrated = Array.from(getFilesState().hydratedFolders);
29952 for (const folderId of hydrated) {
29953 void listPlacements(folderId).then((res) => {
29954 setFolderPlacements(folderId, res.placements);
29955 }).catch(() => {
29956 });
29957 }
29958 }
29959 }
29960 let started = false;
29961 const unsubscribers = [];
29962 function startFilesRestoreSync() {
29963 if (started) {
29964 return;
29965 }
29966 started = true;
29967 const onChange = (payload) => {
29968 const detail = payload;
29969 if (!detail || detail.action !== "untrashed") {
29970 return;
29971 }
29972 resyncFromServer();
29973 };
29974 unsubscribers.push(
29975 subscribe$2("desktop-mode.placement.changed", onChange),
29976 subscribe$2("desktop-mode.shortcut.changed", onChange),
29977 subscribe$2("desktop-mode.folder.changed", onChange)
29978 );
29979 }
29980 function resyncFromServer() {
29981 void listFolders().then((res) => {
29982 setFolders(res.folders);
29983 }).catch((err) => {
29984 console.error(
29985 "[desktop-mode] files restore-sync: listFolders failed",
29986 err
29987 );
29988 });
29989 const hydrated = Array.from(getFilesState().hydratedFolders);
29990 for (const folderId of hydrated) {
29991 void listPlacements(folderId).then((res) => {
29992 setFolderPlacements(folderId, res.placements);
29993 }).catch((err) => {
29994 console.error(
29995 "[desktop-mode] files restore-sync: listPlacements failed for",
29996 folderId,
29997 err
29998 );
29999 });
30000 }
30001 }
30002 const MENU_CLASS = "desktop-mode-wallpaper-menu";
30003 let activeMenu = null;
30004 function isWallpaperMenuOpen() {
30005 return activeMenu !== null;
30006 }
30007 let openGeneration = 0;
30008 function openWallpaperMenu(host, pos, items, options = {}) {
30009 closeWallpaperMenu();
30010 const myGen = ++openGeneration;
30011 openWithShellOverlays(
30012 () => myGen === openGeneration,
30013 () => openWallpaperMenuImmediate(host, pos, items, options)
30014 );
30015 }
30016 function openWallpaperMenuImmediate(host, pos, items, options = {}) {
30017 if (items.length === 0) {
30018 return;
30019 }
30020 items = items.slice().sort((a, b) => {
30021 const sa = typeof a.sort === "number" ? a.sort : 100;
30022 const sb = typeof b.sort === "number" ? b.sort : 100;
30023 if (sa !== sb) {
30024 return sa - sb;
30025 }
30026 return a.label.localeCompare(b.label);
30027 });
30028 const menu = document.createElement("wpd-context-menu");
30029 menu.setAttribute("open", "");
30030 menu.classList.add(MENU_CLASS);
30031 menu.style.left = `${pos.x}px`;
30032 menu.style.top = `${pos.y}px`;
30033 const itemById = /* @__PURE__ */ new Map();
30034 let activeFlyout2 = null;
30035 let activeFlyoutParent = null;
30036 const closeActiveFlyout = () => {
30037 if (activeFlyout2) {
30038 activeFlyout2.remove();
30039 activeFlyout2 = null;
30040 activeFlyoutParent = null;
30041 }
30042 };
30043 for (const item of items) {
30044 itemById.set(item.id, item);
30045 const opt = document.createElement("wpd-context-menu-option");
30046 opt.dataset.menuItemId = item.id;
30047 opt.setAttribute("value", item.id);
30048 if (item.heading) {
30049 opt.setAttribute("heading", "");
30050 }
30051 if (item.disabled) {
30052 opt.setAttribute("disabled", "");
30053 }
30054 if (item.icon) {
30055 opt.setAttribute("icon", sanitizeClass(item.icon));
30056 }
30057 const hasChildren2 = Array.isArray(item.children) && item.children.length > 0;
30058 if (hasChildren2) {
30059 opt.setAttribute("has-children", "");
30060 }
30061 opt.textContent = item.label;
30062 opt.addEventListener("mouseenter", () => {
30063 if (hasChildren2) {
30064 openFlyout2(item, opt);
30065 return;
30066 }
30067 closeActiveFlyout();
30068 });
30069 menu.appendChild(opt);
30070 }
30071 menu.addEventListener("wpd-context-menu-pick", (e) => {
30072 const detail = e.detail;
30073 const item = itemById.get(detail.id) ?? null;
30074 if (!item) {
30075 return;
30076 }
30077 if (Array.isArray(item.children) && item.children.length > 0) {
30078 e.stopPropagation();
30079 if (activeFlyoutParent && activeFlyoutParent.id === item.id) {
30080 closeActiveFlyout();
30081 return;
30082 }
30083 const anchor = menu.querySelector(
30084 `[data-menu-item-id="${item.id}"]`
30085 );
30086 if (anchor) {
30087 openFlyout2(item, anchor);
30088 }
30089 return;
30090 }
30091 closeWallpaperMenu();
30092 void item.onClick(new MouseEvent("click"));
30093 });
30094 function openFlyout2(parent, anchor) {
30095 closeActiveFlyout();
30096 const fly = document.createElement("wpd-context-menu");
30097 fly.setAttribute("open", "");
30098 fly.classList.add(MENU_CLASS, `${MENU_CLASS}--flyout`);
30099 fly.dataset.parentId = parent.id;
30100 const sortedKids = (parent.children ?? []).slice().sort((a, b) => {
30101 const sa = typeof a.sort === "number" ? a.sort : 100;
30102 const sb = typeof b.sort === "number" ? b.sort : 100;
30103 if (sa !== sb) {
30104 return sa - sb;
30105 }
30106 return a.label.localeCompare(b.label);
30107 });
30108 for (const child of sortedKids) {
30109 const kopt = document.createElement("wpd-context-menu-option");
30110 kopt.dataset.menuItemId = child.id;
30111 kopt.setAttribute("value", child.id);
30112 if (child.icon) {
30113 kopt.setAttribute("icon", sanitizeClass(child.icon));
30114 }
30115 if (child.disabled) {
30116 kopt.setAttribute("disabled", "");
30117 }
30118 if (child.checked) {
30119 kopt.setAttribute("checked", "");
30120 }
30121 kopt.textContent = child.label;
30122 kopt.addEventListener("wpd-context-menu-pick", (e) => {
30123 e.stopPropagation();
30124 closeWallpaperMenu();
30125 void child.onClick(new MouseEvent("click"));
30126 });
30127 fly.appendChild(kopt);
30128 }
30129 document.body.appendChild(fly);
30130 activeFlyout2 = fly;
30131 activeFlyoutParent = parent;
30132 positionFlyout2(fly, anchor);
30133 }
30134 function positionFlyout2(fly, anchor) {
30135 const ar = anchor.getBoundingClientRect();
30136 fly.style.position = "fixed";
30137 fly.style.left = `${ar.right}px`;
30138 fly.style.top = `${ar.top}px`;
30139 const fr = fly.getBoundingClientRect();
30140 if (fr.right > window.innerWidth) {
30141 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
30142 }
30143 if (fr.bottom > window.innerHeight) {
30144 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
30145 }
30146 }
30147 host.appendChild(menu);
30148 activeMenu = menu;
30149 const rect = menu.getBoundingClientRect();
30150 if (rect.right > window.innerWidth) {
30151 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
30152 }
30153 if (rect.bottom > window.innerHeight) {
30154 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
30155 }
30156 const detach = attachDismissable(menu, {
30157 close: () => closeWallpaperMenu(),
30158 siblingSelectors: [`.${MENU_CLASS}--flyout`],
30159 excludeOutsideTarget: options.excludeOutsideTarget
30160 });
30161 menu.addEventListener("wallpaper-menu-closed", detach);
30162 doAction("desktop-mode.wallpaper-menu.opened", { items: items.map((i) => i.id) });
30163 }
30164 function closeWallpaperMenu() {
30165 if (!activeMenu) {
30166 return;
30167 }
30168 document.querySelectorAll(`.${MENU_CLASS}--flyout`).forEach((el) => el.remove());
30169 activeMenu.dispatchEvent(new CustomEvent("wallpaper-menu-closed"));
30170 activeMenu.remove();
30171 activeMenu = null;
30172 doAction("desktop-mode.wallpaper-menu.closed", {});
30173 }
30174 function buildMenuItems(deps2) {
30175 const builtIn = [
30176 {
30177 id: "create-folder",
30178 label: deps2.labels.createFolder,
30179 icon: "dashicons-portfolio",
30180 sort: 10,
30181 onClick: () => deps2.createFolder()
30182 },
30183 {
30184 id: "new-url",
30185 label: deps2.labels.newUrl,
30186 icon: "dashicons-admin-links",
30187 sort: 12,
30188 onClick: () => deps2.createUrl()
30189 },
30190 {
30191 id: "sort-by",
30192 label: deps2.labels.sortHeading,
30193 icon: "dashicons-sort",
30194 sort: 16,
30195 onClick: () => void 0,
30196 children: [
30197 {
30198 id: "sort-name-asc",
30199 label: deps2.labels.sortNameAsc,
30200 sort: 10,
30201 checked: deps2.currentSortMode === "name-asc",
30202 onClick: () => deps2.sortIcons("name-asc")
30203 },
30204 {
30205 id: "sort-name-desc",
30206 label: deps2.labels.sortNameDesc,
30207 sort: 20,
30208 checked: deps2.currentSortMode === "name-desc",
30209 onClick: () => deps2.sortIcons("name-desc")
30210 },
30211 {
30212 id: "sort-date-desc",
30213 label: deps2.labels.sortDateDesc,
30214 sort: 30,
30215 checked: deps2.currentSortMode === "date-desc",
30216 onClick: () => deps2.sortIcons("date-desc")
30217 },
30218 {
30219 id: "sort-date-asc",
30220 label: deps2.labels.sortDateAsc,
30221 sort: 40,
30222 checked: deps2.currentSortMode === "date-asc",
30223 onClick: () => deps2.sortIcons("date-asc")
30224 }
30225 ]
30226 },
30227 ...deps2.includeShowDesktop === false ? [] : [
30228 {
30229 id: "show-desktop",
30230 label: deps2.labels.showDesktop,
30231 icon: "dashicons-desktop",
30232 sort: 20,
30233 onClick: () => deps2.toggleShowDesktop()
30234 }
30235 ],
30236 {
30237 id: "os-settings",
30238 label: deps2.labels.osSettings,
30239 icon: "dashicons-admin-generic",
30240 sort: 30,
30241 onClick: () => deps2.openOsSettings()
30242 }
30243 ];
30244 const serverItems = (deps2.serverItems ?? []).map(
30245 (s) => serverItemToMenuItem(s, deps2)
30246 );
30247 const merged = [...builtIn, ...serverItems];
30248 const filtered = applyFilters(
30249 "desktop-mode.wallpaper-context-menu",
30250 merged
30251 );
30252 return Array.isArray(filtered) ? filtered : merged;
30253 }
30254 function serverItemToMenuItem(server, deps2) {
30255 return {
30256 id: server.id,
30257 label: server.label,
30258 icon: server.icon,
30259 sort: server.sort,
30260 disabled: server.disabled,
30261 onClick: () => {
30262 if (server.callbackId) {
30263 const cb = deps2.serverCallbacks?.[server.callbackId];
30264 if (typeof cb === "function") {
30265 return cb();
30266 }
30267 }
30268 doAction("desktop-mode.wallpaper-context-menu.activated", {
30269 id: server.id,
30270 callbackId: server.callbackId ?? ""
30271 });
30272 }
30273 };
30274 }
30275 function sanitizeClass(raw) {
30276 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
30277 }
30278 const ROOT_CLASS = "desktop-mode-url-dialog";
30279 let active = null;
30280 function closeUrlDialog() {
30281 if (!active) {
30282 return;
30283 }
30284 active.dispatchEvent(new CustomEvent("url-dialog-closed"));
30285 active.remove();
30286 active = null;
30287 doAction("desktop-mode.files.url-dialog.closed", {});
30288 }
30289 function openUrlDialog(options) {
30290 closeUrlDialog();
30291 const decision = applyFilters(
30292 "desktop-mode.files.url-dialog",
30293 null,
30294 options
30295 );
30296 if (decision === false) {
30297 return;
30298 }
30299 const overlay = document.createElement("div");
30300 overlay.className = `${ROOT_CLASS}__overlay desktop-mode-create-folder-dialog__overlay`;
30301 overlay.setAttribute("role", "presentation");
30302 const dialog2 = document.createElement("div");
30303 dialog2.className = `${ROOT_CLASS} desktop-mode-create-folder-dialog`;
30304 dialog2.setAttribute("role", "dialog");
30305 dialog2.setAttribute("aria-modal", "true");
30306 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS}-title`);
30307 const title = document.createElement("h2");
30308 title.id = `${ROOT_CLASS}-title`;
30309 title.className = "desktop-mode-create-folder-dialog__title";
30310 title.textContent = options.title;
30311 dialog2.appendChild(title);
30312 if (options.description) {
30313 const desc = document.createElement("p");
30314 desc.className = `${ROOT_CLASS}__description`;
30315 desc.textContent = options.description;
30316 dialog2.appendChild(desc);
30317 }
30318 const nameField = document.createElement("wpd-text-field");
30319 nameField.setAttribute("label", options.nameLabel ?? "Name");
30320 nameField.setAttribute("value", options.initialName ?? "");
30321 nameField.setAttribute("placeholder", "My web app");
30322 nameField.setAttribute("autocomplete", "off");
30323 dialog2.appendChild(nameField);
30324 const urlField = document.createElement("wpd-text-field");
30325 urlField.setAttribute("label", options.urlLabel ?? "URL");
30326 urlField.setAttribute("value", options.initialUrl ?? "https://");
30327 urlField.setAttribute("placeholder", "https://example.com");
30328 urlField.setAttribute("type", "url");
30329 urlField.setAttribute("autocomplete", "off");
30330 dialog2.appendChild(urlField);
30331 const error = document.createElement("p");
30332 error.className = "desktop-mode-create-folder-dialog__error";
30333 error.hidden = true;
30334 error.setAttribute("role", "alert");
30335 dialog2.appendChild(error);
30336 const actions = document.createElement("div");
30337 actions.className = "desktop-mode-create-folder-dialog__actions";
30338 const cancel = document.createElement("button");
30339 cancel.type = "button";
30340 cancel.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--secondary";
30341 cancel.textContent = "Cancel";
30342 const submit = document.createElement("button");
30343 submit.type = "button";
30344 submit.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--primary";
30345 submit.textContent = options.submitLabel ?? "Create";
30346 actions.appendChild(cancel);
30347 actions.appendChild(submit);
30348 dialog2.appendChild(actions);
30349 overlay.appendChild(dialog2);
30350 document.body.appendChild(overlay);
30351 active = overlay;
30352 queueMicrotask(() => {
30353 const input = nameField.shadowRoot?.querySelector("input");
30354 input?.focus();
30355 input?.select();
30356 });
30357 doAction("desktop-mode.files.url-dialog.opened", {});
30358 const readValue = (field) => {
30359 const v = field.value;
30360 if (typeof v === "string") {
30361 return v;
30362 }
30363 return field.shadowRoot?.querySelector("input")?.value ?? "";
30364 };
30365 const setBusy = (busy) => {
30366 nameField.disabled = busy;
30367 urlField.disabled = busy;
30368 cancel.disabled = busy;
30369 submit.disabled = busy;
30370 dialog2.classList.toggle("desktop-mode-create-folder-dialog--busy", busy);
30371 };
30372 const showError = (msg) => {
30373 error.textContent = msg;
30374 error.hidden = false;
30375 };
30376 const doCancel = () => {
30377 closeUrlDialog();
30378 options.onCancel?.();
30379 };
30380 const doSubmit = async () => {
30381 const url = readValue(urlField).trim();
30382 if (!url) {
30383 showError("Please enter a URL.");
30384 return;
30385 }
30386 const finalUrl = /^[a-z][a-z0-9+\-.]*:/i.test(url) ? url : `https://${url}`;
30387 try {
30388 new URL(finalUrl);
30389 } catch {
30390 showError("That doesn't look like a valid URL.");
30391 return;
30392 }
30393 const name = readValue(nameField).trim();
30394 error.hidden = true;
30395 setBusy(true);
30396 try {
30397 await options.onSubmit({ name, url: finalUrl });
30398 closeUrlDialog();
30399 } catch (err) {
30400 setBusy(false);
30401 showError(err instanceof Error ? err.message : "Could not save.");
30402 }
30403 };
30404 cancel.addEventListener("click", () => doCancel());
30405 submit.addEventListener("click", () => void doSubmit());
30406 overlay.addEventListener("click", (e) => {
30407 if (e.target === overlay) {
30408 doCancel();
30409 }
30410 });
30411 const onKey = (e) => {
30412 if (e.key === "Escape") {
30413 e.preventDefault();
30414 doCancel();
30415 } else if (e.key === "Enter" && !e.isComposing) {
30416 e.preventDefault();
30417 void doSubmit();
30418 }
30419 };
30420 dialog2.addEventListener("keydown", onKey);
30421 overlay.addEventListener("url-dialog-closed", () => {
30422 dialog2.removeEventListener("keydown", onKey);
30423 });
30424 }
30425 const _earlyReadyQueue = [];
30426 let _earlyReady = false;
30427 (function installEarlyDesktopShim() {
30428 const w = window;
30429 if (!w.wp) {
30430 w.wp = {};
30431 }
30432 if (w.wp.desktop) {
30433 return;
30434 }
30435 const shim = {
30436 whenReady(cb) {
30437 if (typeof cb !== "function") {
30438 return;
30439 }
30440 if (_earlyReady) {
30441 Promise.resolve().then(cb);
30442 return;
30443 }
30444 _earlyReadyQueue.push(cb);
30445 },
30446 ready(cb) {
30447 shim.whenReady(cb);
30448 },
30449 isReady() {
30450 return _earlyReady;
30451 }
30452 };
30453 w.wp.desktop = shim;
30454 })();
30455 const OS_SETTINGS_WINDOW_ID = "desktop-mode-os-settings";
30456 let _idleBootQueue = [];
30457 let _idleBootTimeout = Number.POSITIVE_INFINITY;
30458 let _idleBootScheduled = false;
30459 function scheduleIdleBoot(cb, timeout = 1500) {
30460 _idleBootQueue.push(cb);
30461 if (timeout < _idleBootTimeout) {
30462 _idleBootTimeout = timeout;
30463 }
30464 if (_idleBootScheduled) {
30465 return;
30466 }
30467 _idleBootScheduled = true;
30468 const drain = () => {
30469 const callbacks = _idleBootQueue;
30470 _idleBootQueue = [];
30471 _idleBootTimeout = Number.POSITIVE_INFINITY;
30472 _idleBootScheduled = false;
30473 for (const fn of callbacks) {
30474 try {
30475 fn();
30476 } catch (err) {
30477 if (typeof console !== "undefined") {
30478 console.error(
30479 "[desktop-mode] scheduleIdleBoot callback threw:",
30480 err
30481 );
30482 }
30483 }
30484 }
30485 };
30486 if (typeof window.requestIdleCallback === "function") {
30487 window.requestIdleCallback(drain, { timeout: _idleBootTimeout });
30488 } else {
30489 window.setTimeout(drain, 0);
30490 }
30491 }
30492 function init() {
30493 const config = window.desktopModeConfig;
30494 if (!config) {
30495 return;
30496 }
30497 const desktopArea = document.getElementById("desktop-mode-area");
30498 if (!desktopArea) {
30499 return;
30500 }
30501 const manager = new WindowManager(desktopArea);
30502 const wallpaperEl = document.getElementById("desktop-mode-wallpaper");
30503 const pluginUrl = config.pluginUrl || "";
30504 let wallpaperLayer = null;
30505 if (wallpaperEl) {
30506 wallpaperLayer = new WallpaperLayer(wallpaperEl, pluginUrl);
30507 }
30508 const widgetsEl = document.getElementById("desktop-mode-widgets");
30509 let widgetLayer = null;
30510 registerBuiltInWidgets();
30511 installDefaultDockRailRenderer();
30512 if (widgetsEl) {
30513 widgetLayer = new WidgetLayer(widgetsEl, pluginUrl);
30514 }
30515 registerModule({
30516 id: "pixijs",
30517 url: `${pluginUrl}/assets/vendor/pixi.min.js`,
30518 isReady: () => typeof window.PIXI !== "undefined"
30519 });
30520 const osSettings = new OsSettings(
30521 {
30522 mediaUrl: config.mediaUrl,
30523 restNonce: config.restNonce,
30524 canUpload: !!config.canUpload,
30525 isAdmin: !!config.currentUserIsAdmin,
30526 extendedOptions: config.extendedOptions ?? null,
30527 extendedOptionsUrl: config.extendedOptionsUrl ?? "",
30528 osSettingsPanelBundleUrl: config.osSettingsPanelBundleUrl ?? ""
30529 },
30530 wallpaperLayer ?? new WallpaperLayer(document.createElement("div"), pluginUrl)
30531 );
30532 osSettings.apply();
30533 if (widgetLayer) {
30534 setupDevModeWidgetGate({ osSettings, layer: widgetLayer });
30535 }
30536 const aiAssistant = new AiAssistantStub(
30537 {
30538 aiSearchUrl: config.aiSearchUrl ?? "",
30539 aiSearchStreamUrl: config.aiSearchStreamUrl ?? "",
30540 restNonce: config.restNonce,
30541 // Progress streaming is on by default now that the per-user
30542 // transport picker is gone; the assistant falls back gracefully
30543 // if the host drops the SSE connection.
30544 getTransport: () => "sse"
30545 },
30546 config.aiAssistantBundleUrl ?? ""
30547 );
30548 aiAssistant.attachAsk(
30549 createAsk({
30550 config: () => config,
30551 fallbackContext: () => ({
30552 close: () => aiAssistant.close(),
30553 openInWindow: (url, title, icon) => {
30554 manager.open({
30555 url,
30556 title,
30557 icon: icon ?? "dashicons-admin-generic"
30558 });
30559 },
30560 confirm: (msg) => wpdConfirm({ message: msg })
30561 })
30562 })
30563 );
30564 const dragBridge = new DragBridge();
30565 const dragManager = new DragManager();
30566 document.addEventListener(DRAG_EVENTS.START, (e) => {
30567 const detail = e.detail;
30568 const payload = detail?.payload;
30569 if (!payload) {
30570 return;
30571 }
30572 if (payload.type !== "shortcut" && payload.type !== "desktop-file") {
30573 return;
30574 }
30575 const bridgePayload = payload.data?.bridgePayload;
30576 if (bridgePayload) {
30577 dragBridge.start(bridgePayload);
30578 }
30579 });
30580 document.addEventListener(DRAG_EVENTS.END, () => {
30581 dragBridge.end();
30582 });
30583 scheduleIdleBoot(() => installIframeDropTargets(dragManager));
30584 scheduleIdleBoot(() => installFocusWindowOnDragHover(manager));
30585 window.addEventListener("message", (e) => {
30586 if (e.origin !== window.location.origin) {
30587 return;
30588 }
30589 const data = e.data;
30590 if (!data || data.type !== "desktop-mode-drop-failed") {
30591 return;
30592 }
30593 showToast({
30594 message: "Could not insert into the editor."
30595 });
30596 });
30597 const aiAvailable = config.aiAssistant?.available === true;
30598 const isAiAssistantActive = () => aiAvailable && config.aiAssistant?.assistantProviderConfigured === true && osSettings.getOsSettingsSnapshot().ai.enabled !== false;
30599 let unregisterAiPalette = null;
30600 const syncAiAssistant = () => {
30601 const active2 = isAiAssistantActive();
30602 document.body.classList.toggle("desktop-mode-ai-enabled", active2);
30603 if (active2 && !unregisterAiPalette) {
30604 unregisterAiPalette = registerPalette({
30605 id: "desktop-mode-ai-assistant",
30606 label: "AI Assistant",
30607 open: () => aiAssistant.open(),
30608 close: () => aiAssistant.close(),
30609 isOpen: () => aiAssistant.isOpen
30610 });
30611 } else if (!active2 && unregisterAiPalette) {
30612 aiAssistant.close();
30613 unregisterAiPalette();
30614 unregisterAiPalette = null;
30615 }
30616 };
30617 syncAiAssistant();
30618 osSettings.subscribeOsSettings(() => syncAiAssistant());
30619 document.addEventListener(
30620 "desktop-mode-ai-status-changed",
30621 () => syncAiAssistant()
30622 );
30623 installPaletteShortcut();
30624 installWindowSwitcherShortcut(manager);
30625 installDesktopArrowShortcuts(manager);
30626 scheduleIdleBoot(() => {
30627 new IframeCommandBridge({
30628 manager,
30629 adminUrl: config.adminUrl
30630 }).install();
30631 new ShellCommandHarvester({
30632 manager,
30633 adminUrl: config.adminUrl
30634 }).install();
30635 });
30636 document.addEventListener("desktop-mode-open-ai", () => {
30637 if (!isAiAssistantActive()) {
30638 return;
30639 }
30640 openPaletteOnly("desktop-mode-ai-assistant");
30641 });
30642 const bottomDockEl = document.getElementById("desktop-mode-dock");
30643 const shellEl = document.getElementById("desktop-mode-shell");
30644 const shellBody = shellEl?.querySelector(
30645 ".desktop-mode-shell__body"
30646 );
30647 let layoutDispatcher = null;
30648 const nativeWindows = createNativeWindowSync({
30649 manager,
30650 appendSystemTile: (item) => layoutDispatcher?.appendSystemTile(item),
30651 removeSystemTile: (id) => layoutDispatcher?.removeSystemTile(id)
30652 });
30653 const syncNativeWindows = nativeWindows.sync;
30654 bindNativeUrlRemap({
30655 getSnapshot: () => osSettings.getOsSettingsSnapshot(),
30656 openById: (id) => nativeWindows.openById(id),
30657 adminUrl: config.adminUrl
30658 });
30659 const findDockEntryForUrl2 = (url) => {
30660 const targetSlug = deriveWindowId(url, config.adminUrl);
30661 const items = layoutDispatcher ? layoutDispatcher.getMenuItems() : config.dockItems ?? [];
30662 for (const item of items) {
30663 if (deriveWindowId(item.url, config.adminUrl) === targetSlug) {
30664 return {
30665 title: item.title,
30666 icon: item.icon,
30667 url: item.url,
30668 submenu: item.submenu,
30669 multi: item.multi
30670 };
30671 }
30672 for (const sub of item.submenu ?? []) {
30673 if (deriveWindowId(sub.url, config.adminUrl) === targetSlug) {
30674 return {
30675 title: sub.title,
30676 // Sub-menu entries inherit the parent tile's
30677 // icon — that's the dock's own convention and
30678 // avoids painting a generic glyph on a window
30679 // the user knows by its parent's identity.
30680 icon: item.icon,
30681 // `url` holds the PARENT tile's landing page, so
30682 // the new window's synthetic "back to parent"
30683 // tab links to the dock URL (themes.php) rather
30684 // than to the sub-page itself.
30685 url: item.url,
30686 multi: item.multi
30687 };
30688 }
30689 }
30690 }
30691 return null;
30692 };
30693 bindAdminLinkDispatch({
30694 adminUrl: config.adminUrl,
30695 deriveSlug: (url) => deriveWindowId(url, config.adminUrl),
30696 openWindow: (windowConfig) => {
30697 void manager.open(windowConfig);
30698 },
30699 findDockEntry: findDockEntryForUrl2
30700 });
30701 registerNativeUrlRemap({
30702 id: "desktop-mode-posts",
30703 nativeWindowId: "desktop-mode-posts",
30704 matches: (_url, parsed) => {
30705 if (!parsed.pathname.endsWith("/edit.php")) {
30706 return false;
30707 }
30708 const postType = parsed.searchParams.get("post_type");
30709 return !postType || postType === "post";
30710 },
30711 enabled: (snapshot) => snapshot.nativePostsEnabled === true
30712 });
30713 registerNativeUrlRemap({
30714 id: "desktop-mode-pages",
30715 nativeWindowId: "desktop-mode-pages",
30716 matches: (_url, parsed) => {
30717 if (!parsed.pathname.endsWith("/edit.php")) {
30718 return false;
30719 }
30720 return parsed.searchParams.get("post_type") === "page";
30721 },
30722 enabled: (snapshot) => snapshot.nativePagesEnabled === true
30723 });
30724 registerNativeUrlRemap({
30725 id: "desktop-mode-users",
30726 nativeWindowId: "desktop-mode-users",
30727 matches: (_url, parsed) => parsed.pathname.endsWith("/users.php"),
30728 enabled: (snapshot) => snapshot.nativeUsersEnabled === true
30729 });
30730 registerNativeUrlRemap({
30731 id: "desktop-mode-user-edit",
30732 nativeWindowId: "desktop-mode-user-edit",
30733 matches: (_url, parsed) => {
30734 const path = parsed.pathname;
30735 if (path.endsWith("/profile.php")) {
30736 return true;
30737 }
30738 if (path.endsWith("/user-edit.php")) {
30739 return parsed.searchParams.has("user_id");
30740 }
30741 return false;
30742 },
30743 enabled: (snapshot) => snapshot.nativeUsersEnabled === true,
30744 onMatch: (_url, parsed) => {
30745 const userId = parseInt(
30746 parsed.searchParams.get("user_id") ?? "0",
30747 10
30748 );
30749 if (userId > 0) {
30750 setUserEditTarget(userId);
30751 }
30752 }
30753 });
30754 registerNativeUrlRemap({
30755 id: "desktop-mode-comments",
30756 nativeWindowId: "desktop-mode-comments",
30757 matches: (_url, parsed) => parsed.pathname.endsWith("/edit-comments.php"),
30758 enabled: (snapshot) => snapshot.nativeCommentsEnabled === true
30759 });
30760 registerNativeUrlRemap({
30761 id: "desktop-mode-plugins",
30762 nativeWindowId: "desktop-mode-plugins",
30763 matches: (_url, parsed) => {
30764 const path = parsed.pathname;
30765 return path.endsWith("/plugins.php") || path.endsWith("/plugin-install.php");
30766 },
30767 enabled: (snapshot) => snapshot.nativePluginsEnabled === true,
30768 onMatch: (_url, parsed) => {
30769 const tab = parsed.pathname.endsWith("/plugin-install.php") ? "browse" : "installed";
30770 void Promise.resolve().then(() => tabTarget).then((m) => {
30771 m.setPluginsWindowTab(tab);
30772 });
30773 }
30774 });
30775 if (bottomDockEl && shellEl && shellBody && config.dockItems) {
30776 desktopArea.classList.add("desktop-mode-area--with-dock");
30777 const initialLayout = osSettings.getOsSettingsSnapshot().desktopLayout;
30778 const renderIcons2 = (icons) => {
30779 renderDesktopIcons(desktopArea, icons, {
30780 openWindow: nativeWindows.openById,
30781 manager,
30782 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
30783 });
30784 };
30785 layoutDispatcher = createLayoutDispatcher(
30786 {
30787 shellRoot: shellEl,
30788 shellBody,
30789 bottomDockEl,
30790 desktopArea,
30791 windowManager: manager,
30792 adminUrl: config.adminUrl,
30793 renderIcons: renderIcons2,
30794 getSettings: () => {
30795 const snap = osSettings.getOsSettingsSnapshot();
30796 return {
30797 itemVisibility: snap.itemVisibility,
30798 dockOrder: snap.dockOrder
30799 };
30800 }
30801 },
30802 initialLayout,
30803 config.dockItems,
30804 config.desktopIcons
30805 );
30806 layoutDispatcher.appendSystemTile(
30807 {
30808 id: OS_SETTINGS_WINDOW_ID,
30809 title: "OS Settings",
30810 icon: "dashicons-desktop",
30811 // "Open" for the dock dot means "open on the currently
30812 // active desktop." OS Settings on another desktop
30813 // shouldn't paint the dot on the active view.
30814 isOpen: () => {
30815 const win = manager.getById(OS_SETTINGS_WINDOW_ID);
30816 if (!win) {
30817 return false;
30818 }
30819 return (win.config.desktopId || manager.getActiveDesktopId()) === manager.getActiveDesktopId();
30820 },
30821 onOpen: openOsSettings
30822 },
30823 "core"
30824 );
30825 if (!isStandaloneDisplay()) {
30826 layoutDispatcher.appendSystemTile(
30827 getInstallTileDef(
30828 config.pwa?.appName || "WordPress",
30829 showToast
30830 ),
30831 "core"
30832 );
30833 }
30834 window.matchMedia("(display-mode: standalone)").addEventListener("change", (e) => {
30835 if (e.matches) {
30836 layoutDispatcher?.removeSystemTile(
30837 "desktop-mode-pwa-install"
30838 );
30839 }
30840 });
30841 void isLikelyInstalled().then((installed2) => {
30842 if (installed2) {
30843 layoutDispatcher?.removeSystemTile(
30844 "desktop-mode-pwa-install"
30845 );
30846 }
30847 });
30848 }
30849 function openOsSettings(opts = {}) {
30850 if (opts.tabId === "extended") {
30851 opts = { ...opts, tabId: "features" };
30852 }
30853 if (opts.tabId) {
30854 osSettings.activeTabId = opts.tabId;
30855 }
30856 void manager.open({
30857 id: OS_SETTINGS_WINDOW_ID,
30858 baseId: OS_SETTINGS_WINDOW_ID,
30859 url: "#os-settings",
30860 title: "OS Settings",
30861 icon: "dashicons-desktop",
30862 native: true,
30863 render: (body) => osSettings.renderPanel(body),
30864 width: 820,
30865 height: 720,
30866 minWidth: 560,
30867 minHeight: 480
30868 });
30869 if (opts.tabId) {
30870 osSettings.focusTab(opts.tabId);
30871 }
30872 }
30873 function openBugReport() {
30874 void manager.open({
30875 id: BUG_REPORT_WINDOW_ID,
30876 baseId: BUG_REPORT_WINDOW_ID,
30877 url: `#${BUG_REPORT_WINDOW_ID}`,
30878 title: "Report a bug",
30879 icon: "dashicons-buddicons-replies",
30880 native: true,
30881 render: (body) => renderBugReport(body),
30882 width: 560,
30883 height: 620,
30884 minWidth: 420,
30885 minHeight: 480
30886 });
30887 }
30888 document.addEventListener("desktop-mode-open-bug-report", () => {
30889 openBugReport();
30890 });
30891 if (layoutDispatcher) {
30892 layoutDispatcher.appendSystemTile(
30893 {
30894 id: BUG_REPORT_WINDOW_ID,
30895 title: "Report a bug",
30896 icon: "dashicons-buddicons-replies",
30897 isOpen: () => {
30898 const win = manager.getById(BUG_REPORT_WINDOW_ID);
30899 if (!win) {
30900 return false;
30901 }
30902 return (win.config.desktopId || manager.getActiveDesktopId()) === manager.getActiveDesktopId();
30903 },
30904 onOpen: openBugReport
30905 },
30906 "core"
30907 );
30908 layoutDispatcher.appendSystemTile(
30909 getExitDesktopModeTileDef(),
30910 "core"
30911 );
30912 }
30913 const dock = layoutDispatcher?.getPrimary() ?? null;
30914 void syncNativeWindows(
30915 Array.isArray(config.nativeWindows) ? config.nativeWindows : []
30916 );
30917 const hasSession = hasRestorableSession(config.session);
30918 const sessionRestore = hasSession ? restoreSession(manager, config, desktopArea).catch((err) => {
30919 if (typeof console !== "undefined") {
30920 console.error("[desktop-mode] session restore failed:", err);
30921 }
30922 }) : Promise.resolve();
30923 const defaultEnabled = config.defaultWindow?.enabled !== false;
30924 const defaultUrlEarly = config.defaultWindow?.url ?? "";
30925 const isNativeDefault = typeof defaultUrlEarly === "string" && defaultUrlEarly.startsWith("native:");
30926 if (shouldAutoOpenCurrentPage({
30927 fromPortal: config.fromPortal,
30928 fromPortalIntent: config.fromPortalIntent,
30929 hasSession,
30930 defaultEnabled,
30931 isNativeDefault
30932 })) {
30933 void sessionRestore.then(
30934 () => openCurrentPage(manager, config).catch((err) => {
30935 if (typeof console !== "undefined") {
30936 console.error("[desktop-mode] openCurrentPage failed:", err);
30937 }
30938 })
30939 );
30940 }
30941 const saveSession = createSessionSaver(manager, config);
30942 wireSessionEvents(saveSession);
30943 const setDefaultWindow = async (url) => {
30944 try {
30945 const response = await trackedFetch(
30946 manager,
30947 config.defaultWindowUrl,
30948 {
30949 method: "POST",
30950 credentials: "same-origin",
30951 headers: {
30952 "Content-Type": "application/json",
30953 "X-WP-Nonce": config.restNonce
30954 },
30955 body: JSON.stringify({ url })
30956 },
30957 { source: "desktop-mode/default-window" }
30958 );
30959 if (!response.ok) {
30960 throw new Error(`HTTP ${response.status}`);
30961 }
30962 const data = await response.json();
30963 config.defaultWindow = data;
30964 document.dispatchEvent(
30965 new CustomEvent("desktop-mode-default-window-changed", {
30966 detail: data
30967 })
30968 );
30969 } catch (err) {
30970 doAction(HOOKS.SHELL_ERROR, { scope: "default-window-save", error: err });
30971 if (typeof console !== "undefined") {
30972 console.error(
30973 "[desktop-mode] Failed to save default window:",
30974 err
30975 );
30976 }
30977 }
30978 };
30979 manager.onToggleStartupRequested = (win) => {
30980 const currentPref = config.defaultWindow;
30981 const isNative = !!win.config.native;
30982 const winValue = isNative ? `native:${win.id}` : win.getCurrentUrl();
30983 const matchesCurrent = isNative ? currentPref?.url === winValue : urlMatchKey(currentPref?.url ?? "") === urlMatchKey(winValue);
30984 const alreadyDefault = !!currentPref?.enabled && matchesCurrent;
30985 void setDefaultWindow(alreadyDefault ? null : winValue);
30986 };
30987 if (config.defaultWindow?.enabled && config.fromPortal && !config.fromPortalIntent && !hasSession && isNativeDefault) {
30988 const nativeId = defaultUrlEarly.slice("native:".length);
30989 queueMicrotask(() => {
30990 if (nativeId === OS_SETTINGS_WINDOW_ID) {
30991 openOsSettings();
30992 return;
30993 }
30994 void nativeWindows.openById(nativeId);
30995 });
30996 }
30997 const placeSystemTile = (item) => {
30998 layoutDispatcher?.appendSystemTile(item);
30999 };
31000 const syncServerWidgets = createWidgetRegistrySync({
31001 layer: widgetLayer
31002 });
31003 void syncServerWidgets(
31004 Array.isArray(config.serverWidgets) ? config.serverWidgets : []
31005 );
31006 const syncServerWallpapers = createWallpaperRegistrySync({
31007 osSettings
31008 });
31009 void syncServerWallpapers(
31010 Array.isArray(config.serverWallpapers) ? config.serverWallpapers : []
31011 );
31012 const syncServerCommands = createCommandRegistrySync();
31013 void syncServerCommands(
31014 Array.isArray(config.serverCommandScripts) ? config.serverCommandScripts : [],
31015 Array.isArray(config.serverCommands) ? config.serverCommands : []
31016 );
31017 const syncServerSettingsTabs = createSettingsTabRegistrySync();
31018 void syncServerSettingsTabs(
31019 Array.isArray(config.serverSettingsTabScripts) ? config.serverSettingsTabScripts : [],
31020 Array.isArray(config.serverSettingsTabs) ? config.serverSettingsTabs : []
31021 );
31022 const syncServerTitleBarButtons = createTitleBarButtonRegistrySync();
31023 void syncServerTitleBarButtons(
31024 Array.isArray(config.serverTitleBarButtonScripts) ? config.serverTitleBarButtonScripts : []
31025 );
31026 const syncServerUnfocusEffects = createUnfocusEffectRegistrySync();
31027 void syncServerUnfocusEffects(
31028 Array.isArray(config.serverUnfocusEffectScripts) ? config.serverUnfocusEffectScripts : []
31029 );
31030 const syncServerWindowLinkRenderers = createWindowLinkRendererRegistrySync();
31031 void syncServerWindowLinkRenderers(
31032 Array.isArray(config.serverWindowLinkRendererScripts) ? config.serverWindowLinkRendererScripts : []
31033 );
31034 startUnfocusEngine({ manager, osSettings });
31035 startWindowLinksEngine({ manager });
31036 startWindowLinkRenderHost({ manager, osSettings });
31037 const syncServerDockRailRenderers = createDockRailRendererSync();
31038 void syncServerDockRailRenderers(
31039 Array.isArray(config.serverDockRailRendererScripts) ? config.serverDockRailRendererScripts : []
31040 );
31041 const syncServerWindowThemes = createWindowThemeRegistrySync();
31042 void syncServerWindowThemes(
31043 Array.isArray(config.serverWindowThemeScripts) ? config.serverWindowThemeScripts : [],
31044 Array.isArray(config.serverWindowThemes) ? config.serverWindowThemes : []
31045 );
31046 registerBuiltInControls();
31047 const syncServerWindowControls = createWindowControlRegistrySync();
31048 void syncServerWindowControls(
31049 Array.isArray(config.serverWindowControlScripts) ? config.serverWindowControlScripts : [],
31050 Array.isArray(config.serverWindowControls) ? config.serverWindowControls : []
31051 );
31052 const syncServerWindowSlots = createWindowSlotRegistrySync();
31053 void syncServerWindowSlots(
31054 Array.isArray(config.serverWindowSlotScripts) ? config.serverWindowSlotScripts : [],
31055 Array.isArray(config.serverWindowSlots) ? config.serverWindowSlots : []
31056 );
31057 applyServerWindowNotices(
31058 Array.isArray(config.serverWindowNotices) ? config.serverWindowNotices : []
31059 );
31060 const syncServerWindowChromes = createWindowChromeRegistrySync();
31061 void syncServerWindowChromes(
31062 Array.isArray(config.serverWindowChromeScripts) ? config.serverWindowChromeScripts : [],
31063 Array.isArray(config.serverWindowChromes) ? config.serverWindowChromes : []
31064 );
31065 const connectionBridge = createConnectionBridge(manager);
31066 attachBroadcastBus(manager);
31067 scheduleIdleBoot(() => installBroadcastReceiver());
31068 installWindowLoadingTransitions();
31069 addAction(
31070 "desktop-mode.shell.toast",
31071 "desktop-mode/shell-toast",
31072 (payload) => {
31073 if (!payload || typeof payload.message !== "string") {
31074 return;
31075 }
31076 showToast({
31077 message: payload.message,
31078 action: payload.action,
31079 duration: payload.duration
31080 });
31081 }
31082 );
31083 const cfgWithBin = config;
31084 const cfgCountRaw = cfgWithBin.recycleBinCount;
31085 startRecycleBinBadge(
31086 Number(cfgCountRaw) || 0,
31087 typeof cfgWithBin.recycleBinCountUrl === "string" ? cfgWithBin.recycleBinCountUrl : ""
31088 );
31089 registerBuiltInPeekRenderers({
31090 getRecycleBinCount: _currentRecycleBinBadge
31091 });
31092 window.__desktopModeConnectionBridge = connectionBridge;
31093 addAction(HOOKS.WINDOW_CLOSED, "desktop-mode/connection-cleanup", (e) => {
31094 if (e?.windowId) {
31095 connectionBridge.onWindowClosed(e.windowId);
31096 }
31097 });
31098 addAction(HOOKS.IFRAME_READY, "desktop-mode/connection-rearm", (e) => {
31099 if (e?.windowId) {
31100 connectionBridge.onIframeReady(e.windowId);
31101 }
31102 });
31103 const registerWindow = createRegisterWindow(manager);
31104 const renderIcons = (icons) => {
31105 if (layoutDispatcher) {
31106 layoutDispatcher.applyDesktopIcons(icons);
31107 return;
31108 }
31109 renderDesktopIcons(desktopArea, icons, {
31110 openWindow: nativeWindows.openById,
31111 manager,
31112 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
31113 });
31114 };
31115 const refreshMenu = bindMenuRefresh({
31116 layoutDispatcher,
31117 desktopArea,
31118 config,
31119 syncNativeWindows,
31120 syncServerWidgets,
31121 syncServerWallpapers,
31122 syncServerCommands,
31123 syncServerSettingsTabs,
31124 syncServerTitleBarButtons,
31125 syncServerUnfocusEffects,
31126 syncServerWindowLinkRenderers,
31127 syncServerDockRailRenderers,
31128 renderIcons,
31129 syncShortcuts: () => {
31130 const snapshot = osSettings.getOsSettingsSnapshot();
31131 syncShortcutsWithVisibility(
31132 snapshot.itemVisibility,
31133 snapshot.dockPromotedPositions,
31134 snapshot.desktopLayout
31135 );
31136 }
31137 });
31138 osSettings.subscribeOsSettings((snapshot) => {
31139 if (!layoutDispatcher) {
31140 return;
31141 }
31142 const prevLayout = layoutDispatcher.getLayout();
31143 layoutDispatcher.setLayout(snapshot.desktopLayout);
31144 desktopApi.dock = layoutDispatcher.getPrimary();
31145 desktopApi.sideDock = layoutDispatcher.getSide();
31146 desktopApi.desktopLayout = snapshot.desktopLayout;
31147 if (prevLayout === snapshot.desktopLayout) {
31148 layoutDispatcher.refresh();
31149 }
31150 syncShortcutsWithVisibility(
31151 snapshot.itemVisibility,
31152 snapshot.dockPromotedPositions,
31153 snapshot.desktopLayout
31154 );
31155 setCurrentLayout(snapshot.desktopLayout);
31156 });
31157 installShortcutsSync(
31158 () => osSettings.getOsSettingsSnapshot().itemVisibility,
31159 () => osSettings.getOsSettingsSnapshot().dockPromotedPositions,
31160 () => osSettings.getOsSettingsSnapshot().desktopLayout
31161 );
31162 setCurrentLayout(osSettings.getOsSettingsSnapshot().desktopLayout);
31163 const desktopApi = buildPublicApi({
31164 manager,
31165 dock,
31166 layoutDispatcher,
31167 osSettings,
31168 iconsApi,
31169 filesApi,
31170 saveSession,
31171 widgetLayer,
31172 registerWindow,
31173 openWindowById: nativeWindows.openById,
31174 openNewWindowById: nativeWindows.openNewById,
31175 placeSystemTile,
31176 setDefaultWindow,
31177 refreshMenu,
31178 openOsSettings,
31179 aiAssistant,
31180 dragBridge,
31181 dragManager,
31182 connect: connectionBridge.connect,
31183 getConnection: connectionBridge.getConnection,
31184 config
31185 });
31186 installPublicApi(desktopApi);
31187 scheduleIdleBoot(() => installRecycleBinDropTargets(dragManager));
31188 bootHeartbeatBus();
31189 scheduleIdleBoot(() => bootNonceRefresh());
31190 bootStickyNotes({
31191 host: desktopArea,
31192 config,
31193 // Only boot when the Gutenberg Guidelines experiment is live
31194 // server-side; otherwise the layer's REST probes would 404. The
31195 // flag is `undefined` on shells older than the one that added it
31196 // → the layer treats that as available (boot and swallow).
31197 available: config.stickyNotes?.available,
31198 getActiveDesktopId: () => manager.getActiveDesktopId(),
31199 openArtifact: (url, title) => {
31200 const id = deriveWindowId(url, config.adminUrl);
31201 void manager.open({
31202 id,
31203 baseId: id,
31204 url,
31205 title,
31206 icon: "dashicons-edit-page"
31207 });
31208 },
31209 onError: (message) => {
31210 showToast({ message });
31211 }
31212 });
31213 installOpenDeps({
31214 openUrl: ({ id, url, title, icon }) => {
31215 if (tryNativeUrlRemap(url)) {
31216 return true;
31217 }
31218 void manager.open({ id, baseId: id, url, title, icon });
31219 return true;
31220 },
31221 openNativeWindow: (id) => nativeWindows.openById(id),
31222 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
31223 });
31224 setUserAssociations(
31225 config.userFileAssociations ?? {}
31226 );
31227 void maybeShowUpdate({
31228 update: config.coreUpdate,
31229 openUrl: ({ url, title }) => {
31230 if (tryNativeUrlRemap(url)) {
31231 return;
31232 }
31233 void manager.open({
31234 id: "update-core",
31235 baseId: "update-core",
31236 url,
31237 title,
31238 icon: "dashicons-update"
31239 });
31240 }
31241 });
31242 if (typeof config.filesUrl === "string" && config.filesUrl) {
31243 installRestDeps({
31244 baseUrl: config.filesUrl,
31245 nonce: config.restNonce
31246 });
31247 const rootHost = document.getElementById("desktop-mode-area");
31248 if (rootHost) {
31249 const layerHandle = mountFilesLayer(rootHost, 0);
31250 const reveal = () => {
31251 if (!desktopArea.classList.contains("desktop-mode-area--booting")) {
31252 return;
31253 }
31254 requestAnimationFrame(() => {
31255 desktopArea.classList.remove("desktop-mode-area--booting");
31256 });
31257 };
31258 const safetyTimer = setTimeout(reveal, 2e3);
31259 void layerHandle.hydrated.then(() => {
31260 clearTimeout(safetyTimer);
31261 reveal();
31262 });
31263 }
31264 }
31265 scheduleIdleBoot(() => startFilesHeartbeat());
31266 scheduleIdleBoot(() => startFilesRestoreSync());
31267 scheduleIdleBoot(() => bootPresenceProbe());
31268 doAction(HOOKS.COMPONENTS_REGISTERED, { tags: [...WPD_COMPONENT_TAGS] });
31269 registerBuiltInCommands();
31270 bootstrapPwa(config, showToast);
31271 const overlayPreload = () => {
31272 preloadShellOverlays(config.shellOverlaysBundleUrl ?? "");
31273 preloadWindowSystem(config.windowSystemBundleUrl ?? "");
31274 };
31275 if (typeof window.requestIdleCallback === "function") {
31276 window.requestIdleCallback(overlayPreload, { timeout: 1500 });
31277 } else {
31278 window.setTimeout(overlayPreload, 0);
31279 }
31280 doAction(HOOKS.INIT, { config });
31281 _earlyReady = true;
31282 const queued = _earlyReadyQueue.splice(0);
31283 for (const cb of queued) {
31284 try {
31285 cb();
31286 } catch (err) {
31287 doAction(HOOKS.SHELL_ERROR, {
31288 scope: "when-ready-cb",
31289 error: err
31290 });
31291 if (typeof console !== "undefined") {
31292 console.error("[desktop-mode] whenReady cb threw:", err);
31293 }
31294 }
31295 }
31296 osSettings.apply();
31297 widgetLayer?.hydrate();
31298 window.addEventListener("pagehide", () => {
31299 wallpaperLayer?.teardownActive();
31300 widgetLayer?.disposeAll();
31301 });
31302 bindShellLifecycle();
31303 bindTopWindowLinkInterceptor(manager, config);
31304 const relayoutRoot = (transform, persist2 = true) => {
31305 const root = filesApi.store.getState().placementsByFolder.get(0) ?? [];
31306 const ordered = transform(root);
31307 const rowsPerCol = Math.max(
31308 1,
31309 Math.floor((desktopArea.clientHeight - 16) / 110)
31310 );
31311 const occupied = /* @__PURE__ */ new Set();
31312 let i = 0;
31313 for (const p of ordered) {
31314 const cell = snapToEmptyCell(
31315 16 + Math.floor(i / rowsPerCol) * 96,
31316 16 + i % rowsPerCol * 110,
31317 occupied,
31318 desktopArea
31319 );
31320 occupied.add(`${cell.col},${cell.row}`);
31321 i++;
31322 if (p.x === cell.x && p.y === cell.y) {
31323 continue;
31324 }
31325 filesApi.store.upsertPlacement({
31326 ...p,
31327 x: cell.x,
31328 y: cell.y,
31329 sortOrder: i
31330 });
31331 if (!persist2) {
31332 continue;
31333 }
31334 void updatePlacement(p.id, {
31335 x: cell.x,
31336 y: cell.y,
31337 sortOrder: i
31338 }).catch((err) => {
31339 console.error("[desktop-mode] relayout persist failed", err);
31340 });
31341 }
31342 };
31343 const rootSortTransform = (mode) => (arr) => {
31344 const sorted = arr.slice();
31345 switch (mode) {
31346 case "name-asc":
31347 sorted.sort(
31348 (a, b) => a.file.title.localeCompare(b.file.title)
31349 );
31350 break;
31351 case "name-desc":
31352 sorted.sort(
31353 (a, b) => b.file.title.localeCompare(a.file.title)
31354 );
31355 break;
31356 case "date-asc":
31357 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
31358 break;
31359 case "date-desc":
31360 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
31361 break;
31362 }
31363 return sorted;
31364 };
31365 const ROOT_SORT_MODE_KEY = "desktop-mode:root-sort-mode";
31366 const isRootSortMode = (v) => v === "name-asc" || v === "name-desc" || v === "date-asc" || v === "date-desc";
31367 let rootSortMode = (() => {
31368 try {
31369 const raw = window.localStorage.getItem(ROOT_SORT_MODE_KEY);
31370 return isRootSortMode(raw) ? raw : null;
31371 } catch {
31372 return null;
31373 }
31374 })();
31375 const setRootSortMode = (mode) => {
31376 rootSortMode = mode;
31377 try {
31378 if (mode) {
31379 window.localStorage.setItem(ROOT_SORT_MODE_KEY, mode);
31380 } else {
31381 window.localStorage.removeItem(ROOT_SORT_MODE_KEY);
31382 }
31383 } catch {
31384 }
31385 };
31386 addAction(
31387 "desktop-mode.files.tile-manually-placed",
31388 "desktop-mode/root-sort-clear",
31389 (payload) => {
31390 const folderId = payload?.folderId;
31391 if (folderId === 0) {
31392 setRootSortMode(null);
31393 }
31394 }
31395 );
31396 if (typeof ResizeObserver !== "undefined") {
31397 let lastW = desktopArea.clientWidth;
31398 let lastH = desktopArea.clientHeight;
31399 const ro = new ResizeObserver(() => {
31400 if (!rootSortMode) {
31401 return;
31402 }
31403 const w = desktopArea.clientWidth;
31404 const h = desktopArea.clientHeight;
31405 if (w === lastW && h === lastH) {
31406 return;
31407 }
31408 lastW = w;
31409 lastH = h;
31410 relayoutRoot(rootSortTransform(rootSortMode), false);
31411 });
31412 ro.observe(desktopArea);
31413 }
31414 let pointerdownOnWallpaper = false;
31415 desktopArea.addEventListener("pointerdown", (e) => {
31416 if (!e.isPrimary) {
31417 return;
31418 }
31419 pointerdownOnWallpaper = e.target === desktopArea;
31420 });
31421 desktopArea.addEventListener("click", (e) => {
31422 if (!osSettings.state.showDesktopOnWallpaperClick) {
31423 return;
31424 }
31425 if (e.target !== desktopArea) {
31426 return;
31427 }
31428 if (!pointerdownOnWallpaper) {
31429 return;
31430 }
31431 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
31432 return;
31433 }
31434 if (isWallpaperMenuOpen()) {
31435 return;
31436 }
31437 if (dragManager.recentlyEndedDrag()) {
31438 return;
31439 }
31440 manager.toggleShowDesktop();
31441 });
31442 desktopArea.addEventListener("contextmenu", (e) => {
31443 if (e.target !== desktopArea) {
31444 return;
31445 }
31446 e.preventDefault();
31447 const clientX = e.clientX;
31448 const clientY = e.clientY;
31449 (() => {
31450 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
31451 return;
31452 }
31453 if (isWallpaperMenuOpen()) {
31454 closeWallpaperMenu();
31455 return;
31456 }
31457 const dropClient = { x: clientX, y: clientY };
31458 const cellAtClick = () => {
31459 const rect = desktopArea.getBoundingClientRect();
31460 const rawX = Math.max(0, dropClient.x - rect.left);
31461 const rawY = Math.max(0, dropClient.y - rect.top);
31462 const occupied = buildOccupiedSet(
31463 filesApi.store.getState().placementsByFolder.get(0) ?? []
31464 );
31465 return snapToEmptyCell(rawX, rawY, occupied, desktopArea);
31466 };
31467 const createUrlPlacement = (dialogTitle, description) => {
31468 openUrlDialog({
31469 title: dialogTitle,
31470 description,
31471 nameLabel: "Name",
31472 urlLabel: "URL",
31473 submitLabel: "Create",
31474 onSubmit: async ({ name, url }) => {
31475 const cell = cellAtClick();
31476 const placement = await createPlacement({
31477 type: "link",
31478 ref: url,
31479 parentId: 0,
31480 x: cell.x,
31481 y: cell.y,
31482 meta: name ? { name } : void 0
31483 });
31484 filesApi.store.upsertPlacement(placement);
31485 }
31486 });
31487 };
31488 const items = buildMenuItems({
31489 createFolder: () => {
31490 openCreateFolderDialog({
31491 onSubmit: async (name) => {
31492 const folder = await createFolder({ name });
31493 const cell = cellAtClick();
31494 const placement = await createPlacement({
31495 type: "folder",
31496 ref: String(folder.id),
31497 parentId: 0,
31498 x: cell.x,
31499 y: cell.y
31500 });
31501 filesApi.store.upsertFolder(folder);
31502 filesApi.store.upsertPlacement(placement);
31503 }
31504 });
31505 },
31506 createUrl: () => createUrlPlacement(
31507 "New URL",
31508 "Opens the URL in a new browser tab."
31509 ),
31510 toggleShowDesktop: () => manager.toggleShowDesktop(),
31511 openOsSettings: () => openOsSettings(),
31512 sortIcons: (mode) => {
31513 setRootSortMode(mode);
31514 relayoutRoot(rootSortTransform(mode));
31515 },
31516 currentSortMode: rootSortMode,
31517 includeShowDesktop: !osSettings.state.showDesktopOnWallpaperClick,
31518 labels: {
31519 createFolder: "New folder",
31520 showDesktop: "Show desktop",
31521 osSettings: "OS Settings",
31522 sortHeading: "Sort by",
31523 sortNameAsc: "Name (A → Z)",
31524 sortNameDesc: "Name (Z → A)",
31525 sortDateAsc: "Date (oldest first)",
31526 sortDateDesc: "Date (newest first)",
31527 newUrl: "New URL"
31528 },
31529 serverItems: config.serverWallpaperMenuItems ?? []
31530 });
31531 openWallpaperMenu(
31532 document.body,
31533 { x: clientX, y: clientY },
31534 items
31535 );
31536 })();
31537 });
31538 void Promise.resolve().then(() => index).then((mod) => {
31539 mod.bootOsFileDrop({
31540 config: config.dropConfig,
31541 mediaUrl: config.mediaUrl,
31542 restNonce: config.restNonce
31543 });
31544 });
31545 document.dispatchEvent(
31546 new CustomEvent("desktop-mode-init", {
31547 detail: { config, restored: hasSession }
31548 })
31549 );
31550 }
31551 startMissingImportWarner();
31552 if (document.readyState === "loading") {
31553 document.addEventListener("DOMContentLoaded", init);
31554 } else {
31555 init();
31556 }
31557 const _initial = {
31558 tab: null,
31559 requestedAt: 0
31560 };
31561 let _store = null;
31562 function getStore() {
31563 if (_store) {
31564 return _store;
31565 }
31566 const w = window;
31567 const factory = w.wp?.desktop?.createSharedStore;
31568 if (typeof factory !== "function") {
31569 return null;
31570 }
31571 _store = factory(
31572 "desktop-mode/plugins-window/tab-target",
31573 () => ({ ..._initial })
31574 );
31575 return _store;
31576 }
31577 function setPluginsWindowTab(tab) {
31578 const store2 = getStore();
31579 if (store2) {
31580 store2.state.tab = tab;
31581 store2.state.requestedAt = Date.now();
31582 store2.notify();
31583 return;
31584 }
31585 const w = window;
31586 w._wpdPluginsWindowTab = { tab, requestedAt: Date.now() };
31587 }
31588 function consumePluginsWindowTab() {
31589 const store2 = getStore();
31590 if (store2) {
31591 const tab = store2.state.tab;
31592 if (tab !== null) {
31593 store2.state.tab = null;
31594 store2.state.requestedAt = 0;
31595 store2.notify();
31596 }
31597 return tab;
31598 }
31599 const w = window;
31600 const prev = w._wpdPluginsWindowTab;
31601 if (prev) {
31602 w._wpdPluginsWindowTab = { tab: null, requestedAt: 0 };
31603 return prev.tab;
31604 }
31605 return null;
31606 }
31607 function subscribePluginsWindowTab(cb) {
31608 const store2 = getStore();
31609 if (!store2) {
31610 return () => {
31611 };
31612 }
31613 return store2.subscribe((state2) => cb({ ...state2 }));
31614 }
31615 const tabTarget = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
31616 __proto__: null,
31617 consumePluginsWindowTab,
31618 setPluginsWindowTab,
31619 subscribePluginsWindowTab
31620 }, Symbol.toStringTag, { value: "Module" }));
31621 const FILE_DROP_HOOKS = {
31622 /**
31623 * Filter — fires once per drop, after the manager has parsed
31624 * the OS `DataTransfer` into `File[]` and BEFORE the mime /
31625 * size filter runs.
31626 *
31627 * Signature: `(files: File[], ctx: DropContext) => File[]`.
31628 * Return an empty array to abort the drop silently.
31629 */
31630 FILES_DETECTED: "desktop-mode.drop.files-detected",
31631 /**
31632 * Action — fires after the mime / size filter has rejected
31633 * one or more files. Payload: `{ rejections: DropRejection[],
31634 * context: DropContext }`. The shell toasts a default message;
31635 * subscribers can surface a custom UX (a side panel with the
31636 * list, an analytics call).
31637 */
31638 FILES_REJECTED: "desktop-mode.drop.files-rejected",
31639 /**
31640 * Filter — fires per file before the upload dialog renders.
31641 * Receives `DropFileEntry` (the underlying file + the
31642 * manager's default `fields`). Mutate `fields` (or return a
31643 * new object) to change what the user sees in the form.
31644 *
31645 * Signature: `(entry: DropFileEntry, ctx: DropContext)
31646 * => DropFileEntry`.
31647 */
31648 DIALOG_FIELDS: "desktop-mode.drop.dialog-fields",
31649 /**
31650 * Filter — last call before the manager `POST`s to
31651 * `wp/v2/media`. Receives `{ file: File, fields:
31652 * DropDialogFields, mime: string }`. Return `null` to cancel
31653 * the upload entirely (e.g. a plugin handled it via a
31654 * different endpoint).
31655 *
31656 * Signature: `(payload, ctx: DropContext) => payload | null`.
31657 */
31658 BEFORE_UPLOAD: "desktop-mode.drop.before-upload",
31659 /**
31660 * Action — fires once `BEFORE_UPLOAD` has cleared and the XHR
31661 * is `open()`ed, immediately before `send()`. Payload:
31662 * `{ file: File, fields: DropDialogFields, context: DropContext,
31663 * abort: () => void }`. The `abort` handle aborts the in-flight
31664 * request; the manager rejects with `UploadAbortedError` and
31665 * fires `UPLOAD_FAILED` with that error.
31666 *
31667 * Pair with `UPLOAD_PROGRESS` to drive a progress UI; pair with
31668 * `AFTER_UPLOAD` / `UPLOAD_FAILED` to know when the upload ends.
31669 *
31670 * @since 0.31.0
31671 */
31672 UPLOAD_STARTED: "desktop-mode.drop.upload-started",
31673 /**
31674 * Action — fires for every `XMLHttpRequestUpload.progress` event.
31675 * Payload: `{ file: File, fields: DropDialogFields, context:
31676 * DropContext, loaded: number, total: number, indeterminate:
31677 * boolean }`. `total` is `0` and `indeterminate` is `true` when
31678 * the request body length isn't known (rare for multipart, but
31679 * possible on transcoding proxies); subscribers should treat
31680 * that as an indeterminate state.
31681 *
31682 * A synthetic 100%-loaded event is dispatched once the `upload`
31683 * stream emits `load` so a HUD can show a definite "wrapping up"
31684 * state while the server finishes the response.
31685 *
31686 * @since 0.31.0
31687 */
31688 UPLOAD_PROGRESS: "desktop-mode.drop.upload-progress",
31689 /**
31690 * Action — fires after a successful upload. Payload:
31691 * `{ file: File, result: DropUploadResult, fields:
31692 * DropDialogFields, context: DropContext }`.
31693 *
31694 * The `file` field carries the same `File` reference that
31695 * `UPLOAD_STARTED` / `UPLOAD_PROGRESS` exposed (i.e. the
31696 * payload returned by the `BEFORE_UPLOAD` filter, in case a
31697 * plugin swapped the file). Subscribers tracking per-file
31698 * state — progress HUDs, sequence counters — should match on
31699 * this identity rather than the filename: two drops of
31700 * `photo.jpg` from different folders would otherwise route
31701 * each other's success event to the wrong row.
31702 *
31703 * @since 0.31.0 the `file` field was added; pre-0.31.0 code
31704 * that destructured `{ result, fields, context }` keeps working.
31705 */
31706 AFTER_UPLOAD: "desktop-mode.drop.after-upload",
31707 /**
31708 * Action — fires after an upload fails. Payload:
31709 * `{ file: File, error: Error, context: DropContext }`.
31710 * `error` is an `UploadAbortedError` when the failure came
31711 * from the caller invoking the `abort()` handle on
31712 * `UPLOAD_STARTED`.
31713 *
31714 * `file` carries the same identity as `UPLOAD_STARTED` /
31715 * `UPLOAD_PROGRESS` / `AFTER_UPLOAD` — the post-`BEFORE_UPLOAD`
31716 * `File`, in case a plugin swapped it. Match by reference, not
31717 * filename: a HUD that keys its row map on the started-File
31718 * needs the same key here, otherwise the row stays stuck in
31719 * "running" after a failure when a `BEFORE_UPLOAD` filter
31720 * replaced the file.
31721 */
31722 UPLOAD_FAILED: "desktop-mode.drop.upload-failed"
31723 };
31724 const IFRAME_PASSTHROUGH_SELECTORS = [
31725 ".components-drop-zone",
31726 "[data-drop-zone]",
31727 ".uploader-window",
31728 ".media-frame-content"
31729 ];
31730 function dragHasFiles(ev) {
31731 const types = ev.dataTransfer?.types;
31732 if (!types) {
31733 return false;
31734 }
31735 const list2 = types;
31736 if (typeof list2.includes === "function") {
31737 return list2.includes("Files");
31738 }
31739 if (typeof list2.contains === "function") {
31740 return list2.contains("Files");
31741 }
31742 for (let i = 0; i < list2.length; i++) {
31743 if (list2[i] === "Files") {
31744 return true;
31745 }
31746 }
31747 return false;
31748 }
31749 function resolveWindowIdFromSource(source) {
31750 if (!source) {
31751 return void 0;
31752 }
31753 const iframes = document.querySelectorAll("iframe");
31754 for (const f of Array.from(iframes)) {
31755 if (f.contentWindow === source) {
31756 const host = f.closest("[data-window-id]");
31757 return host?.getAttribute("data-window-id") || void 0;
31758 }
31759 }
31760 return void 0;
31761 }
31762 function mountOsFileDropManager(opts) {
31763 const host = window;
31764 if (host.__desktopModeOsFileDropMounted) {
31765 return host.__desktopModeOsFileDropMounted;
31766 }
31767 if (!opts.config.enabled) {
31768 return mountNoOp();
31769 }
31770 const overlayEl = ensureDropOverlay();
31771 let dragDepth = 0;
31772 let dragWatchdog = null;
31773 const resetOverlay = () => {
31774 dragDepth = 0;
31775 overlayEl.classList.remove("is-active");
31776 if (dragWatchdog !== null) {
31777 clearTimeout(dragWatchdog);
31778 dragWatchdog = null;
31779 }
31780 };
31781 const bumpWatchdog2 = () => {
31782 if (dragWatchdog !== null) {
31783 clearTimeout(dragWatchdog);
31784 }
31785 dragWatchdog = setTimeout(resetOverlay, 250);
31786 };
31787 const onDragEnter = (ev) => {
31788 if (!dragHasFiles(ev)) {
31789 return;
31790 }
31791 ev.preventDefault();
31792 dragDepth++;
31793 overlayEl.classList.add("is-active");
31794 bumpWatchdog2();
31795 };
31796 const onDragOver = (ev) => {
31797 if (!dragHasFiles(ev)) {
31798 return;
31799 }
31800 if (ev.defaultPrevented) {
31801 resetOverlay();
31802 return;
31803 }
31804 ev.preventDefault();
31805 if (ev.dataTransfer) {
31806 ev.dataTransfer.dropEffect = "copy";
31807 }
31808 bumpWatchdog2();
31809 };
31810 const onDragLeave = () => {
31811 dragDepth = Math.max(0, dragDepth - 1);
31812 if (dragDepth === 0) {
31813 overlayEl.classList.remove("is-active");
31814 }
31815 };
31816 const onDrop = (ev) => {
31817 if (!dragHasFiles(ev)) {
31818 return;
31819 }
31820 if (ev.defaultPrevented) {
31821 resetOverlay();
31822 return;
31823 }
31824 ev.preventDefault();
31825 resetOverlay();
31826 const files = ev.dataTransfer?.files ? Array.from(ev.dataTransfer.files) : [];
31827 if (files.length === 0) {
31828 return;
31829 }
31830 const ctx = classifyDropTarget(ev);
31831 void handleFiles(files, ctx, opts);
31832 };
31833 const onDragEnd2 = () => resetOverlay();
31834 const onVisibilityChange = () => {
31835 if (document.visibilityState === "hidden") {
31836 resetOverlay();
31837 }
31838 };
31839 const onIframeMessage = (ev) => {
31840 if (ev.origin !== window.location.origin) {
31841 return;
31842 }
31843 const data = ev.data;
31844 if (!data || data.type !== "desktop-mode-os-file-drop") {
31845 return;
31846 }
31847 if (!Array.isArray(data.files) || data.files.length === 0) {
31848 return;
31849 }
31850 const files = data.files.filter((f) => f instanceof File);
31851 if (files.length === 0) {
31852 return;
31853 }
31854 const windowId = resolveWindowIdFromSource(ev.source);
31855 if (!windowId) {
31856 return;
31857 }
31858 const ctx = {
31859 surface: "iframe",
31860 windowId,
31861 x: typeof data.x === "number" ? data.x : 0,
31862 y: typeof data.y === "number" ? data.y : 0
31863 };
31864 dragDepth = 0;
31865 overlayEl.classList.remove("is-active");
31866 void handleFiles(files, ctx, opts);
31867 };
31868 window.addEventListener("dragenter", onDragEnter);
31869 window.addEventListener("dragover", onDragOver);
31870 window.addEventListener("dragleave", onDragLeave);
31871 window.addEventListener("drop", onDrop);
31872 window.addEventListener("dragend", onDragEnd2);
31873 document.addEventListener("visibilitychange", onVisibilityChange);
31874 window.addEventListener("blur", onDragEnd2);
31875 window.addEventListener("message", onIframeMessage);
31876 const manager = {
31877 dispose: () => {
31878 window.removeEventListener("dragenter", onDragEnter);
31879 window.removeEventListener("dragover", onDragOver);
31880 window.removeEventListener("dragleave", onDragLeave);
31881 window.removeEventListener("drop", onDrop);
31882 window.removeEventListener("dragend", onDragEnd2);
31883 document.removeEventListener(
31884 "visibilitychange",
31885 onVisibilityChange
31886 );
31887 window.removeEventListener("blur", onDragEnd2);
31888 window.removeEventListener("message", onIframeMessage);
31889 overlayEl.remove();
31890 delete window.__desktopModeOsFileDropMounted;
31891 }
31892 };
31893 host.__desktopModeOsFileDropMounted = manager;
31894 return manager;
31895 }
31896 function ensureDropOverlay() {
31897 const existing = document.querySelector(".desktop-mode-os-drop-overlay");
31898 if (existing) {
31899 return existing;
31900 }
31901 const el = document.createElement("div");
31902 el.className = "desktop-mode-os-drop-overlay";
31903 el.setAttribute("aria-hidden", "true");
31904 el.style.cssText = [
31905 "position:fixed",
31906 "inset:0",
31907 "pointer-events:none",
31908 "z-index:200",
31909 "opacity:0",
31910 "transition:opacity 120ms ease",
31911 "background:radial-gradient(circle at center, rgba(34,113,177,0.18) 0%, rgba(34,113,177,0.06) 60%, transparent 100%)",
31912 "box-shadow:inset 0 0 0 3px rgba(34,113,177,0.55)"
31913 ].join(";");
31914 const label = document.createElement("div");
31915 label.style.cssText = [
31916 "position:absolute",
31917 "top:50%",
31918 "left:50%",
31919 "transform:translate(-50%,-50%)",
31920 "padding:14px 22px",
31921 "border-radius:12px",
31922 "background:rgba(20,20,24,0.78)",
31923 "color:#fff",
31924 "font:600 14px/1.2 -apple-system,BlinkMacSystemFont,sans-serif",
31925 "letter-spacing:0.02em"
31926 ].join(";");
31927 label.textContent = "Drop to upload";
31928 el.appendChild(label);
31929 document.body.appendChild(el);
31930 const style = document.createElement("style");
31931 style.textContent = ".desktop-mode-os-drop-overlay.is-active{opacity:1!important;}";
31932 document.head.appendChild(style);
31933 return el;
31934 }
31935 function mountNoOp() {
31936 const cancel = (ev) => {
31937 if (!dragHasFiles(ev)) {
31938 return;
31939 }
31940 const target2 = ev.target;
31941 if (target2?.closest && IFRAME_PASSTHROUGH_SELECTORS.some((s) => target2.closest(s))) {
31942 return;
31943 }
31944 ev.preventDefault();
31945 };
31946 window.addEventListener("dragover", cancel);
31947 window.addEventListener("drop", cancel);
31948 const host = window;
31949 const manager = {
31950 dispose: () => {
31951 window.removeEventListener("dragover", cancel);
31952 window.removeEventListener("drop", cancel);
31953 delete host.__desktopModeOsFileDropMounted;
31954 }
31955 };
31956 host.__desktopModeOsFileDropMounted = manager;
31957 return manager;
31958 }
31959 function classifyDropTarget(ev) {
31960 const x = ev.clientX;
31961 const y = ev.clientY;
31962 let node = ev.target;
31963 while (node && node !== document.body) {
31964 if (node.tagName === "IFRAME") {
31965 const id = node.closest(
31966 "[data-window-id]"
31967 );
31968 return {
31969 surface: "iframe",
31970 windowId: id?.getAttribute("data-window-id") || void 0,
31971 x,
31972 y
31973 };
31974 }
31975 if (node.hasAttribute("data-window-id")) {
31976 return {
31977 surface: "window",
31978 windowId: node.getAttribute("data-window-id") || void 0,
31979 x,
31980 y
31981 };
31982 }
31983 if (node.classList.contains("desktop-mode-folder-grid")) {
31984 return { surface: "folder", x, y };
31985 }
31986 if (node.id === "desktop-mode-wallpaper" || node.classList.contains("desktop-mode-wallpaper") || node.classList.contains("desktop-mode-desktop")) {
31987 return { surface: "wallpaper", x, y };
31988 }
31989 node = node.parentElement;
31990 }
31991 return { surface: "unknown", x, y };
31992 }
31993 async function handleFiles(rawFiles, ctx, opts) {
31994 const detected = applyFilters(
31995 FILE_DROP_HOOKS.FILES_DETECTED,
31996 rawFiles,
31997 ctx
31998 );
31999 if (!Array.isArray(detected) || detected.length === 0) {
32000 return;
32001 }
32002 const { accepted, rejected } = partitionByPolicy(
32003 detected,
32004 opts.config
32005 );
32006 if (rejected.length > 0) {
32007 doAction(FILE_DROP_HOOKS.FILES_REJECTED, {
32008 rejections: rejected,
32009 context: ctx
32010 });
32011 showToast({
32012 message: rejected.length === 1 ? rejected[0].message : `${rejected.length} files couldn't be uploaded.`
32013 });
32014 }
32015 if (accepted.length === 0) {
32016 return;
32017 }
32018 const entries = accepted.map(({ file, mime }) => {
32019 const base = {
32020 file,
32021 mime,
32022 fields: defaultFields(file, mime)
32023 };
32024 const filtered = applyFilters(
32025 FILE_DROP_HOOKS.DIALOG_FIELDS,
32026 base,
32027 ctx
32028 );
32029 if (!filtered || typeof filtered !== "object" || !("fields" in filtered) || typeof filtered.fields !== "object") {
32030 return base;
32031 }
32032 return filtered;
32033 });
32034 await opts.openDialog(entries, ctx);
32035 }
32036 function partitionByPolicy(files, config) {
32037 const accepted = [];
32038 const rejected = [];
32039 for (const file of files) {
32040 if (file.size === 0) {
32041 rejected.push({
32042 file,
32043 reason: "empty",
32044 message: `“${file.name}” is empty.`
32045 });
32046 continue;
32047 }
32048 if (config.maxSize > 0 && file.size > config.maxSize) {
32049 rejected.push({
32050 file,
32051 reason: "size",
32052 message: `“${file.name}” exceeds the ${formatBytes$1(
32053 config.maxSize
32054 )} upload limit.`
32055 });
32056 continue;
32057 }
32058 const mime = resolveAllowedMime(
32059 file,
32060 config.allowedMimes,
32061 config.extToMime
32062 );
32063 if (!mime) {
32064 rejected.push({
32065 file,
32066 reason: "mime",
32067 message: `“${file.name}” is not an allowed file type.`
32068 });
32069 continue;
32070 }
32071 accepted.push({ file, mime });
32072 }
32073 return { accepted, rejected };
32074 }
32075 function resolveAllowedMime(file, allowedMimes, extToMime) {
32076 if (allowedMimes.length === 0) {
32077 return null;
32078 }
32079 const lower = file.type.toLowerCase();
32080 if (lower && allowedMimes.includes(lower)) {
32081 return lower;
32082 }
32083 const ext = extensionOf(file.name);
32084 if (!ext) {
32085 return null;
32086 }
32087 if (extToMime) {
32088 for (const [key, mime] of Object.entries(extToMime)) {
32089 if (key.split("|").includes(ext) && allowedMimes.includes(mime)) {
32090 return mime;
32091 }
32092 }
32093 return null;
32094 }
32095 const guess = EXTENSION_GUESSES[ext];
32096 if (guess && allowedMimes.includes(guess)) {
32097 return guess;
32098 }
32099 return null;
32100 }
32101 const EXTENSION_GUESSES = {
32102 jpg: "image/jpeg",
32103 jpeg: "image/jpeg",
32104 png: "image/png",
32105 gif: "image/gif",
32106 webp: "image/webp",
32107 avif: "image/avif",
32108 heic: "image/heic",
32109 heif: "image/heif",
32110 svg: "image/svg+xml",
32111 mp4: "video/mp4",
32112 mov: "video/quicktime",
32113 webm: "video/webm",
32114 mp3: "audio/mpeg",
32115 wav: "audio/wav",
32116 pdf: "application/pdf"
32117 };
32118 function extensionOf(name) {
32119 const dot = name.lastIndexOf(".");
32120 if (dot < 0) {
32121 return "";
32122 }
32123 return name.slice(dot + 1).toLowerCase();
32124 }
32125 function defaultFields(file, mime) {
32126 const safeName = sanitizeFilename(file.name);
32127 const ext = extensionOf(safeName);
32128 const stem = ext ? safeName.slice(0, safeName.length - ext.length - 1) : safeName;
32129 const title = humanize(stem);
32130 return {
32131 title,
32132 altText: mime.startsWith("image/") ? title : "",
32133 caption: "",
32134 description: "",
32135 filename: safeName
32136 };
32137 }
32138 function sanitizeFilename(name) {
32139 const cleaned = name.replace(/[\\/]/g, "-").replace(/[\x00-\x1f\x7f]/g, "").replace(/\s+/g, " ").replace(/ *- */g, "-").replace(/-+/g, "-").trim().replace(/^[-.]+|[-.]+$/g, "");
32140 return cleaned || "upload";
32141 }
32142 function humanize(stem) {
32143 const spaced = stem.replace(/[-_]+/g, " ").trim();
32144 if (!spaced) {
32145 return "Upload";
32146 }
32147 return spaced.charAt(0).toUpperCase() + spaced.slice(1);
32148 }
32149 function formatBytes$1(bytes) {
32150 if (bytes >= 1024 * 1024) {
32151 return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
32152 }
32153 if (bytes >= 1024) {
32154 return `${(bytes / 1024).toFixed(0)} KB`;
32155 }
32156 return `${bytes} B`;
32157 }
32158 function formatBytes(bytes) {
32159 if (!Number.isFinite(bytes) || bytes <= 0) {
32160 return "0 B";
32161 }
32162 const units = ["B", "KB", "MB", "GB", "TB"];
32163 let v = bytes;
32164 let i = 0;
32165 while (v >= 1024 && i < units.length - 1) {
32166 v /= 1024;
32167 i++;
32168 }
32169 const decimals = v >= 100 || i === 0 ? 0 : 1;
32170 return `${v.toFixed(decimals)} ${units[i]}`;
32171 }
32172 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}}`;
32173 const _WpdProgressBar = class _WpdProgressBar extends Component {
32174 constructor() {
32175 super(...arguments);
32176 this._ownedAriaLabel = null;
32177 }
32178 render() {
32179 return html`<div class="root" part="root">
32180 <div class="header" part="header" hidden>
32181 <span class="label" part="label"></span>
32182 <span class="percent" part="percent"></span>
32183 </div>
32184 <div class="track" part="track">
32185 <div class="fill" part="fill"></div>
32186 </div>
32187 </div>`;
32188 }
32189 requestUpdate() {
32190 super.requestUpdate();
32191 queueMicrotask(() => this._paint());
32192 }
32193 connectedCallback() {
32194 super.connectedCallback();
32195 queueMicrotask(() => this._paint());
32196 }
32197 _paint() {
32198 const root = this.shadowRoot;
32199 if (!root) {
32200 return;
32201 }
32202 const max = this._readMax();
32203 const indeterminate = this.hasAttribute("indeterminate") || max <= 0;
32204 const value = indeterminate ? 0 : this._readValue(max);
32205 const ratio = indeterminate ? 0 : value / max;
32206 const percent = Math.round(ratio * 100);
32207 const label = this.getAttribute("label") ?? "";
32208 const showPercent = this.hasAttribute("show-percent");
32209 const fill = root.querySelector(".fill");
32210 if (fill && !indeterminate) {
32211 fill.style.width = `${(ratio * 100).toFixed(2)}%`;
32212 } else if (fill && indeterminate) {
32213 fill.style.removeProperty("width");
32214 }
32215 const header = root.querySelector(".header");
32216 const labelEl = root.querySelector(".label");
32217 const percentEl = root.querySelector(".percent");
32218 if (header && labelEl && percentEl) {
32219 const visible = label || showPercent && !indeterminate;
32220 header.hidden = !visible;
32221 labelEl.textContent = label;
32222 percentEl.hidden = !(showPercent && !indeterminate);
32223 percentEl.textContent = `${percent}%`;
32224 }
32225 this._syncAria(max, value, indeterminate, label);
32226 const track = root.querySelector(".track");
32227 if (track) {
32228 track.setAttribute("role", "progressbar");
32229 track.setAttribute("aria-valuemin", "0");
32230 if (indeterminate) {
32231 track.removeAttribute("aria-valuenow");
32232 track.removeAttribute("aria-valuemax");
32233 } else {
32234 track.setAttribute("aria-valuemax", String(max));
32235 track.setAttribute("aria-valuenow", String(value));
32236 }
32237 if (label) {
32238 track.setAttribute("aria-label", label);
32239 } else {
32240 track.removeAttribute("aria-label");
32241 }
32242 }
32243 }
32244 _syncAria(max, value, indeterminate, label) {
32245 this.setAttribute("role", "progressbar");
32246 this.setAttribute("aria-valuemin", "0");
32247 if (indeterminate) {
32248 this.removeAttribute("aria-valuenow");
32249 this.removeAttribute("aria-valuemax");
32250 } else {
32251 this.setAttribute("aria-valuemax", String(max));
32252 this.setAttribute("aria-valuenow", String(value));
32253 }
32254 const existing = this.getAttribute("aria-label");
32255 if (label) {
32256 if (existing === null || existing === this._ownedAriaLabel) {
32257 this.setAttribute("aria-label", label);
32258 this._ownedAriaLabel = label;
32259 }
32260 } else if (existing !== null && existing === this._ownedAriaLabel) {
32261 this.removeAttribute("aria-label");
32262 this._ownedAriaLabel = null;
32263 }
32264 }
32265 _readMax() {
32266 const attr = this.getAttribute("max");
32267 if (attr === null) {
32268 return 100;
32269 }
32270 const raw = parseFloat(attr);
32271 return Number.isFinite(raw) ? raw : 100;
32272 }
32273 _readValue(max) {
32274 const raw = parseFloat(this.getAttribute("value") ?? "0");
32275 if (!Number.isFinite(raw)) {
32276 return 0;
32277 }
32278 if (raw < 0) {
32279 return 0;
32280 }
32281 if (raw > max) {
32282 return max;
32283 }
32284 return raw;
32285 }
32286 };
32287 _WpdProgressBar.props = [
32288 "value",
32289 "max",
32290 "indeterminate",
32291 "tone",
32292 "label",
32293 "showPercent"
32294 ];
32295 _WpdProgressBar.styles = [styles];
32296 _WpdProgressBar.help = {
32297 title: "Progress bar",
32298 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.",
32299 status: "experimental",
32300 since: "0.31.0",
32301 props: [
32302 {
32303 name: "value",
32304 type: "number",
32305 default: "0",
32306 description: "Current progress. Clamped to `[0, max]`."
32307 },
32308 {
32309 name: "max",
32310 type: "number",
32311 default: "100",
32312 description: "Maximum value. Setting `max <= 0` forces indeterminate."
32313 },
32314 {
32315 name: "indeterminate",
32316 type: "boolean",
32317 description: "Show the sweeping indeterminate animation instead of a value-driven fill. The `value` attribute is ignored while this is set."
32318 },
32319 {
32320 name: "tone",
32321 type: '"default" | "success" | "warning" | "danger"',
32322 default: "default",
32323 description: "Tints the fill from the shared status palette."
32324 },
32325 {
32326 name: "label",
32327 type: "string",
32328 description: "Optional inline label rendered above the track. Also wired into `aria-label` when set."
32329 },
32330 {
32331 name: "show-percent",
32332 type: "boolean",
32333 description: "Render a right-aligned percent readout next to the label. Only meaningful in determinate mode."
32334 }
32335 ],
32336 cssProps: [
32337 {
32338 name: "--wpd-progress-track-bg",
32339 default: "var(--desktop-mode-control-bg, rgba(0,0,0,0.08))"
32340 },
32341 {
32342 name: "--wpd-progress-fill",
32343 default: "var(--wp-admin-theme-color, #2271b1)"
32344 },
32345 { name: "--wpd-progress-height", default: "6px" },
32346 { name: "--wpd-progress-radius", default: "999px" },
32347 { name: "--wpd-progress-label-color", default: "inherit" },
32348 { name: "--wpd-progress-label-size", default: "12px" },
32349 { name: "--wpd-progress-label-gap", default: "4px" }
32350 ],
32351 example: html`<wpd-progress-bar
32352 value="42"
32353 label="Uploading hero.jpg"
32354 show-percent
32355 ></wpd-progress-bar>`
32356 };
32357 let WpdProgressBar = _WpdProgressBar;
32358 defineComponent("wpd-progress-bar", WpdProgressBar);
32359 const ROWS = /* @__PURE__ */ new Map();
32360 let panel = null;
32361 function mountUploadProgressHud() {
32362 if (document.body.hasAttribute("data-desktop-mode-suppress-upload-hud")) {
32363 return;
32364 }
32365 if (window.__wpdUploadHud) {
32366 return;
32367 }
32368 window.__wpdUploadHud = true;
32369 const ns = "desktop-mode/os-file-drop-hud";
32370 addAction(
32371 FILE_DROP_HOOKS.UPLOAD_STARTED,
32372 ns,
32373 (payload) => onStarted(payload.file, payload.fields, payload.abort)
32374 );
32375 addAction(
32376 FILE_DROP_HOOKS.UPLOAD_PROGRESS,
32377 ns,
32378 (payload) => onProgress(
32379 payload.file,
32380 payload.loaded,
32381 payload.total,
32382 payload.indeterminate
32383 )
32384 );
32385 addAction(
32386 FILE_DROP_HOOKS.AFTER_UPLOAD,
32387 ns,
32388 (payload) => onComplete(payload.file, payload.fields, payload.result)
32389 );
32390 addAction(
32391 FILE_DROP_HOOKS.UPLOAD_FAILED,
32392 ns,
32393 (payload) => onFailed(payload.file, payload.error)
32394 );
32395 }
32396 function onStarted(file, fields, abort) {
32397 const p = ensurePanel();
32398 const row = document.createElement("div");
32399 row.className = "desktop-mode-upload-hud__row";
32400 const meta = document.createElement("div");
32401 meta.className = "desktop-mode-upload-hud__meta";
32402 const name = document.createElement("div");
32403 name.className = "desktop-mode-upload-hud__name";
32404 name.textContent = fields.filename || file.name;
32405 name.title = fields.filename || file.name;
32406 const statusEl = document.createElement("div");
32407 statusEl.className = "desktop-mode-upload-hud__status";
32408 statusEl.textContent = "Uploading…";
32409 meta.append(name, statusEl);
32410 const bar = document.createElement("wpd-progress-bar");
32411 bar.setAttribute("indeterminate", "");
32412 bar.setAttribute("show-percent", "");
32413 const actions = document.createElement("div");
32414 actions.className = "desktop-mode-upload-hud__actions";
32415 const cancelBtn = document.createElement("wpd-button");
32416 cancelBtn.setAttribute("variant", "tertiary");
32417 cancelBtn.setAttribute("size", "small");
32418 cancelBtn.textContent = "Cancel";
32419 cancelBtn.addEventListener("click", () => {
32420 const r = ROWS.get(file);
32421 if (!r) {
32422 return;
32423 }
32424 if (r.state === "running") {
32425 r.statusEl.textContent = "Cancelling…";
32426 r.cancelBtn.disabled = true;
32427 r.abort();
32428 } else {
32429 dismissRow(r);
32430 }
32431 });
32432 actions.appendChild(cancelBtn);
32433 row.append(meta, bar, actions);
32434 p.querySelector(".desktop-mode-upload-hud__list").appendChild(row);
32435 ROWS.set(file, {
32436 file,
32437 abort,
32438 root: row,
32439 bar,
32440 statusEl,
32441 cancelBtn,
32442 state: "running",
32443 lingerTimer: null
32444 });
32445 updateHeader();
32446 }
32447 function onProgress(file, loaded, total, indeterminate) {
32448 const r = ROWS.get(file);
32449 if (!r || r.state !== "running") {
32450 return;
32451 }
32452 if (indeterminate || total <= 0) {
32453 r.bar.setAttribute("indeterminate", "");
32454 r.statusEl.textContent = `${formatBytes(loaded)} sent`;
32455 } else {
32456 r.bar.removeAttribute("indeterminate");
32457 r.bar.setAttribute("max", String(total));
32458 r.bar.setAttribute("value", String(loaded));
32459 r.statusEl.textContent = `${formatBytes(loaded)} / ${formatBytes(total)}`;
32460 }
32461 }
32462 function onComplete(file, fields, result) {
32463 const r = ROWS.get(file);
32464 if (!r) {
32465 return;
32466 }
32467 r.state = "success";
32468 r.bar.removeAttribute("indeterminate");
32469 r.bar.setAttribute("value", "100");
32470 r.bar.setAttribute("max", "100");
32471 r.bar.setAttribute("tone", "success");
32472 r.statusEl.textContent = "Uploaded";
32473 r.cancelBtn.textContent = "Dismiss";
32474 r.lingerTimer = setTimeout(() => dismissRow(r), 2500);
32475 updateHeader();
32476 activity.publish("desktop-mode/upload-hud-complete", {
32477 filename: fields.filename || result.filename,
32478 attachmentId: result.id
32479 });
32480 }
32481 function onFailed(file, error) {
32482 const r = ROWS.get(file);
32483 if (!r) {
32484 return;
32485 }
32486 r.bar.removeAttribute("indeterminate");
32487 r.bar.setAttribute("tone", "danger");
32488 r.cancelBtn.textContent = "Dismiss";
32489 r.cancelBtn.disabled = false;
32490 if (error.name === "UploadAbortedError") {
32491 r.state = "aborted";
32492 r.statusEl.textContent = "Cancelled";
32493 } else {
32494 r.state = "failed";
32495 r.statusEl.textContent = error.message || "Upload failed";
32496 }
32497 updateHeader();
32498 }
32499 function dismissRow(r) {
32500 if (r.lingerTimer) {
32501 clearTimeout(r.lingerTimer);
32502 }
32503 ROWS.delete(r.file);
32504 r.root.remove();
32505 updateHeader();
32506 if (ROWS.size === 0 && panel) {
32507 panel.hidden = true;
32508 }
32509 }
32510 function ensurePanel() {
32511 if (panel && panel.isConnected) {
32512 panel.hidden = false;
32513 return panel;
32514 }
32515 const p = document.createElement("div");
32516 p.className = "desktop-mode-upload-hud";
32517 p.setAttribute("role", "region");
32518 p.setAttribute("aria-label", "Uploads");
32519 const header = document.createElement("div");
32520 header.className = "desktop-mode-upload-hud__header";
32521 const title = document.createElement("div");
32522 title.className = "desktop-mode-upload-hud__title";
32523 title.textContent = "Uploads";
32524 const closeBtn = document.createElement("button");
32525 closeBtn.type = "button";
32526 closeBtn.className = "desktop-mode-upload-hud__close";
32527 closeBtn.setAttribute("aria-label", "Hide upload panel");
32528 closeBtn.textContent = "×";
32529 closeBtn.addEventListener("click", () => {
32530 for (const r of [...ROWS.values()]) {
32531 if (r.state !== "running") {
32532 dismissRow(r);
32533 }
32534 }
32535 if (ROWS.size === 0) {
32536 p.hidden = true;
32537 }
32538 });
32539 header.append(title, closeBtn);
32540 const list2 = document.createElement("div");
32541 list2.className = "desktop-mode-upload-hud__list";
32542 p.append(header, list2);
32543 document.body.appendChild(p);
32544 panel = p;
32545 return p;
32546 }
32547 function updateHeader() {
32548 if (!panel) {
32549 return;
32550 }
32551 const title = panel.querySelector(
32552 ".desktop-mode-upload-hud__title"
32553 );
32554 if (!title) {
32555 return;
32556 }
32557 const total = ROWS.size;
32558 const running = [...ROWS.values()].filter((r) => r.state === "running").length;
32559 if (running > 0) {
32560 title.textContent = running === total ? `Uploading ${running} file${running === 1 ? "" : "s"}…` : `${running} of ${total} uploading…`;
32561 } else if (total > 0) {
32562 title.textContent = `Uploads (${total})`;
32563 } else {
32564 title.textContent = "Uploads";
32565 }
32566 }
32567 function mountMediaLibraryRefresher() {
32568 if (document.body.hasAttribute(
32569 "data-desktop-mode-suppress-media-library-refresh"
32570 )) {
32571 return;
32572 }
32573 const sentinel = window;
32574 if (sentinel.__wpdMediaLibraryRefresher) {
32575 return;
32576 }
32577 sentinel.__wpdMediaLibraryRefresher = true;
32578 addAction(
32579 FILE_DROP_HOOKS.AFTER_UPLOAD,
32580 "desktop-mode/os-file-drop-library-refresh",
32581 () => refreshOpenLibraries()
32582 );
32583 }
32584 function refreshOpenLibraries() {
32585 const iframes = document.querySelectorAll("iframe");
32586 for (const frame of Array.from(iframes)) {
32587 if (!isMediaLibraryUrl(resolveIframeUrl(frame))) {
32588 continue;
32589 }
32590 try {
32591 frame.contentWindow?.location.reload();
32592 } catch {
32593 const reloadHref = resolveIframeUrl(frame);
32594 if (reloadHref) {
32595 frame.setAttribute("src", reloadHref);
32596 }
32597 }
32598 }
32599 }
32600 function resolveIframeUrl(frame) {
32601 try {
32602 return frame.contentWindow?.location.href ?? frame.src ?? "";
32603 } catch {
32604 return frame.src ?? "";
32605 }
32606 }
32607 function isMediaLibraryUrl(url) {
32608 if (!url) {
32609 return false;
32610 }
32611 return /\/wp-admin\/upload\.php(?:[?#]|$)/.test(url);
32612 }
32613 function bootOsFileDrop(args) {
32614 const config = args.config || {
32615 enabled: false,
32616 allowedMimes: [],
32617 maxSize: 0
32618 };
32619 mountUploadProgressHud();
32620 mountMediaLibraryRefresher();
32621 mountOsFileDropManager({
32622 config,
32623 mediaUrl: args.mediaUrl,
32624 restNonce: args.restNonce,
32625 openDialog: async (entries, ctx) => {
32626 const { openUploadDialog: openUploadDialog2 } = await Promise.resolve().then(() => dialog);
32627 await openUploadDialog2({
32628 entries,
32629 context: ctx,
32630 mediaUrl: args.mediaUrl,
32631 restNonce: args.restNonce
32632 });
32633 }
32634 });
32635 }
32636 const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
32637 __proto__: null,
32638 FILE_DROP_HOOKS,
32639 bootOsFileDrop
32640 }, Symbol.toStringTag, { value: "Module" }));
32641 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}`;
32642 const _WpdTextField = class _WpdTextField extends Component {
32643 constructor() {
32644 super(...arguments);
32645 this._revealed = false;
32646 }
32647 connectedCallback() {
32648 super.connectedCallback();
32649 ensureAutoId(this);
32650 }
32651 render() {
32652 const label = this.label || "";
32653 const value = this.value ?? "";
32654 const placeholder = this.placeholder || "";
32655 const disabled = this.disabled !== null;
32656 const readonly = this.readonly !== null;
32657 const declaredAutocomplete = this.autocomplete;
32658 const declaredType = this.type || "text";
32659 const isPassword = declaredType === "password";
32660 let autocomplete = declaredAutocomplete || "off";
32661 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
32662 autocomplete = "new-password";
32663 }
32664 const maxLength = this.maxlength;
32665 const minLength = this.minlength;
32666 const pattern = this.pattern || "";
32667 const name = this.name || "";
32668 const suffix = this.suffix || "";
32669 const invalid = this.invalid !== null;
32670 const reveal = this.reveal !== null;
32671 const isPasswordIntent = declaredType === "password";
32672 const isMasked = isPasswordIntent && !(reveal && this._revealed);
32673 let effectiveType;
32674 if (isPasswordIntent) {
32675 effectiveType = "text";
32676 } else if (reveal && this._revealed) {
32677 effectiveType = "text";
32678 } else {
32679 effectiveType = declaredType;
32680 }
32681 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
32682 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
32683 const hostId = this.id || "wpd-unnamed";
32684 const inputId = `${hostId}__input`;
32685 return html`
32686 ${label ? html`<label
32687 class="wpd-text-field__label"
32688 for=${inputId}
32689 >${label}</label>` : html``}
32690 <span class=${rowClass}>
32691 <input
32692 id=${inputId}
32693 class=${inputClass}
32694 type=${effectiveType}
32695 .value=${value}
32696 placeholder=${placeholder}
32697 ?disabled=${disabled}
32698 ?readonly=${readonly}
32699 autocomplete=${autocomplete}
32700 maxlength=${maxLength ?? ""}
32701 minlength=${minLength ?? ""}
32702 pattern=${pattern}
32703 name=${name}
32704 aria-invalid=${invalid ? "true" : "false"}
32705 aria-label=${label || ""}
32706 @input=${(e) => this._onInput(e)}
32707 @change=${(e) => this._onChange(e)}
32708 @keydown=${(e) => this._onKeyDown(e)}
32709 />
32710 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
32711 ${reveal ? this._renderRevealButton(disabled) : html``}
32712 </span>
32713 `;
32714 }
32715 _renderRevealButton(disabled) {
32716 const label = this._revealed ? "Hide" : "Show";
32717 return html`
32718 <button
32719 type="button"
32720 class="wpd-text-field__reveal"
32721 aria-label=${label}
32722 aria-pressed=${this._revealed ? "true" : "false"}
32723 ?disabled=${disabled}
32724 tabindex="0"
32725 @click=${() => this._onToggleReveal()}
32726 >
32727 ${this._revealed ? _iconEyeOff() : _iconEye()}
32728 </button>
32729 `;
32730 }
32731 _onToggleReveal() {
32732 this._revealed = !this._revealed;
32733 this.requestUpdate();
32734 }
32735 _onInput(e) {
32736 const input = e.target;
32737 this.value = input.value;
32738 this.emit("wpd-input-change", { value: input.value });
32739 }
32740 _onChange(e) {
32741 const input = e.target;
32742 this.emit("wpd-input-commit", { value: input.value });
32743 }
32744 _onKeyDown(e) {
32745 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
32746 const input = e.target;
32747 this.emit("wpd-submit", { value: input.value });
32748 }
32749 }
32750 };
32751 _WpdTextField.props = [
32752 "label",
32753 "value",
32754 "placeholder",
32755 "disabled",
32756 "readonly",
32757 "autocomplete",
32758 "type",
32759 "maxlength",
32760 "minlength",
32761 "pattern",
32762 "name",
32763 "suffix",
32764 "invalid",
32765 "reveal"
32766 ];
32767 _WpdTextField.styles = [textFieldStyles];
32768 _WpdTextField.help = {
32769 title: "Text field",
32770 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.",
32771 status: "stable",
32772 since: "0.5.0",
32773 props: [
32774 { name: "label", type: "string", description: "Visible label above the input." },
32775 { name: "value", type: "string", description: "Current input value; reflected two-way." },
32776 { name: "placeholder", type: "string", description: "Native placeholder string." },
32777 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
32778 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
32779 {
32780 name: "autocomplete",
32781 type: "string",
32782 default: "off",
32783 description: "Forwarded to the native input autocomplete attribute."
32784 },
32785 {
32786 name: "type",
32787 type: "string",
32788 default: "text",
32789 description: "Native input type (text, password, email, search, tel, url)."
32790 },
32791 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
32792 { name: "minlength", type: "integer (string)", description: "Native minlength." },
32793 { name: "pattern", type: "regex string", description: "Native validation pattern." },
32794 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
32795 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
32796 {
32797 name: "invalid",
32798 type: "boolean attribute",
32799 description: "Marks the field aria-invalid and applies the error style."
32800 },
32801 {
32802 name: "reveal",
32803 type: "boolean attribute",
32804 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
32805 }
32806 ],
32807 events: [
32808 {
32809 name: "wpd-input-change",
32810 description: "Fires on every input keystroke.",
32811 detail: "{ value: string }"
32812 },
32813 {
32814 name: "wpd-input-commit",
32815 description: "Fires on the native change event (blur / Enter).",
32816 detail: "{ value: string }"
32817 },
32818 {
32819 name: "wpd-submit",
32820 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
32821 detail: "{ value: string }"
32822 }
32823 ],
32824 cssProps: [
32825 { name: "--desktop-mode-text", description: "Text colour." },
32826 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
32827 { name: "--desktop-mode-border", description: "Input outline." },
32828 { name: "--desktop-mode-window-bg", description: "Input background." }
32829 ],
32830 example: html`
32831 <wpd-stack gap="8">
32832 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
32833 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
32834 </wpd-stack>
32835 `
32836 };
32837 let WpdTextField = _WpdTextField;
32838 defineComponent("wpd-text-field", WpdTextField);
32839 function _iconEye() {
32840 return html`
32841 <svg
32842 viewBox="0 0 16 16"
32843 width="14"
32844 height="14"
32845 fill="none"
32846 stroke="currentColor"
32847 stroke-width="1.5"
32848 stroke-linecap="round"
32849 stroke-linejoin="round"
32850 aria-hidden="true"
32851 focusable="false"
32852 >
32853 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
32854 <circle cx="8" cy="8" r="2" />
32855 </svg>
32856 `;
32857 }
32858 function _iconEyeOff() {
32859 return html`
32860 <svg
32861 viewBox="0 0 16 16"
32862 width="14"
32863 height="14"
32864 fill="none"
32865 stroke="currentColor"
32866 stroke-width="1.5"
32867 stroke-linecap="round"
32868 stroke-linejoin="round"
32869 aria-hidden="true"
32870 focusable="false"
32871 >
32872 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
32873 <circle cx="8" cy="8" r="2" />
32874 <line x1="2" y1="2" x2="14" y2="14" />
32875 </svg>
32876 `;
32877 }
32878 async function uploadFile(args) {
32879 const initial = {
32880 file: args.file,
32881 mime: args.mime,
32882 fields: args.fields
32883 };
32884 const filtered = applyFilters(
32885 FILE_DROP_HOOKS.BEFORE_UPLOAD,
32886 initial,
32887 args.context
32888 );
32889 if (!filtered) {
32890 throw new UploadCancelledError();
32891 }
32892 const body = new FormData();
32893 const renamed = filtered.fields.filename !== filtered.file.name ? new File([filtered.file], filtered.fields.filename, {
32894 type: filtered.mime || filtered.file.type
32895 }) : filtered.file;
32896 body.append("file", renamed);
32897 body.append("title", filtered.fields.title);
32898 body.append("alt_text", filtered.fields.altText);
32899 body.append("caption", filtered.fields.caption);
32900 body.append("description", filtered.fields.description);
32901 return new Promise((resolve2, reject) => {
32902 const xhr = new XMLHttpRequest();
32903 xhr.open("POST", args.mediaUrl, true);
32904 xhr.withCredentials = true;
32905 xhr.setRequestHeader("X-WP-Nonce", args.restNonce);
32906 xhr.responseType = "text";
32907 let aborted = false;
32908 let bodyFullySent = false;
32909 let cancelRequested = false;
32910 const abort = () => {
32911 cancelRequested = true;
32912 if (bodyFullySent) {
32913 return;
32914 }
32915 aborted = true;
32916 try {
32917 xhr.abort();
32918 } catch {
32919 }
32920 };
32921 doAction(FILE_DROP_HOOKS.UPLOAD_STARTED, {
32922 file: filtered.file,
32923 fields: filtered.fields,
32924 context: args.context,
32925 abort
32926 });
32927 xhr.upload.addEventListener("progress", (e) => {
32928 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
32929 file: filtered.file,
32930 fields: filtered.fields,
32931 context: args.context,
32932 loaded: e.loaded,
32933 total: e.lengthComputable ? e.total : 0,
32934 indeterminate: !e.lengthComputable
32935 });
32936 });
32937 xhr.upload.addEventListener("load", () => {
32938 bodyFullySent = true;
32939 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
32940 file: filtered.file,
32941 fields: filtered.fields,
32942 context: args.context,
32943 loaded: filtered.file.size,
32944 total: filtered.file.size,
32945 indeterminate: false
32946 });
32947 });
32948 xhr.addEventListener("error", () => {
32949 if (aborted) {
32950 return;
32951 }
32952 const error = new Error("Network error during upload.");
32953 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
32954 // `filtered.file` — same identity as UPLOAD_STARTED /
32955 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
32956 // that swapped the File would otherwise route this
32957 // failure to a row keyed by the original (pre-swap)
32958 // File, leaving the HUD row stuck in "running".
32959 file: filtered.file,
32960 error,
32961 context: args.context
32962 });
32963 reject(error);
32964 });
32965 xhr.addEventListener("abort", () => {
32966 const error = new UploadAbortedError();
32967 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
32968 // `filtered.file` — same identity as UPLOAD_STARTED /
32969 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
32970 // that swapped the File would otherwise route this
32971 // failure to a row keyed by the original (pre-swap)
32972 // File, leaving the HUD row stuck in "running".
32973 file: filtered.file,
32974 error,
32975 context: args.context
32976 });
32977 reject(error);
32978 });
32979 xhr.addEventListener("load", () => {
32980 if (aborted) {
32981 return;
32982 }
32983 if (xhr.status < 200 || xhr.status >= 300) {
32984 const message = extractXhrMessage(xhr);
32985 const error = new Error(message);
32986 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
32987 file: filtered.file,
32988 error,
32989 context: args.context
32990 });
32991 reject(error);
32992 return;
32993 }
32994 let data;
32995 try {
32996 data = JSON.parse(xhr.responseText);
32997 } catch (err) {
32998 const error = err instanceof Error ? err : new Error("Could not parse server response.");
32999 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
33000 file: filtered.file,
33001 error,
33002 context: args.context
33003 });
33004 reject(error);
33005 return;
33006 }
33007 if (cancelRequested && data.id) {
33008 void deleteAttachment(
33009 args.mediaUrl,
33010 args.restNonce,
33011 data.id
33012 );
33013 const error = new UploadAbortedError();
33014 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
33015 file: filtered.file,
33016 error,
33017 context: args.context
33018 });
33019 reject(error);
33020 return;
33021 }
33022 const result = {
33023 id: data.id,
33024 url: data.source_url,
33025 mime: data.mime_type || filtered.mime,
33026 title: data.title?.rendered || filtered.fields.title,
33027 filename: data.media_details?.file || filtered.fields.filename
33028 };
33029 doAction(FILE_DROP_HOOKS.AFTER_UPLOAD, {
33030 file: filtered.file,
33031 result,
33032 fields: filtered.fields,
33033 context: args.context
33034 });
33035 resolve2(result);
33036 });
33037 xhr.send(body);
33038 });
33039 }
33040 class UploadCancelledError extends Error {
33041 constructor() {
33042 super("Upload cancelled by desktop-mode.drop.before-upload filter.");
33043 this.name = "UploadCancelledError";
33044 }
33045 }
33046 class UploadAbortedError extends Error {
33047 constructor() {
33048 super("Upload aborted by the caller.");
33049 this.name = "UploadAbortedError";
33050 }
33051 }
33052 function deleteAttachment(mediaUrl, restNonce, id) {
33053 const url = `${mediaUrl.replace(/\/$/, "")}/${id}?force=true`;
33054 const cleanup = new XMLHttpRequest();
33055 cleanup.open("DELETE", url, true);
33056 cleanup.withCredentials = true;
33057 cleanup.setRequestHeader("X-WP-Nonce", restNonce);
33058 return new Promise((resolve2) => {
33059 cleanup.addEventListener("loadend", () => {
33060 if (cleanup.status < 200 || cleanup.status >= 300) {
33061 console.warn(
33062 `[os-file-drop] late-cancel cleanup failed for attachment ${id} (HTTP ${cleanup.status}). The attachment remains in the Media Library; delete it manually.`
33063 );
33064 }
33065 resolve2();
33066 });
33067 cleanup.addEventListener("error", () => {
33068 console.warn(
33069 `[os-file-drop] late-cancel cleanup network error for attachment ${id}. The attachment remains in the Media Library; delete it manually.`
33070 );
33071 resolve2();
33072 });
33073 try {
33074 cleanup.send();
33075 } catch (err) {
33076 console.warn(
33077 `[os-file-drop] late-cancel cleanup could not be dispatched for attachment ${id}:`,
33078 err
33079 );
33080 resolve2();
33081 }
33082 });
33083 }
33084 function extractXhrMessage(xhr) {
33085 const fallback = `Upload failed (HTTP ${xhr.status}).`;
33086 const text = xhr.responseText;
33087 if (!text) {
33088 return fallback;
33089 }
33090 try {
33091 const data = JSON.parse(text);
33092 if (data && typeof data.message === "string") {
33093 return data.message;
33094 }
33095 } catch {
33096 }
33097 return fallback;
33098 }
33099 async function openUploadDialog(args) {
33100 if (args.entries.length === 0) {
33101 return;
33102 }
33103 const modal = document.createElement("wpd-modal");
33104 modal.setAttribute("open", "");
33105 modal.setAttribute("size", "md");
33106 modal.setAttribute(
33107 "title",
33108 args.entries.length === 1 ? "Upload to Media Library" : `Upload ${args.entries.length} files to Media Library`
33109 );
33110 document.body.appendChild(modal);
33111 const draft = args.entries.map((entry) => ({
33112 ...entry.fields
33113 }));
33114 const renderBody = () => {
33115 modal.innerHTML = "";
33116 const list2 = document.createElement("div");
33117 list2.style.cssText = "display:flex;flex-direction:column;gap:18px;max-height:60vh;overflow:auto;padding-right:6px;";
33118 args.entries.forEach((entry, i) => {
33119 list2.appendChild(renderEntry(entry, draft[i], i + 1));
33120 });
33121 modal.appendChild(list2);
33122 const footer = document.createElement("div");
33123 footer.setAttribute("slot", "footer");
33124 footer.style.cssText = "display:flex;gap:8px;justify-content:flex-end;";
33125 const cancel = document.createElement("wpd-button");
33126 cancel.setAttribute("variant", "secondary");
33127 cancel.textContent = "Cancel";
33128 cancel.addEventListener("click", () => {
33129 modal.remove();
33130 });
33131 const upload = document.createElement("wpd-button");
33132 upload.setAttribute("variant", "primary");
33133 upload.textContent = args.entries.length === 1 ? "Upload" : `Upload ${args.entries.length} files`;
33134 upload.addEventListener("click", () => {
33135 void runUploads(upload, cancel);
33136 });
33137 footer.appendChild(cancel);
33138 footer.appendChild(upload);
33139 modal.appendChild(footer);
33140 };
33141 const renderEntry = (entry, fields, index2) => {
33142 const wrap = document.createElement("div");
33143 wrap.style.cssText = "display:flex;flex-direction:column;gap:8px;border:1px solid rgba(255,255,255,0.08);border-radius:10px;padding:14px;";
33144 const heading = document.createElement("div");
33145 heading.style.cssText = "display:flex;gap:10px;align-items:center;font-weight:600;";
33146 const tag = document.createElement("span");
33147 tag.textContent = args.entries.length === 1 ? "" : `#${index2} · `;
33148 tag.style.opacity = "0.6";
33149 const fname = document.createElement("span");
33150 fname.textContent = entry.file.name;
33151 fname.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
33152 const size = document.createElement("span");
33153 size.textContent = `${entry.mime || "unknown"} · ${formatBytes(
33154 entry.file.size
33155 )}`;
33156 size.style.cssText = "opacity:0.6;font-size:12px;";
33157 heading.appendChild(tag);
33158 heading.appendChild(fname);
33159 heading.appendChild(size);
33160 wrap.appendChild(heading);
33161 wrap.appendChild(textField("Title", fields.title, (v) => fields.title = v));
33162 wrap.appendChild(textField("Filename", fields.filename, (v) => fields.filename = v));
33163 if (entry.mime.startsWith("image/")) {
33164 wrap.appendChild(
33165 textField("Alt text", fields.altText, (v) => fields.altText = v)
33166 );
33167 }
33168 wrap.appendChild(textField("Caption", fields.caption, (v) => fields.caption = v));
33169 wrap.appendChild(
33170 textareaField("Description", fields.description, (v) => fields.description = v)
33171 );
33172 return wrap;
33173 };
33174 const runUploads = async (uploadBtn, cancelBtn) => {
33175 uploadBtn.disabled = true;
33176 cancelBtn.disabled = true;
33177 uploadBtn.textContent = "Uploading…";
33178 const total = args.entries.length;
33179 let successes = 0;
33180 let failures = 0;
33181 let cancelled = 0;
33182 const failureDetails = [];
33183 for (let i = 0; i < total; i++) {
33184 const entry = args.entries[i];
33185 try {
33186 await uploadFile({
33187 file: entry.file,
33188 mime: entry.mime,
33189 fields: draft[i],
33190 context: args.context,
33191 mediaUrl: args.mediaUrl,
33192 restNonce: args.restNonce
33193 });
33194 successes++;
33195 } catch (err) {
33196 if (err instanceof UploadCancelledError) {
33197 cancelled++;
33198 continue;
33199 }
33200 if (err instanceof UploadAbortedError) {
33201 cancelled++;
33202 continue;
33203 }
33204 failures++;
33205 const message = err instanceof Error ? err.message : "Upload failed.";
33206 failureDetails.push(`“${entry.file.name}” — ${message}`);
33207 }
33208 }
33209 modal.remove();
33210 showBatchSummaryToast({
33211 total,
33212 successes,
33213 failures,
33214 cancelled,
33215 failureDetails
33216 });
33217 };
33218 renderBody();
33219 await new Promise((resolve2) => {
33220 modal.addEventListener("wpd-modal-cancel", () => {
33221 modal.remove();
33222 resolve2();
33223 });
33224 const observer = new MutationObserver(() => {
33225 if (!modal.isConnected) {
33226 observer.disconnect();
33227 resolve2();
33228 }
33229 });
33230 observer.observe(document.body, { childList: true, subtree: true });
33231 });
33232 }
33233 function textField(label, value, onChange) {
33234 const el = document.createElement("wpd-text-field");
33235 el.setAttribute("label", label);
33236 el.setAttribute("value", value);
33237 el.addEventListener("input", () => {
33238 const v = el.value;
33239 if (typeof v === "string") {
33240 onChange(v);
33241 }
33242 });
33243 return el;
33244 }
33245 function textareaField(label, value, onChange) {
33246 const el = document.createElement("wpd-textarea");
33247 el.setAttribute("label", label);
33248 el.setAttribute("value", value);
33249 el.setAttribute("rows", "3");
33250 el.addEventListener("input", () => {
33251 const v = el.value;
33252 if (typeof v === "string") {
33253 onChange(v);
33254 }
33255 });
33256 return el;
33257 }
33258 function showBatchSummaryToast(args) {
33259 const { total, successes, failures, cancelled, failureDetails } = args;
33260 if (total === 0) {
33261 return;
33262 }
33263 if (total === 1) {
33264 if (successes === 1) {
33265 showToast({ message: "Uploaded to Media Library." });
33266 } else if (failures === 1 && failureDetails[0]) {
33267 showToast({ message: failureDetails[0] });
33268 } else if (cancelled === 1) {
33269 showToast({ message: "Upload cancelled." });
33270 }
33271 return;
33272 }
33273 if (successes === total) {
33274 showToast({
33275 message: `Uploaded ${successes} files to Media Library.`
33276 });
33277 return;
33278 }
33279 if (cancelled === total) {
33280 showToast({ message: "All uploads cancelled." });
33281 return;
33282 }
33283 if (failures === total) {
33284 showToast({
33285 message: failures === 1 && failureDetails[0] ? failureDetails[0] : `${failures} uploads failed.`
33286 });
33287 return;
33288 }
33289 const parts = [];
33290 if (successes > 0) {
33291 parts.push(
33292 `Uploaded ${successes} file${successes === 1 ? "" : "s"}.`
33293 );
33294 }
33295 if (cancelled > 0) {
33296 parts.push(`Cancelled ${cancelled}.`);
33297 }
33298 if (failures > 0) {
33299 parts.push(`Failed ${failures}.`);
33300 }
33301 showToast({ message: parts.join(" ") });
33302 }
33303 const dialog = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
33304 __proto__: null,
33305 openUploadDialog
33306 }, Symbol.toStringTag, { value: "Module" }));
33307 exports.clampGeometryToViewport = clampGeometryToViewport;
33308 Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
33309 return exports;
33310 }({});
33311