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

30,574 lines 998.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 var desktopMode = function(exports) {
2 "use strict";
3 var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
4 function installMyWordpressEarlyStub() {
5 const w = window;
6 w.wp = w.wp ?? {};
7 const wp = w.wp;
8 if (!wp.desktop) {
9 wp.desktop = {};
10 }
11 const desktop = wp.desktop;
12 if (desktop.myWordpress) {
13 return;
14 }
15 const queue = [];
16 const stub = {
17 registerEntityKind: (kind, renderer) => {
18 const slot = { unregister: null };
19 const entry = { kind, renderer, slot };
20 queue.push(entry);
21 return () => {
22 if (slot.unregister) {
23 slot.unregister();
24 slot.unregister = null;
25 return;
26 }
27 const i = queue.indexOf(entry);
28 if (i !== -1) {
29 queue.splice(i, 1);
30 }
31 };
32 },
33 __pendingKinds: queue
34 };
35 desktop.myWordpress = stub;
36 }
37 installMyWordpressEarlyStub();
38 function getWpHooks$1() {
39 const hooks = window.wp?.hooks;
40 if (!hooks) {
41 throw new Error(
42 "[desktop-mode] `window.wp.hooks` is not available. The plugin declares `wp-hooks` as a script dependency; if you are seeing this error, verify the enqueue order."
43 );
44 }
45 return hooks;
46 }
47 function addFilter(hookName2, namespace, callback, priority) {
48 getWpHooks$1().addFilter(
49 hookName2,
50 namespace,
51 callback,
52 priority
53 );
54 }
55 function addAction(hookName2, namespace, callback, priority) {
56 getWpHooks$1().addAction(
57 hookName2,
58 namespace,
59 callback,
60 priority
61 );
62 }
63 function removeAction(hookName2, namespace) {
64 return getWpHooks$1().removeAction(hookName2, namespace);
65 }
66 function applyFilters(hookName2, value, ...args) {
67 return getWpHooks$1().applyFilters(hookName2, value, ...args);
68 }
69 function doAction(hookName2, ...args) {
70 getWpHooks$1().doAction(hookName2, ...args);
71 }
72 function didAction(hookName2) {
73 return getWpHooks$1().didAction(hookName2);
74 }
75 function rawHooks() {
76 return getWpHooks$1();
77 }
78 const HOOKS = {
79 /** Action, fires once after shell boot; plugins register here. */
80 INIT: "desktop-mode.init",
81 /** Filter, receives the wallpaper registry array. */
82 WALLPAPERS: "desktop-mode.wallpapers",
83 /** 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 // Observability — iframe errors, iframe network, shell-side errors,
97 // monitor entry aggregation. Designed for dashboard / debug widget
98 // plugins that want genuine admin observability (Gutenberg save
99 // failures, admin-ajax 500s, plugin exceptions) rather than just the
100 // shell's own console-error surface.
101 // ------------------------------------------------------------------
102 /**
103 * Action, fires when a chromeless iframe's `error` or
104 * `unhandledrejection` handler catches an exception. Payload: `{
105 * windowId: string, kind: 'error' | 'unhandledrejection', message:
106 * string, filename: string | null, lineno: number | null, colno:
107 * number | null, stack: string | null }`. Origin-filtered at the
108 * parent shell; cross-origin iframe errors never reach here.
109 */
110 /**
111 * Action, fires once per iframe when the chromeless bridge
112 * script has finished wiring its message listeners. Payload:
113 * `{ windowId: string }`. Subscribers get a reliable "safe to
114 * talk to this iframe" signal — the browser's native `load`
115 * event fires before our bridge attaches, so messages sent on
116 * `load` can be dropped on the floor. Use this instead when
117 * timing matters (first-focus dispatch, auto-fill handshakes).
118 *
119 * @since 0.11.0
120 */
121 IFRAME_READY: "desktop-mode.iframe.ready",
122 IFRAME_ERROR: "desktop-mode.iframe.error",
123 /**
124 * Action, fires when a `fetch` or `XMLHttpRequest` inside a
125 * chromeless iframe completes (success OR failure). Payload: `{
126 * windowId: string, method: string, url: string, status: number,
127 * duration: number, failed: boolean }`. Subscribers get a faithful
128 * view of admin-ajax + REST calls that previously never left the
129 * iframe boundary. `status === 0` indicates a network failure with
130 * no response received.
131 */
132 IFRAME_NETWORK_COMPLETED: "desktop-mode.iframe.network-completed",
133 /**
134 * Action, fires when one of the shell's own try/catch barriers
135 * catches an exception. Payload: `{ scope:
136 * 'widget-mount' | 'widget-teardown' | 'window-open' | 'wallpaper-mount' |
137 * 'wallpaper-teardown' | 'session-save' | 'menu-refresh' | string,
138 * id?: string, error: unknown }`. Paired with the existing
139 * `console.error` calls — a monitor widget can surface these as
140 * first-class entries.
141 */
142 SHELL_ERROR: "desktop-mode.shell.error",
143 /**
144 * Action, fires once per `wp.desktop.broadcast()` call with the
145 * fully-resolved `{ topic, payload }` detail. Lets plugins log,
146 * mirror, or augment broadcast traffic without subscribing for
147 * every individual topic.
148 */
149 BROADCAST: "desktop-mode.broadcast",
150 /**
151 * Filter, applies to a `MonitorEntry` before a monitor widget
152 * renders it. Plugins can mutate the entry (rewrite the message,
153 * add `extra` fields) or return `null` to suppress it. Used by
154 * monitor widgets to converge every plugin on the same shape —
155 * see `MonitorEntry` in `src/types.ts`.
156 */
157 MONITOR_ENTRY: "desktop-mode.monitor.entry",
158 /**
159 * Filter, applies to the list of "solid" surfaces wallpapers
160 * should consider for collision / accumulation effects (snow
161 * piling, leaves settling, rain splash). Seeded by the shell
162 * with: every visible (non-minimized) window's top edge; the
163 * desktop-area floor; the dock's outward-facing edge; and every
164 * mounted widget card's top edge.
165 *
166 * Plugins that own their own DOM (e.g. floating pickers,
167 * custom overlays) can push additional surfaces so snow
168 * accumulates on them too.
169 *
170 * Each entry is a `WallpaperSurface` — see
171 * `src/wallpapers/surfaces.ts` for the shape. Rects are in
172 * viewport coordinates (clientX / clientY), matching what a
173 * canvas mounted inside `#desktop-mode-wallpaper` reads.
174 */
175 WALLPAPER_SURFACES: "desktop-mode.wallpaper.surfaces",
176 // ------------------------------------------------------------------
177 // Window lifecycle actions. All payloads share a `windowId: string`
178 // field; additional fields are documented per-hook in the JS
179 // reference. These mirror the existing `desktop-mode-window-*`
180 // CustomEvents but ship under the hook bus so plugins can use one
181 // idiomatic API for everything the shell emits.
182 // ------------------------------------------------------------------
183 /**
184 * Filter, last call before a window's resolved geometry (x, y,
185 * width, height, initialState) is baked into the `WindowConfig`
186 * passed to the `Window` constructor. Lets a plugin override
187 * default placement for windows it owns, snap restored bounds to
188 * a different region, or force a particular initial state.
189 *
190 * Signature:
191 *
192 * ( geometry: ResolvedWindowGeometry, ctx: WindowGeometryContext )
193 * => ResolvedWindowGeometry
194 *
195 * Where `ResolvedWindowGeometry = { x, y, width, height, state? }`
196 * and `ctx = { windowId, baseId, hasSavedGeometry, callerPinned,
197 * desktopRect }`.
198 *
199 * - `hasSavedGeometry` is `true` when the user previously
200 * dragged or resized this window and the resolved geometry
201 * includes those restored values. Plugins that want to
202 * "leave the user's saved layout alone" should bail when
203 * this is true.
204 * - `callerPinned` is `true` when the caller of `manager.open()`
205 * passed at least one of `{ x, y, width, height, initialState }`
206 * explicitly. For NATIVE windows this is usually true (the
207 * framework's native-window opener passes the registry's
208 * declared dimensions); for admin-page iframe windows opened
209 * from the dock this is usually false. The filter is free to
210 * override registry defaults — `callerPinned: true` does NOT
211 * mean "leave it alone."
212 *
213 * The shell re-clamps `width`/`height` to the registered
214 * `minWidth`/`minHeight` after the filter returns — a buggy
215 * filter cannot ship a sub-minimum window. `x` and `y` are
216 * NOT re-clamped to the desktop rect after the filter (plugins
217 * sometimes want to place windows partially off-screen for
218 * deliberate stylistic reasons); the filter is responsible for
219 * its own viewport math when it cares.
220 *
221 * Companion of `desktop_mode_register_window` server-side
222 * defaults — runs every time a window opens, not just at
223 * registration.
224 *
225 * @since 0.25.0
226 */
227 WINDOW_GEOMETRY: "desktop-mode.window.geometry",
228 /** Action, fires when a window is added to the stack. */
229 WINDOW_OPENED: "desktop-mode.window.opened",
230 /**
231 * Action, fires when a window's body enters the loading state — at
232 * construction (every window starts loading) and whenever a plugin
233 * calls {@link NativeRenderContext.window.markLoading} or
234 * `Window.markContentLoading()` mid-life. Payload: `{ windowId }`.
235 *
236 * The shell shows a `<wpd-spinner>` overlay while the window is in
237 * the loading state and fades content in on the loaded transition.
238 * Subscribe to this hook (or to {@link WINDOW_CONTENT_LOADED}) when
239 * you need to react to either edge — analytics, instrumentation,
240 * decorating the spinner with a per-window message.
241 *
242 * Edge-triggered: idempotent calls don't re-fire. The matching
243 * `desktop-mode-window-content-loading` CustomEvent dispatches on
244 * `document` with the same payload.
245 *
246 * @since 0.6.0
247 */
248 WINDOW_CONTENT_LOADING: "desktop-mode.window.content-loading",
249 /**
250 * Action, fires when a window's body content becomes ready — for
251 * iframe windows the moment the chromeless bridge announces
252 * `desktop-mode-ready`, for native windows after the user's
253 * `render( body )` callback (or its returned promise) resolves, and
254 * whenever a plugin calls {@link NativeRenderContext.window.markReady}
255 * or `Window.markContentLoaded()` mid-life. Payload: `{ windowId }`.
256 *
257 * The unified "window content is ready" signal across both render
258 * strategies — use this instead of branching on iframe vs. native.
259 * Iframe-only consumers can still subscribe to {@link IFRAME_READY},
260 * which fires alongside this hook for iframe windows. The shell
261 * removes the loading overlay and fades the content in on this
262 * transition.
263 *
264 * Edge-triggered: only fires on a loading → ready transition.
265 * The matching `desktop-mode-window-content-loaded` CustomEvent
266 * dispatches on `document` with the same payload.
267 *
268 * @since 0.6.0
269 */
270 WINDOW_CONTENT_LOADED: "desktop-mode.window.content-loaded",
271 /**
272 * Filter, applied to the loading-overlay HTMLElement just after
273 * the shell paints its default `<wpd-spinner>` and after any
274 * per-window inline customization (`config.loading.render`)
275 * runs. Receives the overlay element; context: `{ windowId,
276 * config }`. Plugins may mutate the element (e.g.
277 * `host.replaceChildren( myBrandedLoader )` to swap out the
278 * default entirely, or `host.querySelector('wpd-spinner')!.
279 * setAttribute('preset', 'comet')` to retune the spinner) or
280 * return a different element to replace the overlay wholesale.
281 *
282 * Use cases: a brand-skin plugin that overrides every window's
283 * spinner with its own logo; a status-bar plugin that adds
284 * "Loading… 47% — fetching posts" text; an A/B-test framework
285 * that swaps the loader during an experiment.
286 *
287 * Resolution order for the loading overlay:
288 * 1. Default content (`<wpd-spinner>`) is painted.
289 * 2. Per-window `config.loading.render( host, ctx )` runs.
290 * 3. This filter runs.
291 * 4. The result is appended to the window body.
292 *
293 * @since 0.6.0
294 */
295 WINDOW_LOADING_OVERLAY: "desktop-mode.window.loading-overlay",
296 /**
297 * Action, fires when `manager.open(...)` is called for a baseId
298 * whose window already exists on the active desktop. This is the
299 * unambiguous "user requested to open this window again" signal
300 * — distinct from focus changes (which double-fire on alt-tab and
301 * skip when already focused) and from `WINDOW_OPENED` (which only
302 * fires on first creation). Payload:
303 * `{ windowId: string, baseId: string, wasMinimized: boolean }`.
304 *
305 * Plugins that hold per-window state (e.g. the code-editor's
306 * active file) should listen here to re-orient the existing
307 * window's content to whatever the caller wants to show — the
308 * open-window call is synchronous, so any state the caller sets
309 * BEFORE invoking `openWindow` is already in place when this
310 * fires.
311 */
312 WINDOW_REOPENED: "desktop-mode.window.reopened",
313 /**
314 * Action, fires BEFORE the window's element is detached from the
315 * DOM but AFTER the manager has already removed it from the stack.
316 * Payload: `{ windowId: string, element: HTMLElement }`.
317 *
318 * Use this for cleanup that needs a reference to the live
319 * element (removing anchored snow, wallpaper particles pinned to
320 * window tops, measurement caches keyed by element). `WINDOW_CLOSED`
321 * fires immediately after and only carries the id, which means
322 * subscribers would otherwise have to re-query the DOM — by then
323 * the element is gone, so they can't match at all.
324 */
325 WINDOW_CLOSING: "desktop-mode.window.closing",
326 /** Action, fires when a window is removed from the stack. */
327 WINDOW_CLOSED: "desktop-mode.window.closed",
328 /** Action, fires when focus changes to a different window. */
329 WINDOW_FOCUSED: "desktop-mode.window.focused",
330 /**
331 * Action, fires for the window that LOST focus when another
332 * window takes over. Symmetric counterpart to
333 * `WINDOW_FOCUSED`. Payload: `{ windowId: string, focusedTo:
334 * string | null }` — `focusedTo` identifies the new top of
335 * the stack so blur subscribers can ignore alt-tabs to a
336 * sibling they own.
337 *
338 * No-op when there's no previously-focused window (initial
339 * boot, all-windows-closed). Manager fires this BEFORE
340 * `WINDOW_FOCUSED` so subscribers see "blur old, focus new"
341 * in deterministic order.
342 *
343 * @since 0.5.5
344 */
345 WINDOW_BLURRED: "desktop-mode.window.blurred",
346 /**
347 * Action, fires when a window is minimized. Payload:
348 * `{ windowId: string, element: HTMLElement }`.
349 *
350 * The element ride-along matches {@link WINDOW_CLOSING}'s shape so
351 * wallpaper plugins anchored to window tops (snow, leaves, rain
352 * splash) can match stuck particles by element identity and run
353 * their teardown — minimized windows render at `opacity: 0` so
354 * `offsetParent === null` checks miss them.
355 */
356 WINDOW_MINIMIZED: "desktop-mode.window.minimized",
357 /**
358 * Action, fires when a window is restored from minimized. Payload:
359 * `{ windowId: string, element: HTMLElement }`.
360 */
361 WINDOW_RESTORED: "desktop-mode.window.restored",
362 /**
363 * Action, fires when a window is maximized (fills desktop area).
364 * Payload: `{ windowId: string, element: HTMLElement }`.
365 */
366 WINDOW_MAXIMIZED: "desktop-mode.window.maximized",
367 /**
368 * Action, fires when a window exits maximized state. Payload:
369 * `{ windowId: string, element: HTMLElement }`.
370 */
371 WINDOW_UNMAXIMIZED: "desktop-mode.window.unmaximized",
372 /**
373 * Action, fires when a window enters fullscreen / focus mode.
374 * Payload: `{ windowId: string, element: HTMLElement }`.
375 */
376 WINDOW_FULLSCREEN_ENTERED: "desktop-mode.window.fullscreen-entered",
377 /**
378 * Action, fires when a window exits fullscreen / focus mode.
379 * Payload: `{ windowId: string, element: HTMLElement }`.
380 */
381 WINDOW_FULLSCREEN_EXITED: "desktop-mode.window.fullscreen-exited",
382 /**
383 * Filter, decides whether a fullscreen ("focus mode") window
384 * should auto-exit when focus moves to a different window.
385 *
386 * Default is `true` so a newly-focused window is never silently
387 * occluded by a fullscreen one (its `z-index` sits above all
388 * other windows). Plugins whose fullscreen surface is meant to
389 * persist across focus changes — slideshows, video players,
390 * immersive games — can return `false` to keep their window
391 * fullscreen.
392 *
393 * Signature:
394 *
395 * ( shouldExit: boolean, ctx: {
396 * windowId: string, // the fullscreen window
397 * focusedTo: string, // the window gaining focus
398 * } ) => boolean
399 *
400 * @since 0.8.6
401 */
402 WINDOW_AUTO_EXIT_FULLSCREEN: "desktop-mode.window.auto-exit-fullscreen",
403 /**
404 * Action, fires at most once per animation frame during an
405 * active drag or resize with the live geometry. Payload: `{
406 * windowId: string, x: number, y: number, width: number,
407 * height: number, state: WindowState, phase: 'drag' | 'resize' }`.
408 *
409 * Intended for per-frame collision-aware wallpapers (snow piling
410 * on window tops, rain splash on edges) that would otherwise
411 * poll `getBoundingClientRect` every rAF. Coalesced via
412 * `requestAnimationFrame` so a pointermove storm collapses to
413 * one fire per paint — matches the cadence a wallpaper's own
414 * ticker runs at.
415 *
416 * NOT fired at drag/resize end — `WINDOW_DRAG_END` /
417 * `WINDOW_RESIZE_END` handle the settled geometry. Subscribers
418 * that only want the final position should listen to those
419 * instead.
420 */
421 WINDOW_BOUNDS_CHANGED: "desktop-mode.window.bounds-changed",
422 /** Action, fires at drag-end with the final `{ x, y }` position. */
423 WINDOW_MOVED: "desktop-mode.window.moved",
424 /** Action, fires at resize-end with the final `{ width, height }`. */
425 WINDOW_RESIZED: "desktop-mode.window.resized",
426 /** Action, fires when title-bar drag begins. */
427 WINDOW_DRAG_START: "desktop-mode.window.drag-start",
428 /** Action, fires when title-bar drag ends. Payload mirrors WINDOW_MOVED. */
429 WINDOW_DRAG_END: "desktop-mode.window.drag-end",
430 /** Action, fires when the resize handle is first pressed. */
431 WINDOW_RESIZE_START: "desktop-mode.window.resize-start",
432 /** Action, fires when resize completes. Payload mirrors WINDOW_RESIZED. */
433 WINDOW_RESIZE_END: "desktop-mode.window.resize-end",
434 /** Action, fires when the user "detaches" a window to a classic tab. */
435 WINDOW_DETACHED: "desktop-mode.window.detached",
436 /**
437 * Action, fires when the user clicks the title-bar reload button
438 * on an iframe-backed window. Payload: `{ windowId: string, url:
439 * string }` where `url` is the URL being reloaded (the active
440 * primary or external sub-tab). Subscribers can use this to
441 * invalidate their own cache, force a save before navigation,
442 * track usage as a UX signal, or sync state across companion
443 * surfaces. Native windows do not fire this — they own their
444 * DOM directly and the reload button doesn't apply.
445 */
446 WINDOW_RELOADED: "desktop-mode.window.reloaded",
447 /** Action, fires when iframe title updates change the window title. */
448 WINDOW_TITLE_CHANGED: "desktop-mode.window.title-changed",
449 /**
450 * Action, fires when a window's `setHighlight()` mode changes.
451 * Payload: `{ windowId: string, mode: 'preview' | 'persistent' | null,
452 * color?: string }`. Lets onboarding / guidance / drag-bridge
453 * plugins react when another module flagged one of their
454 * windows as the focus of a multi-step interaction without
455 * having to observe DOM mutations.
456 *
457 * @since 0.24.0
458 */
459 WINDOW_HIGHLIGHT_CHANGED: "desktop-mode.window.highlight-changed",
460 /**
461 * Action, fires when a window's body element's dimensions
462 * change — mount, user resize, viewport reflow. Payload: `{
463 * windowId: string, width: number, height: number }`. Body
464 * dimensions exclude the title bar + tab strip, matching what a
465 * canvas or layout engine inside the body would measure.
466 */
467 WINDOW_BODY_RESIZED: "desktop-mode.window.body-resized",
468 // ------------------------------------------------------------------
469 // Native-window lifecycle. These fire ONLY for windows constructed
470 // with `native: true` — iframe windows have no render phase to
471 // intercept. Use them to wrap / instrument / cancel the paint of
472 // plugin-contributed native windows (the Calculator, Jorvy, custom
473 // native launchers).
474 // ------------------------------------------------------------------
475 /**
476 * Filter, applied to the body element a native window will render
477 * into, just BEFORE the user's `render( body )` callback runs.
478 * Payload: the `HTMLElement`; context: `{ windowId, config }`.
479 *
480 * Return the same element (or a wrapper) to intercept. Subscribers
481 * commonly use this to inject a consistent shell (padding,
482 * background, decorative chrome) around every native window
483 * without every plugin re-implementing the pattern.
484 */
485 NATIVE_WINDOW_BEFORE_RENDER: "desktop-mode.native-window.before-render",
486 /**
487 * Action, fires AFTER a native window's `render( body )` callback
488 * returns. Payload: `{ windowId, body, config }`. Observability
489 * hook — analytics / auto-focus / post-render measurement.
490 */
491 NATIVE_WINDOW_AFTER_RENDER: "desktop-mode.native-window.after-render",
492 /**
493 * Filter, applied when a native window is about to start its
494 * close animation. Return `false` to CANCEL the close — the
495 * window stays open. Payload: `true`; context: `{ windowId,
496 * config }`. Any non-`false` return (including `undefined`) lets
497 * the close proceed.
498 *
499 * Intended for "unsaved changes" guards: a calculator with a
500 * pending operation can prompt the user and abort the close
501 * mid-flight. Does NOT apply to iframe windows — their close is
502 * driven by browser navigation patterns the shell doesn't own.
503 */
504 NATIVE_WINDOW_BEFORE_CLOSE: "desktop-mode.native-window.before-close",
505 // ------------------------------------------------------------------
506 // Window-chrome customization framework. Plugins drive per-window
507 // appearance (theme, controls, slots, full chrome render) through
508 // the `wp.desktop.registerWindow*` registries; these hooks expose
509 // every resolution step so plugins can mutate or observe the
510 // chrome pipeline without owning a registration.
511 //
512 // Layers 1-3 (theme, controls, slots) are Stable. Layer 4 (chrome
513 // render) is Experimental — `WINDOW_CHROME_RENDER` may change.
514 // ------------------------------------------------------------------
515 /**
516 * Filter, applied to the resolved CSS-variable map for a window.
517 * Receives `Record< string, string >`; context: `{ windowId,
518 * config }`. Plugins return a mutated map to override or augment
519 * the per-window theme tokens — e.g. tint every Gutenberg
520 * window's title bar to brand colour.
521 *
522 * Stable since 0.6.0.
523 */
524 WINDOW_CHROME_THEME: "desktop-mode.window.chrome.theme",
525 /**
526 * Filter, applied to the resolved control list for a window.
527 * Receives `WindowControlDef[]`; context: `{ windowId, config,
528 * placement: 'left' | 'right' | 'controls' }`. Plugins return a
529 * mutated array to reorder, hide, or inject controls per-window.
530 *
531 * Stable since 0.6.0.
532 */
533 WINDOW_CHROME_CONTROLS: "desktop-mode.window.chrome.controls",
534 /**
535 * Filter, applied per slot when the chrome paints. Receives the
536 * slot host element; context: `{ windowId, slot, config }`.
537 * Plugins can mutate `host` (append decorative children, set
538 * inline styles) without owning a `WindowSlotDef` registration.
539 * The shell never reads the return value — this is an action-
540 * shaped filter so existing `addFilter` plumbing applies.
541 *
542 * Stable since 0.6.0.
543 */
544 WINDOW_CHROME_SLOT: "desktop-mode.window.chrome.slot",
545 /**
546 * Filter, applied to the chrome id selected for a window.
547 * Receives the resolved id (defaults to `'core/standard'`);
548 * context: `{ windowId, config }`. Returning a different id
549 * swaps the chrome registration. **Experimental** — chrome
550 * render contract may change.
551 *
552 * @since 0.6.0
553 */
554 WINDOW_CHROME_RENDER: "desktop-mode.window.chrome.render",
555 /**
556 * Action, fires after a window's chrome has been mounted /
557 * remounted. Payload: `{ windowId, chromeId }`. Subscribers can
558 * post-decorate the chrome (attach observers, anchor pickers).
559 *
560 * @since 0.6.0
561 */
562 WINDOW_CHROME_APPLIED: "desktop-mode.window.chrome.applied",
563 /**
564 * Action, fires after a window's theme tokens are applied to its
565 * outer element. Payload: `{ windowId, themeId, tokens }`. Lets
566 * plugins react to theme changes without diffing CSS variables.
567 *
568 * @since 0.6.0
569 */
570 WINDOW_CHROME_THEME_CHANGED: "desktop-mode.window.chrome.theme-changed",
571 /**
572 * Action, fires when a user clicks a desktop icon (a shortcut
573 * tile registered server-side via `desktop_mode_register_icon()`
574 * and rendered on the wallpaper). Payload: `{ id: string,
575 * target: 'window' | 'url' }`. Fires BEFORE the default open
576 * action — plugins cannot cancel the open from this hook, but
577 * can use it to track click-throughs or augment behaviour (e.g.
578 * play a sound, surface a confirmation toast).
579 *
580 * @since 0.11.0
581 */
582 DESKTOP_ICON_CLICKED: "desktop-mode.desktop-icon.clicked",
583 /**
584 * Action, fires after the wallpaper icon grid is rendered or
585 * re-rendered. Payload:
586 *
587 * {
588 * ids: string[]; // paint order
589 * container: HTMLElement; // <div class="desktop-mode-icons">
590 * tiles: ReadonlyMap<string, HTMLElement>; // id → tile <button>
591 * }
592 *
593 * Plugins that decorate icons with surfaces the framework doesn't
594 * natively expose (drag handles, status dots, cursor adornments)
595 * subscribe here so their decorations survive a live menu refresh
596 * that legitimately rebuilds the grid. The `container` and
597 * `tiles` map mirror the {@link DOCK_AFTER_RENDER}
598 * `tileElements` contract — reach into them directly instead of
599 * re-`querySelector`ing the rendered DOM.
600 *
601 * Notification badges have a first-class API since 0.24.0 —
602 * use `wp.desktop.icons.setBadge( id, count )` (and subscribe
603 * to {@link ICON_BADGE_CHANGED}) instead of decorating from
604 * here. The framework persists badge state across rebuilds, so
605 * a plugin that uses the API doesn't need to re-decorate on
606 * every render.
607 *
608 * Suppressed entirely when the rendered DOM is unchanged from
609 * the previous call (the fingerprint short-circuit upstream
610 * skips both the rebuild and this signal). When the icon list
611 * is empty the hook does not fire at all — the previous
612 * container is removed and no new one is appended.
613 *
614 * @since 0.21.0
615 * @since 0.25.0 — `container` + `tiles` added to the payload
616 * (`ids` retained for back-compat).
617 */
618 DESKTOP_ICONS_RENDERED: "desktop-mode.desktop-icons.rendered",
619 /**
620 * Action, fires whenever the badge count on a desktop icon
621 * changes. Payload: `{ iconId: string, count: number,
622 * previousCount: number }`. Symmetric to {@link DOCK_ITEM_APPENDED}
623 * and the dock/taskbar `wpd-dock-item-badge-changed` CustomEvent
624 * — the icon rail's lifecycle hook for badge transitions.
625 *
626 * Mirrors `desktop-mode/badge-changed` on the activity bus with
627 * `rail: 'icon'`. Subscribe to whichever surface fits — the
628 * activity channel composes across rails for global widgets,
629 * this hook fires only for icon-rail badges with the previous
630 * count carried alongside for delta-aware consumers.
631 *
632 * @since 0.24.0
633 */
634 ICON_BADGE_CHANGED: "desktop-mode.icon.badge-changed",
635 // ------------------------------------------------------------------
636 // Cross-plugin composition.
637 // ------------------------------------------------------------------
638 /**
639 * Action, fires ONCE after every shell-shipped `<wpd-*>` custom
640 * element has registered with `customElements`. Payload: `{
641 * tags: string[] }` — the list of registered tag names. Plugins
642 * that need to defer work until the component registry is
643 * complete (e.g. hydrate user content that uses these tags)
644 * subscribe here instead of polling `customElements.get()`.
645 */
646 COMPONENTS_REGISTERED: "desktop-mode.components.registered",
647 /**
648 * Action, fires after `wp.desktop.registerSystemTile()` inserts
649 * a tile into the unified dock. Payload: `{ id: string }`. Useful
650 * for plugins that want to decorate tiles they didn't register
651 * themselves — analytics, theming, per-tile badges.
652 */
653 DOCK_ITEM_APPENDED: "desktop-mode.dock.item-appended",
654 /**
655 * Action, fires after a system tile is removed from a rail
656 * via `Dock.removeSystemItem()` (typically the server-driven
657 * native-window-sync path on plugin deactivation). Payload:
658 * `{ id: string, placement: 'dock' | 'taskbar' }`. Symmetric
659 * to {@link DOCK_ITEM_APPENDED}; lets analytics / decorators /
660 * cleanup hooks see the full lifecycle without polling the DOM.
661 *
662 * @since 0.24.0
663 */
664 DOCK_ITEM_REMOVED: "desktop-mode.dock.item-removed",
665 // ------------------------------------------------------------------
666 // Dock decoration hooks — render-pipeline filters and actions the
667 // default `Dock` renderer fires while painting tiles. Plugins
668 // compose decoration (animations, classNames, wrappers, tooltips)
669 // without forking the renderer. Custom rail renderers SHOULD fire
670 // the same hooks for ecosystem compatibility — see
671 // `docs/examples/dock-decoration-hooks.md` for the contract.
672 //
673 // Every detail object carries `{ rail, orientation, dockId,
674 // container }` so a single subscriber can disambiguate when two
675 // rails coexist (Classic layout's left side bar + bottom dock).
676 // `dockId` matches the host element's `id` (e.g. `'desktop-mode-dock'`
677 // or `'desktop-mode-side-dock'`) and is the stable
678 // disambiguator — `rail` and `orientation` are convenience
679 // projections of where the renderer is painting.
680 // ------------------------------------------------------------------
681 /**
682 * Action, fires at the start of every dock paint pass — both the
683 * initial mount and every `replaceItems()` that follows on the
684 * live menu-refresh path. Payload `DockRenderContext`. Use this
685 * to invalidate cached per-render decoration state before the
686 * tiles repopulate.
687 *
688 * @since 0.18.0
689 */
690 DOCK_BEFORE_RENDER: "desktop-mode.dock.before-render",
691 /**
692 * Action, fires once every menu and system tile has landed in
693 * the DOM for a paint pass. Payload `DockRenderContext` plus a
694 * frozen `tileElements: ReadonlyMap<string, HTMLElement>` so a
695 * plugin can decorate every tile in one sweep. Symmetric to
696 * {@link DOCK_BEFORE_RENDER}.
697 *
698 * @since 0.18.0
699 */
700 DOCK_AFTER_RENDER: "desktop-mode.dock.after-render",
701 /**
702 * Filter, runs once per tile while the renderer is composing the
703 * className list. Plugins may add, remove, or reorder classes.
704 * Signature: `( classes: string[], detail: DockTileContext ) =>
705 * string[]`. Order is preserved.
706 *
707 * @since 0.18.0
708 */
709 DOCK_TILE_CLASS: "desktop-mode.dock.tile-class",
710 /**
711 * Filter, runs once per tile after the renderer finishes building
712 * the element but before it lands in the DOM. Return the same
713 * element with mutations, or replace with a wrapper — the shell
714 * inserts whatever you return. Signature:
715 * `( el: HTMLElement, detail: DockTileContext ) => HTMLElement`.
716 *
717 * Returning a different node still has to expose a stable
718 * `[data-menu-slug="<id>"]` (or `[data-system-id="<id>"]`)
719 * descendant for active-state / badge updates to find the tile;
720 * wrap, don't replace.
721 *
722 * @since 0.18.0
723 */
724 DOCK_TILE_ELEMENT: "desktop-mode.dock.tile-element",
725 /**
726 * Action, fires once per tile after it has been inserted into
727 * the DOM. Payload `DockTileContext` plus the resolved `el`. Use
728 * for post-insertion decoration where computed layout matters
729 * (measurements, IntersectionObserver bindings, etc.).
730 *
731 * @since 0.18.0
732 */
733 DOCK_TILE_RENDERED: "desktop-mode.dock.tile-rendered",
734 /**
735 * Filter, resolves the tooltip text for a tile. Runs once at
736 * bind time so the dock doesn't re-filter on every pointerenter.
737 * Signature: `( label: string, detail: DockTileContext ) =>
738 * string`. Return an empty string to suppress the tooltip.
739 *
740 * @since 0.18.0
741 */
742 DOCK_TILE_TOOLTIP: "desktop-mode.dock.tile-tooltip",
743 /**
744 * Filter, resolves the body content of a single hover-peek card.
745 * Runs once per card build (i.e., on every show of the peek for
746 * a multi-instance dock tile that has ≥1 open window). Lets a
747 * plugin render a custom thumbnail, status block, or any other
748 * markup inside the card in place of (or alongside) the default
749 * mini-window styling.
750 *
751 * Signature:
752 * ( body: HTMLElement, detail: DockPeekCardContext ) => HTMLElement
753 *
754 * Where `body` is the `<span class="desktop-mode-dock-peek__card-body">`
755 * element that the peek would otherwise populate with ghosted
756 * content lines. The filter may:
757 * - Mutate `body` in place (e.g., append a custom child) and
758 * return it.
759 * - Empty `body` and append plugin-owned children.
760 * - Return an entirely different element to replace `body`.
761 *
762 * `detail.window` is the live `Window` instance the card represents
763 * — plugins can read `window.config`, call `window.getCurrentUrl()`,
764 * subscribe to lifecycle events, etc. `detail.item` is the dock
765 * item descriptor (id / title / icon / url).
766 *
767 * The filter is invoked under the `applyFilters` namespace
768 * `desktop-mode.dock.peek-card-content`.
769 *
770 * @since 0.6.2
771 */
772 DOCK_PEEK_CARD_CONTENT: "desktop-mode.dock.peek-card-content",
773 /**
774 * Filter, runs once per peek card right before it's appended to
775 * the popover. Receives the fully-built default card (with its
776 * mini-window chrome already populated) and can return either
777 * the same node, a mutated version, or an entirely different
778 * element to replace the card outright. Use this when the
779 * `peek-card-content` body filter isn't enough — e.g., when a
780 * plugin wants to swap the whole card chrome (custom titlebar,
781 * different shape) or wrap the card in a third-party component.
782 *
783 * Signature:
784 * ( card: HTMLElement, detail: DockPeekCardContext ) => HTMLElement
785 *
786 * If a plugin returns a brand-new node, it is responsible for
787 * preserving anything the peek relies on:
788 * - The `desktop-mode-dock-peek__card` class (used by the
789 * fan-out animation timing + hover styles).
790 * - A `click` handler if the card should still focus the
791 * window. The default click handler lives on the original
792 * node — replacing the node loses it.
793 *
794 * @since 0.6.2
795 */
796 DOCK_PEEK_CARD_ELEMENT: "desktop-mode.dock.peek-card-element",
797 // ------------------------------------------------------------------
798 // Overview / Arrange lifecycle actions.
799 //
800 // The "Arrange" admin-bar menu drives two layout algorithms —
801 // Cascade (instantly reposition every window in a staggered
802 // stack) and Overview (zoom-out grid view with click-to-focus).
803 // These hooks surface the state transitions so plugins can
804 // instrument analytics, apply custom transitions, override
805 // thumbnail decorations, etc. All actions; a filter for
806 // mutating the overview layout may be added later if plugins
807 // want to reorder or group thumbnails.
808 // ------------------------------------------------------------------
809 /** Action, fires before the overview enter animation starts. */
810 OVERVIEW_ENTERING: "desktop-mode.overview.entering",
811 /** Action, fires once the overview enter animation has completed. */
812 OVERVIEW_ENTERED: "desktop-mode.overview.entered",
813 /**
814 * Action, fires at the start of the overview-exit animation.
815 * Payload: `{ windowId?: string, reason: 'select' | 'cancel' }` —
816 * `windowId` set when the user clicked a thumbnail (reason
817 * 'select'); omitted when the user pressed Escape or clicked
818 * the backdrop (reason 'cancel').
819 */
820 OVERVIEW_EXITING: "desktop-mode.overview.exiting",
821 /** Action, fires once the overview-exit animation has settled. */
822 OVERVIEW_EXITED: "desktop-mode.overview.exited",
823 /** Action, fires when the cursor enters a thumbnail. Payload `{ windowId }`. */
824 OVERVIEW_WINDOW_HOVER: "desktop-mode.overview.window-hover",
825 /** Action, fires when the cursor leaves a thumbnail. Payload `{ windowId }`. */
826 OVERVIEW_WINDOW_UNHOVER: "desktop-mode.overview.window-unhover",
827 /** Action, fires the instant a thumbnail click is registered (before exit + maximize kick in). Payload `{ windowId }`. */
828 OVERVIEW_WINDOW_CLICK: "desktop-mode.overview.window-click",
829 /** Action, fires before cascade computes + applies new positions. Payload `{ windowCount }`. */
830 ARRANGE_CASCADE_STARTING: "desktop-mode.arrange.cascade.starting",
831 /** Action, fires after cascade has positioned every window. Payload `{ windowCount }`. */
832 ARRANGE_CASCADE_APPLIED: "desktop-mode.arrange.cascade.applied",
833 /** Action, fires before tile computes + applies new positions. Payload `{ windowCount, cols, rows }`. */
834 ARRANGE_TILE_STARTING: "desktop-mode.arrange.tile.starting",
835 /** Action, fires after tile has positioned every window. Payload `{ windowCount, cols, rows }`. */
836 ARRANGE_TILE_APPLIED: "desktop-mode.arrange.tile.applied",
837 /**
838 * Filter on the tile-grid dimensions chosen by the built-in
839 * algorithm. Receives `{ cols, rows }` plus a context arg
840 * `{ windowCount, areaWidth, areaHeight }`. Plugins can return
841 * a different `{ cols, rows }` to enforce a custom layout
842 * (fixed-column newsroom, golden-ratio cells, etc.). Returned
843 * values are validated — non-positive integers, or a product
844 * smaller than `windowCount`, fall back to the original.
845 */
846 ARRANGE_TILE_DIMENSIONS: "desktop-mode.arrange.tile.dimensions",
847 /** Action, fires when snap-to-grid is toggled. Payload `{ enabled }`. */
848 ARRANGE_SNAP_CHANGED: "desktop-mode.arrange.snap.changed",
849 /**
850 * Filter on the snap-grid cell size. Receives
851 * `{ cellWidth, cellHeight }` plus a context arg
852 * `{ areaWidth, areaHeight }`. Plugins can return different
853 * dimensions to enforce a Tetris-style fixed grid, a musical
854 * staff aspect, etc. Non-positive returns fall back to the
855 * original.
856 */
857 ARRANGE_SNAP_CELL_SIZE: "desktop-mode.arrange.snap.cell-size",
858 /**
859 * Action, fires when the user clicks a plugin-registered entry in
860 * the Arrange admin-bar submenu (items added via the
861 * `desktop_mode_arrange_menu_items` PHP filter). Payload `{ id }`
862 * where `id` is the item's `id` field as registered. Plugins
863 * subscribe here to run their custom arrangement logic.
864 */
865 ARRANGE_CUSTOM_ACTION: "desktop-mode.arrange.custom-action",
866 // ------------------------------------------------------------------
867 // Snap-zones — Windows-style edge snapping with a split-overview
868 // picker to fill the opposite half after commit.
869 // ------------------------------------------------------------------
870 /**
871 * Action, fires when the drag cursor enters a snap zone and the
872 * shell shows the target-position preview. Payload
873 * `{ windowId, zone: 'left' | 'right' }`.
874 */
875 SNAP_ZONE_PENDING: "desktop-mode.snap.zone-pending",
876 /**
877 * Action, fires when the drag cursor leaves the snap zone without
878 * releasing — the preview disappears. Payload `{ windowId }`.
879 */
880 SNAP_ZONE_CANCELED: "desktop-mode.snap.zone-canceled",
881 /**
882 * Action, fires once the window has animated into its snapped
883 * bounds. Payload `{ windowId, zone: 'left' | 'right' }`.
884 */
885 SNAP_ZONE_COMMITTED: "desktop-mode.snap.zone-committed",
886 /**
887 * Action, fires when a user picks a thumbnail from the split
888 * overview to fill the opposite half. Payload
889 * `{ windowId, zone: 'left' | 'right' }`.
890 */
891 SNAP_SPLIT_FILLED: "desktop-mode.snap.split-filled",
892 // ------------------------------------------------------------------
893 // Widgets — the right-side column. Widgets paint above the
894 // wallpaper but beneath windows. Lifecycle mirrors canvas
895 // wallpapers: register via filter, mount/unmount actions bracket
896 // each paint, mount-failed fires on sync throws / async rejects.
897 // ------------------------------------------------------------------
898 /** Filter, receives the widget registry array. */
899 WIDGETS: "desktop-mode.widgets",
900 /** Action before a widget mounts. Payload `{ id, container, ctx }`. */
901 WIDGET_MOUNTING: "desktop-mode.widget.mounting",
902 /** Action after a widget mounts successfully. Payload `{ id, container, ctx }`. */
903 WIDGET_MOUNTED: "desktop-mode.widget.mounted",
904 /** Action before a widget tears down. Payload `{ id }`. */
905 WIDGET_UNMOUNTING: "desktop-mode.widget.unmounting",
906 /** Action when a widget's mount throws / rejects. Payload `{ id, error }`. */
907 WIDGET_MOUNT_FAILED: "desktop-mode.widget.mount-failed",
908 /** Action when the user adds a widget via the picker. Payload `{ id }`. */
909 WIDGET_ADDED: "desktop-mode.widget.added",
910 /** Action when the user removes a widget via the card's × button. Payload `{ id }`. */
911 WIDGET_REMOVED: "desktop-mode.widget.removed",
912 // ------------------------------------------------------------------
913 // Virtual-desktop ("Spaces") lifecycle actions.
914 //
915 // Spaces let users group windows into separate workspaces and flip
916 // between them from the overview top bar. These hooks expose every
917 // state change so plugins can persist per-space state, sync custom
918 // indicators, or react to the user's workspace context.
919 // ------------------------------------------------------------------
920 /** Action, fires when a new desktop is created. Payload `{ desktopId }`. */
921 DESKTOP_CREATED: "desktop-mode.desktop.created",
922 /** Action, fires when a desktop is closed. Payload `{ desktopId, migratedTo }`. */
923 DESKTOP_CLOSED: "desktop-mode.desktop.closed",
924 /** Action, fires when the active desktop changes. Payload `{ from, to }`. */
925 DESKTOP_SWITCHED: "desktop-mode.desktop.switched",
926 /**
927 * Filter. Returns the id of the "primary" desktop — the one the
928 * shell treats as canonical for batch operations. Receives the
929 * default (first desktop's id) and the full `Desktop[]` list.
930 * @since 0.14.0
931 */
932 PRIMARY_DESKTOP_ID: "desktop-mode.primary-desktop-id",
933 // ------------------------------------------------------------------
934 // Batch window operations.
935 // ------------------------------------------------------------------
936 /**
937 * Action, fires before {@link WindowManager.closeAll} starts
938 * iterating. Payload `{ candidates: Window[] }` — every window the
939 * shell is about to close (after `exceptIds` was applied).
940 * @since 0.14.0
941 */
942 WINDOWS_BEFORE_CLOSE_ALL: "desktop-mode.windows.before-close-all",
943 /**
944 * Filter, runs inside {@link WindowManager.closeAll}. Receives the
945 * candidate `Window[]` list and returns the (possibly trimmed) list
946 * that will actually be closed. Plugins use this to PROTECT specific
947 * windows from a bulk close — e.g. keep the active draft open.
948 * Returning an empty array cancels the close entirely.
949 * @since 0.14.0
950 */
951 WINDOWS_CLOSE_ALL: "desktop-mode.windows.close-all",
952 /**
953 * Action, fires after {@link WindowManager.closeAll} has finished.
954 * Payload `{ closed: number, skipped: Window[] }`.
955 * @since 0.14.0
956 */
957 WINDOWS_AFTER_CLOSE_ALL: "desktop-mode.windows.after-close-all",
958 // ------------------------------------------------------------------
959 // Slash-command lifecycle.
960 // ------------------------------------------------------------------
961 /**
962 * Filter. Runs immediately before a command's `run()` is invoked.
963 * Receives `{ proceed: true, slug, args, command }` and may return
964 * the same shape with `proceed: false` to cancel the run.
965 * @since 0.14.0
966 */
967 COMMAND_BEFORE_RUN: "desktop-mode.command.before-run",
968 /**
969 * Action, fires after a command's `run()` resolves successfully.
970 * Payload `{ slug, args, command, result }`.
971 * @since 0.14.0
972 */
973 COMMAND_AFTER_RUN: "desktop-mode.command.after-run",
974 /**
975 * Action, fires when a command's `run()` throws. Payload
976 * `{ slug, args, command, error }`.
977 * @since 0.14.0
978 */
979 COMMAND_ERROR: "desktop-mode.command.error",
980 // ------------------------------------------------------------------
981 // Shell-level lifecycle actions.
982 // ------------------------------------------------------------------
983 /**
984 * Action, fires (debounced) after the browser viewport stops
985 * resizing. Payload `{ width, height }` describes the shell's
986 * bounding rect — plugins that render canvas-driven UIs hook here
987 * to adjust their render surface.
988 */
989 SHELL_RESIZED: "desktop-mode.shell.resized",
990 /**
991 * Action mirroring `document.visibilitychange` for the shell as a
992 * whole. Payload `{ state: 'visible' | 'hidden' }`. Different from
993 * the wallpaper-specific visibility action in that it fires
994 * regardless of which wallpaper (if any) is active.
995 */
996 SHELL_VISIBILITY: "desktop-mode.shell.visibility",
997 /**
998 * Action — fires when a `wp.desktop.connect()` connection
999 * completes its iframe handshake. Payload:
1000 * `{ connectionId, targetWindowId, topics }`.
1001 *
1002 * @since 0.17.0
1003 */
1004 CONNECTION_OPENED: "desktop-mode.connection.opened",
1005 /**
1006 * Action — fires when a connection tears down. Payload:
1007 * `{ connectionId, reason: 'disconnect' | 'window-closed' | 'navigated' }`.
1008 *
1009 * @since 0.17.0
1010 */
1011 CONNECTION_CLOSED: "desktop-mode.connection.closed",
1012 /**
1013 * Action — fires for every message routed through a connection.
1014 * Payload: `{ connectionId, topic, direction: 'in' | 'out' }`.
1015 * Used for debug consoles + traffic auditing; high-volume topics
1016 * fire this many times per second, so subscribers should be
1017 * cheap.
1018 *
1019 * @since 0.17.0
1020 */
1021 CONNECTION_MESSAGE: "desktop-mode.connection.message",
1022 /**
1023 * Filter — fires when an iframe calls
1024 * `wp.desktop.iframe.requestConnection()`. Default value is
1025 * `true` (accept). Return `false` to reject, or an object
1026 * `{ topics: string[] }` to accept while narrowing the topic
1027 * list. `$context` carries `{ windowId, requestId, topics }`.
1028 *
1029 * @since 0.18.0
1030 */
1031 IFRAME_CONNECTION_REQUEST: "desktop-mode.iframe.connection-request",
1032 // ------------------------------------------------------------------
1033 // OS-file drop manager (since 0.30.0). Catches files dragged from
1034 // the user's host OS (Finder / Explorer / Nautilus) onto any
1035 // desktop-mode surface and routes them through a confirmation
1036 // dialog before uploading to the Media Library. Authoritative
1037 // constants live in `src/os-file-drop/hooks.ts`; mirrored here so
1038 // every hook the shell fires is reachable from a single `HOOKS`
1039 // import. See `docs/examples/os-file-drop.md`.
1040 // ------------------------------------------------------------------
1041 /** Filter — `(files: File[], ctx) => File[]`, before mime/size check. */
1042 FILE_DROP_FILES_DETECTED: "desktop-mode.drop.files-detected",
1043 /** Action — `{ rejections, context }` for files that failed policy. */
1044 FILE_DROP_FILES_REJECTED: "desktop-mode.drop.files-rejected",
1045 /** Filter — `(entry, ctx) => entry`, per-file dialog defaults. */
1046 FILE_DROP_DIALOG_FIELDS: "desktop-mode.drop.dialog-fields",
1047 /** Filter — `(payload, ctx) => payload | null`, last call before POST. */
1048 FILE_DROP_BEFORE_UPLOAD: "desktop-mode.drop.before-upload",
1049 /** Action — `{ file, fields, context, abort }` once XHR is open and about to send. @since 0.31.0 */
1050 FILE_DROP_UPLOAD_STARTED: "desktop-mode.drop.upload-started",
1051 /** Action — `{ file, fields, context, loaded, total, indeterminate }` per progress tick. @since 0.31.0 */
1052 FILE_DROP_UPLOAD_PROGRESS: "desktop-mode.drop.upload-progress",
1053 /** Action — `{ file, result, fields, context }` after successful upload. `file` since 0.31.0. */
1054 FILE_DROP_AFTER_UPLOAD: "desktop-mode.drop.after-upload",
1055 /** Action — `{ file, error, context }` on upload failure. */
1056 FILE_DROP_UPLOAD_FAILED: "desktop-mode.drop.upload-failed"
1057 };
1058 let _whenReadySeq = 0;
1059 function whenReady(cb) {
1060 if (didAction(HOOKS.INIT) > 0) {
1061 Promise.resolve().then(cb);
1062 return;
1063 }
1064 const ns = `desktop-mode/when-ready-${++_whenReadySeq}`;
1065 addAction(HOOKS.INIT, ns, cb);
1066 }
1067 function isReady() {
1068 return didAction(HOOKS.INIT) > 0;
1069 }
1070 let inflight$1 = null;
1071 function isLoaded$1() {
1072 return !!window.desktopModeWindowSystem;
1073 }
1074 function injectScript$1(scriptUrl) {
1075 return new Promise((resolve2, reject) => {
1076 const existing = document.querySelector(
1077 'script[data-desktop-mode-window-system="1"]'
1078 );
1079 const finish = () => {
1080 if (isLoaded$1()) {
1081 resolve2();
1082 return;
1083 }
1084 reject(
1085 new Error(
1086 "[desktop-mode] window-system bundle loaded but did not register `window.desktopModeWindowSystem`."
1087 )
1088 );
1089 };
1090 if (existing) {
1091 if (isLoaded$1()) {
1092 finish();
1093 } else {
1094 existing.addEventListener("load", finish);
1095 existing.addEventListener(
1096 "error",
1097 () => reject(new Error("failed to load window-system bundle"))
1098 );
1099 }
1100 return;
1101 }
1102 const s = document.createElement("script");
1103 s.src = scriptUrl;
1104 s.async = true;
1105 s.dataset.desktopModeWindowSystem = "1";
1106 s.addEventListener("load", finish);
1107 s.addEventListener(
1108 "error",
1109 () => reject(new Error("failed to load window-system bundle"))
1110 );
1111 document.head.appendChild(s);
1112 });
1113 }
1114 function windowSystemBundleUrl() {
1115 const cfg = window.desktopModeConfig;
1116 return cfg?.windowSystemBundleUrl ?? "";
1117 }
1118 function preloadWindowSystem(scriptUrl) {
1119 if (!scriptUrl || isLoaded$1() || inflight$1) {
1120 return;
1121 }
1122 inflight$1 = injectScript$1(scriptUrl).catch((err) => {
1123 inflight$1 = null;
1124 if (typeof console !== "undefined") {
1125 console.warn(
1126 "[desktop-mode] window-system preload failed; will retry on first open():",
1127 err
1128 );
1129 }
1130 });
1131 }
1132 async function ensureWindowSystemLoaded(scriptUrl) {
1133 if (isLoaded$1()) {
1134 return window.desktopModeWindowSystem;
1135 }
1136 if (!scriptUrl) {
1137 const fn = window.desktopModeWindowSystem;
1138 if (fn) {
1139 return fn;
1140 }
1141 throw new Error(
1142 "[desktop-mode] ensureWindowSystemLoaded(): no bundle URL configured and `window.desktopModeWindowSystem` is not pre-registered."
1143 );
1144 }
1145 if (!inflight$1) {
1146 inflight$1 = injectScript$1(scriptUrl);
1147 }
1148 await inflight$1;
1149 return window.desktopModeWindowSystem;
1150 }
1151 const CANARY_TAG = "wpd-confirm-dialog";
1152 let inflight = null;
1153 function isLoaded() {
1154 return typeof window.customElements !== "undefined" && !!window.customElements.get(CANARY_TAG);
1155 }
1156 function injectScript(scriptUrl) {
1157 return new Promise((resolve2, reject) => {
1158 const existing = document.querySelector(
1159 'script[data-desktop-mode-shell-overlays="1"]'
1160 );
1161 const finish = () => {
1162 if (isLoaded()) {
1163 resolve2();
1164 return;
1165 }
1166 reject(
1167 new Error(
1168 "[desktop-mode] shell-overlays bundle loaded but did not register the overlay components."
1169 )
1170 );
1171 };
1172 if (existing) {
1173 if (isLoaded()) {
1174 finish();
1175 } else {
1176 existing.addEventListener("load", finish);
1177 existing.addEventListener(
1178 "error",
1179 () => reject(new Error("failed to load shell-overlays bundle"))
1180 );
1181 }
1182 return;
1183 }
1184 const s = document.createElement("script");
1185 s.src = scriptUrl;
1186 s.async = true;
1187 s.dataset.desktopModeShellOverlays = "1";
1188 s.addEventListener("load", finish);
1189 s.addEventListener(
1190 "error",
1191 () => reject(new Error("failed to load shell-overlays bundle"))
1192 );
1193 document.head.appendChild(s);
1194 });
1195 }
1196 function preloadShellOverlays(scriptUrl) {
1197 if (!scriptUrl || isLoaded() || inflight) {
1198 return;
1199 }
1200 inflight = injectScript(scriptUrl).catch((err) => {
1201 inflight = null;
1202 if (typeof console !== "undefined") {
1203 console.warn(
1204 "[desktop-mode] shell-overlays preload failed; will retry on first overlay use:",
1205 err
1206 );
1207 }
1208 });
1209 }
1210 function ensureShellOverlaysLoaded(scriptUrl) {
1211 if (isLoaded()) {
1212 return Promise.resolve();
1213 }
1214 if (!scriptUrl) {
1215 return Promise.resolve();
1216 }
1217 if (!inflight) {
1218 inflight = injectScript(scriptUrl);
1219 }
1220 return inflight;
1221 }
1222 function shellOverlaysBundleUrl() {
1223 const cfg = window.desktopModeConfig;
1224 return cfg?.shellOverlaysBundleUrl ?? "";
1225 }
1226 function openWithShellOverlays(isStillCurrent, fn) {
1227 const url = shellOverlaysBundleUrl();
1228 if (isLoaded() || !url) {
1229 fn();
1230 return;
1231 }
1232 void ensureShellOverlaysLoaded(url).then(() => {
1233 if (!isStillCurrent()) {
1234 return;
1235 }
1236 fn();
1237 }).catch((err) => {
1238 if (typeof console !== "undefined") {
1239 console.warn(
1240 "[desktop-mode] shell-overlays failed to load; menu/dialog suppressed:",
1241 err
1242 );
1243 }
1244 });
1245 }
1246 const TEXT_DOMAIN = "desktop-mode";
1247 function i18n() {
1248 return window.wp?.i18n;
1249 }
1250 function __(text, domain = TEXT_DOMAIN) {
1251 return i18n()?.__(text, domain) ?? text;
1252 }
1253 function _n(single, plural, number, domain = TEXT_DOMAIN) {
1254 return i18n()?._n(single, plural, number, domain) ?? (number === 1 ? single : plural);
1255 }
1256 function sprintf(format, ...args) {
1257 const impl = i18n()?.sprintf;
1258 if (impl) {
1259 return impl(format, ...args);
1260 }
1261 let i = 0;
1262 return format.replace(/%[sd]/g, () => String(args[i++] ?? ""));
1263 }
1264 function isValidGrid(candidate, windowCount) {
1265 if (!candidate || typeof candidate !== "object") {
1266 return false;
1267 }
1268 const c = candidate.cols;
1269 const r = candidate.rows;
1270 if (typeof c !== "number" || typeof r !== "number") {
1271 return false;
1272 }
1273 if (!Number.isFinite(c) || !Number.isFinite(r)) {
1274 return false;
1275 }
1276 if (c < 1 || r < 1) {
1277 return false;
1278 }
1279 return Math.floor(c) * Math.floor(r) >= windowCount;
1280 }
1281 function isValidCellSize(candidate) {
1282 if (!candidate || typeof candidate !== "object") {
1283 return false;
1284 }
1285 const w = candidate.cellWidth;
1286 const h = candidate.cellHeight;
1287 if (typeof w !== "number" || typeof h !== "number") {
1288 return false;
1289 }
1290 if (!Number.isFinite(w) || !Number.isFinite(h)) {
1291 return false;
1292 }
1293 return w > 0 && h > 0;
1294 }
1295 function pickGridDimensions(n, width, height) {
1296 if (n <= 1) {
1297 return { cols: 1, rows: 1 };
1298 }
1299 const areaAspect = width / Math.max(1, height);
1300 const max = 6;
1301 let best = { cols: n, rows: 1, score: Infinity };
1302 for (let cols = 1; cols <= Math.min(max, n); cols++) {
1303 const rows = Math.min(max, Math.ceil(n / cols));
1304 if (cols * rows < n) {
1305 continue;
1306 }
1307 const cellAspect = width / cols / Math.max(1, height / rows);
1308 const aspectDelta = Math.abs(cellAspect - areaAspect);
1309 const emptyCells = cols * rows - n;
1310 const score = aspectDelta + emptyCells * 0.05;
1311 if (score < best.score) {
1312 best = { cols, rows, score };
1313 }
1314 }
1315 return { cols: best.cols, rows: best.rows };
1316 }
1317 function computeOverviewLayout(windows, rect, topInset = 0) {
1318 const n = windows.length;
1319 if (n === 0) {
1320 return [];
1321 }
1322 const cols = Math.ceil(Math.sqrt(n));
1323 const rows = Math.ceil(n / cols);
1324 const padding = 40;
1325 const gap = 24;
1326 const labelReserve = 34;
1327 const cellWidth = (rect.width - padding * 2 - gap * (cols - 1)) / cols;
1328 const cellHeight = (rect.height - padding * 2 - topInset - gap * (rows - 1)) / rows;
1329 const thumbCellHeight = Math.max(40, cellHeight - labelReserve);
1330 return windows.map((win, i) => {
1331 const col = i % cols;
1332 const row = Math.floor(i / cols);
1333 const cellX = rect.left + padding + col * (cellWidth + gap);
1334 const cellY = rect.top + topInset + padding + row * (cellHeight + gap) + labelReserve;
1335 const sourceW = win.element.offsetWidth;
1336 const sourceH = win.element.offsetHeight;
1337 const scale = Math.min(
1338 cellWidth / sourceW,
1339 thumbCellHeight / sourceH
1340 );
1341 const scaledW = sourceW * scale;
1342 const scaledH = sourceH * scale;
1343 return {
1344 win,
1345 x: cellX + (cellWidth - scaledW) / 2,
1346 y: cellY + (thumbCellHeight - scaledH) / 2,
1347 scale
1348 };
1349 });
1350 }
1351 const OVERVIEW_TOP_BAR_RESERVE = 120;
1352 function enterOverview(mgr) {
1353 if (mgr._overviewActive) {
1354 return;
1355 }
1356 const onActive = mgr._stack.filter(
1357 (w) => w.config.desktopId === mgr._activeDesktopId
1358 );
1359 if (onActive.length > 0 && onActive.every((w) => w.state === "minimized")) {
1360 for (const w of onActive) {
1361 try {
1362 w.restore();
1363 } catch (err) {
1364 if (typeof console !== "undefined") {
1365 console.error(
1366 "[desktop-mode] enterOverview: window.restore() threw for",
1367 w.id,
1368 err
1369 );
1370 }
1371 }
1372 }
1373 }
1374 const eligible = mgr._stack.filter(
1375 (w) => w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
1376 );
1377 mgr._overviewActive = true;
1378 doAction(HOOKS.OVERVIEW_ENTERING, {});
1379 mgr._overviewSnapshot.clear();
1380 for (const w of eligible) {
1381 mgr._overviewSnapshot.set(w.id, {
1382 transform: w.element.style.transform || "",
1383 transition: w.element.style.transition || ""
1384 });
1385 }
1386 for (const w of eligible) {
1387 if (w.state === "fullscreen") {
1388 w.toggleFullscreen();
1389 }
1390 }
1391 const currentRect = mgr._desktop.getBoundingClientRect();
1392 const docks = Array.from(
1393 document.querySelectorAll(".desktop-mode-dock")
1394 );
1395 let reclaimedWidth = 0;
1396 for (const d of docks) {
1397 const r = d.getBoundingClientRect();
1398 const verticallyOverlaps = r.bottom > currentRect.top && r.top < currentRect.bottom;
1399 const isHorizontalRail = r.height > r.width;
1400 if (verticallyOverlaps && isHorizontalRail) {
1401 reclaimedWidth += r.width;
1402 }
1403 }
1404 const targetRect = new DOMRect(
1405 0,
1406 0,
1407 currentRect.width + reclaimedWidth,
1408 currentRect.height
1409 );
1410 mgr._desktop.classList.add("desktop-mode-area--overview");
1411 const shell = document.getElementById("desktop-mode-shell");
1412 shell?.classList.add("desktop-mode-shell--overview");
1413 mgr._overviewTopBar = buildOverviewTopBar(mgr);
1414 mgr._desktop.appendChild(mgr._overviewTopBar);
1415 const layout = computeOverviewLayout(
1416 eligible,
1417 targetRect,
1418 OVERVIEW_TOP_BAR_RESERVE
1419 );
1420 mgr._overviewLabels.clear();
1421 for (const item of layout) {
1422 const el = item.win.element;
1423 el.classList.add("desktop-mode-window--overview");
1424 const dx = item.x - el.offsetLeft;
1425 const dy = item.y - el.offsetTop;
1426 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
1427 const label = createOverviewLabel(item);
1428 el.insertAdjacentElement("afterend", label);
1429 mgr._overviewLabels.set(item.win.id, label);
1430 }
1431 const pressTargetForEvent = (e) => {
1432 const target2 = e.target;
1433 const winEl = target2?.closest(
1434 ".desktop-mode-window--overview"
1435 );
1436 if (winEl) {
1437 return {
1438 id: winEl.id.replace(/^wp-window-/, ""),
1439 element: winEl
1440 };
1441 }
1442 if (target2 === mgr._desktop) {
1443 return { id: "backdrop", element: mgr._desktop };
1444 }
1445 return null;
1446 };
1447 mgr._overviewPointerDownHandler = (e) => {
1448 if (e.button !== 0) {
1449 mgr._overviewPressTarget = null;
1450 return;
1451 }
1452 mgr._overviewPressTarget = pressTargetForEvent(e);
1453 if (mgr._overviewPressTarget) {
1454 e.preventDefault();
1455 e.stopPropagation();
1456 }
1457 };
1458 mgr._overviewPointerUpHandler = (e) => {
1459 if (e.button !== 0) {
1460 return;
1461 }
1462 const pressed = mgr._overviewPressTarget;
1463 mgr._overviewPressTarget = null;
1464 if (!pressed) {
1465 return;
1466 }
1467 const rect = pressed.element.getBoundingClientRect();
1468 const inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
1469 if (!inside) {
1470 return;
1471 }
1472 e.preventDefault();
1473 e.stopPropagation();
1474 if (pressed.id === "backdrop") {
1475 exitOverview(mgr);
1476 return;
1477 }
1478 const selected = mgr.getById(pressed.id);
1479 doAction(HOOKS.OVERVIEW_WINDOW_CLICK, { windowId: pressed.id });
1480 exitOverview(mgr, selected, true);
1481 };
1482 mgr._overviewKeyHandler = (e) => {
1483 if (e.key === "Escape") {
1484 exitOverview(mgr);
1485 return;
1486 }
1487 if (e.key === "Enter") {
1488 e.preventDefault();
1489 if (mgr._overviewAddTileFocused) {
1490 commitAddTile(mgr);
1491 return;
1492 }
1493 exitOverview(mgr);
1494 }
1495 };
1496 mgr._desktop.addEventListener(
1497 "pointerdown",
1498 mgr._overviewPointerDownHandler,
1499 true
1500 );
1501 mgr._desktop.addEventListener(
1502 "pointerup",
1503 mgr._overviewPointerUpHandler,
1504 true
1505 );
1506 mgr._overviewClickBlocker = (e) => {
1507 const target2 = e.target;
1508 if (target2?.closest(".desktop-mode-overview-top-bar")) {
1509 return;
1510 }
1511 e.stopPropagation();
1512 e.preventDefault();
1513 };
1514 mgr._desktop.addEventListener(
1515 "click",
1516 mgr._overviewClickBlocker,
1517 true
1518 );
1519 document.addEventListener("keydown", mgr._overviewKeyHandler);
1520 mgr._lastOverviewHoverId = null;
1521 mgr._overviewMouseHandler = (e) => {
1522 const target2 = e.target;
1523 const winEl = target2?.closest(
1524 ".desktop-mode-window--overview"
1525 );
1526 const newId = winEl ? winEl.id.replace(/^wp-window-/, "") : null;
1527 if (newId === mgr._lastOverviewHoverId) {
1528 return;
1529 }
1530 if (mgr._lastOverviewHoverId) {
1531 doAction(HOOKS.OVERVIEW_WINDOW_UNHOVER, {
1532 windowId: mgr._lastOverviewHoverId
1533 });
1534 }
1535 if (newId) {
1536 doAction(HOOKS.OVERVIEW_WINDOW_HOVER, { windowId: newId });
1537 }
1538 mgr._lastOverviewHoverId = newId;
1539 };
1540 mgr._desktop.addEventListener("mouseover", mgr._overviewMouseHandler);
1541 window.setTimeout(() => {
1542 if (mgr._overviewActive) {
1543 doAction(HOOKS.OVERVIEW_ENTERED, {});
1544 }
1545 }, 300);
1546 }
1547 function buildOverviewTopBar(mgr) {
1548 const bar = document.createElement("div");
1549 bar.className = "desktop-mode-overview-top-bar";
1550 const list2 = document.createElement("div");
1551 list2.className = "desktop-mode-overview-top-bar__list";
1552 bar.appendChild(list2);
1553 for (const d of mgr._desktops) {
1554 list2.appendChild(buildDesktopTile(mgr, d));
1555 }
1556 const addTile = document.createElement("button");
1557 addTile.type = "button";
1558 addTile.className = "desktop-mode-overview-top-bar__tile desktop-mode-overview-top-bar__tile--add";
1559 if (mgr._overviewAddTileFocused) {
1560 addTile.classList.add(
1561 "desktop-mode-overview-top-bar__tile--cursor"
1562 );
1563 }
1564 addTile.setAttribute("aria-label", __("Add new desktop"));
1565 addTile.innerHTML = '<span class="desktop-mode-overview-top-bar__tile-plus" aria-hidden="true">+</span>';
1566 addTile.addEventListener("click", (e) => {
1567 e.preventDefault();
1568 e.stopPropagation();
1569 commitAddTile(mgr);
1570 });
1571 list2.appendChild(addTile);
1572 return bar;
1573 }
1574 function commitAddTile(mgr) {
1575 const created = createDesktop(mgr);
1576 mgr._overviewAddTileFocused = false;
1577 exitOverviewToDesktop(mgr, created.id);
1578 }
1579 function buildDesktopTile(mgr, d) {
1580 const tile2 = document.createElement("button");
1581 tile2.type = "button";
1582 tile2.className = "desktop-mode-overview-top-bar__tile";
1583 tile2.dataset.desktopId = d.id;
1584 if (d.id === mgr._activeDesktopId && !mgr._overviewAddTileFocused) {
1585 tile2.classList.add("desktop-mode-overview-top-bar__tile--active");
1586 }
1587 tile2.setAttribute("aria-label", sprintf(__("Switch to %s"), d.label));
1588 const preview = document.createElement("span");
1589 preview.className = "desktop-mode-overview-top-bar__tile-preview";
1590 const count = mgr._stack.filter(
1591 (w) => w.config.desktopId === d.id
1592 ).length;
1593 if (count > 0) {
1594 const badge = document.createElement("span");
1595 badge.className = "desktop-mode-overview-top-bar__tile-count";
1596 badge.textContent = String(count);
1597 preview.appendChild(badge);
1598 }
1599 tile2.appendChild(preview);
1600 const label = document.createElement("span");
1601 label.className = "desktop-mode-overview-top-bar__tile-label";
1602 label.textContent = d.label;
1603 tile2.appendChild(label);
1604 const closeBtn = document.createElement("span");
1605 closeBtn.className = "desktop-mode-overview-top-bar__tile-close";
1606 closeBtn.setAttribute("role", "button");
1607 closeBtn.setAttribute("tabindex", "0");
1608 closeBtn.setAttribute("aria-label", sprintf(__("Close %s"), d.label));
1609 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>';
1610 closeBtn.addEventListener("click", (e) => {
1611 e.preventDefault();
1612 e.stopPropagation();
1613 closeDesktop(mgr, d.id);
1614 refreshOverviewTopBar(mgr);
1615 });
1616 tile2.appendChild(closeBtn);
1617 tile2.addEventListener("click", (e) => {
1618 e.preventDefault();
1619 e.stopPropagation();
1620 exitOverviewToDesktop(mgr, d.id);
1621 });
1622 return tile2;
1623 }
1624 function refreshOverviewTopBar(mgr) {
1625 if (!mgr._overviewTopBar) {
1626 return;
1627 }
1628 const fresh = buildOverviewTopBar(mgr);
1629 mgr._overviewTopBar.replaceWith(fresh);
1630 mgr._overviewTopBar = fresh;
1631 }
1632 function exitOverviewToDesktop(mgr, desktopId) {
1633 switchDesktop(mgr, desktopId);
1634 exitOverview(mgr);
1635 }
1636 function createOverviewLabel(item) {
1637 const label = document.createElement("div");
1638 label.className = "desktop-mode-overview-label";
1639 label.dataset.windowId = item.win.id;
1640 const thumbW = item.win.element.offsetWidth * item.scale;
1641 label.style.left = `${item.x}px`;
1642 label.style.top = `${item.y - 34}px`;
1643 label.style.width = `${thumbW}px`;
1644 const iconClass = item.win.config.icon || "dashicons-admin-generic";
1645 const icon = document.createElement("span");
1646 icon.className = `desktop-mode-overview-label__icon dashicons ${iconClass}`;
1647 icon.setAttribute("aria-hidden", "true");
1648 label.appendChild(icon);
1649 const title = document.createElement("span");
1650 title.className = "desktop-mode-overview-label__title";
1651 title.textContent = item.win.config.title;
1652 label.appendChild(title);
1653 const tabCount = item.win.getExternalTabCount();
1654 if (tabCount > 0) {
1655 const meta = document.createElement("span");
1656 meta.className = "desktop-mode-overview-label__meta";
1657 meta.textContent = sprintf(
1658 // translators: %d is the number of external sub-tabs open on this window.
1659 _n("· %d open tab", "· %d open tabs", tabCount),
1660 tabCount
1661 );
1662 label.appendChild(meta);
1663 }
1664 return label;
1665 }
1666 function exitOverview(mgr, selected, maximize = false) {
1667 if (!mgr._overviewActive) {
1668 return;
1669 }
1670 mgr._overviewActive = false;
1671 mgr._overviewAddTileFocused = false;
1672 doAction(HOOKS.OVERVIEW_EXITING, {
1673 windowId: selected && maximize ? selected.id : void 0,
1674 reason: selected && maximize ? "select" : "cancel"
1675 });
1676 mgr._desktop.classList.remove("desktop-mode-area--overview");
1677 const shell = document.getElementById("desktop-mode-shell");
1678 shell?.classList.remove("desktop-mode-shell--overview");
1679 for (const [id, snap] of mgr._overviewSnapshot) {
1680 const w = mgr.getById(id);
1681 if (!w) {
1682 continue;
1683 }
1684 w.element.style.transform = snap.transform;
1685 }
1686 if (selected && maximize) {
1687 mgr.focus(selected);
1688 selected.maximize();
1689 }
1690 for (const label of mgr._overviewLabels.values()) {
1691 label.classList.add("desktop-mode-overview-label--out");
1692 }
1693 if (mgr._overviewTopBar) {
1694 mgr._overviewTopBar.classList.add(
1695 "desktop-mode-overview-top-bar--out"
1696 );
1697 }
1698 const ANIMATION_MS = 280;
1699 window.setTimeout(() => {
1700 for (const w of mgr._stack) {
1701 w.element.classList.remove("desktop-mode-window--overview");
1702 }
1703 for (const label of mgr._overviewLabels.values()) {
1704 label.remove();
1705 }
1706 mgr._overviewLabels.clear();
1707 mgr._overviewSnapshot.clear();
1708 if (mgr._overviewTopBar) {
1709 mgr._overviewTopBar.remove();
1710 mgr._overviewTopBar = null;
1711 }
1712 if (mgr._overviewClickBlocker) {
1713 mgr._desktop.removeEventListener(
1714 "click",
1715 mgr._overviewClickBlocker,
1716 true
1717 );
1718 mgr._overviewClickBlocker = null;
1719 }
1720 doAction(HOOKS.OVERVIEW_EXITED, {
1721 windowId: selected && maximize ? selected.id : void 0,
1722 reason: selected && maximize ? "select" : "cancel"
1723 });
1724 }, ANIMATION_MS);
1725 if (mgr._overviewPointerDownHandler) {
1726 mgr._desktop.removeEventListener(
1727 "pointerdown",
1728 mgr._overviewPointerDownHandler,
1729 true
1730 );
1731 mgr._overviewPointerDownHandler = null;
1732 }
1733 if (mgr._overviewPointerUpHandler) {
1734 mgr._desktop.removeEventListener(
1735 "pointerup",
1736 mgr._overviewPointerUpHandler,
1737 true
1738 );
1739 mgr._overviewPointerUpHandler = null;
1740 }
1741 mgr._overviewPressTarget = null;
1742 if (mgr._overviewKeyHandler) {
1743 document.removeEventListener("keydown", mgr._overviewKeyHandler);
1744 mgr._overviewKeyHandler = null;
1745 }
1746 if (mgr._overviewMouseHandler) {
1747 mgr._desktop.removeEventListener(
1748 "mouseover",
1749 mgr._overviewMouseHandler
1750 );
1751 mgr._overviewMouseHandler = null;
1752 }
1753 if (mgr._lastOverviewHoverId) {
1754 doAction(HOOKS.OVERVIEW_WINDOW_UNHOVER, {
1755 windowId: mgr._lastOverviewHoverId
1756 });
1757 mgr._lastOverviewHoverId = null;
1758 }
1759 }
1760 function getDesktops(mgr) {
1761 return [...mgr._desktops];
1762 }
1763 function getActiveDesktop(mgr) {
1764 const found = mgr._desktops.find((d) => d.id === mgr._activeDesktopId);
1765 return found ?? mgr._desktops[0];
1766 }
1767 function getActiveDesktopId(mgr) {
1768 return getActiveDesktop(mgr).id;
1769 }
1770 function applyDesktopVisibility(mgr, win) {
1771 const visible = win.config.desktopId === mgr._activeDesktopId;
1772 win.element.style.display = visible ? "" : "none";
1773 }
1774 function refreshDesktopVisibility(mgr) {
1775 for (const w of mgr._stack) {
1776 applyDesktopVisibility(mgr, w);
1777 }
1778 }
1779 function createDesktop(mgr) {
1780 mgr._desktopSeq++;
1781 const desktop = {
1782 id: `desktop-${mgr._desktopSeq}`,
1783 // translators: %d is the desktop number (e.g., "Desktop 2")
1784 label: sprintf(__("Desktop %d"), mgr._desktopSeq)
1785 };
1786 mgr._desktops.push(desktop);
1787 doAction(HOOKS.DESKTOP_CREATED, { desktopId: desktop.id });
1788 return desktop;
1789 }
1790 function switchDesktop(mgr, id, opts) {
1791 if (id === mgr._activeDesktopId) {
1792 return;
1793 }
1794 if (!mgr._desktops.some((d) => d.id === id)) {
1795 return;
1796 }
1797 const previousId = mgr._activeDesktopId;
1798 mgr._activeDesktopId = id;
1799 if (mgr._overviewActive) {
1800 relayoutOverviewForActiveDesktop(mgr);
1801 refreshOverviewTopBar(mgr);
1802 } else {
1803 refreshDesktopVisibility(mgr);
1804 if (opts?.direction) {
1805 animateDesktopSwitch(mgr, opts.direction);
1806 }
1807 const topOnNew = [...mgr._stack].reverse().find(
1808 (w) => w.config.desktopId === id && w.state !== "minimized"
1809 );
1810 if (topOnNew) {
1811 mgr.focus(topOnNew);
1812 }
1813 }
1814 doAction(HOOKS.DESKTOP_SWITCHED, {
1815 from: previousId,
1816 to: id
1817 });
1818 }
1819 function animateDesktopSwitch(mgr, direction) {
1820 const el = mgr._desktop;
1821 const cls = direction === "next" ? "desktop-mode-area--sliding-from-right" : "desktop-mode-area--sliding-from-left";
1822 el.classList.remove(
1823 "desktop-mode-area--sliding-from-right",
1824 "desktop-mode-area--sliding-from-left"
1825 );
1826 void el.offsetWidth;
1827 el.classList.add(cls);
1828 const onEnd = (e) => {
1829 if (!e.animationName.startsWith("desktop-mode-area-slide-from-")) {
1830 return;
1831 }
1832 el.classList.remove(cls);
1833 el.removeEventListener("animationend", onEnd);
1834 };
1835 el.addEventListener("animationend", onEnd);
1836 }
1837 function closeDesktop(mgr, id) {
1838 if (mgr._desktops.length <= 1) {
1839 return;
1840 }
1841 const idx = mgr._desktops.findIndex((d) => d.id === id);
1842 if (idx === -1) {
1843 return;
1844 }
1845 const survivorIdx = idx > 0 ? idx - 1 : 1;
1846 const survivor = mgr._desktops[survivorIdx];
1847 for (const w of mgr._stack) {
1848 if (w.config.desktopId === id) {
1849 w.config.desktopId = survivor.id;
1850 }
1851 }
1852 mgr._desktops.splice(idx, 1);
1853 const wasActive = mgr._activeDesktopId === id;
1854 if (wasActive) {
1855 mgr._activeDesktopId = survivor.id;
1856 }
1857 if (mgr._overviewActive) {
1858 relayoutOverviewForActiveDesktop(mgr);
1859 } else {
1860 refreshDesktopVisibility(mgr);
1861 }
1862 doAction(HOOKS.DESKTOP_CLOSED, {
1863 desktopId: id,
1864 migratedTo: survivor.id
1865 });
1866 }
1867 function relayoutOverviewForActiveDesktop(mgr) {
1868 for (const [winId, snap] of mgr._overviewSnapshot) {
1869 const w = mgr.getById(winId);
1870 if (w) {
1871 w.element.style.transform = snap.transform;
1872 w.element.style.transition = snap.transition;
1873 w.element.classList.remove("desktop-mode-window--overview");
1874 }
1875 }
1876 for (const label of mgr._overviewLabels.values()) {
1877 label.remove();
1878 }
1879 mgr._overviewLabels.clear();
1880 mgr._overviewSnapshot.clear();
1881 refreshDesktopVisibility(mgr);
1882 const eligible = mgr._stack.filter(
1883 (w) => w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
1884 );
1885 if (eligible.length === 0) {
1886 return;
1887 }
1888 for (const w of eligible) {
1889 mgr._overviewSnapshot.set(w.id, {
1890 transform: w.element.style.transform || "",
1891 transition: w.element.style.transition || ""
1892 });
1893 }
1894 const live = mgr._desktop.getBoundingClientRect();
1895 const targetRect = new DOMRect(0, 0, live.width, live.height);
1896 const layout = computeOverviewLayout(
1897 eligible,
1898 targetRect,
1899 OVERVIEW_TOP_BAR_RESERVE
1900 );
1901 for (const item of layout) {
1902 const el = item.win.element;
1903 el.classList.add("desktop-mode-window--overview");
1904 const dx = item.x - el.offsetLeft;
1905 const dy = item.y - el.offsetTop;
1906 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
1907 const label = createOverviewLabel(item);
1908 el.insertAdjacentElement("afterend", label);
1909 mgr._overviewLabels.set(item.win.id, label);
1910 }
1911 }
1912 function seedDesktops(mgr, desktops, activeDesktopId) {
1913 if (desktops.length === 0) {
1914 return;
1915 }
1916 mgr._desktops = desktops.map((d) => ({ ...d }));
1917 mgr._activeDesktopId = desktops.some((d) => d.id === activeDesktopId) ? activeDesktopId : desktops[0].id;
1918 let highest = 0;
1919 for (const d of desktops) {
1920 const match = d.id.match(/^desktop-(\d+)$/);
1921 if (match) {
1922 const n = parseInt(match[1], 10);
1923 if (Number.isFinite(n) && n > highest) {
1924 highest = n;
1925 }
1926 }
1927 }
1928 mgr._desktopSeq = Math.max(mgr._desktopSeq, highest);
1929 }
1930 function cascade(mgr) {
1931 const eligible = mgr._stack.filter(
1932 (w) => w.config.desktopId === mgr._activeDesktopId
1933 );
1934 if (eligible.length === 0) {
1935 return;
1936 }
1937 doAction(HOOKS.ARRANGE_CASCADE_STARTING, {
1938 windowCount: eligible.length
1939 });
1940 for (const w of eligible) {
1941 if (w.state === "minimized") {
1942 w.restore();
1943 }
1944 if (w.state === "fullscreen") {
1945 w.toggleFullscreen();
1946 }
1947 if (w.state === "maximized") {
1948 w.toggleMaximize();
1949 }
1950 }
1951 const rect = mgr._desktop.getBoundingClientRect();
1952 const padding = 30;
1953 const offset = 30;
1954 const targetWidth = Math.min(Math.round(rect.width * 0.7), 1100);
1955 const targetHeight = Math.min(Math.round(rect.height * 0.75), 750);
1956 const maxStepsX = Math.max(
1957 1,
1958 Math.floor((rect.width - targetWidth - padding) / offset)
1959 );
1960 const maxStepsY = Math.max(
1961 1,
1962 Math.floor((rect.height - targetHeight - padding) / offset)
1963 );
1964 const maxSteps = Math.min(maxStepsX, maxStepsY);
1965 eligible.forEach((w, i) => {
1966 const step = i % Math.max(1, maxSteps);
1967 w.element.style.left = `${padding + step * offset}px`;
1968 w.element.style.top = `${padding + step * offset}px`;
1969 w.element.style.width = `${targetWidth}px`;
1970 w.element.style.height = `${targetHeight}px`;
1971 });
1972 const focused = mgr.getFocused();
1973 if (focused) {
1974 mgr.focus(focused);
1975 }
1976 document.dispatchEvent(
1977 new CustomEvent("desktop-mode-window-changed", {
1978 detail: { reason: "cascade" }
1979 })
1980 );
1981 doAction(HOOKS.ARRANGE_CASCADE_APPLIED, {
1982 windowCount: eligible.length
1983 });
1984 }
1985 function tile(mgr) {
1986 const eligible = mgr._stack.filter(
1987 (w) => w.config.desktopId === mgr._activeDesktopId
1988 );
1989 if (eligible.length === 0) {
1990 return;
1991 }
1992 for (const w of eligible) {
1993 if (w.state === "minimized") {
1994 w.restore();
1995 }
1996 if (w.state === "fullscreen") {
1997 w.toggleFullscreen();
1998 }
1999 if (w.state === "maximized") {
2000 w.toggleMaximize();
2001 }
2002 }
2003 const rect = mgr._desktop.getBoundingClientRect();
2004 const auto = pickGridDimensions(
2005 eligible.length,
2006 rect.width,
2007 rect.height
2008 );
2009 const filtered = applyFilters(
2010 HOOKS.ARRANGE_TILE_DIMENSIONS,
2011 auto,
2012 {
2013 windowCount: eligible.length,
2014 areaWidth: rect.width,
2015 areaHeight: rect.height
2016 }
2017 );
2018 const { cols, rows } = isValidGrid(filtered, eligible.length) ? { cols: Math.floor(filtered.cols), rows: Math.floor(filtered.rows) } : auto;
2019 doAction(HOOKS.ARRANGE_TILE_STARTING, {
2020 windowCount: eligible.length,
2021 cols,
2022 rows
2023 });
2024 const padding = 16;
2025 const gap = 12;
2026 const cellWidth = Math.floor(
2027 (rect.width - padding * 2 - gap * (cols - 1)) / cols
2028 );
2029 const cellHeight = Math.floor(
2030 (rect.height - padding * 2 - gap * (rows - 1)) / rows
2031 );
2032 eligible.forEach((w, i) => {
2033 const col = i % cols;
2034 const row = Math.floor(i / cols);
2035 w.element.style.left = `${padding + col * (cellWidth + gap)}px`;
2036 w.element.style.top = `${padding + row * (cellHeight + gap)}px`;
2037 w.element.style.width = `${cellWidth}px`;
2038 w.element.style.height = `${cellHeight}px`;
2039 });
2040 const focused = mgr.getFocused();
2041 if (focused) {
2042 mgr.focus(focused);
2043 }
2044 document.dispatchEvent(
2045 new CustomEvent("desktop-mode-window-changed", {
2046 detail: { reason: "tile" }
2047 })
2048 );
2049 doAction(HOOKS.ARRANGE_TILE_APPLIED, {
2050 windowCount: eligible.length,
2051 cols,
2052 rows
2053 });
2054 }
2055 const SNAP_STORAGE_KEY = "desktop-mode-snap-to-grid";
2056 function loadSnapEnabled() {
2057 try {
2058 return window.localStorage.getItem(SNAP_STORAGE_KEY) === "1";
2059 } catch {
2060 return false;
2061 }
2062 }
2063 function setSnapEnabled(mgr, enabled) {
2064 if (mgr._snapEnabled === enabled) {
2065 return;
2066 }
2067 mgr._snapEnabled = enabled;
2068 try {
2069 window.localStorage.setItem(SNAP_STORAGE_KEY, enabled ? "1" : "0");
2070 } catch {
2071 }
2072 doAction(HOOKS.ARRANGE_SNAP_CHANGED, { enabled });
2073 }
2074 function getSnapConfig(mgr) {
2075 if (!mgr._snapEnabled) {
2076 return { enabled: false, cellWidth: 0, cellHeight: 0 };
2077 }
2078 const rect = mgr._desktop.getBoundingClientRect();
2079 const targetCols = rect.width >= rect.height ? 12 : 8;
2080 const auto = {
2081 cellWidth: Math.max(40, Math.round(rect.width / targetCols)),
2082 cellHeight: Math.max(
2083 40,
2084 Math.round(rect.height / Math.round(targetCols * 0.66))
2085 )
2086 };
2087 const filtered = applyFilters(
2088 HOOKS.ARRANGE_SNAP_CELL_SIZE,
2089 auto,
2090 { areaWidth: rect.width, areaHeight: rect.height }
2091 );
2092 const { cellWidth, cellHeight } = isValidCellSize(filtered) ? filtered : auto;
2093 return { enabled: true, cellWidth, cellHeight };
2094 }
2095 function enterSplitOverview(mgr, anchor, zone) {
2096 if (mgr._splitOverviewActive) {
2097 return;
2098 }
2099 mgr._splitOverviewActive = true;
2100 mgr._splitOverviewAnchor = anchor;
2101 mgr._splitOverviewZone = zone;
2102 const eligible = mgr._stack.filter(
2103 (w) => w !== anchor && w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
2104 );
2105 if (eligible.length === 0) {
2106 cleanupSplitOverviewState(mgr);
2107 return;
2108 }
2109 mgr._splitOverviewSnapshot.clear();
2110 for (const w of eligible) {
2111 mgr._splitOverviewSnapshot.set(w.id, {
2112 transform: w.element.style.transform || "",
2113 transition: w.element.style.transition || ""
2114 });
2115 }
2116 mgr._desktop.classList.add("desktop-mode-area--split-overview");
2117 const rect = oppositeHalfRect(mgr, zone);
2118 const layout = computeOverviewLayout(eligible, rect, 0);
2119 mgr._splitOverviewLabels.clear();
2120 for (const item of layout) {
2121 const el = item.win.element;
2122 el.classList.add("desktop-mode-window--overview");
2123 const dx = item.x - el.offsetLeft;
2124 const dy = item.y - el.offsetTop;
2125 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
2126 const label = createOverviewLabel(item);
2127 el.insertAdjacentElement("afterend", label);
2128 mgr._splitOverviewLabels.set(item.win.id, label);
2129 }
2130 const pressTargetForEvent = (e) => {
2131 const target2 = e.target;
2132 const winEl = target2?.closest(
2133 ".desktop-mode-window--overview"
2134 );
2135 if (winEl) {
2136 return {
2137 id: winEl.id.replace(/^wp-window-/, ""),
2138 element: winEl
2139 };
2140 }
2141 if (target2) {
2142 return { id: "dismiss", element: mgr._desktop };
2143 }
2144 return null;
2145 };
2146 mgr._splitOverviewPointerDown = (e) => {
2147 if (e.button !== 0) {
2148 mgr._splitOverviewPressTarget = null;
2149 return;
2150 }
2151 mgr._splitOverviewPressTarget = pressTargetForEvent(e);
2152 if (mgr._splitOverviewPressTarget) {
2153 e.preventDefault();
2154 e.stopPropagation();
2155 }
2156 };
2157 mgr._splitOverviewPointerUp = (e) => {
2158 if (e.button !== 0) {
2159 return;
2160 }
2161 const pressed = mgr._splitOverviewPressTarget;
2162 mgr._splitOverviewPressTarget = null;
2163 if (!pressed) {
2164 return;
2165 }
2166 const r = pressed.element.getBoundingClientRect();
2167 const inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
2168 if (!inside) {
2169 return;
2170 }
2171 e.preventDefault();
2172 e.stopPropagation();
2173 if (pressed.id === "dismiss") {
2174 exitSplitOverview(mgr);
2175 return;
2176 }
2177 const selected = mgr.getById(pressed.id);
2178 if (!selected) {
2179 exitSplitOverview(mgr);
2180 return;
2181 }
2182 fillOppositeHalfAndExit(mgr, selected);
2183 };
2184 mgr._splitOverviewKey = (e) => {
2185 if (e.key === "Escape") {
2186 exitSplitOverview(mgr);
2187 }
2188 };
2189 mgr._splitOverviewClickBlocker = (e) => {
2190 e.stopPropagation();
2191 e.preventDefault();
2192 };
2193 mgr._desktop.addEventListener(
2194 "pointerdown",
2195 mgr._splitOverviewPointerDown,
2196 true
2197 );
2198 mgr._desktop.addEventListener(
2199 "pointerup",
2200 mgr._splitOverviewPointerUp,
2201 true
2202 );
2203 mgr._desktop.addEventListener(
2204 "click",
2205 mgr._splitOverviewClickBlocker,
2206 true
2207 );
2208 document.addEventListener("keydown", mgr._splitOverviewKey);
2209 }
2210 function fillOppositeHalfAndExit(mgr, selected) {
2211 const anchorZone = mgr._splitOverviewZone;
2212 if (!anchorZone) {
2213 exitSplitOverview(mgr);
2214 return;
2215 }
2216 const partnerZone = anchorZone === "left" ? "right" : "left";
2217 selected.element.style.transform = "";
2218 selected.element.classList.remove("desktop-mode-window--overview");
2219 selected.applySnap(partnerZone);
2220 mgr._splitOverviewSnapshot.delete(selected.id);
2221 mgr.focus(selected);
2222 doAction(HOOKS.SNAP_SPLIT_FILLED, {
2223 windowId: selected.id,
2224 zone: partnerZone
2225 });
2226 exitSplitOverview(mgr);
2227 }
2228 function exitSplitOverview(mgr) {
2229 if (!mgr._splitOverviewActive) {
2230 return;
2231 }
2232 mgr._splitOverviewActive = false;
2233 for (const [id, snap] of mgr._splitOverviewSnapshot) {
2234 const w = mgr.getById(id);
2235 if (!w) {
2236 continue;
2237 }
2238 w.element.style.transform = snap.transform;
2239 }
2240 for (const label of mgr._splitOverviewLabels.values()) {
2241 label.classList.add("desktop-mode-overview-label--out");
2242 }
2243 mgr._desktop.classList.remove("desktop-mode-area--split-overview");
2244 const ANIMATION_MS = 260;
2245 window.setTimeout(() => {
2246 for (const w of mgr._stack) {
2247 if (mgr._splitOverviewSnapshot.has(w.id)) {
2248 w.element.classList.remove("desktop-mode-window--overview");
2249 }
2250 }
2251 for (const label of mgr._splitOverviewLabels.values()) {
2252 label.remove();
2253 }
2254 cleanupSplitOverviewState(mgr);
2255 }, ANIMATION_MS);
2256 if (mgr._splitOverviewPointerDown) {
2257 mgr._desktop.removeEventListener(
2258 "pointerdown",
2259 mgr._splitOverviewPointerDown,
2260 true
2261 );
2262 mgr._splitOverviewPointerDown = null;
2263 }
2264 if (mgr._splitOverviewPointerUp) {
2265 mgr._desktop.removeEventListener(
2266 "pointerup",
2267 mgr._splitOverviewPointerUp,
2268 true
2269 );
2270 mgr._splitOverviewPointerUp = null;
2271 }
2272 if (mgr._splitOverviewClickBlocker) {
2273 mgr._desktop.removeEventListener(
2274 "click",
2275 mgr._splitOverviewClickBlocker,
2276 true
2277 );
2278 mgr._splitOverviewClickBlocker = null;
2279 }
2280 if (mgr._splitOverviewKey) {
2281 document.removeEventListener("keydown", mgr._splitOverviewKey);
2282 mgr._splitOverviewKey = null;
2283 }
2284 mgr._splitOverviewPressTarget = null;
2285 }
2286 function cleanupSplitOverviewState(mgr) {
2287 mgr._splitOverviewSnapshot.clear();
2288 mgr._splitOverviewLabels.clear();
2289 mgr._splitOverviewAnchor = null;
2290 mgr._splitOverviewZone = null;
2291 mgr._splitOverviewActive = false;
2292 }
2293 const SNAP_EDGE_THRESHOLD = 30;
2294 const SNAP_COMMIT_MS = 260;
2295 function detectSnapZone(clientX, desktopRect) {
2296 if (clientX <= desktopRect.left + SNAP_EDGE_THRESHOLD) {
2297 return "left";
2298 }
2299 if (clientX >= desktopRect.right - SNAP_EDGE_THRESHOLD) {
2300 return "right";
2301 }
2302 return null;
2303 }
2304 function snapZoneBounds(mgr, zone) {
2305 const rect = mgr._desktop.getBoundingClientRect();
2306 const halfW = Math.floor(rect.width / 2);
2307 const height = Math.floor(rect.height);
2308 return {
2309 x: zone === "left" ? 0 : rect.width - halfW,
2310 y: 0,
2311 width: halfW,
2312 height
2313 };
2314 }
2315 function oppositeHalfRect(mgr, zone) {
2316 const rect = mgr._desktop.getBoundingClientRect();
2317 const halfW = Math.floor(rect.width / 2);
2318 const height = Math.floor(rect.height);
2319 if (zone === "left") {
2320 return new DOMRect(halfW, 0, halfW, height);
2321 }
2322 return new DOMRect(0, 0, halfW, height);
2323 }
2324 function showSnapPreview(mgr, zone) {
2325 if (mgr._snapPendingZone === zone && mgr._snapPreviewEl) {
2326 return;
2327 }
2328 mgr._snapPendingZone = zone;
2329 if (!mgr._snapPreviewEl) {
2330 const el = document.createElement("div");
2331 el.className = "desktop-mode-snap-preview";
2332 el.setAttribute("aria-hidden", "true");
2333 mgr._desktop.appendChild(el);
2334 mgr._snapPreviewEl = el;
2335 Promise.resolve().then(() => {
2336 el.classList.add("desktop-mode-snap-preview--visible");
2337 });
2338 }
2339 const b = snapZoneBounds(mgr, zone);
2340 mgr._snapPreviewEl.style.left = `${b.x}px`;
2341 mgr._snapPreviewEl.style.top = `${b.y}px`;
2342 mgr._snapPreviewEl.style.width = `${b.width}px`;
2343 mgr._snapPreviewEl.style.height = `${b.height}px`;
2344 mgr._snapPreviewEl.dataset.zone = zone;
2345 }
2346 function hideSnapPreview(mgr) {
2347 if (!mgr._snapPreviewEl) {
2348 mgr._snapPendingZone = null;
2349 return;
2350 }
2351 const el = mgr._snapPreviewEl;
2352 mgr._snapPreviewEl = null;
2353 mgr._snapPendingZone = null;
2354 el.classList.remove("desktop-mode-snap-preview--visible");
2355 window.setTimeout(() => {
2356 el.remove();
2357 }, SNAP_COMMIT_MS);
2358 }
2359 function updateSnapZoneForDrag(mgr, win, clientX) {
2360 if (mgr._splitOverviewActive) {
2361 return;
2362 }
2363 const rect = mgr._desktop.getBoundingClientRect();
2364 const zone = detectSnapZone(clientX, rect);
2365 const previous = mgr._snapPendingZone;
2366 if (zone) {
2367 showSnapPreview(mgr, zone);
2368 if (previous !== zone) {
2369 doAction(HOOKS.SNAP_ZONE_PENDING, {
2370 windowId: win.id,
2371 zone
2372 });
2373 }
2374 } else if (previous) {
2375 hideSnapPreview(mgr);
2376 doAction(HOOKS.SNAP_ZONE_CANCELED, { windowId: win.id });
2377 }
2378 }
2379 function commitSnapIfPending(mgr, win) {
2380 const zone = mgr._snapPendingZone;
2381 if (!zone) {
2382 return false;
2383 }
2384 hideSnapPreview(mgr);
2385 if (win.state === "normal") {
2386 win._savedGeometry = {
2387 x: win.element.offsetLeft,
2388 y: win.element.offsetTop,
2389 width: win.element.offsetWidth,
2390 height: win.element.offsetHeight
2391 };
2392 }
2393 win.applySnap(zone);
2394 doAction(HOOKS.SNAP_ZONE_COMMITTED, {
2395 windowId: win.id,
2396 zone
2397 });
2398 window.requestAnimationFrame(() => {
2399 enterSplitOverview(mgr, win, zone);
2400 });
2401 return true;
2402 }
2403 function abortSnapIfPending(mgr) {
2404 if (mgr._snapPendingZone) {
2405 hideSnapPreview(mgr);
2406 }
2407 }
2408 const NATIVE_GEOMETRY_STORAGE_KEY = "desktop-mode-native-window-geometry";
2409 const MAX_ENTRIES = 64;
2410 const MAX_DIMENSION = 8192;
2411 function readMap$1() {
2412 try {
2413 const raw = window.localStorage.getItem(NATIVE_GEOMETRY_STORAGE_KEY);
2414 if (!raw) {
2415 return {};
2416 }
2417 const parsed = JSON.parse(raw);
2418 if (!parsed || typeof parsed !== "object") {
2419 return {};
2420 }
2421 return parsed;
2422 } catch {
2423 return {};
2424 }
2425 }
2426 function writeMap$1(map) {
2427 try {
2428 window.localStorage.setItem(
2429 NATIVE_GEOMETRY_STORAGE_KEY,
2430 JSON.stringify(map)
2431 );
2432 } catch {
2433 }
2434 }
2435 function loadNativeWindowGeometry(baseId) {
2436 if (!baseId) {
2437 return null;
2438 }
2439 const map = readMap$1();
2440 const entry = map[baseId];
2441 if (!entry) {
2442 return null;
2443 }
2444 const width = Number(entry.width);
2445 const height = Number(entry.height);
2446 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2447 return null;
2448 }
2449 const state2 = entry.state === "maximized" ? "maximized" : void 0;
2450 const x = Number(entry.x);
2451 const y = Number(entry.y);
2452 const hasPosition = Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0 && x <= MAX_DIMENSION && y <= MAX_DIMENSION;
2453 return {
2454 width: Math.round(width),
2455 height: Math.round(height),
2456 ...hasPosition ? { x: Math.round(x), y: Math.round(y) } : {},
2457 ...state2 ? { state: state2 } : {}
2458 };
2459 }
2460 function saveNativeWindowGeometry(baseId, geometry) {
2461 if (!baseId) {
2462 return;
2463 }
2464 const width = Math.round(Number(geometry.width));
2465 const height = Math.round(Number(geometry.height));
2466 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2467 return;
2468 }
2469 const map = readMap$1();
2470 const prev = map[baseId];
2471 const state2 = prev && prev.state === "maximized" ? "maximized" : void 0;
2472 const carriedX = typeof prev?.x === "number" ? prev.x : void 0;
2473 const carriedY = typeof prev?.y === "number" ? prev.y : void 0;
2474 if (prev && prev.width === width && prev.height === height && prev.state === state2 && prev.x === carriedX && prev.y === carriedY) {
2475 return;
2476 }
2477 upsertEntry(map, baseId, {
2478 width,
2479 height,
2480 ...typeof carriedX === "number" && typeof carriedY === "number" ? { x: carriedX, y: carriedY } : {},
2481 ...state2 ? { state: state2 } : {}
2482 });
2483 writeMapTrimmed(map);
2484 }
2485 function saveNativeWindowPosition(baseId, position) {
2486 if (!baseId) {
2487 return;
2488 }
2489 const x = Math.round(Number(position.x));
2490 const y = Math.round(Number(position.y));
2491 if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || y < 0 || x > MAX_DIMENSION || y > MAX_DIMENSION) {
2492 return;
2493 }
2494 const map = readMap$1();
2495 const prev = map[baseId];
2496 if (!prev) {
2497 return;
2498 }
2499 if (prev.x === x && prev.y === y) {
2500 return;
2501 }
2502 upsertEntry(map, baseId, {
2503 ...prev,
2504 x,
2505 y
2506 });
2507 writeMapTrimmed(map);
2508 }
2509 function setNativeWindowSavedState(baseId, state2, defaults) {
2510 if (!baseId) {
2511 return;
2512 }
2513 const map = readMap$1();
2514 const prev = map[baseId];
2515 if (!prev) {
2516 if (state2 === null || !defaults) {
2517 return;
2518 }
2519 const width = Math.round(Number(defaults.width));
2520 const height = Math.round(Number(defaults.height));
2521 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2522 return;
2523 }
2524 upsertEntry(map, baseId, { width, height, state: state2 });
2525 writeMapTrimmed(map);
2526 return;
2527 }
2528 if (state2 === null) {
2529 if (!prev.state) {
2530 return;
2531 }
2532 const { state: _state2, ...rest } = prev;
2533 upsertEntry(map, baseId, rest);
2534 writeMapTrimmed(map);
2535 return;
2536 }
2537 if (prev.state === state2) {
2538 return;
2539 }
2540 upsertEntry(map, baseId, {
2541 ...prev,
2542 state: state2
2543 });
2544 writeMapTrimmed(map);
2545 }
2546 function upsertEntry(map, baseId, entry) {
2547 delete map[baseId];
2548 map[baseId] = entry;
2549 }
2550 function writeMapTrimmed(map) {
2551 const keys = Object.keys(map);
2552 if (keys.length > MAX_ENTRIES) {
2553 const trimmed = {};
2554 for (const key of keys.slice(-MAX_ENTRIES)) {
2555 trimmed[key] = map[key];
2556 }
2557 writeMap$1(trimmed);
2558 return;
2559 }
2560 writeMap$1(map);
2561 }
2562 const BASE_Z_INDEX = 100;
2563 const CASCADE_OFFSET = 30;
2564 class WindowManager {
2565 constructor(desktop) {
2566 this._stack = [];
2567 this.cascadeIndex = 0;
2568 this._desktops = [
2569 // translators: default desktop name — "Desktop 1"
2570 { id: "desktop-1", label: "Desktop 1" }
2571 ];
2572 this._activeDesktopId = "desktop-1";
2573 this._desktopSeq = 1;
2574 this.onToggleStartupRequested = null;
2575 this.desktopResizeObserver = null;
2576 this._reflowRestoreTimer = null;
2577 this._snapEnabled = loadSnapEnabled();
2578 this._overviewActive = false;
2579 this._overviewSnapshot = /* @__PURE__ */ new Map();
2580 this._overviewLabels = /* @__PURE__ */ new Map();
2581 this._overviewPointerDownHandler = null;
2582 this._overviewPointerUpHandler = null;
2583 this._overviewKeyHandler = null;
2584 this._overviewPressTarget = null;
2585 this._overviewClickBlocker = null;
2586 this._overviewTopBar = null;
2587 this._overviewMouseHandler = null;
2588 this._lastOverviewHoverId = null;
2589 this._overviewAddTileFocused = false;
2590 this._snapPendingZone = null;
2591 this._snapPreviewEl = null;
2592 this._splitOverviewActive = false;
2593 this._splitOverviewAnchor = null;
2594 this._splitOverviewZone = null;
2595 this._splitOverviewSnapshot = /* @__PURE__ */ new Map();
2596 this._splitOverviewLabels = /* @__PURE__ */ new Map();
2597 this._splitOverviewPointerDown = null;
2598 this._splitOverviewPointerUp = null;
2599 this._splitOverviewPressTarget = null;
2600 this._splitOverviewClickBlocker = null;
2601 this._splitOverviewKey = null;
2602 this._desktop = desktop;
2603 if (typeof ResizeObserver !== "undefined") {
2604 this.desktopResizeObserver = new ResizeObserver(
2605 () => this.reflowStatefulWindows()
2606 );
2607 this.desktopResizeObserver.observe(desktop);
2608 }
2609 this.installIframeFocusBridge();
2610 }
2611 /**
2612 * Clicks inside an iframe don't cross the browsing-context
2613 * boundary — pointerdown / focusin in the iframe's document never
2614 * reach the parent. BUT the parent `window` does lose focus,
2615 * because focus moves to the iframe's content window.
2616 *
2617 * We use that signal: listen for `window.blur` on the parent,
2618 * check `document.activeElement` — if it's an iframe, walk up to
2619 * its owning `.desktop-mode-window`, find the matching Window in
2620 * our stack, and focus it. Covers clicks on the primary iframe
2621 * AND any external-tab sub-iframes mounted as descendants of the
2622 * window element.
2623 */
2624 installIframeFocusBridge() {
2625 window.addEventListener("blur", () => {
2626 window.setTimeout(() => {
2627 const active2 = this._desktop.ownerDocument?.activeElement ?? null;
2628 if (!active2 || active2.tagName !== "IFRAME") {
2629 return;
2630 }
2631 const winEl = active2.closest(
2632 ".desktop-mode-window"
2633 );
2634 if (!winEl) {
2635 return;
2636 }
2637 const id = winEl.id.replace(/^wp-window-/, "");
2638 const win = this.getById(id);
2639 if (!win) {
2640 return;
2641 }
2642 if (this._overviewActive) {
2643 return;
2644 }
2645 if (this.getFocused() === win) {
2646 return;
2647 }
2648 this.focus(win);
2649 }, 0);
2650 });
2651 }
2652 /**
2653 * Re-apply state-driven bounds to any window whose geometry is
2654 * derived from the desktop area's dimensions: maximized (full
2655 * area) and snapped-left / snapped-right (half area). Called from
2656 * the desktop-area ResizeObserver so shrinking the browser window
2657 * drags the stateful windows along with it.
2658 *
2659 * Inlines the geometry writes instead of calling `applySnap` —
2660 * that method emits `_emitChange('state')` which would spam the
2661 * session saver on every resize tick. Viewport resize is an
2662 * INCOMING shape change (the shell reshaped us), not an outgoing
2663 * user action worth persisting.
2664 *
2665 * Also toggles `desktop-mode-window--reflowing` so the base
2666 * left/top/width/height transition doesn't interpolate between
2667 * every ResizeObserver tick — without that, the windows would
2668 * always lag ~250 ms behind a browser edge-drag.
2669 *
2670 * Skipped while overview is active — windows are mid-transform
2671 * and touching their inline geometry would desync the live
2672 * transform math; overview exit re-applies state correctly via
2673 * its own path.
2674 */
2675 reflowStatefulWindows() {
2676 if (this._overviewActive) {
2677 return;
2678 }
2679 for (const w of this._stack) {
2680 const parent = w.element.parentElement;
2681 if (!parent) {
2682 continue;
2683 }
2684 if (w.state === "maximized") {
2685 w.element.classList.add("desktop-mode-window--reflowing");
2686 w.element.style.width = `${parent.clientWidth}px`;
2687 w.element.style.height = `${parent.clientHeight}px`;
2688 } else if (w.state === "snapped-left" || w.state === "snapped-right") {
2689 w.element.classList.add("desktop-mode-window--reflowing");
2690 const halfW = Math.floor(parent.clientWidth / 2);
2691 const height = parent.clientHeight;
2692 const left = w.state === "snapped-left" ? 0 : halfW;
2693 w.element.style.left = `${left}px`;
2694 w.element.style.top = "0px";
2695 w.element.style.width = `${halfW}px`;
2696 w.element.style.height = `${height}px`;
2697 }
2698 }
2699 if (this._reflowRestoreTimer !== null) {
2700 window.clearTimeout(this._reflowRestoreTimer);
2701 }
2702 this._reflowRestoreTimer = window.setTimeout(() => {
2703 this._reflowRestoreTimer = null;
2704 for (const w of this._stack) {
2705 w.element.classList.remove("desktop-mode-window--reflowing");
2706 }
2707 }, 140);
2708 }
2709 /**
2710 * Open a new window — or focus an existing one — for the given
2711 * page.
2712 *
2713 * Matches any existing window sharing the same `baseId`
2714 * (defaulting to the config's `id`). For singleton pages
2715 * (Settings, Dashboard, …) `baseId === id`, so this behaves
2716 * exactly like strict id matching. For multi pages, clicking the
2717 * dock icon while a window is already open focuses the
2718 * most-recent instance rather than creating a twin.
2719 *
2720 * To force a brand-new instance alongside an existing one, use
2721 * {@link openNew}.
2722 */
2723 async open(config) {
2724 if (!config || typeof config !== "object") {
2725 throw new TypeError(
2726 "windowManager.open() requires a config object with at least { id, url, title }; received " + (config === null ? "null" : typeof config)
2727 );
2728 }
2729 if (typeof config.id !== "string" || config.id === "") {
2730 throw new TypeError(
2731 "windowManager.open(): config.id must be a non-empty string."
2732 );
2733 }
2734 if (typeof config.url !== "string" || config.url === "") {
2735 throw new TypeError(
2736 '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.'
2737 );
2738 }
2739 if (typeof config.title !== "string") {
2740 throw new TypeError(
2741 "windowManager.open(): config.title must be a string."
2742 );
2743 }
2744 const baseId = config.baseId || config.id;
2745 const existing = this.getByBaseIdOnActiveDesktop(baseId);
2746 if (existing) {
2747 const wasMinimized = existing.state === "minimized";
2748 this.focus(existing);
2749 if (wasMinimized) {
2750 existing.restore();
2751 }
2752 const reopenedDetail = {
2753 windowId: existing.id,
2754 baseId,
2755 wasMinimized
2756 };
2757 document.dispatchEvent(
2758 new CustomEvent("desktop-mode-window-reopened", { detail: reopenedDetail })
2759 );
2760 doAction(HOOKS.WINDOW_REOPENED, reopenedDetail);
2761 return existing;
2762 }
2763 const id = this.getByBaseId(baseId) ? this.nextInstanceId(baseId) : config.id;
2764 return this.createWindow({ ...config, id, baseId });
2765 }
2766 /**
2767 * Open a brand-new window even if one is already open for this
2768 * page. Only makes sense for pages flagged `multi`.
2769 *
2770 * Duplicates always open in the floating ('normal') state and at
2771 * a fresh cascade slot — the per-baseId saved size / state /
2772 * position preferences apply to the primary instance only.
2773 * Spawning a maximized twin alongside the maximized primary
2774 * would hide the primary; landing a twin on top of the primary's
2775 * remembered position would hide it too. Callers can override
2776 * either default by passing `initialState` / `x` / `y` explicitly.
2777 */
2778 async openNew(config) {
2779 const baseId = config.baseId || config.id;
2780 const nextId2 = this.nextInstanceId(baseId);
2781 const cascadeX = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2782 const cascadeY = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2783 return this.createWindow({
2784 initialState: "normal",
2785 x: cascadeX,
2786 y: cascadeY,
2787 ...config,
2788 id: nextId2,
2789 baseId
2790 });
2791 }
2792 /**
2793 * Build and mount a window element. Common tail shared by
2794 * `open()` and `openNew()`.
2795 */
2796 async createWindow(config) {
2797 const desktopRect = this._desktop.getBoundingClientRect();
2798 const defaultWidth = Math.min(Math.round(desktopRect.width * 0.8), 1200);
2799 const defaultHeight = Math.min(Math.round(desktopRect.height * 0.8), 800);
2800 const cascadeX = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2801 const cascadeY = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2802 const resolvedBaseId = config.baseId || config.id;
2803 const minWidth = config.minWidth ?? 320;
2804 const minHeight = config.minHeight ?? 200;
2805 const hasExplicitWidth = typeof config.width === "number";
2806 const hasExplicitHeight = typeof config.height === "number";
2807 const hasExplicitX = typeof config.x === "number";
2808 const hasExplicitY = typeof config.y === "number";
2809 const hasExplicitState = typeof config.initialState === "string";
2810 const saved = !hasExplicitWidth || !hasExplicitHeight || !hasExplicitState || !hasExplicitX || !hasExplicitY ? loadNativeWindowGeometry(resolvedBaseId) : null;
2811 const resolvedWidth = config.width ?? (saved ? Math.max(saved.width, minWidth) : defaultWidth);
2812 const resolvedHeight = config.height ?? (saved ? Math.max(saved.height, minHeight) : defaultHeight);
2813 const resolvedState = config.initialState ?? (saved?.state === "maximized" ? "maximized" : void 0);
2814 let clampedSavedX;
2815 let clampedSavedY;
2816 if (saved && typeof saved.x === "number" && typeof saved.y === "number") {
2817 const margin = 12;
2818 const maxX = Math.max(
2819 0,
2820 desktopRect.width - resolvedWidth - margin
2821 );
2822 const maxY = Math.max(
2823 0,
2824 desktopRect.height - resolvedHeight - margin
2825 );
2826 clampedSavedX = Math.max(margin, Math.min(saved.x, maxX));
2827 clampedSavedY = Math.max(margin, Math.min(saved.y, maxY));
2828 }
2829 const resolvedX = config.x ?? clampedSavedX ?? cascadeX;
2830 const resolvedY = config.y ?? clampedSavedY ?? cascadeY;
2831 const callerPinned = hasExplicitWidth || hasExplicitHeight || hasExplicitX || hasExplicitY || hasExplicitState;
2832 const hasSavedGeometry = !!saved;
2833 const preFilterGeometry = {
2834 x: resolvedX,
2835 y: resolvedY,
2836 width: resolvedWidth,
2837 height: resolvedHeight,
2838 state: resolvedState
2839 };
2840 let filtered;
2841 try {
2842 filtered = applyFilters(
2843 HOOKS.WINDOW_GEOMETRY,
2844 preFilterGeometry,
2845 {
2846 windowId: config.id,
2847 baseId: resolvedBaseId,
2848 hasSavedGeometry,
2849 callerPinned,
2850 desktopRect: {
2851 width: desktopRect.width,
2852 height: desktopRect.height
2853 }
2854 }
2855 );
2856 } catch (err) {
2857 doAction(HOOKS.SHELL_ERROR, {
2858 scope: "window-geometry-filter",
2859 windowId: config.id,
2860 error: err
2861 });
2862 if (typeof console !== "undefined") {
2863 console.error(
2864 `[desktop-mode] WINDOW_GEOMETRY filter threw for "${config.id}":`,
2865 err
2866 );
2867 }
2868 filtered = preFilterGeometry;
2869 }
2870 const coalesce = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback;
2871 const safeFiltered = filtered && typeof filtered === "object" ? filtered : preFilterGeometry;
2872 const finalWidth = Math.max(
2873 coalesce(safeFiltered.width, resolvedWidth),
2874 minWidth
2875 );
2876 const finalHeight = Math.max(
2877 coalesce(safeFiltered.height, resolvedHeight),
2878 minHeight
2879 );
2880 const finalX = coalesce(safeFiltered.x, resolvedX);
2881 const finalY = coalesce(safeFiltered.y, resolvedY);
2882 const finalState = safeFiltered.state ?? resolvedState;
2883 const fullConfig = {
2884 icon: config.icon || "dashicons-admin-generic",
2885 ...config,
2886 // Spread `config` first so callers can pass through any
2887 // extras (render, ownerHandle, parentUrl, …), then pin the
2888 // dimensions + state we resolved above. The pin has to
2889 // follow the spread because an explicit `width: undefined`
2890 // from the caller would otherwise blow away the default.
2891 x: finalX,
2892 y: finalY,
2893 width: finalWidth,
2894 height: finalHeight,
2895 minWidth,
2896 minHeight,
2897 ...finalState ? { initialState: finalState } : {},
2898 baseId: resolvedBaseId,
2899 // New windows always join the active desktop. A caller can
2900 // pre-seed `desktopId` (e.g. session restore) by passing it
2901 // in `config`, which the spread above preserves.
2902 desktopId: config.desktopId || this._activeDesktopId
2903 };
2904 this.cascadeIndex++;
2905 const [system] = await Promise.all([
2906 ensureWindowSystemLoaded(windowSystemBundleUrl()),
2907 ensureShellOverlaysLoaded(shellOverlaysBundleUrl())
2908 ]);
2909 const win = system.createWindow(fullConfig);
2910 win.onFocusRequest = (w) => this.focus(w);
2911 win.onClose = (w) => this.remove(w);
2912 win.onMinimize = () => {
2913 const visible = this._stack.filter((w) => w.state !== "minimized");
2914 if (visible.length > 0) {
2915 this.focus(visible[visible.length - 1]);
2916 }
2917 };
2918 win.onOpenAnother = (w) => {
2919 const baseId = w.config.baseId || w.id;
2920 if (w.config.native) {
2921 const api = window.wp?.desktop;
2922 if (api?.openNewWindow?.(baseId, { source: "open-another" })) {
2923 return;
2924 }
2925 }
2926 void this.openNew({
2927 id: baseId,
2928 baseId,
2929 url: w.config.url || "",
2930 title: w.config.title,
2931 icon: w.config.icon,
2932 submenu: w.config.submenu,
2933 multi: true
2934 });
2935 };
2936 win.onOpenInNewWindow = (w) => {
2937 const baseId = w.config.baseId || w.id;
2938 if (w.config.native) {
2939 const api = window.wp?.desktop;
2940 if (api?.openNewWindow?.(baseId, { source: "open-in-new-window" })) {
2941 return;
2942 }
2943 }
2944 const currentUrl = w.getCurrentUrl();
2945 void this.openNew({
2946 id: baseId,
2947 baseId,
2948 url: currentUrl || w.config.url || "",
2949 title: w.config.title,
2950 icon: w.config.icon,
2951 submenu: w.config.submenu,
2952 multi: true
2953 });
2954 };
2955 win.onToggleStartup = (w) => {
2956 this.onToggleStartupRequested?.(w);
2957 };
2958 win.snapConfigProvider = () => this.getSnapConfig();
2959 win.onDragMove = (w, clientX) => {
2960 updateSnapZoneForDrag(this, w, clientX);
2961 };
2962 win.onDragEnd = (w) => {
2963 if (this._snapPendingZone) {
2964 return commitSnapIfPending(this, w);
2965 }
2966 abortSnapIfPending(this);
2967 return false;
2968 };
2969 this._stack.push(win);
2970 this._desktop.appendChild(win.element);
2971 applyDesktopVisibility(this, win);
2972 win.hydrateNative();
2973 this.focus(win);
2974 const openedDetail = {
2975 windowId: win.id,
2976 page: config.url,
2977 title: config.title,
2978 url: config.url
2979 };
2980 document.dispatchEvent(
2981 new CustomEvent("desktop-mode-window-opened", { detail: openedDetail })
2982 );
2983 doAction(HOOKS.WINDOW_OPENED, openedDetail);
2984 return win;
2985 }
2986 /**
2987 * Find the next unused suffixed id for a given baseId. Prefers
2988 * the bare baseId itself if free (user closed the original), then
2989 * walks `-2`, `-3`, … until it lands on one not currently in the
2990 * stack.
2991 */
2992 nextInstanceId(baseId) {
2993 const taken = new Set(this._stack.map((w) => w.id));
2994 if (!taken.has(baseId)) {
2995 return baseId;
2996 }
2997 let n = 2;
2998 while (taken.has(`${baseId}-${n}`)) {
2999 n++;
3000 }
3001 return `${baseId}-${n}`;
3002 }
3003 /** Focus a window: bring it to top of z-stack. */
3004 focus(win) {
3005 const previouslyFocused = this._stack.length > 0 ? this._stack[this._stack.length - 1] : null;
3006 const priorFullscreen = this._stack.find(
3007 (w) => w !== win && w.isFocused() && w.isFullscreen()
3008 );
3009 if (priorFullscreen) {
3010 const shouldExit = applyFilters(
3011 HOOKS.WINDOW_AUTO_EXIT_FULLSCREEN,
3012 true,
3013 { windowId: priorFullscreen.id, focusedTo: win.id }
3014 );
3015 if (shouldExit) {
3016 priorFullscreen.toggleFullscreen();
3017 }
3018 }
3019 const idx = this._stack.indexOf(win);
3020 if (idx > -1) {
3021 this._stack.splice(idx, 1);
3022 }
3023 this._stack.push(win);
3024 this._stack.forEach((w, i) => {
3025 w.setZIndex(BASE_Z_INDEX + i);
3026 w.setFocused(i === this._stack.length - 1);
3027 });
3028 if (previouslyFocused && previouslyFocused !== win && previouslyFocused.id !== win.id) {
3029 const blurredDetail = {
3030 windowId: previouslyFocused.id,
3031 focusedTo: win.id
3032 };
3033 document.dispatchEvent(
3034 new CustomEvent("desktop-mode-window-blurred", { detail: blurredDetail })
3035 );
3036 doAction(HOOKS.WINDOW_BLURRED, blurredDetail);
3037 }
3038 const focusedDetail = { windowId: win.id };
3039 document.dispatchEvent(
3040 new CustomEvent("desktop-mode-window-focused", { detail: focusedDetail })
3041 );
3042 doAction(HOOKS.WINDOW_FOCUSED, focusedDetail);
3043 }
3044 /** Remove a window from the stack and DOM. */
3045 remove(win) {
3046 const idx = this._stack.indexOf(win);
3047 if (idx > -1) {
3048 this._stack.splice(idx, 1);
3049 }
3050 for (let i = this._stack.length - 1; i >= 0; i--) {
3051 const candidate = this._stack[i];
3052 if (candidate.state === "minimized") {
3053 continue;
3054 }
3055 const candidateDesktop = candidate.config.desktopId || this._activeDesktopId;
3056 if (candidateDesktop !== this._activeDesktopId) {
3057 continue;
3058 }
3059 this.focus(candidate);
3060 break;
3061 }
3062 const closingDetail = { windowId: win.id, element: win.element };
3063 document.dispatchEvent(
3064 new CustomEvent("desktop-mode-window-closing", { detail: closingDetail })
3065 );
3066 doAction(HOOKS.WINDOW_CLOSING, closingDetail);
3067 const closedDetail = { windowId: win.id };
3068 document.dispatchEvent(
3069 new CustomEvent("desktop-mode-window-closed", { detail: closedDetail })
3070 );
3071 doAction(HOOKS.WINDOW_CLOSED, closedDetail);
3072 }
3073 /** Get a window by its ID. */
3074 getById(id) {
3075 return this._stack.find((w) => w.id === id);
3076 }
3077 /**
3078 * Get the most-recently-focused window for a given baseId.
3079 *
3080 * Multi-instance windows share a baseId; the stack is ordered
3081 * bottom to top by focus, so iterating from the end finds the
3082 * best candidate to bring forward when the user re-clicks the
3083 * dock icon.
3084 */
3085 getByBaseId(baseId) {
3086 for (let i = this._stack.length - 1; i >= 0; i--) {
3087 const w = this._stack[i];
3088 if ((w.config.baseId || w.id) === baseId) {
3089 return w;
3090 }
3091 }
3092 return void 0;
3093 }
3094 /**
3095 * Like {@link getByBaseId} but only considers windows on the
3096 * currently-active virtual desktop. The dock's "open or focus"
3097 * path uses this — a Plugins instance that lives on Desktop 2 is
3098 * invisible from Desktop 1's dock click, so clicking Plugins on
3099 * Desktop 1 should open a fresh instance there instead of trying
3100 * to focus the far-off sibling (which would silently do nothing
3101 * because the other desktop's windows are display: none here).
3102 */
3103 getByBaseIdOnActiveDesktop(baseId) {
3104 for (let i = this._stack.length - 1; i >= 0; i--) {
3105 const w = this._stack[i];
3106 if ((w.config.baseId || w.id) !== baseId) {
3107 continue;
3108 }
3109 const winDesktop = w.config.desktopId || this._activeDesktopId;
3110 if (winDesktop === this._activeDesktopId) {
3111 return w;
3112 }
3113 }
3114 return void 0;
3115 }
3116 /**
3117 * Get every open window sharing the given baseId, ordered by
3118 * instance slot (bare baseId first, then `-2`, `-3`, …) rather
3119 * than z-order — so the dock's instance rail keeps a stable
3120 * left-to-right order even as the user focuses between windows.
3121 */
3122 getAllByBaseId(baseId) {
3123 const instanceSlot = (id) => {
3124 if (id === baseId) {
3125 return 1;
3126 }
3127 const prefix = `${baseId}-`;
3128 if (id.startsWith(prefix)) {
3129 const n = parseInt(id.slice(prefix.length), 10);
3130 return Number.isFinite(n) ? n : 999;
3131 }
3132 return 999;
3133 };
3134 return this._stack.filter((w) => (w.config.baseId || w.id) === baseId).sort((a, b) => instanceSlot(a.id) - instanceSlot(b.id));
3135 }
3136 /** Get all open windows. */
3137 getAll() {
3138 return [...this._stack];
3139 }
3140 /**
3141 * Find the window whose iframe's contentWindow matches the given
3142 * message source. Used by cross-frame bridges to attribute inbound
3143 * `postMessage` events to the originating window without reaching
3144 * into `_stack`.
3145 */
3146 findByIframeSource(source) {
3147 if (!source) {
3148 return void 0;
3149 }
3150 return this._stack.find(
3151 (w) => w.iframe !== null && w.iframe.contentWindow === source
3152 );
3153 }
3154 /** Get the currently focused (topmost) window. */
3155 getFocused() {
3156 return this._stack.length > 0 ? this._stack[this._stack.length - 1] : void 0;
3157 }
3158 /**
3159 * "Is the window with this id currently in front of the user?"
3160 *
3161 * Returns true when the window exists in the manager AND it
3162 * isn't minimized AND it's the currently focused (topmost)
3163 * window. False otherwise — including for unknown ids, closed
3164 * windows, minimized windows, or windows that exist but aren't
3165 * on top.
3166 *
3167 * The canonical query for plugins implementing the "show
3168 * something *only when the user can't already see my
3169 * window*" pattern (badge counts, attention pulses, sounds,
3170 * toasts). Plugins that previously hand-rolled
3171 * `getById(id) && state !== 'minimized' && focused` can
3172 * collapse to this.
3173 *
3174 * @since 0.5.5
3175 *
3176 * @param id Window id to query.
3177 * @return True when the user is actively looking at this window.
3178 */
3179 isActive(id) {
3180 const win = this.getById(id);
3181 if (!win) {
3182 return false;
3183 }
3184 if (win.state === "minimized") {
3185 return false;
3186 }
3187 const focused = this.getFocused();
3188 return !!focused && focused.id === id;
3189 }
3190 // ---- Virtual desktop delegations ----
3191 getDesktops() {
3192 return getDesktops(this);
3193 }
3194 getActiveDesktop() {
3195 return getActiveDesktop(this);
3196 }
3197 getActiveDesktopId() {
3198 return getActiveDesktopId(this);
3199 }
3200 createDesktop() {
3201 return createDesktop(this);
3202 }
3203 switchDesktop(id, opts) {
3204 switchDesktop(this, id, opts);
3205 }
3206 closeDesktop(id) {
3207 closeDesktop(this, id);
3208 }
3209 /**
3210 * Returns the "primary" desktop id — the one new sessions land on
3211 * and that batch operations like {@link closeAll} treat as the
3212 * survivor when an `onlyOnPrimary` mode is requested.
3213 *
3214 * Default: the first desktop in `getDesktops()`. Filterable via
3215 * `desktop-mode.primary-desktop-id` so downstream code that wants a
3216 * different convention (e.g. a pinned "Inbox" desktop) can override
3217 * without having to fork the manager.
3218 *
3219 * @since 0.14.0
3220 */
3221 getPrimaryDesktopId() {
3222 const all2 = this.getDesktops();
3223 const fallback = all2.length > 0 ? all2[0].id : "desktop-1";
3224 const filtered = applyFilters(
3225 HOOKS.PRIMARY_DESKTOP_ID,
3226 fallback,
3227 all2
3228 );
3229 if (typeof filtered !== "string" || filtered === "") {
3230 return fallback;
3231 }
3232 const exists = all2.some((d) => d.id === filtered);
3233 return exists ? filtered : fallback;
3234 }
3235 /**
3236 * Close every open window in batch.
3237 *
3238 * Hook chain:
3239 *
3240 * 1. `desktop-mode.windows.before-close-all` — action. Subscribers
3241 * can prepare for the wipe (cancel pending saves, dismiss
3242 * menus, etc.). Detail: `{ candidates: Window[] }`.
3243 *
3244 * 2. `desktop-mode.windows.close-all` — filter. Receives the
3245 * candidate Window list and returns the (possibly smaller) list
3246 * that will actually be closed. Plugins use this to PROTECT
3247 * specific windows — e.g. keep a draft post window open during
3248 * a "Close all" operation. Returning an empty array cancels
3249 * the close entirely.
3250 *
3251 * 3. Each surviving window's `close()` is called.
3252 *
3253 * 4. `desktop-mode.windows.after-close-all` — action. Detail:
3254 * `{ closed: number, skipped: Window[] }`.
3255 *
3256 * @since 0.14.0
3257 *
3258 * @param options Close options.
3259 * @param options.exceptIds Window ids to skip even before the filter runs.
3260 * @return Number of windows actually closed.
3261 */
3262 closeAll(options) {
3263 const exceptSet = new Set(options?.exceptIds ?? []);
3264 const initialCandidates = this._stack.filter(
3265 (w) => !exceptSet.has(w.id)
3266 );
3267 doAction(HOOKS.WINDOWS_BEFORE_CLOSE_ALL, { candidates: initialCandidates });
3268 const filtered = applyFilters(
3269 HOOKS.WINDOWS_CLOSE_ALL,
3270 initialCandidates
3271 );
3272 const finalList = Array.isArray(filtered) ? filtered : initialCandidates;
3273 const skipped = initialCandidates.filter((w) => !finalList.includes(w));
3274 let closed = 0;
3275 for (const win of finalList.slice()) {
3276 try {
3277 win.close();
3278 closed++;
3279 } catch (err) {
3280 if (typeof console !== "undefined") {
3281 console.error(
3282 "[desktop-mode] closeAll: window.close() threw for",
3283 win.id,
3284 err
3285 );
3286 }
3287 }
3288 }
3289 doAction(HOOKS.WINDOWS_AFTER_CLOSE_ALL, { closed, skipped });
3290 return closed;
3291 }
3292 /**
3293 * Minimize every currently-non-minimized window. Returns the
3294 * exact set that was minimized — i.e., excludes windows already
3295 * in the `'minimized'` state — so callers can pair the call with
3296 * a later {@link restoreFrom} that touches only the windows
3297 * they minimized.
3298 *
3299 * The "Show Desktop" gesture (clicking the wallpaper) routes
3300 * through this method (and {@link restoreFrom} on the second
3301 * click); plugin authors building expand/collapse UIs that
3302 * mimic the gesture should use these primitives instead of
3303 * rolling the loop themselves.
3304 *
3305 * @public
3306 * @since 0.18.0
3307 */
3308 minimizeAll() {
3309 const minimized = [];
3310 for (const win of this._stack.slice()) {
3311 if (win.state === "minimized") {
3312 continue;
3313 }
3314 try {
3315 win.minimize();
3316 minimized.push(win);
3317 } catch (err) {
3318 if (typeof console !== "undefined") {
3319 console.error(
3320 "[desktop-mode] minimizeAll: window.minimize() threw for",
3321 win.id,
3322 err
3323 );
3324 }
3325 }
3326 }
3327 return minimized;
3328 }
3329 /**
3330 * Restore the given window list — the symmetric counterpart to
3331 * {@link minimizeAll}. Skips windows that have since been
3332 * closed and windows the user manually un-minimized between
3333 * the minimize and the restore.
3334 *
3335 * Pass the array {@link minimizeAll} returned to restore
3336 * exactly what you minimized; pass any subset to restore
3337 * selectively.
3338 *
3339 * @public
3340 * @since 0.18.0
3341 */
3342 restoreFrom(windows) {
3343 if (!Array.isArray(windows)) {
3344 return;
3345 }
3346 const live = new Set(this._stack);
3347 for (const win of windows) {
3348 if (!live.has(win)) {
3349 continue;
3350 }
3351 if (win.state !== "minimized") {
3352 continue;
3353 }
3354 try {
3355 win.restore();
3356 } catch (err) {
3357 if (typeof console !== "undefined") {
3358 console.error(
3359 "[desktop-mode] restoreFrom: window.restore() threw for",
3360 win.id,
3361 err
3362 );
3363 }
3364 }
3365 }
3366 }
3367 /**
3368 * Toggle the "Show Desktop" state — if every live window is
3369 * already minimized, restore them all; otherwise minimize the
3370 * non-minimized cohort. Returns `true` when the new state is
3371 * "showing the desktop" (everything minimized after the call),
3372 * `false` when windows have just been restored.
3373 *
3374 * Mirrors the wallpaper-click gesture exactly, in one call.
3375 *
3376 * @public
3377 * @since 0.18.0
3378 */
3379 toggleShowDesktop() {
3380 const all2 = this._stack.slice();
3381 if (all2.length === 0) {
3382 return false;
3383 }
3384 const allMinimized = all2.every((w) => w.state === "minimized");
3385 if (allMinimized) {
3386 for (const win of all2) {
3387 try {
3388 win.restore();
3389 } catch {
3390 }
3391 }
3392 return false;
3393 }
3394 this.minimizeAll();
3395 return true;
3396 }
3397 // ---- Arrange + snap delegations ----
3398 cascade() {
3399 cascade(this);
3400 }
3401 tile() {
3402 tile(this);
3403 }
3404 isSnapEnabled() {
3405 return this._snapEnabled;
3406 }
3407 setSnapEnabled(enabled) {
3408 setSnapEnabled(this, enabled);
3409 }
3410 getSnapConfig() {
3411 return getSnapConfig(this);
3412 }
3413 // ---- Overview delegations ----
3414 enterOverview() {
3415 enterOverview(this);
3416 }
3417 exitOverview(selected, maximize = false) {
3418 exitOverview(this, selected, maximize);
3419 }
3420 /**
3421 * Snapshot every open window's current geometry + state.
3422 *
3423 * Returns a plain array of `{ windowId, rect, state, element }`
3424 * entries — one per window in the stack, regardless of which
3425 * virtual desktop owns it. Rect coordinates are in desktop-area
3426 * space (the same coordinate space the windows themselves use
3427 * inline-style left/top); `state` is the live `WindowState`, and
3428 * `element` is the window's outer DOM node.
3429 *
3430 * Intended for wallpaper / overlay plugins that used to scrape
3431 * `document.querySelectorAll('.desktop-mode-window')` + read the
3432 * `--minimized` / `--maximized` modifier classes by name. The
3433 * accessor decouples plugin code from the shell's CSS class
3434 * naming, so a future refactor of modifier prefixes is not an
3435 * ecosystem break.
3436 *
3437 * The array contains every window in the stack — callers filter
3438 * on `state` if they want only "actually visible" (typically
3439 * `state !== 'minimized'`). Minimized windows are included so
3440 * plugins that care about the "will be restored to X geometry"
3441 * case still have the data; filtering them out would be a
3442 * subtraction the caller can do but the provider can't reverse.
3443 *
3444 * Order matches the internal z-stack: earliest-opened first,
3445 * focused window last.
3446 */
3447 getVisibleRects() {
3448 return this._stack.map((w) => {
3449 const snap = w.getSnapshot();
3450 return {
3451 windowId: w.id,
3452 rect: {
3453 x: snap.x,
3454 y: snap.y,
3455 width: snap.width,
3456 height: snap.height
3457 },
3458 state: snap.state,
3459 element: w.element
3460 };
3461 });
3462 }
3463 /**
3464 * Serialize the current window stack for session persistence.
3465 *
3466 * Order in the returned `windows` array mirrors z-order (earliest
3467 * opened / lowest-z first, focused last) so restoring preserves
3468 * the stacking the user left behind.
3469 */
3470 snapshot() {
3471 const focused = this.getFocused();
3472 const persistable = this._stack.filter((w) => !w.config.native);
3473 const windows = persistable.map((w) => {
3474 const snap = w.getSnapshot();
3475 const externalTabs = w.getExternalTabsSnapshot();
3476 return {
3477 id: w.id,
3478 baseId: w.config.baseId || w.id,
3479 desktopId: w.config.desktopId || this._activeDesktopId,
3480 url: w.getCurrentUrl(),
3481 title: w.config.title,
3482 icon: w.config.icon,
3483 state: snap.state,
3484 x: snap.x,
3485 y: snap.y,
3486 width: snap.width,
3487 height: snap.height,
3488 ...externalTabs.length > 0 ? { externalTabs } : {}
3489 };
3490 });
3491 const focusedId = focused && !focused.config.native ? focused.id : "";
3492 return {
3493 windows,
3494 desktops: this.getDesktops(),
3495 activeDesktop: this._activeDesktopId,
3496 focused: focusedId,
3497 updated: Math.floor(Date.now() / 1e3)
3498 };
3499 }
3500 seedDesktops(desktops, activeDesktopId) {
3501 seedDesktops(this, desktops, activeDesktopId);
3502 }
3503 }
3504 function cycleableWindows(mgr) {
3505 const activeDesktopId = mgr.getActiveDesktopId();
3506 const domOrder = Array.from(mgr._desktop.children);
3507 return mgr.getAll().filter((w) => {
3508 const winDesktop = w.config.desktopId || activeDesktopId;
3509 return winDesktop === activeDesktopId;
3510 }).sort(
3511 (a, b) => domOrder.indexOf(a.element) - domOrder.indexOf(b.element)
3512 );
3513 }
3514 function cycleFocus(mgr, direction) {
3515 if (mgr._overviewActive) {
3516 return;
3517 }
3518 const list2 = cycleableWindows(mgr);
3519 if (list2.length < 2) {
3520 return;
3521 }
3522 const focused = mgr.getFocused();
3523 const currentIdx = focused ? list2.indexOf(focused) : -1;
3524 const step = direction === "next" ? 1 : -1;
3525 const nextIdx = (currentIdx + step + list2.length) % list2.length;
3526 const target2 = list2[nextIdx];
3527 if (target2.state === "minimized") {
3528 target2.restore();
3529 } else {
3530 mgr.focus(target2);
3531 }
3532 }
3533 let installed$3 = false;
3534 function isTextEntryFocus(doc) {
3535 let el = doc.activeElement;
3536 while (el && el.shadowRoot && el.shadowRoot.activeElement) {
3537 el = el.shadowRoot.activeElement;
3538 }
3539 if (!el) {
3540 return false;
3541 }
3542 if (el instanceof HTMLIFrameElement) {
3543 return true;
3544 }
3545 if (el instanceof HTMLTextAreaElement) {
3546 return true;
3547 }
3548 if (el instanceof HTMLInputElement) {
3549 const textTypes = /* @__PURE__ */ new Set([
3550 "text",
3551 "search",
3552 "url",
3553 "email",
3554 "password",
3555 "tel",
3556 "number",
3557 "date",
3558 "datetime-local",
3559 "month",
3560 "week",
3561 "time"
3562 ]);
3563 return textTypes.has(el.type);
3564 }
3565 if (el instanceof HTMLElement && el.isContentEditable === true) {
3566 return true;
3567 }
3568 const ce = el.getAttribute("contenteditable");
3569 return ce !== null && ce !== "false";
3570 }
3571 function installWindowSwitcherShortcut(mgr) {
3572 if (installed$3) {
3573 return;
3574 }
3575 installed$3 = true;
3576 document.addEventListener(
3577 "keydown",
3578 (e) => {
3579 if (e.ctrlKey || e.metaKey || e.altKey) {
3580 return;
3581 }
3582 if (e.code !== "Backquote") {
3583 return;
3584 }
3585 if (isTextEntryFocus(document)) {
3586 return;
3587 }
3588 e.preventDefault();
3589 cycleFocus(mgr, e.shiftKey ? "prev" : "next");
3590 },
3591 true
3592 );
3593 const origin = window.location.origin;
3594 window.addEventListener("message", (e) => {
3595 if (e.origin !== origin) {
3596 return;
3597 }
3598 const data = e.data;
3599 if (!data || data.type !== "desktop-mode-window-switch") {
3600 return;
3601 }
3602 cycleFocus(mgr, data.direction === "prev" ? "prev" : "next");
3603 });
3604 }
3605 function switchToAdjacentDesktop(mgr, direction) {
3606 const desktops = mgr.getDesktops();
3607 if (desktops.length < 2) {
3608 return false;
3609 }
3610 const activeId = mgr.getActiveDesktopId();
3611 const idx = desktops.findIndex((d) => d.id === activeId);
3612 if (idx === -1) {
3613 return false;
3614 }
3615 const step = direction === "next" ? 1 : -1;
3616 const targetIdx = (idx + step + desktops.length) % desktops.length;
3617 if (targetIdx === idx) {
3618 return false;
3619 }
3620 mgr.switchDesktop(desktops[targetIdx].id, { direction });
3621 return true;
3622 }
3623 function cycleOverviewCursor(mgr, direction) {
3624 if (!mgr._overviewActive) {
3625 return false;
3626 }
3627 const desktops = mgr.getDesktops();
3628 const cycleLength = desktops.length + 1;
3629 const ADD_INDEX = desktops.length;
3630 const currentIdx = mgr._overviewAddTileFocused ? ADD_INDEX : desktops.findIndex((d) => d.id === mgr.getActiveDesktopId());
3631 if (currentIdx === -1) {
3632 return false;
3633 }
3634 const step = direction === "next" ? 1 : -1;
3635 const targetIdx = (currentIdx + step + cycleLength) % cycleLength;
3636 if (targetIdx === currentIdx) {
3637 return false;
3638 }
3639 if (targetIdx === ADD_INDEX) {
3640 mgr._overviewAddTileFocused = true;
3641 refreshOverviewTopBar(mgr);
3642 return true;
3643 }
3644 mgr._overviewAddTileFocused = false;
3645 mgr.switchDesktop(desktops[targetIdx].id, { direction });
3646 return true;
3647 }
3648 function toggleOverview(mgr) {
3649 if (mgr._overviewActive) {
3650 mgr.exitOverview();
3651 } else {
3652 mgr.enterOverview();
3653 }
3654 return true;
3655 }
3656 function toggleShowDesktop(mgr) {
3657 if (mgr._overviewActive) {
3658 return false;
3659 }
3660 if (mgr.getAll().length === 0) {
3661 return false;
3662 }
3663 mgr.toggleShowDesktop();
3664 return true;
3665 }
3666 function exitOverviewIfActive(mgr) {
3667 if (!mgr._overviewActive) {
3668 return false;
3669 }
3670 mgr.exitOverview();
3671 return true;
3672 }
3673 function isShowDesktopActive(mgr) {
3674 const all2 = mgr.getAll();
3675 if (all2.length === 0) {
3676 return false;
3677 }
3678 return all2.every((w) => w.state === "minimized");
3679 }
3680 function exitShowDesktopIfActive(mgr) {
3681 if (!isShowDesktopActive(mgr)) {
3682 return false;
3683 }
3684 mgr.toggleShowDesktop();
3685 return true;
3686 }
3687 let installed$2 = false;
3688 function installDesktopArrowShortcuts(mgr) {
3689 if (installed$2) {
3690 return;
3691 }
3692 installed$2 = true;
3693 document.addEventListener(
3694 "keydown",
3695 (e) => {
3696 if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) {
3697 return;
3698 }
3699 if (e.code !== "ArrowLeft" && e.code !== "ArrowRight" && e.code !== "ArrowUp" && e.code !== "ArrowDown") {
3700 return;
3701 }
3702 if (isTextEntryFocus(document)) {
3703 return;
3704 }
3705 let handled = false;
3706 switch (e.code) {
3707 case "ArrowLeft":
3708 handled = mgr._overviewActive ? cycleOverviewCursor(mgr, "prev") : switchToAdjacentDesktop(mgr, "prev");
3709 break;
3710 case "ArrowRight":
3711 handled = mgr._overviewActive ? cycleOverviewCursor(mgr, "next") : switchToAdjacentDesktop(mgr, "next");
3712 break;
3713 case "ArrowUp":
3714 handled = exitOverviewIfActive(mgr) || exitShowDesktopIfActive(mgr) || toggleOverview(mgr);
3715 break;
3716 case "ArrowDown":
3717 handled = exitOverviewIfActive(mgr) || toggleShowDesktop(mgr);
3718 break;
3719 }
3720 if (handled) {
3721 e.preventDefault();
3722 }
3723 },
3724 true
3725 );
3726 }
3727 const IDENTITY_PARAMS = [
3728 "post_type",
3729 "page",
3730 "taxonomy",
3731 // WooCommerce (and other React-app-style plugins) register
3732 // SEPARATE top-level admin menus that all share `?page=wc-admin`
3733 // and only differ by `path` (e.g. `path=/analytics/overview`,
3734 // `path=/marketing`). Without `path` in the identity set, every
3735 // such menu collapses to the same window id — opening any one of
3736 // them lights up the dock indicator for ALL of them. WC's
3737 // /admin/path query is the most prominent example today; future
3738 // plugins that route inside `admin.php?page=` via a custom param
3739 // can either piggyback on `path` or grow this list.
3740 "path",
3741 // The post ID on `post.php?post=X&action=edit`. Without this, every
3742 // individual post edit URL collapses to `post-php`, so clicking a
3743 // second row in the Posts window just refocuses the first post's
3744 // window instead of opening the new one.
3745 "post",
3746 // Site-editor entity path: `site-editor.php?p=/wp_template_part/
3747 // twentytwentyfive//footer-columns`. Each template / template
3748 // part / pattern / navigation entity is a distinct "page" from
3749 // the user's perspective — picking "Header" after "Footer column"
3750 // should open a new window, not refocus the existing footer one.
3751 // Without `p` in identity, every site-editor URL collapses to
3752 // `site-editor-php` and the second pick is a no-op.
3753 "p"
3754 ];
3755 function slugify$1(path) {
3756 return path.replace(/\.php/g, "-php").replace(/[?&=]/g, "-").replace(/[^a-zA-Z0-9_-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "") || "index";
3757 }
3758 function deriveWindowId(url, adminUrl) {
3759 let parsed = null;
3760 try {
3761 parsed = new URL(url, adminUrl);
3762 } catch (err) {
3763 parsed = null;
3764 }
3765 if (parsed) {
3766 const basePath = new URL(adminUrl).pathname;
3767 const filename = parsed.pathname.replace(basePath, "").replace(/^\/+/, "");
3768 const significant = new URLSearchParams();
3769 for (const key of IDENTITY_PARAMS) {
3770 const value = parsed.searchParams.get(key);
3771 if (value) {
3772 significant.set(key, value);
3773 }
3774 }
3775 const query = significant.toString();
3776 return slugify$1(query ? `${filename}?${query}` : filename);
3777 }
3778 let path = url.replace(adminUrl, "");
3779 if (path.startsWith("/")) {
3780 path = path.substring(1);
3781 }
3782 return slugify$1(path);
3783 }
3784 function sanitizeClassName(value) {
3785 return value.replace(/[^a-zA-Z0-9_-]/g, "");
3786 }
3787 function applyTileEntryStagger(tile2) {
3788 tile2.style.setProperty(
3789 "--desktop-mode-file-tile-enter-delay",
3790 `${(Math.random() * 0.25).toFixed(3)}s`
3791 );
3792 tile2.style.setProperty(
3793 "--desktop-mode-file-tile-enter-duration",
3794 `${(0.3 + Math.random() * 0.25).toFixed(3)}s`
3795 );
3796 }
3797 function urlMatchKey(url) {
3798 try {
3799 const parsed = new URL(url, window.location.origin);
3800 parsed.searchParams.delete("desktop_mode_chromeless");
3801 parsed.searchParams.delete("desktop_mode_portal");
3802 return parsed.pathname.replace(/\/+$/, "") + "?" + parsed.searchParams.toString();
3803 } catch {
3804 return url;
3805 }
3806 }
3807 function sanitizeIconSvg(svg) {
3808 if (typeof svg !== "string" || svg === "") {
3809 return "";
3810 }
3811 if (typeof DOMParser === "undefined") {
3812 return "";
3813 }
3814 let doc;
3815 try {
3816 doc = new DOMParser().parseFromString(svg, "image/svg+xml");
3817 } catch {
3818 return "";
3819 }
3820 const root = doc.documentElement;
3821 if (!root || root.nodeName.toLowerCase() !== "svg") {
3822 return "";
3823 }
3824 if (doc.getElementsByTagName("parsererror").length > 0) {
3825 return "";
3826 }
3827 const BANNED_TAGS = /* @__PURE__ */ new Set(["script", "style", "foreignobject", "iframe", "object", "embed"]);
3828 const walk2 = (el) => {
3829 const children = Array.from(el.children);
3830 for (const child of children) {
3831 if (BANNED_TAGS.has(child.nodeName.toLowerCase())) {
3832 child.remove();
3833 continue;
3834 }
3835 for (const attr of Array.from(child.attributes)) {
3836 const name = attr.name.toLowerCase();
3837 const value = attr.value.trim().toLowerCase();
3838 if (name.startsWith("on")) {
3839 child.removeAttribute(attr.name);
3840 continue;
3841 }
3842 if (value.startsWith("javascript:")) {
3843 child.removeAttribute(attr.name);
3844 }
3845 }
3846 walk2(child);
3847 }
3848 };
3849 walk2(root);
3850 for (const attr of Array.from(root.attributes)) {
3851 const name = attr.name.toLowerCase();
3852 const value = attr.value.trim().toLowerCase();
3853 if (name.startsWith("on") || value.startsWith("javascript:")) {
3854 root.removeAttribute(attr.name);
3855 }
3856 }
3857 return root.outerHTML;
3858 }
3859 const _parentSubs = /* @__PURE__ */ new Map();
3860 const _nativeSubs = /* @__PURE__ */ new Map();
3861 function bucket(root, windowId, channel, create) {
3862 let perWindow = root.get(windowId);
3863 if (!perWindow) {
3864 if (!create) {
3865 return void 0;
3866 }
3867 perWindow = /* @__PURE__ */ new Map();
3868 root.set(windowId, perWindow);
3869 }
3870 let bucketSet = perWindow.get(channel);
3871 if (!bucketSet) {
3872 if (!create) {
3873 return void 0;
3874 }
3875 bucketSet = /* @__PURE__ */ new Set();
3876 perWindow.set(channel, bucketSet);
3877 }
3878 return bucketSet;
3879 }
3880 function dispatch(root, windowId, channel, payload) {
3881 const meta = { channel, windowId };
3882 const exact = bucket(root, windowId, channel, false);
3883 if (exact) {
3884 for (const cb of Array.from(exact)) {
3885 try {
3886 cb(payload, meta);
3887 } catch (err) {
3888 if (typeof console !== "undefined") {
3889 console.error(
3890 `[desktop-mode] window-channel subscriber for "${channel}" threw:`,
3891 err
3892 );
3893 }
3894 }
3895 }
3896 }
3897 const wildcard = bucket(root, windowId, "*", false);
3898 if (wildcard) {
3899 for (const cb of Array.from(wildcard)) {
3900 try {
3901 cb(payload, meta);
3902 } catch (err) {
3903 if (typeof console !== "undefined") {
3904 console.error(
3905 `[desktop-mode] window-channel wildcard subscriber for "${windowId}" threw:`,
3906 err
3907 );
3908 }
3909 }
3910 }
3911 }
3912 }
3913 function addParentSubscriber(windowId, channel, cb) {
3914 const set = bucket(_parentSubs, windowId, channel, true);
3915 set.add(cb);
3916 let removed = false;
3917 return () => {
3918 if (removed) {
3919 return;
3920 }
3921 removed = true;
3922 set.delete(cb);
3923 };
3924 }
3925 function dispatchFromWindow(windowId, channel, payload) {
3926 dispatch(_parentSubs, windowId, channel, payload);
3927 }
3928 function dispatchToNative(windowId, channel, payload) {
3929 dispatch(_nativeSubs, windowId, channel, payload);
3930 }
3931 const _readyWindows = /* @__PURE__ */ new Set();
3932 const _loadingWindows = /* @__PURE__ */ new Set();
3933 const _pendingSends = /* @__PURE__ */ new Map();
3934 function markWindowContentReady(windowId) {
3935 if (!_readyWindows.has(windowId)) {
3936 _readyWindows.add(windowId);
3937 const queued = _pendingSends.get(windowId);
3938 if (queued) {
3939 _pendingSends.delete(windowId);
3940 for (const m of queued) {
3941 try {
3942 m.flush();
3943 } catch (err) {
3944 if (typeof console !== "undefined") {
3945 console.error(
3946 `[desktop-mode] flushing queued window-send for "${m.channel}" threw:`,
3947 err
3948 );
3949 }
3950 }
3951 }
3952 }
3953 }
3954 if (_loadingWindows.delete(windowId)) {
3955 doAction(HOOKS.WINDOW_CONTENT_LOADED, { windowId });
3956 if (typeof document !== "undefined") {
3957 document.dispatchEvent(
3958 new CustomEvent("desktop-mode-window-content-loaded", {
3959 detail: { windowId }
3960 })
3961 );
3962 }
3963 }
3964 }
3965 const WINDOW_CONFIG_KEY = Symbol.for("desktop-mode/window-config");
3966 function getWindowConfigFromElement(el) {
3967 return el[WINDOW_CONFIG_KEY];
3968 }
3969 function buildDefaultLoadingOverlay() {
3970 const overlay = document.createElement("div");
3971 overlay.className = "desktop-mode-window__loading";
3972 overlay.setAttribute("aria-hidden", "true");
3973 const spinner = document.createElement("wpd-spinner");
3974 spinner.setAttribute("preset", "classic");
3975 spinner.setAttribute("size", "clamp(96px, 14vw, 192px)");
3976 spinner.setAttribute("label", __("Loading window content"));
3977 overlay.appendChild(spinner);
3978 return overlay;
3979 }
3980 function createLoadingOverlay(config) {
3981 let overlay = buildDefaultLoadingOverlay();
3982 const ctx = { windowId: config.id, config };
3983 if (typeof config.loading?.render === "function") {
3984 try {
3985 config.loading.render(overlay, ctx);
3986 } catch (err) {
3987 if (typeof console !== "undefined") {
3988 console.error(
3989 `[desktop-mode] loading.render threw for "${config.id}":`,
3990 err
3991 );
3992 }
3993 }
3994 }
3995 try {
3996 const filtered = applyFilters(
3997 HOOKS.WINDOW_LOADING_OVERLAY,
3998 overlay,
3999 ctx
4000 );
4001 if (filtered instanceof HTMLElement) {
4002 overlay = filtered;
4003 }
4004 } catch (err) {
4005 if (typeof console !== "undefined") {
4006 console.error(
4007 `[desktop-mode] WINDOW_LOADING_OVERLAY filter threw for "${config.id}":`,
4008 err
4009 );
4010 }
4011 }
4012 if (overlay && !overlay.classList.contains("desktop-mode-window__loading")) {
4013 overlay.classList.add("desktop-mode-window__loading");
4014 }
4015 return overlay;
4016 }
4017 function removeLoadingOverlay(windowEl) {
4018 const overlay = windowEl.querySelector(":scope .desktop-mode-window__loading");
4019 overlay?.remove();
4020 }
4021 function ensureLoadingOverlay(windowEl) {
4022 const body = windowEl.querySelector(
4023 ":scope .desktop-mode-window__body"
4024 );
4025 if (!body) {
4026 return;
4027 }
4028 const existing = body.querySelector(":scope .desktop-mode-window__loading");
4029 if (existing) {
4030 return;
4031 }
4032 const config = getWindowConfigFromElement(windowEl);
4033 body.appendChild(config ? createLoadingOverlay(config) : buildDefaultLoadingOverlay());
4034 }
4035 const FADE_OUT_MS$1 = 250;
4036 let _installed$3 = false;
4037 function findWindowElement(windowId) {
4038 if (!windowId) {
4039 return null;
4040 }
4041 return document.getElementById(`wp-window-${windowId}`);
4042 }
4043 function installWindowLoadingTransitions() {
4044 if (_installed$3) {
4045 return;
4046 }
4047 _installed$3 = true;
4048 _installSubscriptions();
4049 }
4050 function _installSubscriptions() {
4051 addAction(
4052 HOOKS.WINDOW_CONTENT_LOADING,
4053 "desktop-mode/window-loading-enter",
4054 (e) => {
4055 const el = findWindowElement(e?.windowId ?? "");
4056 if (!el) {
4057 return;
4058 }
4059 const body = el.querySelector(
4060 ":scope .desktop-mode-window__body"
4061 );
4062 if (!body) {
4063 return;
4064 }
4065 body.classList.add("desktop-mode-window__body--loading");
4066 ensureLoadingOverlay(el);
4067 }
4068 );
4069 addAction(
4070 HOOKS.WINDOW_CONTENT_LOADED,
4071 "desktop-mode/window-loading-exit",
4072 (e) => {
4073 const el = findWindowElement(e?.windowId ?? "");
4074 if (!el) {
4075 return;
4076 }
4077 const body = el.querySelector(
4078 ":scope .desktop-mode-window__body"
4079 );
4080 if (!body) {
4081 return;
4082 }
4083 body.classList.remove("desktop-mode-window__body--loading");
4084 window.setTimeout(() => {
4085 if (!body.classList.contains("desktop-mode-window__body--loading")) {
4086 removeLoadingOverlay(el);
4087 }
4088 }, FADE_OUT_MS$1);
4089 }
4090 );
4091 addAction(
4092 HOOKS.INIT,
4093 "desktop-mode/loading-overlay-init-sweep",
4094 () => {
4095 queueMicrotask(() => repaintLoadingOverlays());
4096 }
4097 );
4098 }
4099 function repaintLoadingOverlays() {
4100 const bodies = document.querySelectorAll(
4101 ".desktop-mode-window__body--loading"
4102 );
4103 bodies.forEach((body) => {
4104 const windowEl = body.closest(".desktop-mode-window");
4105 if (!windowEl) {
4106 return;
4107 }
4108 body.querySelector(":scope .desktop-mode-window__loading")?.remove();
4109 ensureLoadingOverlay(windowEl);
4110 });
4111 }
4112 const SHARED_STORES_SLOT = "__desktopModeSharedStores";
4113 function resolveSlot() {
4114 const w = window;
4115 let slot = w[SHARED_STORES_SLOT];
4116 if (!slot) {
4117 slot = /* @__PURE__ */ new Map();
4118 w[SHARED_STORES_SLOT] = slot;
4119 }
4120 return slot;
4121 }
4122 function createSharedStore(key, initialState) {
4123 const slot = resolveSlot();
4124 let record = slot.get(key);
4125 if (!record) {
4126 record = {
4127 state: initialState(),
4128 listeners: /* @__PURE__ */ new Set(),
4129 rebuild: initialState
4130 };
4131 slot.set(key, record);
4132 }
4133 const handle = {
4134 // `record.state` is the live reference. The getter on the
4135 // `state` field reads the latest value even if `reset()`
4136 // reassigned it to a fresh object.
4137 get state() {
4138 return record.state;
4139 },
4140 set state(next) {
4141 record.state = next;
4142 },
4143 getState() {
4144 return record.state;
4145 },
4146 notify() {
4147 for (const cb of Array.from(record.listeners)) {
4148 try {
4149 cb(record.state);
4150 } catch (err) {
4151 console.error(
4152 `[desktop-mode/shared-store:${key}] subscriber threw:`,
4153 err
4154 );
4155 }
4156 }
4157 },
4158 subscribe(cb) {
4159 record.listeners.add(cb);
4160 return () => {
4161 record.listeners.delete(cb);
4162 };
4163 },
4164 setState(patch) {
4165 const cur = record.state;
4166 if (typeof cur !== "object" || cur === null) {
4167 console.warn(
4168 `[desktop-mode/shared-store:${key}] setState called on a primitive store; use the state setter instead.`
4169 );
4170 return;
4171 }
4172 Object.assign(cur, patch);
4173 handle.notify();
4174 },
4175 reset() {
4176 const fresh = record.rebuild();
4177 const cur = record.state;
4178 if (typeof cur === "object" && cur !== null && typeof fresh === "object" && fresh !== null) {
4179 const target2 = cur;
4180 for (const k of Object.keys(target2)) {
4181 delete target2[k];
4182 }
4183 Object.assign(target2, fresh);
4184 } else {
4185 record.state = fresh;
4186 }
4187 record.listeners.clear();
4188 }
4189 };
4190 return handle;
4191 }
4192 const remapStore = createSharedStore(
4193 "desktop-mode/native-url-remap",
4194 () => ({ remaps: [], deps: null })
4195 );
4196 function bindNativeUrlRemap(bound) {
4197 remapStore.state.deps = bound;
4198 }
4199 function registerNativeUrlRemap(entry) {
4200 if (!entry || typeof entry.id !== "string" || entry.id.trim() === "") {
4201 return () => {
4202 };
4203 }
4204 if (typeof entry.nativeWindowId !== "string" || entry.nativeWindowId === "") {
4205 return () => {
4206 };
4207 }
4208 if (typeof entry.matches !== "function") {
4209 return () => {
4210 };
4211 }
4212 const remaps = remapStore.state.remaps;
4213 const existingIdx = remaps.findIndex((r) => r.id === entry.id);
4214 if (existingIdx >= 0) {
4215 remaps.splice(existingIdx, 1);
4216 }
4217 remaps.push(entry);
4218 return () => unregisterNativeUrlRemap(entry.id);
4219 }
4220 function unregisterNativeUrlRemap(id) {
4221 const remaps = remapStore.state.remaps;
4222 const i = remaps.findIndex((r) => r.id === id);
4223 if (i >= 0) {
4224 remaps.splice(i, 1);
4225 }
4226 }
4227 function resolveNativeUrlRemap(url) {
4228 const { deps: deps2, remaps } = remapStore.state;
4229 if (!deps2 || !url) {
4230 return null;
4231 }
4232 let parsed;
4233 try {
4234 parsed = new URL(url, deps2.adminUrl);
4235 } catch {
4236 return null;
4237 }
4238 const snapshot = deps2.getSnapshot();
4239 for (const entry of remaps) {
4240 if (!entry.matches(url, parsed)) {
4241 continue;
4242 }
4243 if (entry.enabled && !entry.enabled(snapshot)) {
4244 continue;
4245 }
4246 return entry.nativeWindowId;
4247 }
4248 return null;
4249 }
4250 function tryNativeUrlRemap(url) {
4251 const { deps: deps2, remaps } = remapStore.state;
4252 if (!deps2 || !url) {
4253 return false;
4254 }
4255 let parsed;
4256 try {
4257 parsed = new URL(url, deps2.adminUrl);
4258 } catch {
4259 return false;
4260 }
4261 const snapshot = deps2.getSnapshot();
4262 for (const entry of remaps) {
4263 if (!entry.matches(url, parsed)) {
4264 continue;
4265 }
4266 if (entry.enabled && !entry.enabled(snapshot)) {
4267 continue;
4268 }
4269 if (entry.onMatch) {
4270 try {
4271 entry.onMatch(url, parsed);
4272 } catch (err) {
4273 console.warn(
4274 `[desktop-mode] URL remap onMatch hook threw for "${entry.id}":`,
4275 err
4276 );
4277 }
4278 }
4279 if (deps2.openById(entry.nativeWindowId)) {
4280 return true;
4281 }
4282 }
4283 return false;
4284 }
4285 const HOOK_PREFIX = "desktop-mode.activity.";
4286 function hookName(channel) {
4287 return `${HOOK_PREFIX}${String(channel)}`;
4288 }
4289 let subscribeSeq = 0;
4290 const activity = {
4291 publish(channel, payload) {
4292 doAction(hookName(channel), payload);
4293 },
4294 subscribe(channel, cb) {
4295 const ns = `desktop-mode/activity-sub/${++subscribeSeq}`;
4296 const hook = hookName(channel);
4297 addAction(
4298 hook,
4299 ns,
4300 (payload) => cb(payload)
4301 );
4302 let removed = false;
4303 return () => {
4304 if (removed) {
4305 return;
4306 }
4307 removed = true;
4308 removeAction(hook, ns);
4309 };
4310 },
4311 filter(channel, value, ...args) {
4312 return applyFilters(hookName(channel), value, ...args);
4313 }
4314 };
4315 const DEFAULT_DURATION_MS = 4e3;
4316 const FADE_OUT_MS = 200;
4317 function showToast(options) {
4318 const intent = activity.filter(
4319 "desktop-mode/toast-requested",
4320 { ...options }
4321 );
4322 if (!intent || intent.cancel === true) {
4323 return () => void 0;
4324 }
4325 let dismissRequested = false;
4326 let realDismiss = null;
4327 openWithShellOverlays(
4328 () => !dismissRequested,
4329 () => {
4330 realDismiss = renderToast(intent);
4331 }
4332 );
4333 return () => {
4334 dismissRequested = true;
4335 if (realDismiss) {
4336 realDismiss();
4337 }
4338 };
4339 }
4340 function renderToast(intent) {
4341 const container = ensureContainer();
4342 const toast = document.createElement("wpd-toast");
4343 toast.textContent = intent.message;
4344 if (intent.action) {
4345 toast.setAttribute("action", intent.action.label);
4346 toast.addEventListener("wpd-toast-action", () => {
4347 intent.action?.onClick();
4348 dismiss();
4349 });
4350 }
4351 container.appendChild(toast);
4352 let dismissed = false;
4353 let dismissTimer = null;
4354 const dismiss = () => {
4355 if (dismissed) {
4356 return;
4357 }
4358 dismissed = true;
4359 if (dismissTimer !== null) {
4360 window.clearTimeout(dismissTimer);
4361 dismissTimer = null;
4362 }
4363 toast.setAttribute("state", "out");
4364 window.setTimeout(() => {
4365 toast.remove();
4366 }, FADE_OUT_MS);
4367 };
4368 requestAnimationFrame(() => {
4369 toast.setAttribute("state", "in");
4370 });
4371 dismissTimer = window.setTimeout(
4372 dismiss,
4373 intent.duration ?? DEFAULT_DURATION_MS
4374 );
4375 activity.publish("desktop-mode/toast-shown", { ...intent });
4376 return dismiss;
4377 }
4378 function ensureContainer() {
4379 const existing = document.querySelector(
4380 "wpd-toast-container"
4381 );
4382 if (existing) {
4383 return existing;
4384 }
4385 const el = document.createElement("wpd-toast-container");
4386 document.body.appendChild(el);
4387 return el;
4388 }
4389 const store$e = createSharedStore(
4390 "desktop-mode/destructive-admin-actions",
4391 () => ({ entries: [] })
4392 );
4393 function registerDestructiveAdminAction(entry) {
4394 if (!entry || typeof entry.id !== "string" || entry.id.trim() === "") {
4395 return () => {
4396 };
4397 }
4398 if (typeof entry.matches !== "function") {
4399 return () => {
4400 };
4401 }
4402 const entries = store$e.state.entries;
4403 const idx = entries.findIndex((e) => e.id === entry.id);
4404 if (idx >= 0) {
4405 entries.splice(idx, 1);
4406 }
4407 entries.push(entry);
4408 return () => unregisterDestructiveAdminAction(entry.id);
4409 }
4410 function unregisterDestructiveAdminAction(id) {
4411 const entries = store$e.state.entries;
4412 const idx = entries.findIndex((e) => e.id === id);
4413 if (idx >= 0) {
4414 entries.splice(idx, 1);
4415 }
4416 }
4417 function listDestructiveAdminActions() {
4418 return store$e.state.entries.slice();
4419 }
4420 const adminLinkDepsStore = createSharedStore(
4421 "desktop-mode/admin-link-deps",
4422 () => ({ deps: null })
4423 );
4424 function bindAdminLinkDispatch(deps2) {
4425 adminLinkDepsStore.state.deps = deps2;
4426 }
4427 function collectRegistrationErrors(def, checks) {
4428 if (!def || typeof def !== "object") {
4429 return ["def (not an object)"];
4430 }
4431 const d = def;
4432 const errors = [];
4433 for (const check of checks) {
4434 if (!check.valid(d)) {
4435 errors.push(`${check.field} (${check.message})`);
4436 }
4437 }
4438 return errors;
4439 }
4440 class RegistrationError extends Error {
4441 constructor(kind, errors, def) {
4442 super(
4443 `[desktop-mode] ${kind} registration rejected — fields: ` + errors.join(", ") + "."
4444 );
4445 this.name = "RegistrationError";
4446 this.kind = kind;
4447 this.errors = errors;
4448 this.def = def;
4449 }
4450 }
4451 function throwOnRegistrationErrors(kind, errors, def) {
4452 if (errors.length === 0) {
4453 return;
4454 }
4455 throw new RegistrationError(kind, errors, def);
4456 }
4457 const store$d = createSharedStore(
4458 "desktop-mode/wallpaper-registry",
4459 () => ({
4460 seed: [],
4461 listeners: /* @__PURE__ */ new Set()
4462 })
4463 );
4464 const seed$3 = store$d.state.seed;
4465 const listeners$c = store$d.state.listeners;
4466 function register$2(def) {
4467 throwOnRegistrationErrors(
4468 "Wallpaper",
4469 collectRegistrationErrors(def, WALLPAPER_CHECKS),
4470 def
4471 );
4472 const idx = seed$3.findIndex((w) => w.id === def.id);
4473 if (idx >= 0) {
4474 seed$3[idx] = def;
4475 } else {
4476 seed$3.push(def);
4477 }
4478 notify$e();
4479 }
4480 function unregister$2(id) {
4481 const idx = seed$3.findIndex((w) => w.id === id);
4482 if (idx >= 0) {
4483 seed$3.splice(idx, 1);
4484 notify$e();
4485 }
4486 }
4487 function notify$e() {
4488 const snapshot = Array.from(listeners$c);
4489 for (const cb of snapshot) {
4490 try {
4491 cb();
4492 } catch (err) {
4493 if (typeof console !== "undefined") {
4494 console.error(
4495 "[desktop-mode] wallpaper registry listener threw:",
4496 err
4497 );
4498 }
4499 }
4500 }
4501 }
4502 function all$1() {
4503 const copy = seed$3.slice();
4504 const filtered = applyFilters(HOOKS.WALLPAPERS, copy);
4505 if (!Array.isArray(filtered)) {
4506 if (typeof console !== "undefined") {
4507 console.warn(
4508 "[desktop-mode] `desktop-mode.wallpapers` filter returned a non-array; falling back to seed list."
4509 );
4510 }
4511 return copy;
4512 }
4513 return filtered.filter(isValidDef$1);
4514 }
4515 function get$1(id) {
4516 return all$1().find((w) => w.id === id);
4517 }
4518 const WALLPAPER_CHECKS = [
4519 {
4520 field: "id",
4521 message: "missing or not a non-empty string",
4522 valid: (d) => typeof d.id === "string" && d.id !== ""
4523 },
4524 {
4525 field: "label",
4526 message: "missing or not a non-empty string",
4527 valid: (d) => typeof d.label === "string" && d.label !== ""
4528 },
4529 {
4530 field: "preview",
4531 message: "missing or not a non-empty string",
4532 valid: (d) => typeof d.preview === "string" && d.preview !== ""
4533 },
4534 {
4535 field: "type",
4536 message: 'must be "css" or "canvas"',
4537 valid: (d) => d.type === "css" || d.type === "canvas"
4538 },
4539 {
4540 field: "value/resolveValue/mount",
4541 message: "css types need `value` or `resolveValue`; canvas types need `mount`",
4542 valid: (d) => {
4543 if (d.type === "css") {
4544 return typeof d.value === "string" || typeof d.resolveValue === "function";
4545 }
4546 if (d.type === "canvas") {
4547 return typeof d.mount === "function";
4548 }
4549 return true;
4550 }
4551 }
4552 ];
4553 function isValidDef$1(def) {
4554 return collectRegistrationErrors(def, WALLPAPER_CHECKS).length === 0;
4555 }
4556 const STORAGE_KEY = "desktop-mode-os-settings";
4557 const CUSTOM_GRADIENT_ID = "custom-gradient";
4558 const CUSTOM_IMAGE_ID = "custom-image";
4559 const DEFAULT_WALLPAPER_ID = "dark";
4560 const DEFAULT_ACCENTS = [
4561 { id: "wp-blue", label: "WordPress Blue", value: "#2271b1" },
4562 { id: "indigo", label: "Indigo", value: "#3858e9" },
4563 { id: "teal", label: "Teal", value: "#04a4cc" },
4564 { id: "emerald", label: "Emerald", value: "#059669" },
4565 { id: "amber", label: "Amber", value: "#d97706" },
4566 { id: "rose", label: "Rose", value: "#e11d48" }
4567 ];
4568 function getAccents() {
4569 const config = window.wp?.desktop?.config;
4570 const raw = config?.accentColors;
4571 if (!Array.isArray(raw) || raw.length === 0) {
4572 return DEFAULT_ACCENTS;
4573 }
4574 const clean = [];
4575 for (const entry of raw) {
4576 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)) {
4577 clean.push({ id: entry.id, label: entry.label, value: entry.value });
4578 }
4579 }
4580 return clean.length > 0 ? clean : DEFAULT_ACCENTS;
4581 }
4582 function getDefaultWallpaperId() {
4583 const config = window.wp?.desktop?.config;
4584 const raw = config?.defaultWallpaper;
4585 if (typeof raw === "string" && raw !== "") {
4586 return raw;
4587 }
4588 return DEFAULT_WALLPAPER_ID;
4589 }
4590 const DOCK_SIZES = [
4591 { id: "compact", label: "Compact", width: 48, icon: 18 },
4592 { id: "default", label: "Default", width: 56, icon: 20 },
4593 { id: "large", label: "Large", width: 72, icon: 26 }
4594 ];
4595 const DESKTOP_LAYOUTS = [
4596 { id: "classic", label: "Classic" },
4597 { id: "unified", label: "Unified" },
4598 { id: "spatial", label: "Spatial" }
4599 ];
4600 const DEFAULTS = {
4601 wallpaper: DEFAULT_WALLPAPER_ID,
4602 accent: "wp-blue",
4603 dockSize: "default",
4604 desktopLayout: "classic",
4605 dockRailRenderer: "default",
4606 unfocusEffect: "darken",
4607 customGradient: {
4608 from: "#2271b1",
4609 to: "#7c3aed",
4610 angle: 135
4611 },
4612 customImage: null,
4613 libraryHdOnly: true,
4614 ai: {
4615 enabled: false,
4616 provider: "openai",
4617 apiKey: "",
4618 apiKeys: {},
4619 transport: "off"
4620 },
4621 // Opt-IN Beta as of 0.10.0. Fresh installs land on the classic
4622 // chromeless `edit.php` iframe; a user opts in via OS Settings →
4623 // Features → Beta features to get the native Posts window. The
4624 // native windows used to default ON (opt-out, 0.8.0) but are now
4625 // opt-in so the redesign is a deliberate choice, not imposed.
4626 heartbeatRate: 60,
4627 nativePostsEnabled: false,
4628 nativePostsHiddenColumns: [],
4629 // Same opt-in Beta posture as Posts — fresh installs keep the
4630 // iframe; users opt in to the native Pages window.
4631 nativePagesEnabled: false,
4632 // Native Users window — same opt-in Beta posture. Capability-gated
4633 // server-side (the window is only registered for users with
4634 // `list_users`), so this toggle only affects the small set of
4635 // users who can see the Users tile in the first place.
4636 nativeUsersEnabled: false,
4637 // Native Plugins window — replaces `plugins.php` and
4638 // `plugin-install.php`. Same opt-in Beta posture; cap-gated on
4639 // `activate_plugins` server-side, so this toggle only affects
4640 // users who could see the Plugins tile anyway.
4641 nativePluginsEnabled: false,
4642 // Native Comments window — replaces `edit-comments.php`. Same
4643 // opt-in Beta posture; cap-gated on `edit_posts` server-side.
4644 nativeCommentsEnabled: false,
4645 showDesktopOnWallpaperClick: false,
4646 showPostStatusRibbons: true,
4647 foldersSharingEnabled: true,
4648 itemVisibility: {},
4649 dockOrder: [],
4650 dockPromotedPositions: {}
4651 };
4652 const AI_TRANSPORTS = [
4653 { id: "off", label: "Off" },
4654 { id: "sse", label: "Streaming (SSE)" }
4655 ];
4656 const AI_PROVIDERS = [
4657 {
4658 id: "openai",
4659 label: "OpenAI",
4660 apiKeyLabel: "OpenAI API key",
4661 apiKeyLink: "https://platform.openai.com/api-keys"
4662 }
4663 ];
4664 function getAiProviders() {
4665 const cfg = window.desktopModeConfig;
4666 const list2 = cfg?.aiProviders;
4667 if (!Array.isArray(list2) || list2.length === 0) {
4668 return AI_PROVIDERS;
4669 }
4670 return list2.map((p) => ({
4671 id: p.id,
4672 label: p.label,
4673 description: p.description,
4674 apiKeyLabel: p.api_key_label,
4675 apiKeyLink: p.api_key_link
4676 }));
4677 }
4678 function isHexColor(value) {
4679 return typeof value === "string" && /^#[0-9a-f]{3,8}$/i.test(value);
4680 }
4681 const NONCE_HEADER = "X-WP-Nonce";
4682 function injectRestNonce(input, init2) {
4683 const nonce = readRestNonce$3();
4684 if (!nonce) {
4685 return init2;
4686 }
4687 const url = resolveUrl(input);
4688 if (!url || !isSameOriginRestUrl(url)) {
4689 return init2;
4690 }
4691 const baseHeaders = init2?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
4692 const headers = new Headers(baseHeaders ?? {});
4693 if (headers.has(NONCE_HEADER)) {
4694 return init2;
4695 }
4696 headers.set(NONCE_HEADER, nonce);
4697 return { ...init2 ?? {}, headers };
4698 }
4699 function readRestNonce$3() {
4700 if (typeof window === "undefined") {
4701 return void 0;
4702 }
4703 const cfg = window.desktopModeConfig;
4704 const value = cfg?.restNonce;
4705 return typeof value === "string" && value.length > 0 ? value : void 0;
4706 }
4707 function resolveUrl(input) {
4708 try {
4709 const base = typeof window !== "undefined" && window.location ? window.location.href : void 0;
4710 if (typeof input === "string") {
4711 return new URL(input, base);
4712 }
4713 if (input instanceof URL) {
4714 return input;
4715 }
4716 if (typeof Request !== "undefined" && input instanceof Request) {
4717 return new URL(input.url, base);
4718 }
4719 return null;
4720 } catch {
4721 return null;
4722 }
4723 }
4724 function isSameOriginRestUrl(url) {
4725 if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) {
4726 return false;
4727 }
4728 if (url.pathname.includes("/wp-json/")) {
4729 return true;
4730 }
4731 if (url.searchParams.has("rest_route")) {
4732 return true;
4733 }
4734 return false;
4735 }
4736 function trackedFetch$1(input, init2, opts = {}) {
4737 const fn = window.wp?.desktop?.fetch;
4738 if (typeof fn === "function") {
4739 return fn(input, init2, opts);
4740 }
4741 const finalInit = injectRestNonce(input, init2);
4742 return fetch(input, finalInit);
4743 }
4744 function loadState() {
4745 const serverRaw = _readServerSettings();
4746 if (serverRaw) {
4747 const state2 = _parseRaw(serverRaw);
4748 _writeLocalStorage(state2);
4749 return state2;
4750 }
4751 try {
4752 const cached = window.localStorage.getItem(STORAGE_KEY);
4753 if (cached) {
4754 return _parseRaw(JSON.parse(cached));
4755 }
4756 } catch {
4757 }
4758 return structuredDefaults();
4759 }
4760 function _readServerSettings() {
4761 const config = window.desktopModeConfig;
4762 const raw = config?.osSettings;
4763 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
4764 return null;
4765 }
4766 return raw;
4767 }
4768 function _parseRaw(parsed) {
4769 const accents = getAccents();
4770 return {
4771 wallpaper: typeof parsed.wallpaper === "string" && parsed.wallpaper !== "" ? parsed.wallpaper : getDefaultWallpaperId(),
4772 accent: accents.some((a) => a.id === parsed.accent) ? parsed.accent : DEFAULTS.accent,
4773 dockSize: DOCK_SIZES.some((d) => d.id === parsed.dockSize) ? parsed.dockSize : DEFAULTS.dockSize,
4774 desktopLayout: DESKTOP_LAYOUTS.some(
4775 (l) => l.id === parsed.desktopLayout
4776 ) ? parsed.desktopLayout : DEFAULTS.desktopLayout,
4777 // Dock rail renderer — any sanitize_key()-clean string
4778 // survives; the registry resolves at use time and falls back
4779 // to `'default'` when the picked renderer isn't registered.
4780 dockRailRenderer: typeof parsed.dockRailRenderer === "string" && /^[a-z0-9_-]+$/.test(parsed.dockRailRenderer) ? parsed.dockRailRenderer : DEFAULTS.dockRailRenderer,
4781 // Unfocus effect — any registry id (`vendor/sub-id` allowed) or
4782 // the `'none'` sentinel survives; the engine resolves at use
4783 // time and treats an unknown id as "no effect".
4784 unfocusEffect: typeof parsed.unfocusEffect === "string" && /^[a-z0-9_/-]+$/.test(parsed.unfocusEffect) ? parsed.unfocusEffect : DEFAULTS.unfocusEffect,
4785 customGradient: sanitizeCustomGradient(parsed.customGradient),
4786 customImage: sanitizeCustomImage(parsed.customImage),
4787 libraryHdOnly: typeof parsed.libraryHdOnly === "boolean" ? parsed.libraryHdOnly : DEFAULTS.libraryHdOnly,
4788 ai: sanitizeAi(parsed.ai),
4789 heartbeatRate: parsed.heartbeatRate === 15 || parsed.heartbeatRate === 30 || parsed.heartbeatRate === 45 || parsed.heartbeatRate === 60 ? parsed.heartbeatRate : DEFAULTS.heartbeatRate,
4790 nativePostsEnabled: typeof parsed.nativePostsEnabled === "boolean" ? parsed.nativePostsEnabled : DEFAULTS.nativePostsEnabled,
4791 nativePostsHiddenColumns: Array.isArray(parsed.nativePostsHiddenColumns) ? parsed.nativePostsHiddenColumns.filter((v) => typeof v === "string" && v !== "").slice(0, 32) : DEFAULTS.nativePostsHiddenColumns.slice(),
4792 nativePagesEnabled: typeof parsed.nativePagesEnabled === "boolean" ? parsed.nativePagesEnabled : DEFAULTS.nativePagesEnabled,
4793 nativeUsersEnabled: typeof parsed.nativeUsersEnabled === "boolean" ? parsed.nativeUsersEnabled : DEFAULTS.nativeUsersEnabled,
4794 nativePluginsEnabled: typeof parsed.nativePluginsEnabled === "boolean" ? parsed.nativePluginsEnabled : DEFAULTS.nativePluginsEnabled,
4795 nativeCommentsEnabled: typeof parsed.nativeCommentsEnabled === "boolean" ? parsed.nativeCommentsEnabled : DEFAULTS.nativeCommentsEnabled,
4796 showDesktopOnWallpaperClick: typeof parsed.showDesktopOnWallpaperClick === "boolean" ? parsed.showDesktopOnWallpaperClick : DEFAULTS.showDesktopOnWallpaperClick,
4797 showPostStatusRibbons: typeof parsed.showPostStatusRibbons === "boolean" ? parsed.showPostStatusRibbons : DEFAULTS.showPostStatusRibbons,
4798 foldersSharingEnabled: typeof parsed.foldersSharingEnabled === "boolean" ? parsed.foldersSharingEnabled : DEFAULTS.foldersSharingEnabled,
4799 itemVisibility: sanitizeItemVisibility(parsed.itemVisibility),
4800 dockOrder: sanitizeDockOrder(parsed.dockOrder),
4801 dockPromotedPositions: sanitizeDockPromotedPositions(
4802 parsed.dockPromotedPositions
4803 )
4804 };
4805 }
4806 function sanitizeItemVisibility(raw) {
4807 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
4808 return {};
4809 }
4810 const allowed = [
4811 "both",
4812 "dock",
4813 "desktop",
4814 "hidden"
4815 ];
4816 const out = {};
4817 let count = 0;
4818 for (const [k, v] of Object.entries(raw)) {
4819 if (count >= 256) {
4820 break;
4821 }
4822 if (typeof k !== "string" || k === "") {
4823 continue;
4824 }
4825 if (typeof v !== "string") {
4826 continue;
4827 }
4828 const placement = v;
4829 if (!allowed.includes(placement)) {
4830 continue;
4831 }
4832 out[k] = placement;
4833 count++;
4834 }
4835 return out;
4836 }
4837 function sanitizeDockOrder(raw) {
4838 if (!Array.isArray(raw)) {
4839 return [];
4840 }
4841 const out = [];
4842 const seen = /* @__PURE__ */ new Set();
4843 for (const id of raw) {
4844 if (typeof id !== "string" || id === "" || seen.has(id)) {
4845 continue;
4846 }
4847 seen.add(id);
4848 out.push(id);
4849 if (out.length >= 256) {
4850 break;
4851 }
4852 }
4853 return out;
4854 }
4855 function sanitizeDockPromotedPositions(raw) {
4856 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
4857 return {};
4858 }
4859 const out = {};
4860 let count = 0;
4861 const MAX_COORD = 1e5;
4862 for (const [k, v] of Object.entries(raw)) {
4863 if (count >= 256) {
4864 break;
4865 }
4866 if (typeof k !== "string" || k === "") {
4867 continue;
4868 }
4869 if (!v || typeof v !== "object" || Array.isArray(v)) {
4870 continue;
4871 }
4872 const pos = v;
4873 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) {
4874 continue;
4875 }
4876 out[k] = { x: pos.x, y: pos.y };
4877 count++;
4878 }
4879 return out;
4880 }
4881 let _syncTimer = null;
4882 const SYNC_DEBOUNCE_MS = 250;
4883 let _lastConfirmedState = null;
4884 function setLastConfirmedState(state2) {
4885 _lastConfirmedState = _cloneState(state2);
4886 }
4887 function _cloneState(state2) {
4888 return {
4889 ...state2,
4890 customGradient: { ...state2.customGradient },
4891 customImage: state2.customImage ? { ...state2.customImage } : null,
4892 ai: { ...state2.ai, apiKeys: { ...state2.ai.apiKeys } },
4893 nativePostsHiddenColumns: state2.nativePostsHiddenColumns.slice(),
4894 itemVisibility: { ...state2.itemVisibility },
4895 dockOrder: state2.dockOrder.slice(),
4896 dockPromotedPositions: Object.fromEntries(
4897 Object.entries(state2.dockPromotedPositions).map(([k, v]) => [
4898 k,
4899 { ...v }
4900 ])
4901 )
4902 };
4903 }
4904 function saveState(state2, opts = {}) {
4905 _writeLocalStorage(state2);
4906 _scheduleSyncToServer(state2, opts.windowId);
4907 }
4908 function _writeLocalStorage(state2) {
4909 try {
4910 window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state2));
4911 } catch {
4912 }
4913 }
4914 function _scheduleSyncToServer(state2, windowId) {
4915 if (_syncTimer !== null) {
4916 clearTimeout(_syncTimer);
4917 }
4918 if (windowId) {
4919 _pendingActivityWindowId = windowId;
4920 }
4921 _emitSaveLifecycle("pending");
4922 _syncTimer = setTimeout(() => {
4923 _syncTimer = null;
4924 const id = _pendingActivityWindowId;
4925 _pendingActivityWindowId = null;
4926 _postToServer(state2, id);
4927 }, SYNC_DEBOUNCE_MS);
4928 }
4929 let _pendingActivityWindowId = null;
4930 function _postToServer(state2, windowId) {
4931 const config = window.desktopModeConfig;
4932 const url = config?.osSettingsUrl;
4933 const nonce = config?.restNonce;
4934 if (!url || !nonce) {
4935 _emitSaveLifecycle("saved");
4936 return;
4937 }
4938 _emitSaveLifecycle("saving");
4939 const attributedWindowId = windowId || "desktop-mode-os-settings";
4940 trackedFetch$1(
4941 url,
4942 {
4943 method: "POST",
4944 headers: {
4945 "Content-Type": "application/json",
4946 "X-WP-Nonce": nonce
4947 },
4948 body: JSON.stringify({ settings: state2 })
4949 },
4950 { windowId: attributedWindowId }
4951 ).then((res) => {
4952 if (!res.ok) {
4953 throw new Error(`${res.status} ${res.statusText}`);
4954 }
4955 _lastConfirmedState = _cloneState(state2);
4956 _emitSaveLifecycle("saved");
4957 }).catch((err) => {
4958 if (_lastConfirmedState) {
4959 _writeLocalStorage(_lastConfirmedState);
4960 _emitSaveLifecycle(
4961 "failed",
4962 err instanceof Error ? err.message : String(err),
4963 _cloneState(_lastConfirmedState)
4964 );
4965 } else {
4966 _emitSaveLifecycle(
4967 "failed",
4968 err instanceof Error ? err.message : String(err)
4969 );
4970 }
4971 });
4972 }
4973 function _emitSaveLifecycle(phase, error, rolledBackTo) {
4974 const detail = { phase };
4975 if (error) {
4976 detail.error = error;
4977 }
4978 if (rolledBackTo) {
4979 detail.rolledBackTo = rolledBackTo;
4980 }
4981 document.dispatchEvent(
4982 new CustomEvent("desktop-mode-os-settings-save-lifecycle", { detail })
4983 );
4984 }
4985 function structuredDefaults() {
4986 return {
4987 ...DEFAULTS,
4988 customGradient: { ...DEFAULTS.customGradient },
4989 customImage: null,
4990 ai: { ...DEFAULTS.ai },
4991 // Clone the collection fields too. A shallow `...DEFAULTS`
4992 // aliases these nested objects, so a later in-place mutation
4993 // (e.g. dragging the gradient editor after a Reset, which spreads
4994 // these defaults into live state) would corrupt the module-level
4995 // DEFAULTS singleton for the rest of the session.
4996 //
4997 // These are one-level clones, which is sufficient *because* all
4998 // three defaults are empty (`{}` / `[]`) — there are no inner
4999 // objects to share. If `DEFAULTS.dockPromotedPositions` ever
5000 // ships seeded entries, its `{ x, y }` values would need a
5001 // deeper clone here.
5002 itemVisibility: { ...DEFAULTS.itemVisibility },
5003 dockOrder: [...DEFAULTS.dockOrder],
5004 dockPromotedPositions: { ...DEFAULTS.dockPromotedPositions }
5005 };
5006 }
5007 function sanitizeAi(raw) {
5008 if (!raw || typeof raw !== "object") {
5009 return { ...DEFAULTS.ai, apiKeys: {} };
5010 }
5011 const { enabled, provider, apiKey, apiKeys, transport } = raw;
5012 const known = getAiProviders();
5013 const validProvider = typeof provider === "string" && known.some((p) => p.id === provider) ? provider : DEFAULTS.ai.provider;
5014 const cleanKeys = {};
5015 if (apiKeys && typeof apiKeys === "object") {
5016 for (const [pid, val] of Object.entries(apiKeys)) {
5017 if (typeof val === "string") {
5018 cleanKeys[pid] = val.slice(0, 512);
5019 }
5020 }
5021 }
5022 const validTransport = typeof transport === "string" && AI_TRANSPORTS.some((t) => t.id === transport) ? transport : DEFAULTS.ai.transport;
5023 return {
5024 enabled: typeof enabled === "boolean" ? enabled : DEFAULTS.ai.enabled,
5025 provider: validProvider,
5026 apiKey: typeof apiKey === "string" ? apiKey : DEFAULTS.ai.apiKey,
5027 apiKeys: cleanKeys,
5028 transport: validTransport
5029 };
5030 }
5031 function sanitizeCustomGradient(raw) {
5032 if (!raw || typeof raw !== "object") {
5033 return { ...DEFAULTS.customGradient };
5034 }
5035 const { from, to, angle } = raw;
5036 return {
5037 from: isHexColor(from) ? from : DEFAULTS.customGradient.from,
5038 to: isHexColor(to) ? to : DEFAULTS.customGradient.to,
5039 angle: typeof angle === "number" && Number.isFinite(angle) && angle >= 0 && angle <= 360 ? angle : DEFAULTS.customGradient.angle
5040 };
5041 }
5042 function sanitizeCustomImage(raw) {
5043 if (!raw || typeof raw !== "object") {
5044 return null;
5045 }
5046 const { id, url } = raw;
5047 if (typeof id !== "number" || !Number.isFinite(id) || id <= 0) {
5048 return null;
5049 }
5050 if (typeof url !== "string" || !/^https?:\/\//i.test(url)) {
5051 return null;
5052 }
5053 return { id, url };
5054 }
5055 const store$c = createSharedStore(
5056 "desktop-mode/dock-rail-registry",
5057 () => ({
5058 registry: /* @__PURE__ */ new Map(),
5059 listeners: /* @__PURE__ */ new Set(),
5060 activeId: "default"
5061 })
5062 );
5063 const registry$9 = store$c.state.registry;
5064 const listeners$b = store$c.state.listeners;
5065 const ID_RE = /^[a-z0-9_-]+$/;
5066 function register$1(renderer) {
5067 if (!renderer || typeof renderer !== "object") {
5068 throw new TypeError(
5069 "[desktop-mode] registerDockRailRenderer: renderer must be an object."
5070 );
5071 }
5072 if (typeof renderer.id !== "string" || !ID_RE.test(renderer.id)) {
5073 throw new TypeError(
5074 `[desktop-mode] registerDockRailRenderer: id must match /^[a-z0-9_-]+$/, got: ${String(renderer.id)}`
5075 );
5076 }
5077 if (typeof renderer.label !== "string" || renderer.label === "") {
5078 throw new TypeError(
5079 "[desktop-mode] registerDockRailRenderer: label must be a non-empty string."
5080 );
5081 }
5082 if (typeof renderer.mount !== "function") {
5083 throw new TypeError(
5084 "[desktop-mode] registerDockRailRenderer: mount must be a function."
5085 );
5086 }
5087 if (renderer.apiVersion !== void 0 && renderer.apiVersion !== 1) {
5088 throw new TypeError(
5089 `[desktop-mode] registerDockRailRenderer: unsupported apiVersion ${renderer.apiVersion} (this shell speaks v1).`
5090 );
5091 }
5092 registry$9.set(renderer.id, renderer);
5093 notify$d();
5094 }
5095 function unregister$1(id) {
5096 if (registry$9.delete(id)) {
5097 notify$d();
5098 }
5099 }
5100 function unregisterByOwner$1(owner) {
5101 if (!owner) {
5102 return 0;
5103 }
5104 let removed = 0;
5105 for (const [id, renderer] of Array.from(registry$9.entries())) {
5106 if (renderer.owner === owner) {
5107 registry$9.delete(id);
5108 removed++;
5109 }
5110 }
5111 if (removed > 0) {
5112 notify$d();
5113 }
5114 return removed;
5115 }
5116 function list() {
5117 return Array.from(registry$9.values());
5118 }
5119 function subscribe$3(cb) {
5120 listeners$b.add(cb);
5121 return () => {
5122 listeners$b.delete(cb);
5123 };
5124 }
5125 function setActiveRenderer(id) {
5126 if (store$c.state.activeId === id) {
5127 return;
5128 }
5129 store$c.state.activeId = id;
5130 notify$d();
5131 }
5132 function resolveActive() {
5133 return registry$9.get(store$c.state.activeId) ?? registry$9.get("default") ?? registry$9.values().next().value;
5134 }
5135 function notify$d() {
5136 const snapshot = Array.from(listeners$b);
5137 for (const cb of snapshot) {
5138 try {
5139 cb();
5140 } catch (err) {
5141 if (typeof console !== "undefined") {
5142 console.error(
5143 "[desktop-mode] dock-rail-renderer listener threw:",
5144 err
5145 );
5146 }
5147 }
5148 }
5149 }
5150 function hashTitleToHue(input) {
5151 if (!input) {
5152 return 214;
5153 }
5154 let hash2 = 5381;
5155 for (let i = 0; i < input.length; i++) {
5156 hash2 = Math.imul(hash2, 33) + input.charCodeAt(i);
5157 }
5158 return (hash2 % 360 + 360) % 360;
5159 }
5160 const SHOW_DELAY_MS = 180;
5161 const HIDE_DELAY_MS = 220;
5162 const STAGGER_MS = 32;
5163 function attachDockPeek(deps2) {
5164 const { tile: tile2 } = deps2;
5165 let popover = null;
5166 let showTimer = null;
5167 let hideTimer = null;
5168 let inside = false;
5169 const cancelShow = () => {
5170 if (showTimer !== null) {
5171 window.clearTimeout(showTimer);
5172 showTimer = null;
5173 }
5174 };
5175 const cancelHide = () => {
5176 if (hideTimer !== null) {
5177 window.clearTimeout(hideTimer);
5178 hideTimer = null;
5179 }
5180 };
5181 const tearDown = () => {
5182 cancelShow();
5183 cancelHide();
5184 if (popover) {
5185 popover.remove();
5186 popover = null;
5187 }
5188 deps2.suppressTooltip(false);
5189 };
5190 const onPointerEnterTile = (e) => {
5191 if (e.pointerType !== "mouse") {
5192 return;
5193 }
5194 if (!shouldShowPeek(deps2)) {
5195 return;
5196 }
5197 inside = true;
5198 cancelHide();
5199 if (popover) {
5200 return;
5201 }
5202 showTimer = window.setTimeout(() => {
5203 showTimer = null;
5204 if (!inside) {
5205 return;
5206 }
5207 showPeek();
5208 }, SHOW_DELAY_MS);
5209 };
5210 const onPointerLeaveTile = (e) => {
5211 if (popover && e.relatedTarget instanceof Node && popover.contains(e.relatedTarget)) {
5212 return;
5213 }
5214 inside = false;
5215 cancelShow();
5216 scheduleHide();
5217 };
5218 const scheduleHide = () => {
5219 cancelHide();
5220 hideTimer = window.setTimeout(() => {
5221 hideTimer = null;
5222 if (inside) {
5223 return;
5224 }
5225 tearDown();
5226 }, HIDE_DELAY_MS);
5227 };
5228 const showPeek = () => {
5229 deps2.suppressTooltip(true);
5230 popover = buildPopover(deps2, () => tearDown());
5231 document.body.appendChild(popover);
5232 inheritShellSchemeVars(popover);
5233 positionPopover(popover, tile2, deps2.getOrientation());
5234 requestAnimationFrame(() => {
5235 popover?.classList.add("desktop-mode-dock-peek--open");
5236 });
5237 popover.addEventListener("pointerenter", () => {
5238 inside = true;
5239 cancelHide();
5240 });
5241 popover.addEventListener("pointerleave", (e) => {
5242 if (e.relatedTarget instanceof Node && tile2.contains(e.relatedTarget)) {
5243 return;
5244 }
5245 inside = false;
5246 scheduleHide();
5247 });
5248 };
5249 tile2.addEventListener("pointerenter", onPointerEnterTile);
5250 tile2.addEventListener("pointerleave", onPointerLeaveTile);
5251 return () => {
5252 tile2.removeEventListener("pointerenter", onPointerEnterTile);
5253 tile2.removeEventListener("pointerleave", onPointerLeaveTile);
5254 tearDown();
5255 };
5256 }
5257 function shouldShowPeek(deps2) {
5258 return deps2.getInstances().length >= 1;
5259 }
5260 function buildPopover(deps2, dismiss) {
5261 const root = document.createElement("div");
5262 root.className = "desktop-mode-dock-peek";
5263 root.setAttribute("role", "menu");
5264 root.setAttribute("aria-label", sprintf(
5265 // translators: %s is the dock item's admin-page title (e.g., "Posts")
5266 __("%s — open windows"),
5267 deps2.item.title
5268 ));
5269 const cards = document.createElement("div");
5270 cards.className = "desktop-mode-dock-peek__cards";
5271 root.appendChild(cards);
5272 const instances = deps2.getInstances();
5273 let cardIndex = 0;
5274 for (const win of instances) {
5275 const card = buildInstanceCard(win, deps2, cardIndex++, dismiss);
5276 cards.appendChild(card);
5277 }
5278 if (deps2.enableGhost !== false) {
5279 const ghost = buildGhostCard(deps2, cardIndex, dismiss);
5280 cards.appendChild(ghost);
5281 }
5282 return root;
5283 }
5284 function buildInstanceCard(win, deps2, index2, dismiss) {
5285 const card = document.createElement("button");
5286 card.type = "button";
5287 card.setAttribute("role", "menuitem");
5288 card.className = "desktop-mode-dock-peek__card desktop-mode-dock-peek__card--instance";
5289 card.style.setProperty("--peek-card-index", String(index2));
5290 card.style.setProperty(
5291 "--peek-card-delay",
5292 `${index2 * STAGGER_MS}ms`
5293 );
5294 const title = win.config.title || deps2.item.title;
5295 card.style.setProperty(
5296 "--peek-card-hue",
5297 `${hashTitleToHue(win.id || title)}`
5298 );
5299 card.style.setProperty(
5300 "--peek-card-vt-name",
5301 `desktop-mode-peek-card-${win.id}`
5302 );
5303 const titlebar = document.createElement("span");
5304 titlebar.className = "desktop-mode-dock-peek__card-titlebar";
5305 const dots = document.createElement("span");
5306 dots.className = "desktop-mode-dock-peek__card-dots";
5307 dots.setAttribute("aria-hidden", "true");
5308 for (let i = 0; i < 3; i++) {
5309 dots.appendChild(document.createElement("i"));
5310 }
5311 titlebar.appendChild(dots);
5312 const iconHost = document.createElement("span");
5313 iconHost.className = "desktop-mode-dock-peek__card-icon";
5314 iconHost.setAttribute("aria-hidden", "true");
5315 const iconCls = win.config.icon || deps2.item.icon;
5316 if (iconCls.startsWith("dashicons-")) {
5317 iconHost.classList.add("dashicons", sanitizeClassName(iconCls));
5318 } else {
5319 iconHost.classList.add("dashicons", "dashicons-admin-generic");
5320 }
5321 titlebar.appendChild(iconHost);
5322 const label = document.createElement("span");
5323 label.className = "desktop-mode-dock-peek__card-label";
5324 label.textContent = title;
5325 titlebar.appendChild(label);
5326 card.appendChild(titlebar);
5327 const defaultBody = document.createElement("span");
5328 defaultBody.className = "desktop-mode-dock-peek__card-body";
5329 defaultBody.setAttribute("aria-hidden", "true");
5330 for (let i = 0; i < 3; i++) {
5331 const line = document.createElement("span");
5332 line.className = "desktop-mode-dock-peek__card-line";
5333 defaultBody.appendChild(line);
5334 }
5335 const ctx = { window: win, item: deps2.item };
5336 const body = applyFilters(
5337 HOOKS.DOCK_PEEK_CARD_CONTENT,
5338 defaultBody,
5339 ctx
5340 );
5341 if (body !== defaultBody) {
5342 body.classList.add("desktop-mode-dock-peek__card-body--custom");
5343 }
5344 card.appendChild(body);
5345 card.addEventListener("click", () => {
5346 spawnFocusViewTransition(deps2, win, card, dismiss);
5347 });
5348 card.addEventListener("pointerenter", () => {
5349 if (deps2.windowManager.getFocused() === win) {
5350 return;
5351 }
5352 deps2.windowManager.focus(win);
5353 });
5354 const finalCard = applyFilters(
5355 HOOKS.DOCK_PEEK_CARD_ELEMENT,
5356 card,
5357 ctx
5358 );
5359 return finalCard;
5360 }
5361 function spawnFocusViewTransition(deps2, win, card, dismiss) {
5362 const doc = document;
5363 const vtName = `desktop-mode-peek-card-${win.id}`;
5364 const focus = () => {
5365 dismiss();
5366 deps2.windowManager.focus(win);
5367 };
5368 if (typeof doc.startViewTransition !== "function") {
5369 focus();
5370 return;
5371 }
5372 const targetEl = win.element;
5373 card.style.setProperty("view-transition-name", vtName);
5374 targetEl.style.setProperty("view-transition-name", vtName);
5375 const transition = doc.startViewTransition(focus);
5376 const cleanup = () => {
5377 card.style.removeProperty("view-transition-name");
5378 targetEl.style.removeProperty("view-transition-name");
5379 };
5380 const t = transition;
5381 if (t.finished && typeof t.finished.then === "function") {
5382 t.finished.then(cleanup, cleanup);
5383 } else {
5384 Promise.resolve().then(cleanup);
5385 }
5386 }
5387 function buildGhostCard(deps2, index2, dismiss) {
5388 const card = document.createElement("button");
5389 card.type = "button";
5390 card.setAttribute("role", "menuitem");
5391 card.className = "desktop-mode-dock-peek__card desktop-mode-dock-peek__card--ghost";
5392 card.style.setProperty("--peek-card-index", String(index2));
5393 card.style.setProperty(
5394 "--peek-card-delay",
5395 `${index2 * STAGGER_MS}ms`
5396 );
5397 const plus = document.createElement("span");
5398 plus.className = "desktop-mode-dock-peek__card-plus";
5399 plus.setAttribute("aria-hidden", "true");
5400 plus.textContent = "+";
5401 card.appendChild(plus);
5402 const label = document.createElement("span");
5403 label.className = "desktop-mode-dock-peek__card-label";
5404 label.textContent = sprintf(
5405 // translators: %s is the admin-page title (e.g., "Posts")
5406 __("New %s"),
5407 deps2.item.title
5408 );
5409 card.appendChild(label);
5410 card.addEventListener("click", () => {
5411 spawnWithViewTransition(deps2, dismiss);
5412 });
5413 return card;
5414 }
5415 function spawnWithViewTransition(deps2, dismiss) {
5416 const doc = document;
5417 const spawn = () => {
5418 dismiss();
5419 deps2.openNew();
5420 };
5421 if (typeof doc.startViewTransition === "function") {
5422 doc.startViewTransition(spawn);
5423 return;
5424 }
5425 spawn();
5426 }
5427 const VIEWPORT_MARGIN_PX = 12;
5428 const SHELL_SCHEME_VARS = [
5429 "--wp-admin-theme-color",
5430 "--desktop-mode-titlebar-bg",
5431 "--desktop-mode-titlebar-bg-focused",
5432 "--desktop-mode-titlebar-color",
5433 "--desktop-mode-titlebar-color-focused"
5434 ];
5435 function inheritShellSchemeVars(popover) {
5436 const shell = document.querySelector(".desktop-mode-shell");
5437 if (!shell) {
5438 return;
5439 }
5440 const computed = window.getComputedStyle(shell);
5441 for (const name of SHELL_SCHEME_VARS) {
5442 const value = computed.getPropertyValue(name).trim();
5443 if (value) {
5444 popover.style.setProperty(name, value);
5445 }
5446 }
5447 }
5448 function positionPopover(popover, tile2, orientation) {
5449 const rect = tile2.getBoundingClientRect();
5450 popover.dataset.orientation = orientation;
5451 if (orientation === "bottom") {
5452 popover.style.left = `${rect.left + rect.width / 2}px`;
5453 popover.style.top = `${rect.top - 12}px`;
5454 } else if (orientation === "right") {
5455 popover.style.top = `${rect.top + rect.height / 2}px`;
5456 popover.style.left = `${rect.left - 12}px`;
5457 } else {
5458 popover.style.top = `${rect.top + rect.height / 2}px`;
5459 popover.style.left = `${rect.right + 12}px`;
5460 }
5461 requestAnimationFrame(() => clampToViewport$1(popover));
5462 }
5463 function clampToViewport$1(popover, orientation) {
5464 const rect = popover.getBoundingClientRect();
5465 const vh = window.innerHeight;
5466 const vw = window.innerWidth;
5467 const min = VIEWPORT_MARGIN_PX;
5468 let dy = 0;
5469 let dx = 0;
5470 if (rect.top < min) {
5471 dy = min - rect.top;
5472 } else if (rect.bottom > vh - min) {
5473 dy = vh - min - rect.bottom;
5474 }
5475 if (rect.left < min) {
5476 dx = min - rect.left;
5477 } else if (rect.right > vw - min) {
5478 dx = vw - min - rect.right;
5479 }
5480 if (dx === 0 && dy === 0) {
5481 return;
5482 }
5483 popover.style.setProperty("--peek-clamp-x", `${dx}px`);
5484 popover.style.setProperty("--peek-clamp-y", `${dy}px`);
5485 popover.classList.add("desktop-mode-dock-peek--clamped");
5486 }
5487 function tryOpenExternalUrl(url) {
5488 try {
5489 const parsed = new URL(url, window.location.origin);
5490 if (parsed.origin === window.location.origin) {
5491 return false;
5492 }
5493 window.open(parsed.toString(), "_blank", "noopener,noreferrer");
5494 return true;
5495 } catch {
5496 return false;
5497 }
5498 }
5499 function synthDockId(desktopIconId) {
5500 return `desktop:${desktopIconId}`;
5501 }
5502 function synthIconId(dockItemId) {
5503 return `dock:${dockItemId}`;
5504 }
5505 function canonicalItemId(id) {
5506 if (id.startsWith("dock:")) {
5507 return id.slice(5);
5508 }
5509 if (id.startsWith("desktop:")) {
5510 return id.slice(8);
5511 }
5512 return id;
5513 }
5514 function resolvePlacement(id, nativeRail, visibility) {
5515 const override = visibility[id];
5516 if (override) {
5517 return override;
5518 }
5519 return nativeRail;
5520 }
5521 function shouldShowOnDock(placement) {
5522 return placement === "dock" || placement === "both";
5523 }
5524 function shouldShowOnDesktop(placement) {
5525 return placement === "desktop" || placement === "both";
5526 }
5527 function applyDockPlacement(dockItems, desktopIcons, settings, dockedNativeWindows) {
5528 const visibility = settings.itemVisibility;
5529 const order = settings.dockOrder;
5530 const kept = [];
5531 for (const item of dockItems) {
5532 const placement = resolvePlacement(item.id, "dock", visibility);
5533 if (shouldShowOnDock(placement)) {
5534 kept.push(item);
5535 }
5536 }
5537 for (const icon of desktopIcons) {
5538 const placement = resolvePlacement(icon.id, "desktop", visibility);
5539 if (!shouldShowOnDock(placement)) {
5540 continue;
5541 }
5542 if (icon.window && dockedNativeWindows && dockedNativeWindows.has(icon.window)) {
5543 continue;
5544 }
5545 kept.push({
5546 id: synthIconId(icon.id),
5547 title: icon.title,
5548 icon: icon.icon,
5549 url: icon.url || "",
5550 // Carry the native-window id forward so the dock can light
5551 // the active-dot indicator + show the hover-peek card when
5552 // the target window is open. Without this, window-target
5553 // icons (no `url`) synthesize a tile whose only id-bearing
5554 // field is an empty string — deriveWindowId('') matches
5555 // nothing the window manager has stored.
5556 windowId: icon.window || void 0,
5557 badge: 0,
5558 submenu: [],
5559 isCore: false
5560 });
5561 }
5562 return applyOrder(kept, order);
5563 }
5564 function applyDesktopPlacement(desktopIcons, dockItems, visibility) {
5565 const out = [];
5566 for (const icon of desktopIcons) {
5567 const placement = resolvePlacement(icon.id, "desktop", visibility);
5568 if (shouldShowOnDesktop(placement)) {
5569 out.push(icon);
5570 }
5571 }
5572 let synthIndex = 0;
5573 for (const item of dockItems) {
5574 const placement = resolvePlacement(item.id, "dock", visibility);
5575 if (!shouldShowOnDesktop(placement)) {
5576 continue;
5577 }
5578 out.push({
5579 id: synthDockId(item.id),
5580 title: item.title,
5581 icon: item.icon,
5582 window: "",
5583 url: item.url || "",
5584 // Place synthesized dock-promoted icons after server-registered
5585 // ones. Stable ordering by source-list index inside the bucket.
5586 position: 2e3 + synthIndex++
5587 });
5588 }
5589 return out;
5590 }
5591 function applyOrder(items, order) {
5592 if (order.length === 0 || items.length <= 1) {
5593 return items;
5594 }
5595 const byId = /* @__PURE__ */ new Map();
5596 for (const item of items) {
5597 byId.set(item.id, item);
5598 }
5599 const out = [];
5600 const placed = /* @__PURE__ */ new Set();
5601 for (const id of order) {
5602 const item = byId.get(id);
5603 if (item) {
5604 out.push(item);
5605 placed.add(id);
5606 }
5607 }
5608 for (const item of items) {
5609 if (!placed.has(item.id)) {
5610 out.push(item);
5611 }
5612 }
5613 return out;
5614 }
5615 function html(strings, ...values) {
5616 return { __wpdHtml: true, strings, values };
5617 }
5618 function isTemplateResult(v) {
5619 return !!v && v.__wpdHtml === true;
5620 }
5621 const MARKER_PREFIX = "$$wpd$$";
5622 const MARKER_RE = /\$\$wpd\$\$(\d+)\$\$/g;
5623 function joinWithMarkers(strings) {
5624 let out = strings[0];
5625 for (let i = 1; i < strings.length; i++) {
5626 out += `${MARKER_PREFIX}${i - 1}$$` + strings[i];
5627 }
5628 return out;
5629 }
5630 const compiledCache = /* @__PURE__ */ new WeakMap();
5631 function compile(strings) {
5632 const cached = compiledCache.get(strings);
5633 if (cached) {
5634 return cached;
5635 }
5636 const template = document.createElement("template");
5637 template.innerHTML = joinWithMarkers(strings);
5638 const recipes = [];
5639 const walk2 = (node, path) => {
5640 if (node.nodeType === Node.ELEMENT_NODE) {
5641 const el = node;
5642 for (const attr of Array.from(el.attributes)) {
5643 const rawName = attr.name;
5644 const rawValue = attr.value;
5645 const prefix = rawName[0];
5646 if (MARKER_RE.test(rawValue)) {
5647 MARKER_RE.lastIndex = 0;
5648 if (prefix === "@") {
5649 const match = MARKER_RE.exec(rawValue);
5650 MARKER_RE.lastIndex = 0;
5651 recipes.push({
5652 path,
5653 kind: "event",
5654 name: rawName.slice(1),
5655 valueIndex: match ? Number(match[1]) : 0
5656 });
5657 el.removeAttribute(rawName);
5658 } else if (prefix === ".") {
5659 const match = MARKER_RE.exec(rawValue);
5660 MARKER_RE.lastIndex = 0;
5661 recipes.push({
5662 path,
5663 kind: "prop",
5664 name: rawName.slice(1),
5665 valueIndex: match ? Number(match[1]) : 0
5666 });
5667 el.removeAttribute(rawName);
5668 } else if (prefix === "?") {
5669 const match = MARKER_RE.exec(rawValue);
5670 MARKER_RE.lastIndex = 0;
5671 recipes.push({
5672 path,
5673 kind: "bool",
5674 name: rawName.slice(1),
5675 valueIndex: match ? Number(match[1]) : 0
5676 });
5677 el.removeAttribute(rawName);
5678 } else {
5679 const fragments = [];
5680 const indices = [];
5681 let lastEnd = 0;
5682 let m;
5683 MARKER_RE.lastIndex = 0;
5684 while ((m = MARKER_RE.exec(rawValue)) !== null) {
5685 fragments.push(rawValue.slice(lastEnd, m.index));
5686 indices.push(Number(m[1]));
5687 lastEnd = m.index + m[0].length;
5688 }
5689 fragments.push(rawValue.slice(lastEnd));
5690 recipes.push({
5691 path,
5692 kind: "attr",
5693 name: rawName,
5694 template: fragments,
5695 valueIndices: indices
5696 });
5697 el.setAttribute(rawName, "");
5698 }
5699 }
5700 }
5701 }
5702 const children = Array.from(node.childNodes);
5703 let shift = 0;
5704 for (let i = 0; i < children.length; i++) {
5705 const child = children[i];
5706 const liveIndex = i + shift;
5707 if (child.nodeType === Node.TEXT_NODE) {
5708 const text = child.textContent || "";
5709 if (!MARKER_RE.test(text)) {
5710 MARKER_RE.lastIndex = 0;
5711 continue;
5712 }
5713 MARKER_RE.lastIndex = 0;
5714 const parent = child.parentNode;
5715 let lastEnd = 0;
5716 let m;
5717 const newNodes = [];
5718 const newRecipes = [];
5719 MARKER_RE.lastIndex = 0;
5720 while ((m = MARKER_RE.exec(text)) !== null) {
5721 if (m.index > lastEnd) {
5722 newNodes.push(document.createTextNode(text.slice(lastEnd, m.index)));
5723 }
5724 const placeholder = document.createTextNode("");
5725 newNodes.push(placeholder);
5726 newRecipes.push({
5727 path: [...path, liveIndex + newNodes.length - 1],
5728 kind: "node",
5729 valueIndex: Number(m[1])
5730 });
5731 lastEnd = m.index + m[0].length;
5732 }
5733 if (lastEnd < text.length) {
5734 newNodes.push(document.createTextNode(text.slice(lastEnd)));
5735 }
5736 for (const nn of newNodes) {
5737 parent.insertBefore(nn, child);
5738 }
5739 parent.removeChild(child);
5740 shift += newNodes.length - 1;
5741 recipes.push(...newRecipes);
5742 } else {
5743 walk2(child, [...path, liveIndex]);
5744 }
5745 }
5746 };
5747 walk2(template.content, []);
5748 const buildParts = (fragment) => {
5749 const out = [];
5750 for (const r of recipes) {
5751 let node = fragment;
5752 for (const idx of r.path) {
5753 node = node.childNodes[idx];
5754 }
5755 if (r.kind === "node") {
5756 out.push({
5757 kind: "node",
5758 valueIndex: r.valueIndex,
5759 child: {
5760 anchor: node,
5761 state: null
5762 }
5763 });
5764 } else if (r.kind === "attr") {
5765 out.push({
5766 kind: "attr",
5767 element: node,
5768 name: r.name,
5769 template: r.template,
5770 valueIndices: r.valueIndices
5771 });
5772 } else if (r.kind === "event") {
5773 out.push({
5774 kind: "event",
5775 valueIndex: r.valueIndex,
5776 element: node,
5777 name: r.name
5778 });
5779 } else if (r.kind === "prop") {
5780 out.push({
5781 kind: "prop",
5782 valueIndex: r.valueIndex,
5783 element: node,
5784 name: r.name
5785 });
5786 } else if (r.kind === "bool") {
5787 out.push({
5788 kind: "bool",
5789 valueIndex: r.valueIndex,
5790 element: node,
5791 name: r.name
5792 });
5793 }
5794 }
5795 return out;
5796 };
5797 const entry = { template, buildParts };
5798 compiledCache.set(strings, entry);
5799 return entry;
5800 }
5801 const mountState = /* @__PURE__ */ new WeakMap();
5802 function render$1(result, container) {
5803 const existing = mountState.get(container);
5804 if (existing && existing.strings === result.strings) {
5805 applyValues(existing.parts, result.values);
5806 return;
5807 }
5808 const compiled = compile(result.strings);
5809 const fragment = compiled.template.content.cloneNode(true);
5810 const parts = compiled.buildParts(fragment);
5811 while (container.firstChild) {
5812 container.removeChild(container.firstChild);
5813 }
5814 container.appendChild(fragment);
5815 applyValues(parts, result.values);
5816 mountState.set(container, { strings: result.strings, parts });
5817 }
5818 function applyValues(parts, values) {
5819 for (const part of parts) {
5820 if (part.kind === "node") {
5821 updateChildPart(part.child, values[part.valueIndex]);
5822 } else if (part.kind === "attr") {
5823 let composed = part.template[0];
5824 for (let i = 0; i < part.valueIndices.length; i++) {
5825 composed += formatText(values[part.valueIndices[i]]);
5826 composed += part.template[i + 1];
5827 }
5828 if (composed !== part.last) {
5829 part.last = composed;
5830 if (composed === "") {
5831 part.element.removeAttribute(part.name);
5832 } else {
5833 part.element.setAttribute(part.name, composed);
5834 }
5835 }
5836 } else if (part.kind === "event") {
5837 const next = values[part.valueIndex];
5838 if (next !== part.current) {
5839 if (part.current) {
5840 part.element.removeEventListener(part.name, part.current);
5841 }
5842 if (next) {
5843 part.element.addEventListener(part.name, next);
5844 }
5845 part.current = next;
5846 }
5847 } else if (part.kind === "prop") {
5848 const next = values[part.valueIndex];
5849 if (next !== part.last) {
5850 part.last = next;
5851 part.element[part.name] = next;
5852 }
5853 } else if (part.kind === "bool") {
5854 const next = !!values[part.valueIndex];
5855 if (next !== part.last) {
5856 part.last = next;
5857 if (next) {
5858 part.element.setAttribute(part.name, "");
5859 } else {
5860 part.element.removeAttribute(part.name);
5861 }
5862 }
5863 }
5864 }
5865 }
5866 function updateChildPart(child, value) {
5867 if (value === null || value === void 0 || value === false) {
5868 if (child.state) {
5869 disposeChildState(child.state);
5870 child.state = null;
5871 }
5872 return;
5873 }
5874 if (Array.isArray(value)) {
5875 updateArrayChild(child, value);
5876 return;
5877 }
5878 if (isTemplateResult(value)) {
5879 updateTemplateChild(child, value);
5880 return;
5881 }
5882 if (value instanceof Node) {
5883 updateNodeChild(child, value);
5884 return;
5885 }
5886 updateTextChild(child, formatText(value));
5887 }
5888 function updateNodeChild(child, node) {
5889 const old = child.state;
5890 if (old?.shape === "node" && old.node === node) {
5891 return;
5892 }
5893 if (old) {
5894 disposeChildState(old);
5895 }
5896 insertBeforeAnchor(child, [node]);
5897 child.state = { shape: "node", node };
5898 }
5899 function updateTextChild(child, text) {
5900 const old = child.state;
5901 if (old?.shape === "text") {
5902 if (old.text !== text) {
5903 old.node.textContent = text;
5904 old.text = text;
5905 }
5906 return;
5907 }
5908 if (old) {
5909 disposeChildState(old);
5910 }
5911 const node = document.createTextNode(text);
5912 insertBeforeAnchor(child, [node]);
5913 child.state = { shape: "text", node, text };
5914 }
5915 function updateTemplateChild(child, result) {
5916 const old = child.state;
5917 if (old?.shape === "template" && old.strings === result.strings) {
5918 applyValues(old.parts, result.values);
5919 return;
5920 }
5921 if (old) {
5922 disposeChildState(old);
5923 }
5924 const compiled = compile(result.strings);
5925 const fragment = compiled.template.content.cloneNode(true);
5926 const parts = compiled.buildParts(fragment);
5927 const topNodes = Array.from(fragment.childNodes);
5928 insertBeforeAnchor(child, [fragment]);
5929 applyValues(parts, result.values);
5930 child.state = {
5931 shape: "template",
5932 strings: result.strings,
5933 parts,
5934 nodes: topNodes
5935 };
5936 }
5937 function updateArrayChild(child, arr) {
5938 const old = child.state;
5939 if (old?.shape === "array" && old.entries.length === arr.length) {
5940 for (let i = 0; i < arr.length; i++) {
5941 updateChildPart(old.entries[i], arr[i]);
5942 }
5943 return;
5944 }
5945 if (old) {
5946 disposeChildState(old);
5947 }
5948 const entries = [];
5949 for (const v of arr) {
5950 const entryAnchor = document.createTextNode("");
5951 insertBeforeAnchor(child, [entryAnchor]);
5952 const entry = { anchor: entryAnchor, state: null };
5953 updateChildPart(entry, v);
5954 entries.push(entry);
5955 }
5956 child.state = { shape: "array", entries };
5957 }
5958 function insertBeforeAnchor(child, nodes) {
5959 const parent = child.anchor.parentNode;
5960 if (!parent) {
5961 return;
5962 }
5963 for (const node of nodes) {
5964 parent.insertBefore(node, child.anchor);
5965 }
5966 }
5967 function disposeChildState(state2) {
5968 if (state2.shape === "text") {
5969 state2.node.remove();
5970 return;
5971 }
5972 if (state2.shape === "template") {
5973 for (const node of state2.nodes) {
5974 if (node.parentNode) {
5975 node.parentNode.removeChild(node);
5976 }
5977 }
5978 return;
5979 }
5980 if (state2.shape === "node") {
5981 if (state2.node.parentNode) {
5982 state2.node.parentNode.removeChild(state2.node);
5983 }
5984 return;
5985 }
5986 for (const entry of state2.entries) {
5987 if (entry.state) {
5988 disposeChildState(entry.state);
5989 }
5990 entry.anchor.remove();
5991 }
5992 }
5993 function formatText(v) {
5994 if (v === null || v === void 0 || v === false) {
5995 return "";
5996 }
5997 return String(v);
5998 }
5999 const _Component = class _Component extends HTMLElement {
6000 constructor() {
6001 super();
6002 this._renderScheduled = false;
6003 this._propValues = {};
6004 const ctor = this.constructor;
6005 if (ctor.shadow) {
6006 this.attachShadow({ mode: "open" });
6007 this._renderRoot = this.shadowRoot;
6008 } else {
6009 this._renderRoot = this;
6010 }
6011 this._installPropAccessors();
6012 }
6013 static get observedAttributes() {
6014 return this.props.map(kebab);
6015 }
6016 connectedCallback() {
6017 this._adoptStyles();
6018 this.requestUpdate();
6019 }
6020 attributeChangedCallback(name, oldValue, newValue) {
6021 if (oldValue === newValue) {
6022 return;
6023 }
6024 const prop = camel(name);
6025 this._propValues[prop] = newValue;
6026 this.requestUpdate();
6027 }
6028 /**
6029 * Declarative class-name setter. Assign an array (or a
6030 * space-separated string) and the host's `class` attribute is
6031 * rewritten to match. Intended for programmatic styling — when
6032 * a plugin has enqueued its own stylesheet and wants to apply
6033 * one of those classes to a shell component:
6034 *
6035 * ```js
6036 * element.classNames = [ 'my-plugin-brand', 'is-active' ];
6037 * // → <wpd-select class="my-plugin-brand is-active">
6038 * ```
6039 *
6040 * The plain HTML `class="…"` attribute works just the same and
6041 * is always preferred when writing markup by hand — this setter
6042 * exists for the JS-API case where the caller has an array of
6043 * conditional classes in hand.
6044 *
6045 * Getter returns the current `classList` as a plain array for
6046 * symmetric read/write.
6047 *
6048 * @since 0.13.0
6049 */
6050 get classNames() {
6051 return Array.from(this.classList);
6052 }
6053 set classNames(next) {
6054 if (next === null || next === void 0) {
6055 this.removeAttribute("class");
6056 return;
6057 }
6058 const list2 = Array.isArray(next) ? next : String(next).split(/\s+/);
6059 const cleaned = list2.map((s) => String(s).trim()).filter((s) => s !== "");
6060 this.className = cleaned.join(" ");
6061 }
6062 /**
6063 * Request a re-render explicitly. Components rarely need this —
6064 * declare state via props + attribute observers and the render
6065 * loop picks up changes automatically.
6066 */
6067 requestUpdate() {
6068 this._scheduleRender();
6069 }
6070 /**
6071 * Dispatch a `CustomEvent` with a `detail`. Bubbles + composed
6072 * by default (matches typical WC UX — events cross shadow
6073 * boundaries, parents can listen without knowing about internal
6074 * structure).
6075 */
6076 emit(name, detail) {
6077 return this.dispatchEvent(
6078 new CustomEvent(name, {
6079 detail,
6080 bubbles: true,
6081 composed: true
6082 })
6083 );
6084 }
6085 // ------------------------------------------------------------------
6086 // Internals
6087 // ------------------------------------------------------------------
6088 /**
6089 * Wire every `static props` entry to a matched property getter +
6090 * setter on the element. Setting the property reflects into the
6091 * attribute (so downstream observers + CSS selectors see it);
6092 * reading the property falls back to the attribute.
6093 */
6094 _installPropAccessors() {
6095 const ctor = this.constructor;
6096 for (const prop of ctor.props) {
6097 if (Object.getOwnPropertyDescriptor(this, prop)) {
6098 continue;
6099 }
6100 const attr = kebab(prop);
6101 Object.defineProperty(this, prop, {
6102 get: () => {
6103 if (prop in this._propValues) {
6104 return this._propValues[prop];
6105 }
6106 return this.getAttribute(attr);
6107 },
6108 set: (value) => {
6109 let str;
6110 if (value === null || value === void 0 || value === false) {
6111 str = null;
6112 } else if (value === true) {
6113 str = "";
6114 } else {
6115 str = String(value);
6116 }
6117 this._propValues[prop] = str;
6118 if (str === null) {
6119 this.removeAttribute(attr);
6120 } else {
6121 this.setAttribute(attr, str);
6122 }
6123 this.requestUpdate();
6124 },
6125 enumerable: true,
6126 configurable: true
6127 });
6128 }
6129 }
6130 /**
6131 * Schedule a render on the next microtask. Multiple property
6132 * assignments in the same tick collapse into a single render.
6133 */
6134 _scheduleRender() {
6135 if (this._renderScheduled || !this.isConnected) {
6136 return;
6137 }
6138 this._renderScheduled = true;
6139 queueMicrotask(() => {
6140 this._renderScheduled = false;
6141 if (!this.isConnected) {
6142 return;
6143 }
6144 render$1(this.render(), this._renderRoot);
6145 });
6146 }
6147 /**
6148 * Mount adoptable stylesheets onto the shadow root (via
6149 * `adoptedStyleSheets`) or the light DOM (via one `<style>`
6150 * tag per def). No-op if `static styles` is empty.
6151 */
6152 _adoptStyles() {
6153 const ctor = this.constructor;
6154 if (ctor.styles.length === 0) {
6155 return;
6156 }
6157 if (ctor.shadow && this.shadowRoot) {
6158 const sheets = ctor.styles.map((s) => s.sheet).filter((s) => s !== null);
6159 this.shadowRoot.adoptedStyleSheets = sheets;
6160 if (sheets.length !== ctor.styles.length) {
6161 for (const s of ctor.styles) {
6162 if (!s.sheet) {
6163 const tag = document.createElement("style");
6164 tag.textContent = s.cssText;
6165 this.shadowRoot.appendChild(tag);
6166 }
6167 }
6168 }
6169 } else {
6170 this._adoptLightStyles(ctor);
6171 }
6172 }
6173 _adoptLightStyles(ctor) {
6174 if (_Component._lightStylesAdopted.has(ctor)) {
6175 return;
6176 }
6177 _Component._lightStylesAdopted.add(ctor);
6178 for (const s of ctor.styles) {
6179 const tag = document.createElement("style");
6180 tag.dataset.wpdUi = this.tagName.toLowerCase();
6181 tag.textContent = s.cssText;
6182 document.head.appendChild(tag);
6183 }
6184 }
6185 };
6186 _Component.props = [];
6187 _Component.styles = [];
6188 _Component.shadow = true;
6189 _Component._lightStylesAdopted = /* @__PURE__ */ new WeakSet();
6190 let Component = _Component;
6191 function defineComponent(tag, ctor) {
6192 if (customElements.get(tag)) {
6193 return;
6194 }
6195 customElements.define(tag, ctor);
6196 }
6197 function kebab(s) {
6198 return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
6199 }
6200 function camel(s) {
6201 return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
6202 }
6203 const SUPPORTS_CONSTRUCTABLE_SHEETS = (() => {
6204 try {
6205 const s = new CSSStyleSheet();
6206 return typeof s.replaceSync === "function";
6207 } catch {
6208 return false;
6209 }
6210 })();
6211 function css(strings, ...values) {
6212 let text = strings[0];
6213 for (let i = 1; i < strings.length; i++) {
6214 const v = values[i - 1];
6215 if (typeof v === "string" || typeof v === "number") {
6216 text += String(v);
6217 } else if (v && v.__wpdCss) {
6218 text += v.cssText;
6219 } else {
6220 throw new TypeError(
6221 "[wpd-ui] css`` interpolations must be strings, numbers, or other css`` results. Got: " + typeof v
6222 );
6223 }
6224 text += strings[i];
6225 }
6226 if (SUPPORTS_CONSTRUCTABLE_SHEETS) {
6227 const sheet = new CSSStyleSheet();
6228 sheet.replaceSync(text);
6229 return { __wpdCss: true, sheet, cssText: text };
6230 }
6231 return { __wpdCss: true, sheet: null, cssText: text };
6232 }
6233 function computeAutoId(element) {
6234 const parts = [];
6235 const tabs = [];
6236 let windowId = null;
6237 let node = element.parentElement;
6238 while (node) {
6239 if (node === document.body || node === document.documentElement) {
6240 break;
6241 }
6242 const id = node.id || "";
6243 if (id.startsWith("wp-window-")) {
6244 windowId = id.slice("wp-window-".length);
6245 break;
6246 }
6247 if (node.tagName.toLowerCase() === "wpd-tabpanel") {
6248 const forValue = node.getAttribute("for");
6249 if (forValue) {
6250 tabs.unshift(forValue);
6251 }
6252 }
6253 node = node.parentElement;
6254 }
6255 if (windowId) {
6256 parts.push(slugify(windowId));
6257 }
6258 for (const tab of tabs) {
6259 parts.push("tab-" + slugify(tab));
6260 }
6261 const label = element.getAttribute("label");
6262 if (label) {
6263 parts.push(slugify(label));
6264 }
6265 if (parts.length === 0) {
6266 return "wpd-unnamed";
6267 }
6268 return "wpd-" + parts.filter((p) => p !== "").join("-");
6269 }
6270 function slugify(s) {
6271 return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
6272 }
6273 function ensureAutoId(element) {
6274 if (element.id) {
6275 return element.id;
6276 }
6277 const id = computeAutoId(element);
6278 element.id = id;
6279 return id;
6280 }
6281 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 )}`;
6282 const _WpdConfirmDialog = class _WpdConfirmDialog extends Component {
6283 constructor() {
6284 super(...arguments);
6285 this._onKey = (e) => {
6286 if (e.key === "Escape") {
6287 e.preventDefault();
6288 this._cancel();
6289 }
6290 if (e.key === "Enter" && !e.isComposing) {
6291 e.preventDefault();
6292 this._confirm();
6293 }
6294 };
6295 this._onBackdrop = (e) => {
6296 const path = e.composedPath();
6297 const original = path.length > 0 ? path[0] : e.target;
6298 if (original === this) {
6299 this._cancel();
6300 }
6301 };
6302 this._confirm = () => {
6303 this.emit("wpd-confirm", { confirmed: true });
6304 this.removeAttribute("open");
6305 };
6306 this._cancel = () => {
6307 this.emit("wpd-cancel", { confirmed: false });
6308 this.removeAttribute("open");
6309 };
6310 }
6311 connectedCallback() {
6312 super.connectedCallback();
6313 this.setAttribute("role", "dialog");
6314 this.setAttribute("aria-modal", "true");
6315 this.addEventListener("keydown", this._onKey);
6316 this.addEventListener("click", this._onBackdrop);
6317 }
6318 disconnectedCallback() {
6319 this.removeEventListener("keydown", this._onKey);
6320 this.removeEventListener("click", this._onBackdrop);
6321 }
6322 render() {
6323 const title = this.title ?? "";
6324 const message = this.message ?? "";
6325 const confirmLabel = this["confirm-label"] || "Confirm";
6326 const cancelLabel = this["cancel-label"] || "Cancel";
6327 const isDanger = this.hasAttribute("danger");
6328 const hideCancel = this.hasAttribute("hide-cancel");
6329 const isDismissable = this.hasAttribute("dismissable");
6330 return html`
6331 <div class="dialog" tabindex="-1">
6332 ${isDismissable ? html`<button
6333 type="button"
6334 class="close"
6335 aria-label="Close"
6336 @click=${() => this._cancel()}
6337 >&times;</button>` : html``}
6338 ${title ? html`<h2 class="title">${title}</h2>` : html``}
6339 ${message ? html`<p class="message">${message}</p>` : html``}
6340 <div class="actions">
6341 ${hideCancel ? html`` : html`<button
6342 type="button"
6343 class="btn btn--secondary"
6344 @click=${() => this._cancel()}
6345 >
6346 ${cancelLabel}
6347 </button>`}
6348 <button
6349 type="button"
6350 class="btn ${isDanger ? "btn--danger" : "btn--primary"}"
6351 @click=${() => this._confirm()}
6352 >
6353 ${confirmLabel}
6354 </button>
6355 </div>
6356 </div>
6357 `;
6358 }
6359 };
6360 _WpdConfirmDialog.props = [
6361 "open",
6362 "title",
6363 "message",
6364 "confirm-label",
6365 "cancel-label",
6366 "danger",
6367 "hide-cancel",
6368 "dismissable"
6369 ];
6370 _WpdConfirmDialog.styles = [dialogStyles];
6371 _WpdConfirmDialog.help = {
6372 title: "Confirm dialog",
6373 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.",
6374 status: "experimental",
6375 since: "0.9.0",
6376 props: [
6377 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
6378 { name: "title", type: "string", description: "Heading shown at the top." },
6379 { name: "message", type: "string", description: "Body copy. Newlines preserved." },
6380 { name: "confirm-label", type: "string", default: "Confirm", description: "Confirm-button label." },
6381 { name: "cancel-label", type: "string", default: "Cancel", description: "Cancel-button label." },
6382 { name: "danger", type: "boolean attribute", description: "Renders the confirm button red." },
6383 { 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." },
6384 { name: "dismissable", type: "boolean attribute", description: "Renders an X close button in the top-right corner. Click emits `wpd-cancel`." }
6385 ],
6386 events: [
6387 {
6388 name: "wpd-confirm",
6389 description: "Fires on confirm. Detail: `{ confirmed: true }`."
6390 },
6391 {
6392 name: "wpd-cancel",
6393 description: "Fires on cancel (Cancel button, Escape, backdrop click). Detail: `{ confirmed: false }`."
6394 }
6395 ]
6396 };
6397 let WpdConfirmDialog = _WpdConfirmDialog;
6398 defineComponent("wpd-confirm-dialog", WpdConfirmDialog);
6399 function wpdConfirm$1(options) {
6400 return new Promise((resolve2) => {
6401 const dialog2 = document.createElement("wpd-confirm-dialog");
6402 dialog2.setAttribute("open", "");
6403 if (options.title) {
6404 dialog2.setAttribute("title", options.title);
6405 }
6406 dialog2.setAttribute("message", options.message);
6407 if (options.confirmLabel) {
6408 dialog2.setAttribute("confirm-label", options.confirmLabel);
6409 }
6410 if (options.cancelLabel) {
6411 dialog2.setAttribute("cancel-label", options.cancelLabel);
6412 }
6413 if (options.danger) {
6414 dialog2.setAttribute("danger", "");
6415 }
6416 if (options.hideCancel) {
6417 dialog2.setAttribute("hide-cancel", "");
6418 }
6419 if (options.dismissable) {
6420 dialog2.setAttribute("dismissable", "");
6421 }
6422 const cleanup = (ok) => {
6423 dialog2.remove();
6424 resolve2(ok);
6425 };
6426 dialog2.addEventListener("wpd-confirm", () => cleanup(true));
6427 dialog2.addEventListener("wpd-cancel", () => cleanup(false));
6428 document.body.appendChild(dialog2);
6429 const inner = dialog2.shadowRoot?.querySelector(".dialog");
6430 (inner ?? dialog2).focus?.();
6431 });
6432 }
6433 const FALLBACK_BASE = "http://localhost/";
6434 function joinRestUrl(restRoot2, path) {
6435 const base = typeof window !== "undefined" && window.location ? window.location.href : FALLBACK_BASE;
6436 const url = new URL(restRoot2, base);
6437 const trimmed = path.replace(/^\/+/, "");
6438 const queryAt = trimmed.indexOf("?");
6439 const route = queryAt === -1 ? trimmed : trimmed.slice(0, queryAt);
6440 const extraQuery = queryAt === -1 ? "" : trimmed.slice(queryAt + 1);
6441 if (url.searchParams.has("rest_route")) {
6442 const existing = url.searchParams.get("rest_route") ?? "/";
6443 const prefix = existing.endsWith("/") ? existing : existing + "/";
6444 url.searchParams.set("rest_route", prefix + route);
6445 } else {
6446 const pathname = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
6447 url.pathname = pathname + route;
6448 }
6449 if (extraQuery) {
6450 const extras = new URLSearchParams(extraQuery);
6451 extras.forEach((value, key) => {
6452 url.searchParams.append(key, value);
6453 });
6454 }
6455 return url.toString();
6456 }
6457 function getApi() {
6458 const w = window;
6459 return w.wp?.desktop ?? null;
6460 }
6461 let activeMenu$3 = null;
6462 function closeMenu$1() {
6463 if (activeMenu$3) {
6464 activeMenu$3.remove();
6465 activeMenu$3 = null;
6466 }
6467 }
6468 function writeVisibility(canonicalId, placement) {
6469 const api = getApi();
6470 if (!api?.getOsSettings || !api?.updateOsSettings) {
6471 return;
6472 }
6473 const snap = api.getOsSettings();
6474 const next = { ...snap.itemVisibility };
6475 next[canonicalId] = placement;
6476 api.updateOsSettings({ itemVisibility: next });
6477 }
6478 function railFromId(id, surface) {
6479 if (id.startsWith("dock:")) {
6480 return "dock";
6481 }
6482 if (id.startsWith("desktop:")) {
6483 return "desktop";
6484 }
6485 return surface;
6486 }
6487 function computeHideTarget(canonicalId, nativeRail, hideSurface, visibility) {
6488 const current = resolvePlacement(canonicalId, nativeRail, visibility);
6489 if (current === "both") {
6490 return hideSurface === "dock" ? "desktop" : "dock";
6491 }
6492 return "hidden";
6493 }
6494 let openGeneration$2 = 0;
6495 function openItemVisibilityMenu(opts) {
6496 closeMenu$1();
6497 const myGen = ++openGeneration$2;
6498 openWithShellOverlays(
6499 () => myGen === openGeneration$2,
6500 () => openItemVisibilityMenuImmediate(opts)
6501 );
6502 }
6503 function openItemVisibilityMenuImmediate(opts) {
6504 closeMenu$1();
6505 const canonical = canonicalItemId(opts.id);
6506 const nativeRail = railFromId(opts.id, opts.surface);
6507 const currentPlacement = resolvePlacement(
6508 canonical,
6509 nativeRail,
6510 getApi()?.getOsSettings?.().itemVisibility ?? {}
6511 );
6512 const options = [];
6513 if (opts.surface === "dock") {
6514 options.push({
6515 id: "hide-from-dock",
6516 label: __("Hide from dock"),
6517 icon: "dashicons-hidden",
6518 onPick: () => writeVisibility(
6519 canonical,
6520 computeHideTarget(
6521 canonical,
6522 nativeRail,
6523 "dock",
6524 getApi()?.getOsSettings?.().itemVisibility ?? {}
6525 )
6526 )
6527 });
6528 if (currentPlacement !== "both") {
6529 options.push({
6530 id: "show-on-desktop-too",
6531 label: __("Also show on desktop"),
6532 icon: "dashicons-desktop",
6533 onPick: () => writeVisibility(canonical, "both")
6534 });
6535 }
6536 } else {
6537 options.push({
6538 id: "hide-from-desktop",
6539 label: __("Hide from desktop"),
6540 icon: "dashicons-hidden",
6541 onPick: () => writeVisibility(
6542 canonical,
6543 computeHideTarget(
6544 canonical,
6545 nativeRail,
6546 "desktop",
6547 getApi()?.getOsSettings?.().itemVisibility ?? {}
6548 )
6549 )
6550 });
6551 if (currentPlacement !== "both") {
6552 options.push({
6553 id: "show-on-dock-too",
6554 label: __("Also show on dock"),
6555 icon: "dashicons-menu",
6556 onPick: () => writeVisibility(canonical, "both")
6557 });
6558 }
6559 }
6560 options.push({
6561 id: "hide-everywhere",
6562 label: __("Hide everywhere"),
6563 icon: "dashicons-no",
6564 danger: true,
6565 onPick: () => writeVisibility(canonical, "hidden")
6566 });
6567 options.push({
6568 id: "open-settings",
6569 label: __("Apps & Icons settings…"),
6570 icon: "dashicons-admin-generic",
6571 onPick: () => {
6572 const api = getApi();
6573 api?.openOsSettings?.({ tabId: "apps-icons" });
6574 }
6575 });
6576 if (opts.pluginFile) {
6577 const pluginFile = opts.pluginFile;
6578 const pluginLabel = opts.pluginName || opts.title;
6579 options.push({ kind: "separator" });
6580 options.push({
6581 id: "deactivate-plugin",
6582 // translators: %s is the owning plugin's display name.
6583 label: sprintf(__("Deactivate %s…"), pluginLabel),
6584 icon: "dashicons-trash",
6585 danger: true,
6586 onPick: () => {
6587 void confirmAndDeactivatePlugin(pluginFile, pluginLabel);
6588 }
6589 });
6590 }
6591 const menu = document.createElement("wpd-context-menu");
6592 menu.setAttribute("open", "");
6593 menu.classList.add("desktop-mode-item-visibility-menu");
6594 menu.dataset.itemId = opts.id;
6595 menu.style.position = "fixed";
6596 menu.style.left = "-9999px";
6597 menu.style.top = "-9999px";
6598 menu.style.visibility = "hidden";
6599 menu.style.zIndex = "1000000";
6600 const byKey = /* @__PURE__ */ new Map();
6601 for (const opt of options) {
6602 if (opt.kind === "separator") {
6603 const hr = document.createElement("hr");
6604 hr.style.cssText = "border: 0; border-top: 1px solid var( --wpd-context-menu-separator-color, rgba(255,255,255,0.12) ); margin: 4px 6px;";
6605 menu.appendChild(hr);
6606 continue;
6607 }
6608 byKey.set(opt.id, opt);
6609 const node = document.createElement("wpd-context-menu-option");
6610 node.dataset.menuItemId = opt.id;
6611 node.setAttribute("value", opt.id);
6612 if (opt.icon) {
6613 node.setAttribute("icon", opt.icon);
6614 }
6615 if (opt.danger) {
6616 node.setAttribute("danger", "");
6617 }
6618 node.textContent = opt.label;
6619 menu.appendChild(node);
6620 }
6621 menu.addEventListener("wpd-context-menu-pick", (e) => {
6622 const detail = e.detail;
6623 const key = detail?.id || detail?.value || "";
6624 const opt = byKey.get(key);
6625 closeMenu$1();
6626 try {
6627 opt?.onPick();
6628 } catch {
6629 }
6630 });
6631 document.body.appendChild(menu);
6632 activeMenu$3 = menu;
6633 const positionMenu = () => {
6634 if (menu !== activeMenu$3) {
6635 return;
6636 }
6637 const rect = menu.getBoundingClientRect();
6638 const margin = 8;
6639 let left = opts.x;
6640 let top;
6641 if (opts.surface === "dock") {
6642 top = Math.max(margin, opts.y - rect.height - margin);
6643 } else {
6644 top = opts.y;
6645 if (top + rect.height + margin > window.innerHeight) {
6646 top = Math.max(margin, opts.y - rect.height);
6647 }
6648 }
6649 if (left + rect.width + margin > window.innerWidth) {
6650 left = Math.max(margin, opts.x - rect.width);
6651 }
6652 menu.style.left = `${left}px`;
6653 menu.style.top = `${top}px`;
6654 menu.style.visibility = "";
6655 };
6656 requestAnimationFrame(positionMenu);
6657 const onOutside = (ev) => {
6658 if (!activeMenu$3) {
6659 return;
6660 }
6661 if (!activeMenu$3.contains(ev.target)) {
6662 closeMenu$1();
6663 document.removeEventListener("mousedown", onOutside, true);
6664 document.removeEventListener("keydown", onKey, true);
6665 }
6666 };
6667 const onKey = (ev) => {
6668 if (ev.key === "Escape") {
6669 closeMenu$1();
6670 document.removeEventListener("mousedown", onOutside, true);
6671 document.removeEventListener("keydown", onKey, true);
6672 }
6673 };
6674 document.addEventListener("mousedown", onOutside, true);
6675 document.addEventListener("keydown", onKey, true);
6676 }
6677 async function confirmAndDeactivatePlugin(pluginFile, title) {
6678 const confirmed = await wpdConfirm$1({
6679 /* translators: %s: plugin title. */
6680 title: sprintf(__("Deactivate %s?"), title),
6681 message: __(
6682 "This plugin will stop running on the site. You can re-activate it later from the Plugins screen."
6683 ),
6684 confirmLabel: __("Deactivate"),
6685 cancelLabel: __("Cancel"),
6686 danger: true
6687 });
6688 if (!confirmed) {
6689 return;
6690 }
6691 const cfg = window.desktopModeConfig ?? {};
6692 const restRoot2 = typeof cfg.restRoot === "string" && cfg.restRoot ? cfg.restRoot : `${window.location.origin}/wp-json/`;
6693 const restNonce = typeof cfg.restNonce === "string" && cfg.restNonce ? cfg.restNonce : "";
6694 const stripped = pluginFile.endsWith(".php") ? pluginFile.slice(0, -4) : pluginFile;
6695 const encoded = stripped.split("/").map(encodeURIComponent).join("/");
6696 const url = joinRestUrl(restRoot2, `wp/v2/plugins/${encoded}`);
6697 try {
6698 const res = await trackedFetch$1(
6699 url,
6700 {
6701 method: "PUT",
6702 headers: {
6703 "Content-Type": "application/json",
6704 "X-WP-Nonce": restNonce
6705 },
6706 body: JSON.stringify({ status: "inactive" }),
6707 credentials: "same-origin"
6708 },
6709 { source: "desktop-mode/dock-deactivate-plugin" }
6710 );
6711 if (!res.ok) {
6712 throw new Error(`HTTP ${res.status}`);
6713 }
6714 } catch (err) {
6715 showToast({
6716 message: sprintf(
6717 /* translators: %s: plugin title. */
6718 __("Could not deactivate %s."),
6719 title
6720 ),
6721 duration: 4e3
6722 });
6723 console.error("[desktop-mode] deactivate plugin failed", err);
6724 return;
6725 }
6726 const closedTitles = closeWindowsForPlugin(pluginFile);
6727 const deactivatedMsg = closedTitles.length > 0 ? sprintf(
6728 /* translators: 1: plugin title. 2: number of windows that were closed. */
6729 __("%1$s deactivated. Closed %2$d window(s)."),
6730 title,
6731 closedTitles.length
6732 ) : sprintf(
6733 /* translators: %s: plugin title. */
6734 __("%s deactivated."),
6735 title
6736 );
6737 showToast({ message: deactivatedMsg, duration: 3e3 });
6738 const w = window;
6739 w.wp?.desktop?.refreshMenu?.();
6740 }
6741 function closeWindowsForPlugin(pluginFile) {
6742 const api = window.wp?.desktop;
6743 if (!api?.windowManager?.getAll) {
6744 return [];
6745 }
6746 const items = api.getMenuItems?.() ?? [];
6747 const owned = items.filter((i) => i.pluginFile === pluginFile);
6748 if (owned.length === 0) {
6749 return [];
6750 }
6751 const ownedKeys = /* @__PURE__ */ new Set();
6752 for (const item of owned) {
6753 ownedKeys.add(item.id);
6754 if (api.deriveWindowId) {
6755 ownedKeys.add(api.deriveWindowId(item.url));
6756 }
6757 }
6758 const toClose = /* @__PURE__ */ new Map();
6759 const windows = api.windowManager.getAll() ?? [];
6760 const derive = api.deriveWindowId;
6761 for (const w of windows) {
6762 if (ownedKeys.has(w.id)) {
6763 toClose.set(w.id, w);
6764 continue;
6765 }
6766 if (w.config?.baseId && ownedKeys.has(w.config.baseId)) {
6767 toClose.set(w.id, w);
6768 continue;
6769 }
6770 if (derive && w.config?.url) {
6771 const derivedFromConfig = derive(w.config.url);
6772 if (ownedKeys.has(derivedFromConfig)) {
6773 toClose.set(w.id, w);
6774 continue;
6775 }
6776 }
6777 if (derive && w.iframe) {
6778 let liveUrl = "";
6779 try {
6780 liveUrl = w.iframe.src || "";
6781 } catch {
6782 }
6783 if (liveUrl) {
6784 const derivedFromLive = derive(liveUrl);
6785 if (ownedKeys.has(derivedFromLive)) {
6786 toClose.set(w.id, w);
6787 }
6788 }
6789 }
6790 }
6791 const titles = [];
6792 for (const w of toClose.values()) {
6793 titles.push(w.config?.title ?? w.id);
6794 try {
6795 w.close();
6796 } catch {
6797 }
6798 }
6799 return titles;
6800 }
6801 const _Dock = class _Dock {
6802 constructor(container, windowManager, items, adminUrl, orientation = "left") {
6803 this.itemElements = /* @__PURE__ */ new Map();
6804 this.systemItems = [];
6805 this.systemItemElements = /* @__PURE__ */ new Map();
6806 this.systemSeparator = null;
6807 this.badgeOverrides = /* @__PURE__ */ new Map();
6808 this.attentionTimers = /* @__PURE__ */ new Map();
6809 this.peekTeardowns = /* @__PURE__ */ new Map();
6810 this.boundRefresh = () => void 0;
6811 this.container = container;
6812 this.windowManager = windowManager;
6813 this.items = items;
6814 this.adminUrl = adminUrl;
6815 this.orientation = orientation;
6816 this.rail = orientation === "bottom" ? "taskbar" : "dock";
6817 this.hooksNamespace = `desktop-mode/dock/${++_Dock.instanceCounter}`;
6818 this.container.setAttribute(
6819 "data-desktop-mode-dock-placement",
6820 orientation
6821 );
6822 const scroll = document.createElement("div");
6823 scroll.className = "desktop-mode-dock__scroll";
6824 const pinned = document.createElement("div");
6825 pinned.className = "desktop-mode-dock__pinned";
6826 container.appendChild(scroll);
6827 container.appendChild(pinned);
6828 this.itemHost = scroll;
6829 this.systemHost = pinned;
6830 this.tooltip = document.createElement("div");
6831 this.tooltip.className = "desktop-mode-dock__tooltip";
6832 this.tooltip.setAttribute("role", "tooltip");
6833 if (orientation === "bottom") {
6834 this.tooltip.classList.add("desktop-mode-dock__tooltip--above");
6835 } else if (orientation === "right") {
6836 this.tooltip.classList.add("desktop-mode-dock__tooltip--before");
6837 } else {
6838 this.tooltip.classList.add("desktop-mode-dock__tooltip--after");
6839 }
6840 document.body.appendChild(this.tooltip);
6841 this.render();
6842 this.bindWindowEvents();
6843 }
6844 /**
6845 * Build the base context object every dock decoration hook
6846 * receives. Read from `this` so a single subscriber can
6847 * disambiguate two coexisting rails by `dockId`.
6848 */
6849 buildHookContextBase() {
6850 return {
6851 rail: this.rail,
6852 orientation: this.orientation,
6853 dockId: this.container.id,
6854 container: this.container
6855 };
6856 }
6857 /**
6858 * Replace the menu-derived tile list with a fresh one, preserving
6859 * any JS-registered system tiles. Used by the live menu-refresh
6860 * path: after a plugin is activated or deactivated, the chromeless
6861 * bridge postMessages a fresh payload built from real admin
6862 * context, and the shell calls this so the dock repaints without
6863 * a tab reload.
6864 *
6865 * Old menu tiles are removed from both the DOM and the lookup
6866 * map; new tiles are inserted before the system separator (or
6867 * appended at the end if none exists yet), so the menu-items →
6868 * hairline → system-items ordering stays intact. Active-state
6869 * classes are re-computed once the new tiles are in place so
6870 * window indicators survive the swap.
6871 *
6872 * @param items New DockItem list. Pass `[]` to clear everything
6873 * menu-derived.
6874 */
6875 /**
6876 * Update the dock's orientation. Writes the new value to the
6877 * dock element's `data-desktop-mode-dock-placement` attribute (CSS
6878 * keys off it for layout) and keeps the tooltip anchor in sync.
6879 *
6880 * In practice, the layout dispatcher in `desktop.ts` rebuilds the
6881 * dock(s) from scratch on a layout change rather than re-orienting
6882 * a live instance — but this stays correct in case any caller
6883 * wants to flip orientation without the rebuild.
6884 */
6885 setOrientation(orientation) {
6886 if (this.orientation === orientation) {
6887 return;
6888 }
6889 this.orientation = orientation;
6890 this.container.setAttribute(
6891 "data-desktop-mode-dock-placement",
6892 orientation
6893 );
6894 this.tooltip.classList.remove(
6895 "desktop-mode-dock__tooltip--above",
6896 "desktop-mode-dock__tooltip--before",
6897 "desktop-mode-dock__tooltip--after"
6898 );
6899 if (orientation === "bottom") {
6900 this.tooltip.classList.add("desktop-mode-dock__tooltip--above");
6901 } else if (orientation === "right") {
6902 this.tooltip.classList.add("desktop-mode-dock__tooltip--before");
6903 } else {
6904 this.tooltip.classList.add("desktop-mode-dock__tooltip--after");
6905 }
6906 }
6907 replaceItems(items) {
6908 for (const itemId of this.itemElements.keys()) {
6909 const teardown = this.peekTeardowns.get(itemId);
6910 if (teardown) {
6911 teardown();
6912 this.peekTeardowns.delete(itemId);
6913 }
6914 }
6915 for (const el of this.itemElements.values()) {
6916 el.remove();
6917 }
6918 this.itemHost.querySelectorAll(
6919 ".desktop-mode-dock__separator--group"
6920 ).forEach((el) => el.remove());
6921 this.itemElements.clear();
6922 this.items = items;
6923 const base = this.buildHookContextBase();
6924 doAction(HOOKS.DOCK_BEFORE_RENDER, {
6925 ...base,
6926 items,
6927 tileElements: this.itemElements
6928 });
6929 let insertedGroupSeparator = false;
6930 let tilesInsertedThisPass = 0;
6931 for (const item of items) {
6932 if (!insertedGroupSeparator && item.isCore === false) {
6933 if (tilesInsertedThisPass > 0) {
6934 const sep = document.createElement("div");
6935 sep.className = "desktop-mode-dock__separator desktop-mode-dock__separator--group";
6936 sep.setAttribute("aria-hidden", "true");
6937 this.itemHost.appendChild(sep);
6938 }
6939 insertedGroupSeparator = true;
6940 }
6941 const btn = this.createItemButton(item);
6942 this.itemElements.set(item.id, btn);
6943 this.itemHost.appendChild(btn);
6944 tilesInsertedThisPass++;
6945 const override = this.badgeOverrides.get(item.id);
6946 if (override !== void 0) {
6947 const primary = btn.querySelector(
6948 ".desktop-mode-dock__item-primary"
6949 );
6950 _applyBadgeNode(primary ?? btn, override);
6951 }
6952 doAction(HOOKS.DOCK_TILE_RENDERED, {
6953 ...base,
6954 item,
6955 isSystem: false,
6956 el: btn
6957 });
6958 }
6959 this.updateActiveStates();
6960 doAction(HOOKS.DOCK_AFTER_RENDER, {
6961 ...base,
6962 items,
6963 tileElements: this.itemElements
6964 });
6965 }
6966 /**
6967 * True when the rail currently has ANY renderable tile —
6968 * either a menu-derived item or a JS-registered system item.
6969 * Lets callers (the shell's live-refresh path) decide whether
6970 * to hide the whole rail without having to peek into two
6971 * internal maps. "System tiles keep the rail alive even when
6972 * menu items are empty" is the user-visible contract we enforce.
6973 */
6974 hasItems() {
6975 return this.itemElements.size > 0 || this.systemItemElements.size > 0;
6976 }
6977 /**
6978 * Remove a previously-registered system item. Used by the
6979 * server-driven native-window sync path — when a plugin is
6980 * deactivated, its native-window entry disappears from the
6981 * server's payload and the shell calls this to pull the tile
6982 * back off the rail without a reload.
6983 *
6984 * Idempotent: an unknown id is a silent no-op. The system
6985 * separator is kept in place as long as at least one system
6986 * item remains; removing the last system item also strips the
6987 * separator so the rail doesn't dangle a divider under nothing.
6988 */
6989 removeSystemItem(id) {
6990 const tile2 = this.systemItemElements.get(id);
6991 if (!tile2) {
6992 return;
6993 }
6994 tile2.remove();
6995 this.systemItemElements.delete(id);
6996 this.systemItems = this.systemItems.filter((s) => s.id !== id);
6997 this.badgeOverrides.delete(id);
6998 if (this.systemItemElements.size === 0 && this.systemSeparator) {
6999 this.systemSeparator.remove();
7000 this.systemSeparator = null;
7001 }
7002 doAction(HOOKS.DOCK_ITEM_REMOVED, { id, placement: this.rail });
7003 }
7004 /**
7005 * Set the badge count on a tile. Live-updates without a full
7006 * dock re-render — the existing tile's badge node is mutated in
7007 * place (or created if missing). Pass `0` to remove the badge.
7008 *
7009 * Resolves the tile in id order: menu items (`data-menu-slug`)
7010 * first, then system items (`data-system-id`), so callers can
7011 * use the same id surface regardless of which rail the tile
7012 * happens to live on.
7013 *
7014 * Idempotent: applying the same count is a no-op (no DOM mutation).
7015 *
7016 * @since 0.22.0
7017 *
7018 * @param itemId Tile id (menu slug for admin pages, system id
7019 * for `appendSystemItem` / `registerSystemTile`).
7020 * @param count Non-negative integer. `>99` renders as `99+`.
7021 */
7022 setBadge(itemId, count) {
7023 const tile2 = this._resolveTileElement(itemId);
7024 if (!tile2) {
7025 return;
7026 }
7027 const safe = Math.max(0, Math.floor(Number(count) || 0));
7028 if (safe === 0) {
7029 this.badgeOverrides.delete(itemId);
7030 } else {
7031 this.badgeOverrides.set(itemId, safe);
7032 }
7033 const primary = tile2.querySelector(
7034 ".desktop-mode-dock__item-primary"
7035 );
7036 _applyBadgeNode(primary ?? tile2, safe);
7037 activity.publish("desktop-mode/badge-changed", {
7038 itemId,
7039 count: safe,
7040 rail: this.rail
7041 });
7042 }
7043 /**
7044 * Clear the badge on a tile. Equivalent to `setBadge( id, 0 )`.
7045 *
7046 * @since 0.22.0
7047 */
7048 clearBadge(itemId) {
7049 this.setBadge(itemId, 0);
7050 }
7051 /**
7052 * Apply or clear an attention animation on a tile.
7053 *
7054 * - `'pulse'` — soft halo + scale, ~1.4 s loop. Default.
7055 * - `'shake'` — short horizontal jiggle.
7056 * - `'bounce'` — vertical bob, attention-grabbing.
7057 * - `null` — clear any active attention.
7058 *
7059 * Animations are gated on `prefers-reduced-motion: no-preference`;
7060 * the reduced-motion fallback shows a static accent ring for the
7061 * same duration so the affordance still works. `durationMs` of
7062 * `0` keeps the attention until the next call clears it.
7063 *
7064 * @since 0.22.0
7065 *
7066 * @param itemId Tile id.
7067 * @param mode Animation mode or `null` to clear.
7068 * @param opts Optional duration / intensity overrides.
7069 */
7070 setAttention(itemId, mode, opts = {}) {
7071 const tile2 = this._resolveTileElement(itemId);
7072 if (!tile2) {
7073 return;
7074 }
7075 const pending2 = this.attentionTimers.get(itemId);
7076 if (pending2 !== void 0) {
7077 window.clearTimeout(pending2);
7078 this.attentionTimers.delete(itemId);
7079 }
7080 tile2.classList.remove(
7081 "desktop-mode-dock__item--attention-pulse",
7082 "desktop-mode-dock__item--attention-shake",
7083 "desktop-mode-dock__item--attention-bounce",
7084 "desktop-mode-dock__item--intensity-subtle",
7085 "desktop-mode-dock__item--intensity-normal",
7086 "desktop-mode-dock__item--intensity-strong"
7087 );
7088 if (mode === null) {
7089 return;
7090 }
7091 tile2.classList.add(`desktop-mode-dock__item--attention-${mode}`);
7092 const intensity = opts.intensity ?? "normal";
7093 tile2.classList.add(`desktop-mode-dock__item--intensity-${intensity}`);
7094 const duration = opts.durationMs ?? 4e3;
7095 if (duration > 0) {
7096 const handle = window.setTimeout(() => {
7097 this.attentionTimers.delete(itemId);
7098 this.setAttention(itemId, null);
7099 }, duration);
7100 this.attentionTimers.set(itemId, handle);
7101 }
7102 }
7103 /**
7104 * Resolve a tile element by id — checks menu items first
7105 * (`data-menu-slug`), then system items (`data-system-id`). Used
7106 * by `setBadge` / `setAttention` so callers can reach either rail
7107 * with one id surface.
7108 */
7109 _resolveTileElement(itemId) {
7110 return this.itemElements.get(itemId) ?? this.systemItemElements.get(itemId) ?? null;
7111 }
7112 /**
7113 * Append a JS-registered system item to the dock.
7114 *
7115 * System items render after the menu-derived items, separated by a
7116 * hairline divider. Use for shell affordances that don't live in
7117 * the admin menu: OS Settings today, Jorvy and desktop widgets
7118 * later. Callers supply their own `onOpen` — the dock doesn't
7119 * assume the item opens a window at all.
7120 */
7121 appendSystemItem(item) {
7122 this.systemItems.push(item);
7123 if (!this.systemSeparator) {
7124 this.systemSeparator = document.createElement("div");
7125 this.systemSeparator.className = "desktop-mode-dock__separator";
7126 this.systemSeparator.setAttribute("aria-hidden", "true");
7127 this.systemHost.appendChild(this.systemSeparator);
7128 }
7129 const tile2 = this.createSystemItemButton(item);
7130 this.systemItemElements.set(item.id, tile2);
7131 this.systemHost.appendChild(tile2);
7132 this.updateActiveStates();
7133 doAction(HOOKS.DOCK_TILE_RENDERED, {
7134 ...this.buildHookContextBase(),
7135 item,
7136 isSystem: true,
7137 el: tile2
7138 });
7139 }
7140 /**
7141 * Render the dock contents.
7142 *
7143 * Items are ordered server-side with core WordPress menus first and
7144 * plugin-contributed menus after. We insert a `--group` separator
7145 * at the first core→plugin transition so the two clusters read as
7146 * distinct groups of tiles — "default apps" and "installed apps"
7147 * in macOS-dock parlance. The separator is skipped when the menu
7148 * contains only one kind (no plugin menus, or a theme's filter
7149 * reordered everything into one class).
7150 */
7151 render() {
7152 if (_Dock.activeDragReset) {
7153 const prev = _Dock.activeDragReset;
7154 _Dock.activeDragReset = null;
7155 prev();
7156 }
7157 for (const teardown of this.peekTeardowns.values()) {
7158 teardown();
7159 }
7160 this.peekTeardowns.clear();
7161 this.itemHost.innerHTML = "";
7162 const base = this.buildHookContextBase();
7163 doAction(HOOKS.DOCK_BEFORE_RENDER, {
7164 ...base,
7165 items: this.items,
7166 tileElements: this.itemElements
7167 });
7168 let insertedGroupSeparator = false;
7169 for (const item of this.items) {
7170 if (!insertedGroupSeparator && item.isCore === false) {
7171 if (this.itemHost.childElementCount > 0) {
7172 const sep = document.createElement("div");
7173 sep.className = "desktop-mode-dock__separator desktop-mode-dock__separator--group";
7174 sep.setAttribute("aria-hidden", "true");
7175 this.itemHost.appendChild(sep);
7176 }
7177 insertedGroupSeparator = true;
7178 }
7179 const btn = this.createItemButton(item);
7180 this.itemElements.set(item.id, btn);
7181 this.itemHost.appendChild(btn);
7182 doAction(HOOKS.DOCK_TILE_RENDERED, {
7183 ...base,
7184 item,
7185 isSystem: false,
7186 el: btn
7187 });
7188 }
7189 doAction(HOOKS.DOCK_AFTER_RENDER, {
7190 ...base,
7191 items: this.items,
7192 tileElements: this.itemElements
7193 });
7194 }
7195 /**
7196 * Create a tile for a JS-registered system item. Structurally simpler
7197 * than a menu tile — no submenu, no multi-instance rail, no badge —
7198 * but uses the same base classes so the hover / focus / active
7199 * styling is shared.
7200 */
7201 createSystemItemButton(item) {
7202 const ctx = {
7203 ...this.buildHookContextBase(),
7204 item,
7205 isSystem: true
7206 };
7207 const tile2 = document.createElement("div");
7208 const baseClasses = [
7209 "desktop-mode-dock__item",
7210 "desktop-mode-dock__item--system"
7211 ];
7212 const filteredClasses = applyFilters(
7213 HOOKS.DOCK_TILE_CLASS,
7214 baseClasses,
7215 ctx
7216 );
7217 tile2.className = filteredClasses.join(" ");
7218 tile2.dataset.systemId = item.id;
7219 const primary = document.createElement("button");
7220 primary.className = "desktop-mode-dock__item-primary";
7221 primary.setAttribute("type", "button");
7222 primary.setAttribute("aria-label", item.title);
7223 primary.appendChild(this.resolveIcon(item.icon, item.title));
7224 primary.addEventListener("click", () => item.onOpen());
7225 tile2.appendChild(primary);
7226 this.bindTooltipFiltered(tile2, item.title, ctx);
7227 const teardown = attachDockPeek({
7228 tile: tile2,
7229 item: {
7230 id: item.id,
7231 title: item.title,
7232 icon: item.icon,
7233 url: ""
7234 },
7235 // System tiles target a single native-window id; that id
7236 // is also the baseId the manager stores duplicates under
7237 // when the user opens additional instances via the Ghost
7238 // Card. `getAllByBaseId` returns `[]` / `[one]` for the
7239 // singleton cases and the full set when a multi-capable
7240 // system tile (`multi: true`) has been duplicated.
7241 getInstances: () => this.windowManager.getAllByBaseId(item.id),
7242 enableGhost: !!item.multi,
7243 windowManager: this.windowManager,
7244 getOrientation: () => this.orientation,
7245 openNew: () => {
7246 const fn = item.onOpenNew ?? item.onOpen;
7247 fn();
7248 },
7249 suppressTooltip: (on) => {
7250 if (on) {
7251 this.tooltip.classList.remove(
7252 "desktop-mode-dock__tooltip--visible"
7253 );
7254 }
7255 }
7256 });
7257 this.peekTeardowns.set(`system:${item.id}`, teardown);
7258 return applyFilters(
7259 HOOKS.DOCK_TILE_ELEMENT,
7260 tile2,
7261 ctx
7262 );
7263 }
7264 /**
7265 * Create a single dock icon tile.
7266 *
7267 * A tile is a vertical stack: the primary icon button, plus — for
7268 * multi-capable pages — an instance rail rendered below it showing one
7269 * dot per open window and a trailing "+" to open another. The rail is
7270 * hydrated by {@link updateActiveStates}; here we only place the empty
7271 * container so the DOM is stable.
7272 */
7273 createItemButton(item) {
7274 const ctx = {
7275 ...this.buildHookContextBase(),
7276 item,
7277 isSystem: false
7278 };
7279 const tile2 = document.createElement("div");
7280 const baseClasses = ["desktop-mode-dock__item"];
7281 if (item.multi) {
7282 baseClasses.push("desktop-mode-dock__item--multi");
7283 }
7284 const filteredClasses = applyFilters(
7285 HOOKS.DOCK_TILE_CLASS,
7286 baseClasses,
7287 ctx
7288 );
7289 tile2.className = filteredClasses.join(" ");
7290 tile2.dataset.menuSlug = item.id;
7291 const primary = document.createElement("button");
7292 primary.className = "desktop-mode-dock__item-primary";
7293 primary.setAttribute("type", "button");
7294 primary.setAttribute("aria-label", item.title);
7295 const iconEl = this.resolveIcon(item.icon, item.title, item.url);
7296 primary.appendChild(iconEl);
7297 if (item.badge > 0) {
7298 const displayCount = item.badge > 99 ? "99+" : String(item.badge);
7299 const badge = document.createElement("span");
7300 badge.className = "desktop-mode-dock__badge";
7301 badge.textContent = displayCount;
7302 badge.setAttribute(
7303 "aria-label",
7304 sprintf(
7305 // translators: %d is the number of pending updates / items.
7306 _n("%d update", "%d updates", item.badge),
7307 item.badge
7308 )
7309 );
7310 primary.appendChild(badge);
7311 }
7312 primary.addEventListener("click", () => {
7313 this.openPage(item);
7314 });
7315 tile2.addEventListener("contextmenu", (ev) => {
7316 ev.preventDefault();
7317 openItemVisibilityMenu({
7318 x: ev.clientX,
7319 y: ev.clientY,
7320 id: item.id,
7321 title: item.title,
7322 surface: "dock",
7323 pluginFile: item.pluginFile ?? null,
7324 pluginName: item.pluginName ?? null
7325 });
7326 });
7327 tile2.appendChild(primary);
7328 this.bindTooltipFiltered(tile2, item.title, ctx);
7329 const baseId = this.resolveItemBaseId(item);
7330 const teardown = attachDockPeek({
7331 tile: tile2,
7332 item: {
7333 id: item.id,
7334 title: item.title,
7335 icon: item.icon,
7336 url: item.url
7337 },
7338 // Source instances from `getAllByBaseId` regardless of
7339 // `item.multi`. The Ghost Card spawns duplicates on every
7340 // tile (the `enableGhost: true` below), so any tile —
7341 // including ones synthesized from a desktop icon, where
7342 // `multi` is never set — can end up with >1 open instance.
7343 // A `multi`-gated singleton lookup would only return the
7344 // first window and the peek would silently underreport.
7345 // For genuine singletons that never get duplicated, the
7346 // returned array is just `[one]` (or `[]`), same shape the
7347 // old branch produced.
7348 getInstances: () => this.windowManager.getAllByBaseId(baseId),
7349 // Ghost Card on EVERY tile, regardless of `multi`. The
7350 // affordance reads consistently across the dock — every
7351 // hover-peek surfaces a "+ open another <Page>" card. For
7352 // multi-capable items, clicking it spawns a fresh
7353 // instance. For singletons it falls through to the same
7354 // open-or-focus path the tile click takes — usually a
7355 // no-op (focuses the existing window) but cheap and
7356 // visually consistent.
7357 enableGhost: true,
7358 windowManager: this.windowManager,
7359 getOrientation: () => this.orientation,
7360 openNew: () => this.openNewInstance(item),
7361 suppressTooltip: (on) => {
7362 if (on) {
7363 this.tooltip.classList.remove(
7364 "desktop-mode-dock__tooltip--visible"
7365 );
7366 }
7367 }
7368 });
7369 this.peekTeardowns.set(item.id, teardown);
7370 this.attachDragReorder(tile2, item.id);
7371 return applyFilters(
7372 HOOKS.DOCK_TILE_ELEMENT,
7373 tile2,
7374 ctx
7375 );
7376 }
7377 /**
7378 * Drag-to-reorder for menu tiles. Fixed slots — no interpolated
7379 * positioning. While dragging:
7380 *
7381 * 1. Pointer down on the primary button starts a tentative drag.
7382 * Click handling is preserved by requiring movement past a
7383 * small threshold before we claim the gesture.
7384 * 2. Once claimed, the tile gets a `--dragging` modifier so CSS
7385 * can lift it visually. Every `pointermove` checks which other
7386 * menu tile the cursor is currently over; if it's a different
7387 * tile, we splice the dragged tile in front of it (so adjacent
7388 * tiles slide into the vacated slot).
7389 * 3. On `pointerup` we read the resulting DOM order, persist the
7390 * new id list to `dockOrder` via the public settings writer,
7391 * and the layout-dispatcher subscriber re-applies. Cancellation
7392 * (Escape, pointercancel) reverts to the original order.
7393 *
7394 * @since 0.25.0
7395 */
7396 attachDragReorder(tile2, itemId) {
7397 const THRESHOLD = 5;
7398 const FLIP_MS = 200;
7399 let active2 = false;
7400 let startX = 0;
7401 let startY = 0;
7402 let originalOrder = [];
7403 let originalNext = null;
7404 let pointerId = -1;
7405 let originRect = null;
7406 let justDragged = false;
7407 const hardReset = () => {
7408 active2 = false;
7409 tile2.classList.remove("desktop-mode-dock__item--dragging");
7410 tile2.style.transform = "";
7411 tile2.style.transition = "";
7412 document.removeEventListener("pointermove", onMove);
7413 document.removeEventListener("pointerup", onUp);
7414 document.removeEventListener("pointercancel", onCancel);
7415 document.removeEventListener("keydown", onKey, true);
7416 window.removeEventListener("blur", onBlur);
7417 document.removeEventListener("visibilitychange", onVisibility);
7418 pointerId = -1;
7419 originRect = null;
7420 };
7421 const isMenuTile = (el) => {
7422 return !!el && el instanceof HTMLElement && el.classList.contains("desktop-mode-dock__item") && !el.classList.contains("desktop-mode-dock__item--system") && !!el.dataset.menuSlug;
7423 };
7424 const eachSiblingTile = (fn) => {
7425 for (const child of Array.from(this.itemHost.children)) {
7426 if (child instanceof HTMLElement && child !== tile2 && isMenuTile(child)) {
7427 fn(child);
7428 }
7429 }
7430 };
7431 const snapshotMenuOrder = () => {
7432 const ids = [];
7433 for (const child of Array.from(this.itemHost.children)) {
7434 if (isMenuTile(child)) {
7435 ids.push(child.dataset.menuSlug);
7436 }
7437 }
7438 return ids;
7439 };
7440 const flipSiblings = (prevRects) => {
7441 eachSiblingTile((sib) => {
7442 const prev = prevRects.get(sib);
7443 if (!prev) {
7444 return;
7445 }
7446 const now = sib.getBoundingClientRect();
7447 const dx = prev.left - now.left;
7448 const dy = prev.top - now.top;
7449 if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) {
7450 return;
7451 }
7452 sib.style.transition = "none";
7453 sib.style.transform = `translate(${dx}px, ${dy}px)`;
7454 void sib.offsetHeight;
7455 sib.style.transition = `transform ${FLIP_MS}ms cubic-bezier(0.2, 0.7, 0.3, 1)`;
7456 sib.style.transform = "";
7457 const onEnd = () => {
7458 sib.style.transition = "";
7459 sib.style.transform = "";
7460 sib.removeEventListener("transitionend", onEnd);
7461 };
7462 sib.addEventListener("transitionend", onEnd);
7463 });
7464 };
7465 const onMove = (ev) => {
7466 if (pointerId !== -1 && ev.pointerId !== pointerId) {
7467 return;
7468 }
7469 if (!active2) {
7470 const dx2 = ev.clientX - startX;
7471 const dy2 = ev.clientY - startY;
7472 if (dx2 * dx2 + dy2 * dy2 < THRESHOLD * THRESHOLD) {
7473 return;
7474 }
7475 active2 = true;
7476 originalOrder = snapshotMenuOrder();
7477 originalNext = tile2.nextSibling;
7478 originRect = tile2.getBoundingClientRect();
7479 tile2.classList.add("desktop-mode-dock__item--dragging");
7480 this.tooltip.classList.remove(
7481 "desktop-mode-dock__tooltip--visible"
7482 );
7483 }
7484 if (!originRect) {
7485 return;
7486 }
7487 const dx = ev.clientX - startX;
7488 const dy = ev.clientY - startY;
7489 tile2.style.transform = `translate(${dx}px, ${dy}px)`;
7490 const under = document.elementFromPoint(ev.clientX, ev.clientY);
7491 const targetTile = under?.closest(
7492 ".desktop-mode-dock__item"
7493 );
7494 if (!targetTile || targetTile === tile2) {
7495 return;
7496 }
7497 if (!isMenuTile(targetTile)) {
7498 return;
7499 }
7500 const rect = targetTile.getBoundingClientRect();
7501 let insertBefore;
7502 if (this.orientation === "bottom") {
7503 insertBefore = ev.clientX < rect.left + rect.width / 2;
7504 } else {
7505 insertBefore = ev.clientY < rect.top + rect.height / 2;
7506 }
7507 const prevRects = /* @__PURE__ */ new Map();
7508 eachSiblingTile((sib) => {
7509 prevRects.set(sib, sib.getBoundingClientRect());
7510 });
7511 let reordered = false;
7512 if (insertBefore) {
7513 if (targetTile !== tile2.nextSibling) {
7514 this.itemHost.insertBefore(tile2, targetTile);
7515 reordered = true;
7516 }
7517 } else if (targetTile.nextSibling !== tile2) {
7518 this.itemHost.insertBefore(tile2, targetTile.nextSibling);
7519 reordered = true;
7520 }
7521 if (reordered) {
7522 tile2.style.transform = "";
7523 const fresh = tile2.getBoundingClientRect();
7524 startX = fresh.left + fresh.width / 2;
7525 startY = fresh.top + fresh.height / 2;
7526 tile2.style.transform = `translate(${ev.clientX - startX}px, ${ev.clientY - startY}px)`;
7527 flipSiblings(prevRects);
7528 }
7529 };
7530 const cleanup = () => {
7531 tile2.classList.remove("desktop-mode-dock__item--dragging");
7532 tile2.style.transform = "";
7533 tile2.style.transition = "";
7534 document.removeEventListener("pointermove", onMove);
7535 document.removeEventListener("pointerup", onUp);
7536 document.removeEventListener("pointercancel", onCancel);
7537 document.removeEventListener("keydown", onKey, true);
7538 window.removeEventListener("blur", onBlur);
7539 document.removeEventListener("visibilitychange", onVisibility);
7540 pointerId = -1;
7541 originRect = null;
7542 active2 = false;
7543 if (_Dock.activeDragReset === hardReset) {
7544 _Dock.activeDragReset = null;
7545 }
7546 };
7547 const animateHome = () => {
7548 tile2.style.transition = `transform ${FLIP_MS}ms cubic-bezier(0.2, 0.7, 0.3, 1)`;
7549 tile2.style.transform = "";
7550 const onEnd = () => {
7551 tile2.style.transition = "";
7552 tile2.removeEventListener("transitionend", onEnd);
7553 };
7554 tile2.addEventListener("transitionend", onEnd);
7555 };
7556 const persistDockOrder = (finalOrder) => {
7557 const api = window.wp?.desktop;
7558 if (!api?.getOsSettings || !api?.updateOsSettings) {
7559 return;
7560 }
7561 const existing = api.getOsSettings().dockOrder;
7562 const finalSet = new Set(finalOrder);
7563 const merged = [];
7564 let injected = false;
7565 for (const id of existing) {
7566 if (finalSet.has(id)) {
7567 if (!injected) {
7568 merged.push(...finalOrder);
7569 injected = true;
7570 }
7571 continue;
7572 }
7573 merged.push(id);
7574 }
7575 if (!injected) {
7576 merged.push(...finalOrder);
7577 }
7578 api.updateOsSettings({ dockOrder: merged });
7579 };
7580 const onUp = (ev) => {
7581 if (pointerId !== -1 && ev.pointerId !== pointerId) {
7582 return;
7583 }
7584 if (!active2) {
7585 cleanup();
7586 return;
7587 }
7588 justDragged = true;
7589 const finalOrder = snapshotMenuOrder();
7590 animateHome();
7591 cleanup();
7592 const same = finalOrder.length === originalOrder.length && finalOrder.every((id, i) => id === originalOrder[i]);
7593 if (!same) {
7594 persistDockOrder(finalOrder);
7595 }
7596 setTimeout(() => {
7597 justDragged = false;
7598 }, 200);
7599 };
7600 const onCancel = (ev) => {
7601 if (ev && pointerId !== -1 && ev.pointerId !== pointerId) {
7602 return;
7603 }
7604 if (active2 && originalNext !== void 0) {
7605 const prevRects = /* @__PURE__ */ new Map();
7606 eachSiblingTile((sib) => {
7607 prevRects.set(sib, sib.getBoundingClientRect());
7608 });
7609 this.itemHost.insertBefore(tile2, originalNext);
7610 flipSiblings(prevRects);
7611 }
7612 animateHome();
7613 cleanup();
7614 };
7615 const onKey = (ev) => {
7616 if (ev.key === "Escape") {
7617 onCancel();
7618 }
7619 };
7620 const onBlur = () => onCancel();
7621 const onVisibility = () => {
7622 if (document.visibilityState !== "visible") {
7623 onCancel();
7624 }
7625 };
7626 tile2.addEventListener("pointerdown", (ev) => {
7627 if (ev.button !== 0) {
7628 return;
7629 }
7630 if (_Dock.activeDragReset) {
7631 const prev = _Dock.activeDragReset;
7632 _Dock.activeDragReset = null;
7633 prev();
7634 }
7635 if (active2 || pointerId !== -1) {
7636 hardReset();
7637 }
7638 startX = ev.clientX;
7639 startY = ev.clientY;
7640 pointerId = ev.pointerId;
7641 active2 = false;
7642 _Dock.activeDragReset = hardReset;
7643 document.addEventListener("pointermove", onMove);
7644 document.addEventListener("pointerup", onUp);
7645 document.addEventListener("pointercancel", onCancel);
7646 document.addEventListener("keydown", onKey, true);
7647 window.addEventListener("blur", onBlur);
7648 document.addEventListener("visibilitychange", onVisibility);
7649 });
7650 tile2.addEventListener(
7651 "click",
7652 (ev) => {
7653 if (justDragged) {
7654 ev.preventDefault();
7655 ev.stopImmediatePropagation();
7656 }
7657 },
7658 true
7659 );
7660 }
7661 /**
7662 * Resolve a registered icon value into a DOM element.
7663 *
7664 * Priority: dashicons class → inline SVG data URI → image URL →
7665 * letter badge derived from the item's title. The letter fallback is
7666 * important for plugin tiles: plugin authors routinely register
7667 * top-level menus with `add_menu_page()` and omit the icon argument
7668 * (defaulting to `'div'` or empty), which would otherwise render as
7669 * an indistinguishable wall of generic wrenches. A colored letter
7670 * tile gives each plugin a stable, unique-ish visual identity with
7671 * zero plugin-side effort — the hue derives deterministically from
7672 * the title so the same plugin always gets the same color.
7673 *
7674 * @param icon The icon value from the menu entry.
7675 * @param title Human-readable title, used when falling back to a
7676 * letter badge.
7677 */
7678 resolveIcon(icon, title, url) {
7679 if (icon.startsWith("dashicons-") && icon !== "dashicons-admin-generic") {
7680 const el = document.createElement("span");
7681 el.className = `dashicons ${icon}`;
7682 el.setAttribute("aria-hidden", "true");
7683 return el;
7684 }
7685 if (icon.startsWith("data:image/svg+xml;base64,")) {
7686 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
7687 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
7688 return this._makeSvgIcon(icon);
7689 }
7690 }
7691 if (icon.startsWith("url(")) {
7692 return this._makeSvgIcon(icon);
7693 }
7694 if (icon.startsWith("http://") || icon.startsWith("https://")) {
7695 const img = document.createElement("img");
7696 img.className = "desktop-mode-dock__item-img";
7697 img.src = icon;
7698 img.alt = "";
7699 img.setAttribute("aria-hidden", "true");
7700 return img;
7701 }
7702 if (url) {
7703 const native = this._extractNativeMenuIcon(url);
7704 if (native) {
7705 return native;
7706 }
7707 }
7708 if (icon === "dashicons-admin-generic") {
7709 const el = document.createElement("span");
7710 el.className = "dashicons dashicons-admin-generic";
7711 el.setAttribute("aria-hidden", "true");
7712 return el;
7713 }
7714 return this.createLetterBadge(title);
7715 }
7716 /**
7717 * Build an SVG-background icon tile. Shared between the data-URI
7718 * branch of {@link resolveIcon} and the native-menu extractor.
7719 */
7720 _makeSvgIcon(bgValue) {
7721 const el = document.createElement("span");
7722 el.className = "desktop-mode-dock__item-svg";
7723 el.style.backgroundImage = bgValue.startsWith("url(") ? bgValue : `url("${bgValue}")`;
7724 el.style.backgroundSize = "contain";
7725 el.style.backgroundRepeat = "no-repeat";
7726 el.style.backgroundPosition = "center";
7727 el.setAttribute("aria-hidden", "true");
7728 return el;
7729 }
7730 /**
7731 * Extract a plugin's icon from the hidden `#adminmenu` that still
7732 * exists in the parent shell DOM (display:none'd by desktop.css).
7733 * Handles the three shapes plugins commonly use when the menu-page
7734 * icon_url is 'none' or 'div':
7735 *
7736 * (a) `<img src="...">` nested inside `.wp-menu-image`
7737 * (b) a dashicon class on `.wp-menu-image` itself
7738 * (c) a CSS background-image on `.wp-menu-image::before` (the
7739 * `menu-icon-XYZ` pattern Yoast, WooCommerce, Jetpack, etc. use)
7740 *
7741 * Returns null when the URL doesn't match any admin-menu entry or
7742 * none of the three shapes are detectable.
7743 */
7744 _extractNativeMenuIcon(url) {
7745 const adminMenu = document.getElementById("adminmenu");
7746 if (!adminMenu) {
7747 return null;
7748 }
7749 let target2;
7750 try {
7751 const u = new URL(url, window.location.href);
7752 const filename = u.pathname.split("/").pop() || "";
7753 target2 = filename + u.search;
7754 } catch {
7755 return null;
7756 }
7757 if (!target2) {
7758 return null;
7759 }
7760 const links = adminMenu.querySelectorAll("li.menu-top > a");
7761 let matchLi = null;
7762 for (const link of Array.from(links)) {
7763 if (link.href.endsWith(target2)) {
7764 matchLi = link.closest("li.menu-top");
7765 break;
7766 }
7767 }
7768 if (!matchLi) {
7769 return null;
7770 }
7771 const imgWrap = matchLi.querySelector(".wp-menu-image");
7772 if (!imgWrap) {
7773 return null;
7774 }
7775 const img = imgWrap.querySelector("img");
7776 if (img && img.src) {
7777 const el = document.createElement("img");
7778 el.className = "desktop-mode-dock__item-img";
7779 el.src = img.src;
7780 el.alt = "";
7781 el.setAttribute("aria-hidden", "true");
7782 return el;
7783 }
7784 const dashMatch = imgWrap.className.match(/\bdashicons-[\w-]+\b/);
7785 if (dashMatch && dashMatch[0] !== "dashicons-before") {
7786 const el = document.createElement("span");
7787 el.className = `dashicons ${dashMatch[0]}`;
7788 el.setAttribute("aria-hidden", "true");
7789 return el;
7790 }
7791 const before = window.getComputedStyle(imgWrap, "::before");
7792 const bg = before.backgroundImage;
7793 if (bg && bg !== "none" && !bg.includes('url("")')) {
7794 return this._makeSvgIcon(bg);
7795 }
7796 const bgWrap = window.getComputedStyle(imgWrap).backgroundImage;
7797 if (bgWrap && bgWrap !== "none" && !bgWrap.includes('url("")')) {
7798 return this._makeSvgIcon(bgWrap);
7799 }
7800 return null;
7801 }
7802 /**
7803 * Create a letter-badge icon — a rounded square tinted with a
7804 * deterministic hue derived from the title, displaying the first
7805 * letter of the title. Mirrors the "app icon placeholder" look
7806 * macOS uses when an app ships without artwork.
7807 *
7808 * The title always drives both the letter and the hue — same plugin,
7809 * same color across reloads. An empty title falls through to a `?`
7810 * on a neutral gray tile, but the menu builder upstream guards
7811 * against empty titles, so this is a defensive branch.
7812 */
7813 createLetterBadge(title) {
7814 const el = document.createElement("span");
7815 el.className = "desktop-mode-dock__item-letter";
7816 el.setAttribute("aria-hidden", "true");
7817 const trimmed = title.trim();
7818 const firstCodePoint = trimmed ? Array.from(trimmed)[0] : "?";
7819 el.textContent = firstCodePoint.toUpperCase();
7820 const hue = hashTitleToHue(trimmed);
7821 el.style.background = `linear-gradient(135deg, hsl(${hue} 62% 55%), hsl(${(hue + 24) % 360} 58% 42%))`;
7822 return el;
7823 }
7824 /**
7825 * Bind tooltip show/hide on hover. Tooltip anchor differs per
7826 * orientation: left dock → tile's right side, right dock → tile's
7827 * left side, bottom dock → above the tile. We set the relevant
7828 * coordinate inline each enter; the CSS takes care of the rest.
7829 */
7830 /**
7831 * Resolves the tooltip text through {@link HOOKS.DOCK_TILE_TOOLTIP}
7832 * once at bind time (so the dock doesn't re-filter on every
7833 * pointerenter) and stashes the resolved text on
7834 * `tile.dataset.dockTooltip` so the multi-instance chip can
7835 * restore it on its own pointerleave without going through the
7836 * filter again.
7837 *
7838 * Returning an empty string from the filter suppresses the
7839 * tooltip — the listener short-circuits and never adds the
7840 * `--visible` class.
7841 */
7842 bindTooltipFiltered(tile2, text, ctx) {
7843 const filtered = applyFilters(
7844 HOOKS.DOCK_TILE_TOOLTIP,
7845 text,
7846 ctx
7847 );
7848 tile2.dataset.dockTooltip = filtered;
7849 if (filtered === "") {
7850 return;
7851 }
7852 tile2.addEventListener("pointerenter", () => {
7853 this.positionTooltip(tile2, filtered);
7854 this.tooltip.classList.add("desktop-mode-dock__tooltip--visible");
7855 });
7856 tile2.addEventListener("pointerleave", () => {
7857 this.tooltip.classList.remove("desktop-mode-dock__tooltip--visible");
7858 });
7859 }
7860 /**
7861 * Write the tooltip text + anchor coordinate for `el`. Split out
7862 * because the multi-instance chip's pointerenter handler also
7863 * needs to anchor to a specific element (the chip, not the tile).
7864 */
7865 positionTooltip(el, text) {
7866 const rect = el.getBoundingClientRect();
7867 this.tooltip.textContent = text;
7868 if (this.orientation === "bottom") {
7869 this.tooltip.style.left = `${rect.left + rect.width / 2}px`;
7870 this.tooltip.style.top = `${rect.top - 14}px`;
7871 } else if (this.orientation === "right") {
7872 this.tooltip.style.top = `${rect.top + rect.height / 2 - 14}px`;
7873 this.tooltip.style.left = `${rect.left}px`;
7874 } else {
7875 this.tooltip.style.top = `${rect.top + rect.height / 2 - 14}px`;
7876 this.tooltip.style.left = `${rect.right + 8}px`;
7877 }
7878 }
7879 /**
7880 * Open an admin page in a window (or focus if already open).
7881 *
7882 * Consults the native URL-remap registry first — when an opt-in
7883 * native window has registered itself as the replacement for this
7884 * admin URL (e.g. the native Posts window for `edit.php` when the
7885 * user has flipped `nativePostsEnabled`), the click is rerouted
7886 * to that window and the iframe path is skipped. The dock item
7887 * itself is untouched: same icon, same tooltip, same position —
7888 * only the destination changes.
7889 */
7890 openPage(item) {
7891 if (item.id.startsWith("dock:")) {
7892 const iconId = item.id.slice(5);
7893 const cfg = window.desktopModeConfig;
7894 const icon = cfg?.desktopIcons?.find((i) => i.id === iconId);
7895 if (icon?.window) {
7896 const wp = window.wp?.desktop;
7897 wp?.openWindow?.(icon.window);
7898 return;
7899 }
7900 if (icon?.url) {
7901 if (tryOpenExternalUrl(icon.url)) {
7902 return;
7903 }
7904 const baseId2 = this.deriveWindowId(icon.url);
7905 this.windowManager.open({
7906 id: baseId2,
7907 baseId: baseId2,
7908 url: icon.url,
7909 parentUrl: icon.url,
7910 title: icon.title,
7911 icon: icon.icon.startsWith("dashicons-") ? icon.icon : "dashicons-admin-generic",
7912 submenu: [],
7913 multi: false
7914 });
7915 return;
7916 }
7917 return;
7918 }
7919 if (tryOpenExternalUrl(item.url)) {
7920 return;
7921 }
7922 if (tryNativeUrlRemap(item.url)) {
7923 return;
7924 }
7925 const baseId = this.deriveWindowId(item.url);
7926 this.windowManager.open({
7927 id: baseId,
7928 baseId,
7929 url: item.url,
7930 parentUrl: item.url,
7931 title: item.title,
7932 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
7933 submenu: item.submenu,
7934 multi: !!item.multi
7935 });
7936 }
7937 /**
7938 * Open a brand-new instance of a page, even if one is already
7939 * open. Invoked by the "+" ghost card in the dock peek.
7940 *
7941 * The user explicitly asked for "another window of this thing,"
7942 * so we honour the request even when {@link tryNativeUrlRemap}
7943 * would otherwise route the click into a native-window
7944 * singleton. Result: clicking + while a native Posts window is
7945 * open opens a fresh iframe of `edit.php` alongside it. Two
7946 * windows of Posts is the explicit ask — that's what + is for.
7947 */
7948 openNewInstance(item) {
7949 if (tryOpenExternalUrl(item.url)) {
7950 return;
7951 }
7952 const openNewWindow = window.wp?.desktop?.openNewWindow;
7953 if (item.windowId && !item.url) {
7954 if (openNewWindow?.(item.windowId, { source: "dock-peek" })) {
7955 return;
7956 }
7957 }
7958 const remappedId = resolveNativeUrlRemap(item.url);
7959 if (remappedId) {
7960 if (openNewWindow?.(remappedId, { source: "dock-peek" })) {
7961 return;
7962 }
7963 }
7964 const baseId = this.deriveWindowId(item.url);
7965 void this.windowManager.openNew({
7966 id: baseId,
7967 baseId,
7968 url: item.url,
7969 parentUrl: item.url,
7970 title: item.title,
7971 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
7972 submenu: item.submenu,
7973 multi: true
7974 });
7975 }
7976 /**
7977 * Derive a window ID from an admin page URL.
7978 */
7979 deriveWindowId(url) {
7980 return deriveWindowId(url, this.adminUrl);
7981 }
7982 /**
7983 * Resolve the window-manager key for a dock tile, in this order:
7984 *
7985 * 1. `item.windowId` — set by `applyDockPlacement` when the tile
7986 * is synthesized from a `desktop_mode_register_icon()` entry
7987 * whose target is a native window. Native-window ids never
7988 * pass through the URL → native-window remap layer, so we
7989 * short-circuit before touching it.
7990 * 2. {@link resolveNativeUrlRemap} on `item.url` — captures the
7991 * `nativePostsEnabled` / `nativePagesEnabled` opt-ins that
7992 * repoint a URL-based tile at a native window.
7993 * 3. {@link deriveWindowId} on `item.url` — the URL-based
7994 * fallback for ordinary admin-menu tiles.
7995 *
7996 * Shared by the hover-peek card and the active/focused-dot
7997 * indicator; the two stayed in lockstep before this method existed
7998 * by hand-rolling the same chain at each call site.
7999 */
8000 resolveItemBaseId(item) {
8001 if (item.windowId) {
8002 return item.windowId;
8003 }
8004 const remapped = resolveNativeUrlRemap(item.url);
8005 return remapped ?? this.deriveWindowId(item.url);
8006 }
8007 /**
8008 * Listen to window events to update active/focused/minimized
8009 * indicators on dock items, plus the global Show Desktop body class.
8010 *
8011 * The event detail isn't used — we just need to re-query the
8012 * window manager on every change — so the handlers take no
8013 * argument and the type cast is gone with it.
8014 *
8015 * `WINDOW_MINIMIZED` / `WINDOW_RESTORED` route through the hook bus
8016 * (no DOM CustomEvent equivalent today). Without these, minimizing
8017 * a window via Show Desktop / the title-bar minimize button left
8018 * the dock's active-dot rendering stuck on "visible window" — the
8019 * user had no cue that everything had collapsed to minimized.
8020 */
8021 bindWindowEvents() {
8022 const refresh = () => this.updateActiveStates();
8023 this.boundRefresh = refresh;
8024 document.addEventListener("desktop-mode-window-opened", refresh);
8025 document.addEventListener("desktop-mode-window-closed", refresh);
8026 document.addEventListener("desktop-mode-window-focused", refresh);
8027 window.wp?.hooks?.addAction?.(
8028 "desktop-mode.desktop.switched",
8029 this.hooksNamespace,
8030 refresh
8031 );
8032 window.wp?.hooks?.addAction?.(
8033 "desktop-mode.desktop.closed",
8034 this.hooksNamespace,
8035 refresh
8036 );
8037 window.wp?.hooks?.addAction?.(
8038 HOOKS.WINDOW_MINIMIZED,
8039 this.hooksNamespace,
8040 refresh
8041 );
8042 window.wp?.hooks?.addAction?.(
8043 HOOKS.WINDOW_RESTORED,
8044 this.hooksNamespace,
8045 refresh
8046 );
8047 }
8048 /**
8049 * Tear the dock down: detach window-lifecycle listeners, clear
8050 * pending attention timers, remove the floating tooltip from
8051 * `document.body`, and empty the container's children. Used by
8052 * the layout dispatcher when the user switches `desktopLayout`
8053 * in OS Settings — old dock(s) get destroyed and a fresh set is
8054 * constructed for the new layout.
8055 *
8056 * Idempotent: calling twice is safe.
8057 */
8058 destroy() {
8059 document.removeEventListener(
8060 "desktop-mode-window-opened",
8061 this.boundRefresh
8062 );
8063 document.removeEventListener(
8064 "desktop-mode-window-closed",
8065 this.boundRefresh
8066 );
8067 document.removeEventListener(
8068 "desktop-mode-window-focused",
8069 this.boundRefresh
8070 );
8071 window.wp?.hooks?.removeAction?.(
8072 "desktop-mode.desktop.switched",
8073 this.hooksNamespace
8074 );
8075 window.wp?.hooks?.removeAction?.(
8076 "desktop-mode.desktop.closed",
8077 this.hooksNamespace
8078 );
8079 window.wp?.hooks?.removeAction?.(
8080 HOOKS.WINDOW_MINIMIZED,
8081 this.hooksNamespace
8082 );
8083 window.wp?.hooks?.removeAction?.(
8084 HOOKS.WINDOW_RESTORED,
8085 this.hooksNamespace
8086 );
8087 for (const handle of this.attentionTimers.values()) {
8088 window.clearTimeout(handle);
8089 }
8090 this.attentionTimers.clear();
8091 for (const teardown of this.peekTeardowns.values()) {
8092 teardown();
8093 }
8094 this.peekTeardowns.clear();
8095 this.tooltip.remove();
8096 while (this.container.firstChild) {
8097 this.container.removeChild(this.container.firstChild);
8098 }
8099 this.itemElements.clear();
8100 this.systemItemElements.clear();
8101 this.systemItems = [];
8102 this.systemSeparator = null;
8103 this.container.removeAttribute("data-desktop-mode-dock-placement");
8104 }
8105 /**
8106 * Update the active/focused/minimized classes on every dock item in
8107 * response to a window lifecycle event, and toggle the global Show
8108 * Desktop body class.
8109 *
8110 * For singletons the rail is absent; "active" means "the one window
8111 * is open". For multi-capable items, active means "≥1 instance is
8112 * open" and focused means "the focused window belongs to this item".
8113 *
8114 * `--all-minimized` is layered on top of `--active` and fires only
8115 * when EVERY open instance of the tile is minimized — so a partial
8116 * minimize (one of two windows hidden) keeps the solid dot. CSS
8117 * swaps the dot for a hollow ring on minimized-only tiles so the
8118 * user can tell at a glance "I have something here, it's just
8119 * hidden right now."
8120 */
8121 updateActiveStates() {
8122 const focused = this.windowManager.getFocused();
8123 const focusedBaseId = focused ? focused.config.baseId || focused.id : null;
8124 const activeDesktopId = this.windowManager.getActiveDesktopId();
8125 const onActiveDesktop = (w) => (w.config.desktopId || activeDesktopId) === activeDesktopId;
8126 const isMinimized = (w) => w.state === "minimized";
8127 for (const item of this.items) {
8128 const tile2 = this.itemElements.get(item.id);
8129 if (!tile2) {
8130 continue;
8131 }
8132 const baseId = this.resolveItemBaseId(item);
8133 const instances = this.windowManager.getAllByBaseId(baseId).filter(onActiveDesktop);
8134 const isOpen = instances.length > 0;
8135 const allMinimized = isOpen && instances.every(isMinimized);
8136 const isFocused = focusedBaseId === baseId && !!focused && onActiveDesktop(focused) && !isMinimized(focused);
8137 tile2.classList.toggle("desktop-mode-dock__item--active", isOpen);
8138 tile2.classList.toggle("desktop-mode-dock__item--focused", isFocused);
8139 tile2.classList.toggle(
8140 "desktop-mode-dock__item--all-minimized",
8141 allMinimized
8142 );
8143 }
8144 for (const sys of this.systemItems) {
8145 const tile2 = this.systemItemElements.get(sys.id);
8146 if (!tile2) {
8147 continue;
8148 }
8149 const sysWin = this.windowManager.getById(sys.id);
8150 const isOpen = sys.isOpen ? sys.isOpen() : !!sysWin;
8151 const allMinimized = !!sysWin && isMinimized(sysWin);
8152 const isFocused = !!focused && focused.id === sys.id && !isMinimized(focused);
8153 tile2.classList.toggle("desktop-mode-dock__item--active", isOpen);
8154 tile2.classList.toggle("desktop-mode-dock__item--focused", isFocused);
8155 tile2.classList.toggle(
8156 "desktop-mode-dock__item--all-minimized",
8157 allMinimized
8158 );
8159 }
8160 this.updateShowDesktopBodyClass();
8161 }
8162 /**
8163 * Toggle `body.desktop-mode-show-desktop-active` based on whether
8164 * every live window on the active desktop is minimized. Mirrors
8165 * the heuristic inside {@link WindowManager.toggleShowDesktop} so
8166 * the visual cue tracks the actual state — set by Show Desktop
8167 * gestures, restored when any window is brought back, automatically
8168 * cleared when no windows exist.
8169 *
8170 * @internal
8171 */
8172 updateShowDesktopBodyClass() {
8173 const activeDesktopId = this.windowManager.getActiveDesktopId();
8174 const live = this.windowManager.getAll().filter(
8175 (w) => (w.config.desktopId || activeDesktopId) === activeDesktopId
8176 );
8177 const showDesktop = live.length > 0 && live.every((w) => w.state === "minimized");
8178 document.body.classList.toggle(
8179 "desktop-mode-show-desktop-active",
8180 showDesktop
8181 );
8182 }
8183 };
8184 _Dock.instanceCounter = 0;
8185 _Dock.activeDragReset = null;
8186 let Dock = _Dock;
8187 function _applyBadgeNode(host, count) {
8188 const existing = host.querySelector(
8189 ":scope > .desktop-mode-dock__badge"
8190 );
8191 if (count <= 0) {
8192 existing?.remove();
8193 return;
8194 }
8195 const display = count > 99 ? "99+" : String(count);
8196 if (existing) {
8197 if (existing.textContent !== display) {
8198 existing.textContent = display;
8199 }
8200 existing.setAttribute(
8201 "aria-label",
8202 sprintf(
8203 // translators: %d is the number of pending items in a dock badge.
8204 _n("%d notification", "%d notifications", count),
8205 count
8206 )
8207 );
8208 return;
8209 }
8210 const badge = document.createElement("span");
8211 badge.className = "desktop-mode-dock__badge";
8212 badge.textContent = display;
8213 badge.setAttribute(
8214 "aria-label",
8215 sprintf(
8216 // translators: %d is the number of pending items in a dock badge.
8217 _n("%d notification", "%d notifications", count),
8218 count
8219 )
8220 );
8221 host.appendChild(badge);
8222 }
8223 const DEFAULT_RENDERER_DOCK = Symbol.for(
8224 "desktop-mode/default-dock-rail-renderer/dock"
8225 );
8226 const defaultDockRailRenderer = {
8227 id: "default",
8228 label: "Icon strip",
8229 description: "The shipped baseline — icon tiles with badges, tooltips, multi-instance chips, and attention animations.",
8230 icon: "dashicons-menu-alt",
8231 apiVersion: 1,
8232 mount(deps2) {
8233 const dock = new Dock(
8234 deps2.container,
8235 deps2.windowManager,
8236 deps2.items,
8237 deps2.adminUrl,
8238 deps2.orientation
8239 );
8240 const controller = {
8241 [DEFAULT_RENDERER_DOCK]: dock,
8242 replaceItems: (items) => dock.replaceItems(items),
8243 appendSystemItem: (item) => dock.appendSystemItem(item),
8244 removeSystemItem: (id) => dock.removeSystemItem(id),
8245 setBadge: (itemId, count) => dock.setBadge(itemId, count),
8246 setAttention: (itemId, mode, opts) => dock.setAttention(itemId, mode, opts),
8247 setOrientation: (orientation) => dock.setOrientation(orientation),
8248 destroy: () => dock.destroy()
8249 };
8250 return controller;
8251 }
8252 };
8253 function unwrapDefaultDock(controller) {
8254 if (!controller) {
8255 return null;
8256 }
8257 const probe = controller;
8258 const dock = probe[DEFAULT_RENDERER_DOCK];
8259 return dock instanceof Dock ? dock : null;
8260 }
8261 function installDefaultDockRailRenderer() {
8262 register$1(defaultDockRailRenderer);
8263 }
8264 function customGradientCss(state2) {
8265 const { from, to, angle } = state2.customGradient;
8266 return `linear-gradient(${angle}deg, ${from}, ${to})`;
8267 }
8268 function registerCustomGradient(ctx) {
8269 register$2({
8270 id: CUSTOM_GRADIENT_ID,
8271 label: __("Custom gradient"),
8272 type: "css",
8273 preview: customGradientCss(ctx.state),
8274 resolveValue: () => customGradientCss(ctx.state)
8275 });
8276 }
8277 function registerCustomImageIfPresent(state2) {
8278 if (!state2.customImage) {
8279 unregister$2(CUSTOM_IMAGE_ID);
8280 return;
8281 }
8282 const safeUrl = encodeURI(state2.customImage.url);
8283 const value = `url("${safeUrl}") center/cover no-repeat, #1d2327`;
8284 register$2({
8285 id: CUSTOM_IMAGE_ID,
8286 label: __("Custom image"),
8287 type: "css",
8288 value,
8289 preview: value
8290 });
8291 }
8292 let _panelLoadPromise = null;
8293 function loadOsSettingsPanelBundle(scriptUrl) {
8294 if (window.desktopModeRenderOsSettingsPanel) {
8295 return Promise.resolve(window.desktopModeRenderOsSettingsPanel);
8296 }
8297 if (_panelLoadPromise) {
8298 return _panelLoadPromise;
8299 }
8300 _panelLoadPromise = new Promise((resolve2, reject) => {
8301 const existing = document.querySelector(
8302 'script[data-desktop-mode-os-settings-panel="1"]'
8303 );
8304 const finish = () => {
8305 const fn = window.desktopModeRenderOsSettingsPanel;
8306 if (!fn) {
8307 reject(
8308 new Error(
8309 "[desktop-mode] os-settings-panel bundle loaded but did not register desktopModeRenderOsSettingsPanel"
8310 )
8311 );
8312 return;
8313 }
8314 resolve2(fn);
8315 };
8316 if (existing) {
8317 if (window.desktopModeRenderOsSettingsPanel) {
8318 finish();
8319 } else {
8320 existing.addEventListener("load", finish);
8321 existing.addEventListener(
8322 "error",
8323 () => reject(new Error("failed to load os-settings-panel bundle"))
8324 );
8325 }
8326 return;
8327 }
8328 const s = document.createElement("script");
8329 s.src = scriptUrl;
8330 s.async = true;
8331 s.dataset.desktopModeOsSettingsPanel = "1";
8332 s.addEventListener("load", finish);
8333 s.addEventListener(
8334 "error",
8335 () => reject(new Error("failed to load os-settings-panel bundle"))
8336 );
8337 document.head.appendChild(s);
8338 });
8339 return _panelLoadPromise;
8340 }
8341 class OsSettings {
8342 constructor(config, layer) {
8343 this.activeEditorTeardown = null;
8344 this.tabRegistryUnsubscribe = null;
8345 this.activeTabId = null;
8346 this.osSettingsListeners = /* @__PURE__ */ new Set();
8347 this._lastRenderedBody = null;
8348 this.config = config;
8349 this.layer = layer;
8350 this.state = loadState();
8351 setLastConfirmedState(this.state);
8352 document.addEventListener(
8353 "desktop-mode-os-settings-save-lifecycle",
8354 (e) => {
8355 const detail = e.detail;
8356 if (!detail || detail.phase !== "failed" || !detail.rolledBackTo) {
8357 return;
8358 }
8359 this.state = detail.rolledBackTo;
8360 this.apply();
8361 if (this._lastRenderedBody?.isConnected) {
8362 this.renderPanel(this._lastRenderedBody);
8363 }
8364 }
8365 );
8366 registerCustomGradient(this);
8367 registerCustomImageIfPresent(this.state);
8368 }
8369 /** Project the private state into the public snapshot shape. */
8370 getOsSettingsSnapshot() {
8371 return {
8372 wallpaper: this.state.wallpaper,
8373 accent: this.state.accent,
8374 dockSize: this.state.dockSize,
8375 desktopLayout: this.state.desktopLayout,
8376 dockRailRenderer: this.state.dockRailRenderer,
8377 unfocusEffect: this.state.unfocusEffect,
8378 ai: { ...this.state.ai },
8379 nativePostsEnabled: this.state.nativePostsEnabled,
8380 nativePostsHiddenColumns: this.state.nativePostsHiddenColumns.slice(),
8381 nativePagesEnabled: this.state.nativePagesEnabled,
8382 nativeUsersEnabled: this.state.nativeUsersEnabled,
8383 nativePluginsEnabled: this.state.nativePluginsEnabled,
8384 nativeCommentsEnabled: this.state.nativeCommentsEnabled,
8385 foldersSharingEnabled: this.state.foldersSharingEnabled,
8386 itemVisibility: { ...this.state.itemVisibility },
8387 dockOrder: this.state.dockOrder.slice(),
8388 dockPromotedPositions: Object.fromEntries(
8389 Object.entries(this.state.dockPromotedPositions).map(
8390 ([k, v]) => [k, { ...v }]
8391 )
8392 )
8393 };
8394 }
8395 subscribeOsSettings(cb) {
8396 this.osSettingsListeners.add(cb);
8397 return () => {
8398 this.osSettingsListeners.delete(cb);
8399 };
8400 }
8401 /**
8402 * Apply the current state: wallpaper via the layer, accent + dock
8403 * size as CSS custom properties on the shell.
8404 *
8405 * Safe to call repeatedly — calls into `layer.apply` dedupe via
8406 * generation counter; CSS property writes are idempotent.
8407 */
8408 apply() {
8409 const shell = document.getElementById("desktop-mode-shell");
8410 if (!shell) {
8411 return;
8412 }
8413 const def = get$1(this.state.wallpaper) || get$1(getDefaultWallpaperId()) || get$1(DEFAULT_WALLPAPER_ID) || all$1()[0];
8414 if (def) {
8415 this.layer.apply(def);
8416 }
8417 const accents = getAccents();
8418 const accent = accents.find((a) => a.id === this.state.accent) ?? accents[0];
8419 const dockSize = DOCK_SIZES.find((d) => d.id === this.state.dockSize) ?? DOCK_SIZES[1];
8420 const root = document.documentElement;
8421 root.style.setProperty("--wp-admin-theme-color", accent.value);
8422 root.style.setProperty("--desktop-mode-dock-width", `${dockSize.width}px`);
8423 root.style.setProperty("--desktop-mode-dock-icon-size", `${dockSize.icon}px`);
8424 shell.setAttribute(
8425 "data-desktop-mode-layout",
8426 this.state.desktopLayout
8427 );
8428 setActiveRenderer(this.state.dockRailRenderer);
8429 }
8430 save(opts = {}) {
8431 saveState(this.state, opts);
8432 if (this.osSettingsListeners.size > 0) {
8433 const snapshot = this.getOsSettingsSnapshot();
8434 const listeners2 = Array.from(this.osSettingsListeners);
8435 for (const cb of listeners2) {
8436 try {
8437 cb(snapshot);
8438 } catch (err) {
8439 if (typeof console !== "undefined") {
8440 console.error(
8441 "[desktop-mode] os-settings listener threw:",
8442 err
8443 );
8444 }
8445 }
8446 }
8447 }
8448 }
8449 /**
8450 * Render the settings panel into the given native-window body.
8451 *
8452 * Builds three sections (wallpaper, accent, dock size) and wires
8453 * each to save/apply on change. The panel is a one-shot build per
8454 * window open — closing and re-opening renders a fresh tree.
8455 */
8456 /**
8457 * Render the settings panel into the given native-window body.
8458 *
8459 * Lazy since 0.8.4 — the actual rendering logic plus every
8460 * `<wpd-*>` component the panel uses lives in
8461 * `src/settings/panel.ts`, compiled into its own Vite target
8462 * `os-settings-panel[.min].js`. The script is injected on the
8463 * first call below and the matching
8464 * `window.desktopModeRenderOsSettingsPanel( ctx, body )` global
8465 * is then invoked. Subsequent calls (registry-driven re-render,
8466 * save-failure rollback) skip the load and forward immediately.
8467 *
8468 * Why this is a `<script>`-injected sibling bundle rather than
8469 * an in-bundle dynamic import: Vite IIFE lib mode inlines
8470 * `import()` calls, so an in-bundle lazy import would give zero
8471 * byte savings. A separate Vite target is the only mechanism
8472 * that actually shrinks `desktop.min.js`. See the Stage 8
8473 * section of `BUNDLE-SIZE-REPORT.md` for the full picture.
8474 */
8475 /**
8476 * Switch the active settings tab. Records the choice on
8477 * {@link activeTabId} (so the next render mounts on it) and, when
8478 * the panel is currently mounted, flips the live `<wpd-tabs>` value
8479 * in place so an already-open OS Settings window jumps to the tab
8480 * without a full re-render. Deep-linking entry points
8481 * (`openOsSettings({ tabId })`) call this after opening the window.
8482 *
8483 * @param tabId Settings tab id, e.g. `'ai'`, `'apps-icons'`.
8484 */
8485 focusTab(tabId) {
8486 this.activeTabId = tabId;
8487 const body = this._lastRenderedBody;
8488 if (!body?.isConnected) {
8489 return;
8490 }
8491 const tabs = body.querySelector("wpd-tabs");
8492 if (tabs) {
8493 tabs.value = tabId;
8494 }
8495 }
8496 renderPanel(body) {
8497 this._lastRenderedBody = body;
8498 const fn = window.desktopModeRenderOsSettingsPanel;
8499 if (fn) {
8500 fn(this, body);
8501 return;
8502 }
8503 void loadOsSettingsPanelBundle(
8504 this.config.osSettingsPanelBundleUrl ?? ""
8505 ).then((render2) => {
8506 if (!body.isConnected) {
8507 return;
8508 }
8509 render2(this, body);
8510 }).catch((err) => {
8511 if (typeof console !== "undefined") {
8512 console.error(
8513 "[desktop-mode] OS Settings panel failed to load:",
8514 err
8515 );
8516 }
8517 });
8518 }
8519 }
8520 const EXIT_DESKTOP_MODE_TILE_ID = "desktop-mode-exit";
8521 function getExitDesktopModeTileDef() {
8522 return {
8523 id: EXIT_DESKTOP_MODE_TILE_ID,
8524 title: __("Exit Desktop Mode"),
8525 // `dashicons-exit` (door with arrow) is the clearest "leave"
8526 // glyph in the WordPress set, distinct from `dashicons-desktop`
8527 // used by OS Settings.
8528 icon: "dashicons-exit",
8529 onOpen: () => {
8530 void exitDesktopMode();
8531 }
8532 };
8533 }
8534 async function exitDesktopMode() {
8535 const cfg = window.desktopModeAdminBar;
8536 const fallback = cfg?.classicUrl || "/wp-admin/";
8537 if (!cfg?.ajaxUrl || !cfg?.nonce) {
8538 navigateTop(fallback);
8539 return;
8540 }
8541 const body = new URLSearchParams();
8542 body.set("action", "save-desktop-mode");
8543 body.set("nonce", cfg.nonce);
8544 body.set("enabled", "");
8545 let target2 = fallback;
8546 try {
8547 const res = await fetch(cfg.ajaxUrl, {
8548 method: "POST",
8549 headers: {
8550 "Content-Type": "application/x-www-form-urlencoded"
8551 },
8552 body: body.toString(),
8553 credentials: "same-origin"
8554 });
8555 if (res.ok) {
8556 const json = await res.json();
8557 if (json?.success && json.data?.redirect) {
8558 target2 = json.data.redirect;
8559 }
8560 }
8561 } catch {
8562 }
8563 navigateTop(target2);
8564 }
8565 function navigateTop(url) {
8566 try {
8567 window.top.location.href = url;
8568 } catch {
8569 window.location.href = url;
8570 }
8571 }
8572 const _initial$1 = {
8573 userId: null,
8574 requestedAt: 0,
8575 tabRequested: false
8576 };
8577 let _store$2 = null;
8578 function getStore$1() {
8579 if (_store$2) {
8580 return _store$2;
8581 }
8582 const w = window;
8583 const factory = w.wp?.desktop?.createSharedStore;
8584 if (typeof factory !== "function") {
8585 return null;
8586 }
8587 _store$2 = factory(
8588 "desktop-mode/user-edit/target",
8589 () => ({ ..._initial$1 })
8590 );
8591 return _store$2;
8592 }
8593 function setUserEditTarget(userId) {
8594 const store2 = getStore$1();
8595 if (store2) {
8596 store2.state.userId = userId;
8597 store2.state.requestedAt = Date.now();
8598 store2.state.tabRequested = true;
8599 store2.notify();
8600 return;
8601 }
8602 const w = window;
8603 w._wpdUserEditTarget = {
8604 userId,
8605 requestedAt: Date.now(),
8606 tabRequested: true
8607 };
8608 }
8609 const pending = /* @__PURE__ */ new Map();
8610 function loadVendorScript(url, extras) {
8611 const existing = pending.get(url);
8612 if (existing) {
8613 return existing;
8614 }
8615 const promise = new Promise((resolve2, reject) => {
8616 const selector = `script[data-desktop-mode-vendor="${cssEscape(url)}"]`;
8617 const preexisting = document.querySelector(selector);
8618 if (preexisting) {
8619 if (preexisting.dataset.loaded === "1") {
8620 resolve2();
8621 return;
8622 }
8623 preexisting.addEventListener("load", () => resolve2(), { once: true });
8624 preexisting.addEventListener(
8625 "error",
8626 () => reject(new Error(`Failed to load ${url}`)),
8627 { once: true }
8628 );
8629 return;
8630 }
8631 if (extras?.translations) {
8632 injectInline(extras.translations);
8633 }
8634 for (const code of extras?.l10n ?? []) {
8635 injectInline(code);
8636 }
8637 for (const code of extras?.before ?? []) {
8638 injectInline(code);
8639 }
8640 const script = document.createElement("script");
8641 script.src = url;
8642 script.async = true;
8643 script.dataset.desktopModeVendor = url;
8644 script.addEventListener(
8645 "load",
8646 () => {
8647 script.dataset.loaded = "1";
8648 for (const code of extras?.after ?? []) {
8649 injectInline(code);
8650 }
8651 resolve2();
8652 },
8653 { once: true }
8654 );
8655 script.addEventListener(
8656 "error",
8657 () => {
8658 pending.delete(url);
8659 script.remove();
8660 reject(new Error(`Failed to load ${url}`));
8661 },
8662 { once: true }
8663 );
8664 document.head.appendChild(script);
8665 });
8666 pending.set(url, promise);
8667 return promise;
8668 }
8669 function injectInline(code) {
8670 if (!code) {
8671 return;
8672 }
8673 const tag = document.createElement("script");
8674 tag.textContent = code;
8675 tag.dataset.desktopModeVendorInline = "1";
8676 document.head.appendChild(tag);
8677 }
8678 function cssEscape(value) {
8679 if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
8680 return CSS.escape(value);
8681 }
8682 return value.replace(/["\\]/g, "\\$&");
8683 }
8684 const registry$8 = /* @__PURE__ */ new Map();
8685 function registerModule(def) {
8686 if (!def || typeof def.id !== "string" || def.id === "") {
8687 if (typeof console !== "undefined") {
8688 console.warn("[desktop-mode] Ignored invalid module registration:", def);
8689 }
8690 return;
8691 }
8692 if (typeof def.url !== "string" || def.url === "") {
8693 if (typeof console !== "undefined") {
8694 console.warn(
8695 `[desktop-mode] Module "${def.id}" has no url; ignored.`
8696 );
8697 }
8698 return;
8699 }
8700 registry$8.set(def.id, def);
8701 }
8702 function moduleIds() {
8703 return Array.from(registry$8.keys());
8704 }
8705 async function loadModules(ids) {
8706 if (!ids || ids.length === 0) {
8707 return;
8708 }
8709 const unknown = ids.filter((id) => !registry$8.has(id));
8710 if (unknown.length > 0) {
8711 throw new Error(
8712 `[desktop-mode] Unknown module(s) in needs: ${unknown.map((id) => `"${id}"`).join(", ")}. Known modules: ${moduleIds().join(", ") || "(none)"}.`
8713 );
8714 }
8715 await Promise.all(
8716 ids.map((id) => {
8717 const def = registry$8.get(id);
8718 if (!def) {
8719 return Promise.resolve();
8720 }
8721 if (def.isReady && def.isReady()) {
8722 return Promise.resolve();
8723 }
8724 return loadVendorScript(def.url);
8725 })
8726 );
8727 }
8728 function createContext(id, pluginUrl) {
8729 return {
8730 id,
8731 pluginUrl,
8732 prefersReducedMotion: prefersReducedMotion(),
8733 visible: !document.hidden
8734 };
8735 }
8736 function prefersReducedMotion() {
8737 if (typeof window.matchMedia !== "function") {
8738 return false;
8739 }
8740 return window.matchMedia("( prefers-reduced-motion: reduce )").matches;
8741 }
8742 class WallpaperLayer {
8743 constructor(element, pluginUrl) {
8744 this.generation = 0;
8745 this.active = null;
8746 this.boundVisibilityChange = () => {
8747 if (!this.active) {
8748 return;
8749 }
8750 doAction(HOOKS.WALLPAPER_VISIBILITY, {
8751 id: this.active.id,
8752 state: document.hidden ? "hidden" : "visible"
8753 });
8754 };
8755 this.element = element;
8756 this.pluginUrl = pluginUrl;
8757 document.addEventListener("visibilitychange", this.boundVisibilityChange);
8758 }
8759 /**
8760 * Apply a wallpaper definition. Safe to call from any event
8761 * handler — handles type dispatch, teardown of the prior active
8762 * canvas, and race-safe async mounts.
8763 */
8764 apply(def) {
8765 const gen = ++this.generation;
8766 this.teardownActive();
8767 if (def.type === "css") {
8768 this.applyCss(def);
8769 return;
8770 }
8771 this.applyCanvas(def, gen);
8772 }
8773 /**
8774 * Imperative teardown entry point — called from desktop.ts on
8775 * `pagehide` so a canvas wallpaper's ticker doesn't compete with
8776 * the session-beacon flush at unload.
8777 */
8778 teardownActive() {
8779 if (!this.active) {
8780 return;
8781 }
8782 const { id, teardown } = this.active;
8783 this.active = null;
8784 doAction(HOOKS.WALLPAPER_UNMOUNTING, { id });
8785 try {
8786 teardown();
8787 } catch (err) {
8788 doAction(HOOKS.SHELL_ERROR, { scope: "wallpaper-teardown", id, error: err });
8789 if (typeof console !== "undefined") {
8790 console.error(
8791 `[desktop-mode] Wallpaper "${id}" teardown threw:`,
8792 err
8793 );
8794 }
8795 }
8796 this.element.innerHTML = "";
8797 }
8798 /** Remove listeners. Not called in normal flow — reserved for tests. */
8799 dispose() {
8800 this.teardownActive();
8801 document.removeEventListener("visibilitychange", this.boundVisibilityChange);
8802 }
8803 applyCss(def) {
8804 const value = def.resolveValue ? def.resolveValue(createContext(def.id, this.pluginUrl)) : def.value;
8805 if (typeof value === "string") {
8806 this.element.style.setProperty("--desktop-mode-bg", value);
8807 const shell = document.getElementById("desktop-mode-shell");
8808 shell?.style.setProperty("--desktop-mode-bg", value);
8809 }
8810 }
8811 applyCanvas(def, gen) {
8812 const ctx = createContext(def.id, this.pluginUrl);
8813 doAction(HOOKS.WALLPAPER_MOUNTING, { id: def.id, container: this.element, ctx });
8814 const depsReady = def.needs && def.needs.length > 0 ? loadModules(def.needs) : Promise.resolve();
8815 const onResolve = (teardown) => {
8816 if (gen !== this.generation) {
8817 try {
8818 teardown();
8819 } catch {
8820 }
8821 return;
8822 }
8823 this.active = { id: def.id, teardown };
8824 doAction(HOOKS.WALLPAPER_MOUNTED, { id: def.id, container: this.element, ctx });
8825 };
8826 depsReady.then(
8827 () => {
8828 if (gen !== this.generation) {
8829 return;
8830 }
8831 let result;
8832 try {
8833 result = def.mount(this.element, ctx);
8834 } catch (err) {
8835 this.handleMountFailure(def.id, err);
8836 return;
8837 }
8838 if (isThenable$1(result)) {
8839 result.then(onResolve, (err) => {
8840 if (gen !== this.generation) {
8841 return;
8842 }
8843 this.handleMountFailure(def.id, err);
8844 });
8845 return;
8846 }
8847 onResolve(result);
8848 },
8849 (err) => {
8850 if (gen !== this.generation) {
8851 return;
8852 }
8853 this.handleMountFailure(def.id, err);
8854 }
8855 );
8856 }
8857 handleMountFailure(id, err) {
8858 this.element.innerHTML = "";
8859 doAction(HOOKS.WALLPAPER_MOUNT_FAILED, { id, error: err });
8860 doAction(HOOKS.SHELL_ERROR, { scope: "wallpaper-mount", id, error: err });
8861 if (typeof console !== "undefined") {
8862 console.error(
8863 `[desktop-mode] Wallpaper "${id}" failed to mount:`,
8864 err
8865 );
8866 }
8867 }
8868 }
8869 function isThenable$1(value) {
8870 return !!value && typeof value === "object" && typeof value.then === "function";
8871 }
8872 function createWallpaperRegistrySync(deps2) {
8873 const { osSettings } = deps2;
8874 const registered = /* @__PURE__ */ new Set();
8875 const loadedScripts = /* @__PURE__ */ new Set();
8876 const ensureScript = async (entry) => {
8877 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
8878 return;
8879 }
8880 try {
8881 await loadVendorScript(entry.scriptUrl, {
8882 translations: entry.scriptTranslations,
8883 l10n: entry.scriptL10n,
8884 before: entry.scriptBefore,
8885 after: entry.scriptAfter
8886 });
8887 } catch (err) {
8888 doAction(HOOKS.SHELL_ERROR, {
8889 scope: "wallpaper-script-load",
8890 id: entry.id,
8891 error: err
8892 });
8893 }
8894 loadedScripts.add(entry.scriptUrl);
8895 };
8896 const readDef = (id) => {
8897 const globals = window.desktopModeWallpapers || {};
8898 return globals[id] ?? null;
8899 };
8900 const defFromCssEntry = (entry) => {
8901 if (entry.type !== "css" || entry.value === "") {
8902 return null;
8903 }
8904 return {
8905 id: entry.id,
8906 label: entry.label,
8907 type: "css",
8908 value: entry.value,
8909 preview: entry.preview !== "" ? entry.preview : entry.value
8910 };
8911 };
8912 const registerEntry = async (entry) => {
8913 if (registered.has(entry.id)) {
8914 return;
8915 }
8916 const cssDef = defFromCssEntry(entry);
8917 if (cssDef) {
8918 register$2(cssDef);
8919 registered.add(entry.id);
8920 osSettings.apply();
8921 return;
8922 }
8923 await ensureScript(entry);
8924 const def = readDef(entry.id);
8925 if (!def) {
8926 doAction(HOOKS.SHELL_ERROR, {
8927 scope: "wallpaper-missing-def",
8928 id: entry.id,
8929 error: new Error(
8930 `[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.`
8931 )
8932 });
8933 return;
8934 }
8935 try {
8936 register$2(def);
8937 } catch (err) {
8938 doAction(HOOKS.SHELL_ERROR, {
8939 scope: "wallpaper-register",
8940 id: entry.id,
8941 error: err
8942 });
8943 return;
8944 }
8945 registered.add(entry.id);
8946 osSettings.apply();
8947 };
8948 const unregisterEntry = (id) => {
8949 if (!registered.has(id)) {
8950 return;
8951 }
8952 unregister$2(id);
8953 registered.delete(id);
8954 osSettings.apply();
8955 };
8956 return async (list2) => {
8957 const incoming = /* @__PURE__ */ new Set();
8958 for (const entry of list2) {
8959 incoming.add(entry.id);
8960 }
8961 for (const id of Array.from(registered)) {
8962 if (!incoming.has(id)) {
8963 unregisterEntry(id);
8964 }
8965 }
8966 for (const entry of list2) {
8967 if (!registered.has(entry.id)) {
8968 await registerEntry(entry);
8969 }
8970 }
8971 };
8972 }
8973 const COMMAND_SLUG = /^[a-z0-9_/-]+$/;
8974 const commandRegistryStore = createSharedStore(
8975 "desktop-mode/commands-registry",
8976 () => ({
8977 registry: /* @__PURE__ */ new Map(),
8978 listeners: /* @__PURE__ */ new Set()
8979 })
8980 );
8981 const registry$7 = commandRegistryStore.state.registry;
8982 const listeners$a = commandRegistryStore.state.listeners;
8983 function registerCommand(cmd) {
8984 const errors = [];
8985 const slug = typeof cmd?.slug === "string" ? cmd.slug.trim().toLowerCase() : "";
8986 if (!cmd || typeof cmd !== "object") {
8987 errors.push("def (not an object)");
8988 } else {
8989 if (typeof cmd.slug !== "string" || cmd.slug.trim() === "") {
8990 errors.push("slug (missing)");
8991 } else if (!COMMAND_SLUG.test(slug)) {
8992 errors.push(
8993 `slug (must match ${COMMAND_SLUG} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
8994 );
8995 }
8996 if (typeof cmd.label !== "string" || cmd.label.trim() === "") {
8997 errors.push("label (missing)");
8998 }
8999 if (typeof cmd.run !== "function") {
9000 errors.push("run (must be a function)");
9001 }
9002 }
9003 throwOnRegistrationErrors("Command", errors, cmd);
9004 registry$7.set(slug, { ...cmd, slug });
9005 notify$c();
9006 }
9007 function unregisterCommand(slug) {
9008 if (registry$7.delete(slug.toLowerCase())) {
9009 notify$c();
9010 }
9011 }
9012 function unregisterByOwner(owner) {
9013 if (!owner) {
9014 return 0;
9015 }
9016 let removed = 0;
9017 for (const [slug, cmd] of Array.from(registry$7.entries())) {
9018 if (cmd.owner === owner) {
9019 registry$7.delete(slug);
9020 removed++;
9021 }
9022 }
9023 if (removed > 0) {
9024 notify$c();
9025 }
9026 return removed;
9027 }
9028 function listCommands() {
9029 return Array.from(registry$7.values());
9030 }
9031 function listAiCallableCommands() {
9032 const out = [];
9033 for (const cmd of registry$7.values()) {
9034 if (cmd.aiCallable !== true) {
9035 continue;
9036 }
9037 out.push({
9038 slug: cmd.slug,
9039 label: cmd.label,
9040 description: cmd.description ?? "",
9041 hint: cmd.hint ?? ""
9042 });
9043 }
9044 return out;
9045 }
9046 function findCommand(slug) {
9047 return registry$7.get(slug.toLowerCase()) ?? null;
9048 }
9049 function notify$c() {
9050 const snapshot = Array.from(listeners$a);
9051 for (const cb of snapshot) {
9052 try {
9053 cb();
9054 } catch (err) {
9055 if (typeof console !== "undefined") {
9056 console.error("[desktop-mode] command-registry listener threw:", err);
9057 }
9058 }
9059 }
9060 }
9061 function createCommandRegistrySync() {
9062 const loadedHandles = /* @__PURE__ */ new Set();
9063 const loadedUrls = /* @__PURE__ */ new Set();
9064 let prevSlugsByHandle = /* @__PURE__ */ new Map();
9065 const ensureScript = async (entry) => {
9066 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9067 loadedHandles.add(entry.handle);
9068 return;
9069 }
9070 try {
9071 await loadVendorScript(entry.scriptUrl, {
9072 translations: entry.scriptTranslations,
9073 l10n: entry.scriptL10n,
9074 before: entry.scriptBefore,
9075 after: entry.scriptAfter
9076 });
9077 } catch (err) {
9078 doAction(HOOKS.SHELL_ERROR, {
9079 scope: "command-script-load",
9080 handle: entry.handle,
9081 url: entry.scriptUrl,
9082 error: err
9083 });
9084 return;
9085 }
9086 loadedUrls.add(entry.scriptUrl);
9087 loadedHandles.add(entry.handle);
9088 };
9089 const slugsByHandleFrom = (commands) => {
9090 const map = /* @__PURE__ */ new Map();
9091 if (!commands) {
9092 return map;
9093 }
9094 for (const entry of commands) {
9095 if (!entry.scriptHandle || !entry.slug) {
9096 continue;
9097 }
9098 let set = map.get(entry.scriptHandle);
9099 if (!set) {
9100 set = /* @__PURE__ */ new Set();
9101 map.set(entry.scriptHandle, set);
9102 }
9103 set.add(entry.slug);
9104 }
9105 return map;
9106 };
9107 const collectSlugsToRemove = (handle) => {
9108 const slugs = /* @__PURE__ */ new Set();
9109 for (const cmd of listCommands()) {
9110 if (cmd.owner === handle) {
9111 slugs.add(cmd.slug);
9112 }
9113 }
9114 const declared = prevSlugsByHandle.get(handle);
9115 if (declared) {
9116 for (const slug of declared) {
9117 slugs.add(slug);
9118 }
9119 }
9120 return slugs;
9121 };
9122 return async (scripts, commands) => {
9123 const incomingHandles = /* @__PURE__ */ new Set();
9124 for (const entry of scripts) {
9125 if (entry.handle) {
9126 incomingHandles.add(entry.handle);
9127 }
9128 }
9129 for (const handle of Array.from(loadedHandles)) {
9130 if (incomingHandles.has(handle)) {
9131 continue;
9132 }
9133 for (const slug of collectSlugsToRemove(handle)) {
9134 unregisterCommand(slug);
9135 }
9136 loadedHandles.delete(handle);
9137 }
9138 for (const entry of scripts) {
9139 if (!entry.handle || loadedHandles.has(entry.handle)) {
9140 continue;
9141 }
9142 await ensureScript(entry);
9143 }
9144 prevSlugsByHandle = slugsByHandleFrom(commands);
9145 };
9146 }
9147 const store$b = createSharedStore(
9148 "desktop-mode/settings-tab-registry",
9149 () => ({
9150 registry: /* @__PURE__ */ new Map(),
9151 listeners: /* @__PURE__ */ new Set()
9152 })
9153 );
9154 const registry$6 = store$b.state.registry;
9155 const listeners$9 = store$b.state.listeners;
9156 function registerSettingsTab(tab) {
9157 if (!tab || typeof tab.id !== "string" || tab.id.trim() === "") {
9158 return;
9159 }
9160 if (typeof tab.label !== "string" || tab.label.trim() === "") {
9161 return;
9162 }
9163 if (typeof tab.render !== "function") {
9164 return;
9165 }
9166 const id = tab.id.trim().toLowerCase();
9167 if (!/^[a-z0-9_\-]+$/.test(id)) {
9168 if (typeof console !== "undefined") {
9169 console.warn(
9170 "[desktop-mode] registerSettingsTab: id must be [a-z0-9_-]+, got",
9171 tab.id
9172 );
9173 }
9174 return;
9175 }
9176 registry$6.set(id, { ...tab, id });
9177 notify$b();
9178 }
9179 function unregisterSettingsTab(id) {
9180 if (registry$6.delete(id.toLowerCase())) {
9181 notify$b();
9182 }
9183 }
9184 function unregisterSettingsTabsByOwner(owner) {
9185 if (!owner) {
9186 return 0;
9187 }
9188 let removed = 0;
9189 for (const [id, tab] of Array.from(registry$6.entries())) {
9190 if (tab.owner === owner) {
9191 registry$6.delete(id);
9192 removed++;
9193 }
9194 }
9195 if (removed > 0) {
9196 notify$b();
9197 }
9198 return removed;
9199 }
9200 function listSettingsTabs() {
9201 return Array.from(registry$6.values()).sort(
9202 (a, b) => (a.order ?? 100) - (b.order ?? 100)
9203 );
9204 }
9205 function notify$b() {
9206 const snapshot = Array.from(listeners$9);
9207 for (const cb of snapshot) {
9208 try {
9209 cb();
9210 } catch (err) {
9211 if (typeof console !== "undefined") {
9212 console.error(
9213 "[desktop-mode] settings-tab-registry listener threw:",
9214 err
9215 );
9216 }
9217 }
9218 }
9219 }
9220 function createSettingsTabRegistrySync() {
9221 const loadedHandles = /* @__PURE__ */ new Set();
9222 const loadedUrls = /* @__PURE__ */ new Set();
9223 let prevIdsByHandle = /* @__PURE__ */ new Map();
9224 const ensureScript = async (entry) => {
9225 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9226 loadedHandles.add(entry.handle);
9227 return;
9228 }
9229 try {
9230 await loadVendorScript(entry.scriptUrl, {
9231 translations: entry.scriptTranslations,
9232 l10n: entry.scriptL10n,
9233 before: entry.scriptBefore,
9234 after: entry.scriptAfter
9235 });
9236 } catch (err) {
9237 doAction(HOOKS.SHELL_ERROR, {
9238 scope: "settings-tab-script-load",
9239 handle: entry.handle,
9240 url: entry.scriptUrl,
9241 error: err
9242 });
9243 return;
9244 }
9245 loadedUrls.add(entry.scriptUrl);
9246 loadedHandles.add(entry.handle);
9247 };
9248 const idsByHandleFrom = (tabs) => {
9249 const map = /* @__PURE__ */ new Map();
9250 if (!tabs) {
9251 return map;
9252 }
9253 for (const entry of tabs) {
9254 if (!entry.scriptHandle || !entry.id) {
9255 continue;
9256 }
9257 let set = map.get(entry.scriptHandle);
9258 if (!set) {
9259 set = /* @__PURE__ */ new Set();
9260 map.set(entry.scriptHandle, set);
9261 }
9262 set.add(entry.id);
9263 }
9264 return map;
9265 };
9266 const removeByHandle = (handle) => {
9267 unregisterSettingsTabsByOwner(handle);
9268 const declared = prevIdsByHandle.get(handle);
9269 if (declared) {
9270 const present = new Set(
9271 listSettingsTabs().map((t) => t.id)
9272 );
9273 for (const id of declared) {
9274 if (present.has(id)) {
9275 unregisterSettingsTab(id);
9276 }
9277 }
9278 }
9279 };
9280 return async (scripts, tabs) => {
9281 const incomingHandles = /* @__PURE__ */ new Set();
9282 for (const entry of scripts) {
9283 if (entry.handle) {
9284 incomingHandles.add(entry.handle);
9285 }
9286 }
9287 for (const handle of Array.from(loadedHandles)) {
9288 if (incomingHandles.has(handle)) {
9289 continue;
9290 }
9291 removeByHandle(handle);
9292 loadedHandles.delete(handle);
9293 }
9294 for (const entry of scripts) {
9295 if (!entry.handle || loadedHandles.has(entry.handle)) {
9296 continue;
9297 }
9298 await ensureScript(entry);
9299 }
9300 prevIdsByHandle = idsByHandleFrom(tabs);
9301 };
9302 }
9303 const store$a = createSharedStore(
9304 "desktop-mode/title-bar-buttons-registry",
9305 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9306 );
9307 const registry$5 = store$a.state.registry;
9308 const listeners$8 = store$a.state.listeners;
9309 const TITLE_BAR_BUTTON_ID = /^[a-z0-9_/-]+$/;
9310 function registerTitleBarButton(def) {
9311 const errors = [];
9312 if (!def || typeof def !== "object") {
9313 errors.push("def (not an object)");
9314 } else {
9315 if (typeof def.id !== "string" || def.id.trim() === "") {
9316 errors.push("id (missing)");
9317 } else if (!TITLE_BAR_BUTTON_ID.test(def.id.trim().toLowerCase())) {
9318 errors.push(
9319 `id (must match ${TITLE_BAR_BUTTON_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9320 );
9321 }
9322 if (typeof def.label !== "string" || def.label.trim() === "") {
9323 errors.push("label (missing)");
9324 }
9325 if (typeof def.icon !== "string" || def.icon.trim() === "") {
9326 errors.push("icon (missing)");
9327 }
9328 if (typeof def.match !== "function") {
9329 errors.push("match (must be a function)");
9330 }
9331 if (typeof def.onClick !== "function" && typeof def.render !== "function") {
9332 errors.push("onClick|render (at least one must be a function)");
9333 }
9334 }
9335 throwOnRegistrationErrors("TitleBarButton", errors, def);
9336 const id = def.id.trim().toLowerCase();
9337 registry$5.set(id, { ...def, id });
9338 notify$a();
9339 }
9340 function unregisterTitleBarButton(id) {
9341 if (registry$5.delete(id.toLowerCase())) {
9342 notify$a();
9343 }
9344 }
9345 function unregisterTitleBarButtonsByOwner(owner) {
9346 if (!owner) {
9347 return 0;
9348 }
9349 let removed = 0;
9350 for (const [id, def] of Array.from(registry$5.entries())) {
9351 if (def.owner === owner) {
9352 registry$5.delete(id);
9353 removed++;
9354 }
9355 }
9356 if (removed > 0) {
9357 notify$a();
9358 }
9359 return removed;
9360 }
9361 function listTitleBarButtons() {
9362 return Array.from(registry$5.values()).sort(
9363 (a, b) => (a.order ?? 100) - (b.order ?? 100)
9364 );
9365 }
9366 function notify$a() {
9367 const snapshot = Array.from(listeners$8);
9368 for (const cb of snapshot) {
9369 try {
9370 cb();
9371 } catch (err) {
9372 if (typeof console !== "undefined") {
9373 console.error(
9374 "[desktop-mode] title-bar-button registry listener threw:",
9375 err
9376 );
9377 }
9378 }
9379 }
9380 }
9381 function createTitleBarButtonRegistrySync() {
9382 const loadedHandles = /* @__PURE__ */ new Set();
9383 const loadedUrls = /* @__PURE__ */ new Set();
9384 const ensureScript = async (entry) => {
9385 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9386 loadedHandles.add(entry.handle);
9387 return;
9388 }
9389 try {
9390 await loadVendorScript(entry.scriptUrl, {
9391 translations: entry.scriptTranslations,
9392 l10n: entry.scriptL10n,
9393 before: entry.scriptBefore,
9394 after: entry.scriptAfter
9395 });
9396 } catch (err) {
9397 doAction(HOOKS.SHELL_ERROR, {
9398 scope: "titlebar-button-script-load",
9399 handle: entry.handle,
9400 url: entry.scriptUrl,
9401 error: err
9402 });
9403 return;
9404 }
9405 loadedUrls.add(entry.scriptUrl);
9406 loadedHandles.add(entry.handle);
9407 };
9408 return async (scripts) => {
9409 const incomingHandles = /* @__PURE__ */ new Set();
9410 for (const entry of scripts) {
9411 if (entry.handle) {
9412 incomingHandles.add(entry.handle);
9413 }
9414 }
9415 for (const handle of Array.from(loadedHandles)) {
9416 if (incomingHandles.has(handle)) {
9417 continue;
9418 }
9419 unregisterTitleBarButtonsByOwner(handle);
9420 loadedHandles.delete(handle);
9421 }
9422 for (const entry of scripts) {
9423 if (!entry.handle || loadedHandles.has(entry.handle)) {
9424 continue;
9425 }
9426 await ensureScript(entry);
9427 }
9428 };
9429 }
9430 const UNFOCUS_EFFECT_NONE = "none";
9431 const store$9 = createSharedStore(
9432 "desktop-mode/unfocus-effect-registry",
9433 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9434 );
9435 const registry$4 = store$9.state.registry;
9436 const listeners$7 = store$9.state.listeners;
9437 const UNFOCUS_EFFECT_ID = /^[a-z0-9_/-]+$/;
9438 function registerUnfocusEffect(def) {
9439 const errors = [];
9440 if (!def || typeof def !== "object") {
9441 errors.push("def (not an object)");
9442 } else {
9443 if (typeof def.id !== "string" || def.id.trim() === "") {
9444 errors.push("id (missing)");
9445 } else if (!UNFOCUS_EFFECT_ID.test(def.id.trim().toLowerCase())) {
9446 errors.push(
9447 `id (must match ${UNFOCUS_EFFECT_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9448 );
9449 } else if (def.id.trim().toLowerCase() === UNFOCUS_EFFECT_NONE) {
9450 errors.push('id ("none" is reserved)');
9451 }
9452 if (typeof def.label !== "string" || def.label.trim() === "") {
9453 errors.push("label (missing)");
9454 }
9455 if (typeof def.className !== "string" && typeof def.apply !== "function") {
9456 errors.push(
9457 "className|apply (at least one must be provided — a CSS class to toggle or an apply callback)"
9458 );
9459 }
9460 }
9461 throwOnRegistrationErrors("UnfocusEffect", errors, def);
9462 const id = def.id.trim().toLowerCase();
9463 registry$4.set(id, { ...def, id });
9464 notify$9();
9465 }
9466 function unregisterUnfocusEffect(id) {
9467 if (registry$4.delete(id.toLowerCase())) {
9468 notify$9();
9469 }
9470 }
9471 function unregisterUnfocusEffectsByOwner(owner) {
9472 if (!owner) {
9473 return 0;
9474 }
9475 let removed = 0;
9476 for (const [id, def] of Array.from(registry$4.entries())) {
9477 if (def.owner === owner) {
9478 registry$4.delete(id);
9479 removed++;
9480 }
9481 }
9482 if (removed > 0) {
9483 notify$9();
9484 }
9485 return removed;
9486 }
9487 function listUnfocusEffects() {
9488 const copy = Array.from(registry$4.values());
9489 const filtered = applyFilters(
9490 HOOKS.UNFOCUS_EFFECTS,
9491 copy
9492 );
9493 if (!Array.isArray(filtered)) {
9494 if (typeof console !== "undefined") {
9495 console.warn(
9496 "[desktop-mode] `desktop-mode.unfocus-effects` filter returned a non-array; falling back to registry list."
9497 );
9498 }
9499 return copy;
9500 }
9501 return filtered;
9502 }
9503 function getUnfocusEffect(id) {
9504 return listUnfocusEffects().find((e) => e.id === id);
9505 }
9506 function subscribeUnfocusEffects(cb) {
9507 listeners$7.add(cb);
9508 return () => {
9509 listeners$7.delete(cb);
9510 };
9511 }
9512 function notify$9() {
9513 const snapshot = Array.from(listeners$7);
9514 for (const cb of snapshot) {
9515 try {
9516 cb();
9517 } catch (err) {
9518 if (typeof console !== "undefined") {
9519 console.error(
9520 "[desktop-mode] unfocus-effect registry listener threw:",
9521 err
9522 );
9523 }
9524 }
9525 }
9526 }
9527 registerUnfocusEffect({
9528 id: "darken",
9529 label: __("Darken"),
9530 description: __("Dim unfocused windows so the focused one stands out."),
9531 className: "desktop-mode-window--fx-darken"
9532 });
9533 registerUnfocusEffect({
9534 id: "frost",
9535 label: __("Frost"),
9536 description: __(
9537 "Throw unfocused windows out of focus — a soft, frosted-glass blur, as if you were looking at them through an iced-over pane."
9538 ),
9539 className: "desktop-mode-window--fx-frost"
9540 });
9541 registerUnfocusEffect({
9542 id: "grayscale",
9543 label: __("Grayscale"),
9544 description: __(
9545 "Drain the colour from unfocused windows so the focused one is the only thing still in colour — your eye snaps right to it."
9546 ),
9547 className: "desktop-mode-window--fx-grayscale"
9548 });
9549 function createUnfocusEffectRegistrySync() {
9550 const loadedHandles = /* @__PURE__ */ new Set();
9551 const loadedUrls = /* @__PURE__ */ new Set();
9552 const ensureScript = async (entry) => {
9553 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9554 loadedHandles.add(entry.handle);
9555 return;
9556 }
9557 try {
9558 await loadVendorScript(entry.scriptUrl, {
9559 translations: entry.scriptTranslations,
9560 l10n: entry.scriptL10n,
9561 before: entry.scriptBefore,
9562 after: entry.scriptAfter
9563 });
9564 } catch (err) {
9565 doAction(HOOKS.SHELL_ERROR, {
9566 scope: "unfocus-effect-script-load",
9567 handle: entry.handle,
9568 url: entry.scriptUrl,
9569 error: err
9570 });
9571 return;
9572 }
9573 loadedUrls.add(entry.scriptUrl);
9574 loadedHandles.add(entry.handle);
9575 };
9576 return async (scripts) => {
9577 const incomingHandles = /* @__PURE__ */ new Set();
9578 for (const entry of scripts) {
9579 if (entry.handle) {
9580 incomingHandles.add(entry.handle);
9581 }
9582 }
9583 for (const handle of Array.from(loadedHandles)) {
9584 if (incomingHandles.has(handle)) {
9585 continue;
9586 }
9587 unregisterUnfocusEffectsByOwner(handle);
9588 loadedHandles.delete(handle);
9589 }
9590 for (const entry of scripts) {
9591 if (!entry.handle || loadedHandles.has(entry.handle)) {
9592 continue;
9593 }
9594 await ensureScript(entry);
9595 }
9596 };
9597 }
9598 const EFFECT_ATTR = "data-desktop-unfocus-effect";
9599 const EFFECT_CLASS_ATTR = "data-desktop-unfocus-effect-class";
9600 let _started = false;
9601 function hostsCanvas(el) {
9602 return el.querySelector("canvas") !== null;
9603 }
9604 function startUnfocusEngine({ manager, osSettings }) {
9605 if (_started) {
9606 return;
9607 }
9608 _started = true;
9609 let currentId = osSettings.getOsSettingsSnapshot().unfocusEffect;
9610 const clear = (el, allEffects) => {
9611 const storedClass = el.getAttribute(EFFECT_CLASS_ATTR);
9612 if (storedClass) {
9613 el.classList.remove(storedClass);
9614 el.removeAttribute(EFFECT_CLASS_ATTR);
9615 }
9616 const priorId = el.getAttribute(EFFECT_ATTR);
9617 if (priorId) {
9618 getUnfocusEffect(priorId)?.clear?.(el);
9619 }
9620 for (const def of allEffects) {
9621 if (def.className) {
9622 el.classList.remove(def.className);
9623 }
9624 }
9625 el.removeAttribute(EFFECT_ATTR);
9626 };
9627 const apply = (el, def) => {
9628 if (def.className) {
9629 el.classList.add(def.className);
9630 el.setAttribute(EFFECT_CLASS_ATTR, def.className);
9631 }
9632 el.setAttribute(EFFECT_ATTR, def.id);
9633 def.apply?.(el);
9634 };
9635 const recompute = () => {
9636 const def = currentId === UNFOCUS_EFFECT_NONE ? void 0 : getUnfocusEffect(currentId);
9637 const allEffects = listUnfocusEffects();
9638 for (const win of manager.getAll()) {
9639 const el = win.element;
9640 if (!el) {
9641 continue;
9642 }
9643 clear(el, allEffects);
9644 if (!def || win.isFocused() || win.state === "minimized") {
9645 continue;
9646 }
9647 if (hostsCanvas(el)) {
9648 continue;
9649 }
9650 apply(el, def);
9651 }
9652 };
9653 for (const name of [
9654 "desktop-mode-window-opened",
9655 "desktop-mode-window-reopened",
9656 "desktop-mode-window-closed",
9657 "desktop-mode-window-focused",
9658 "desktop-mode-window-blurred"
9659 ]) {
9660 document.addEventListener(name, () => recompute());
9661 }
9662 osSettings.subscribeOsSettings((snapshot) => {
9663 currentId = snapshot.unfocusEffect;
9664 recompute();
9665 });
9666 subscribeUnfocusEffects(() => recompute());
9667 recompute();
9668 }
9669 function createDockRailRendererSync() {
9670 const loadedHandles = /* @__PURE__ */ new Set();
9671 const loadedUrls = /* @__PURE__ */ new Set();
9672 const ensureScript = async (entry) => {
9673 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9674 loadedHandles.add(entry.handle);
9675 return;
9676 }
9677 try {
9678 await loadVendorScript(entry.scriptUrl, {
9679 translations: entry.scriptTranslations,
9680 l10n: entry.scriptL10n,
9681 before: entry.scriptBefore,
9682 after: entry.scriptAfter
9683 });
9684 } catch (err) {
9685 doAction(HOOKS.SHELL_ERROR, {
9686 scope: "dock-rail-renderer-script-load",
9687 handle: entry.handle,
9688 url: entry.scriptUrl,
9689 error: err
9690 });
9691 return;
9692 }
9693 loadedUrls.add(entry.scriptUrl);
9694 loadedHandles.add(entry.handle);
9695 };
9696 return async (scripts) => {
9697 const incomingHandles = /* @__PURE__ */ new Set();
9698 for (const entry of scripts) {
9699 if (entry.handle) {
9700 incomingHandles.add(entry.handle);
9701 }
9702 }
9703 for (const handle of Array.from(loadedHandles)) {
9704 if (incomingHandles.has(handle)) {
9705 continue;
9706 }
9707 unregisterByOwner$1(handle);
9708 loadedHandles.delete(handle);
9709 }
9710 for (const entry of scripts) {
9711 if (!entry.handle || loadedHandles.has(entry.handle)) {
9712 continue;
9713 }
9714 await ensureScript(entry);
9715 }
9716 };
9717 }
9718 const store$8 = createSharedStore(
9719 "desktop-mode/window-themes-registry",
9720 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9721 );
9722 const registry$3 = store$8.state.registry;
9723 const listeners$6 = store$8.state.listeners;
9724 const WINDOW_THEME_ID = /^[a-z0-9_/-]+$/;
9725 function registerWindowTheme(def) {
9726 const errors = [];
9727 if (!def || typeof def !== "object") {
9728 errors.push("def (not an object)");
9729 } else {
9730 if (typeof def.id !== "string" || def.id.trim() === "") {
9731 errors.push("id (missing)");
9732 } else if (!WINDOW_THEME_ID.test(def.id.trim().toLowerCase())) {
9733 errors.push(
9734 `id (must match ${WINDOW_THEME_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9735 );
9736 }
9737 if (!def.tokens || typeof def.tokens !== "object") {
9738 errors.push("tokens (must be an object of CSS custom-property → value)");
9739 } else {
9740 for (const key of Object.keys(def.tokens)) {
9741 if (!key.startsWith("--")) {
9742 errors.push(
9743 `tokens.${key} (CSS custom-property keys must start with "--")`
9744 );
9745 break;
9746 }
9747 }
9748 }
9749 if (typeof def.match !== "function") {
9750 errors.push("match (must be a function)");
9751 }
9752 }
9753 throwOnRegistrationErrors("WindowTheme", errors, def);
9754 const id = def.id.trim().toLowerCase();
9755 registry$3.set(id, { ...def, id });
9756 notify$8();
9757 }
9758 function unregisterWindowTheme(id) {
9759 if (registry$3.delete(id.toLowerCase())) {
9760 notify$8();
9761 }
9762 }
9763 function unregisterWindowThemesByOwner(owner) {
9764 if (!owner) {
9765 return 0;
9766 }
9767 let removed = 0;
9768 for (const [id, def] of Array.from(registry$3.entries())) {
9769 if (def.owner === owner) {
9770 registry$3.delete(id);
9771 removed++;
9772 }
9773 }
9774 if (removed > 0) {
9775 notify$8();
9776 }
9777 return removed;
9778 }
9779 function listWindowThemes() {
9780 return Array.from(registry$3.values()).sort(
9781 (a, b) => (a.priority ?? 100) - (b.priority ?? 100)
9782 );
9783 }
9784 function notify$8() {
9785 const snapshot = Array.from(listeners$6);
9786 for (const cb of snapshot) {
9787 try {
9788 cb();
9789 } catch (err) {
9790 if (typeof console !== "undefined") {
9791 console.error(
9792 "[desktop-mode] window-theme registry listener threw:",
9793 err
9794 );
9795 }
9796 }
9797 }
9798 }
9799 function createWindowThemeRegistrySync() {
9800 const loadedHandles = /* @__PURE__ */ new Set();
9801 const loadedUrls = /* @__PURE__ */ new Set();
9802 let prevIdsByHandle = /* @__PURE__ */ new Map();
9803 const shellRegistered = /* @__PURE__ */ new Set();
9804 const ensureScript = async (entry) => {
9805 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9806 loadedHandles.add(entry.handle);
9807 return;
9808 }
9809 try {
9810 await loadVendorScript(entry.scriptUrl, {
9811 translations: entry.scriptTranslations,
9812 l10n: entry.scriptL10n,
9813 before: entry.scriptBefore,
9814 after: entry.scriptAfter
9815 });
9816 } catch (err) {
9817 doAction(HOOKS.SHELL_ERROR, {
9818 scope: "window-theme-script-load",
9819 handle: entry.handle,
9820 url: entry.scriptUrl,
9821 error: err
9822 });
9823 return;
9824 }
9825 loadedUrls.add(entry.scriptUrl);
9826 loadedHandles.add(entry.handle);
9827 };
9828 const idsByHandleFrom = (themes) => {
9829 const map = /* @__PURE__ */ new Map();
9830 if (!themes) {
9831 return map;
9832 }
9833 for (const entry of themes) {
9834 if (!entry.scriptHandle || !entry.id) {
9835 continue;
9836 }
9837 let set = map.get(entry.scriptHandle);
9838 if (!set) {
9839 set = /* @__PURE__ */ new Set();
9840 map.set(entry.scriptHandle, set);
9841 }
9842 set.add(entry.id);
9843 }
9844 return map;
9845 };
9846 const collectIdsToRemove = (handle) => {
9847 const ids = /* @__PURE__ */ new Set();
9848 for (const def of listWindowThemes()) {
9849 if (def.owner === handle) {
9850 ids.add(def.id);
9851 }
9852 }
9853 const declared = prevIdsByHandle.get(handle);
9854 if (declared) {
9855 for (const id of declared) {
9856 ids.add(id);
9857 }
9858 }
9859 return ids;
9860 };
9861 const applyMetadata = (themes) => {
9862 if (!themes) {
9863 return;
9864 }
9865 for (const entry of themes) {
9866 if (!entry.id || !entry.tokens) {
9867 continue;
9868 }
9869 try {
9870 registerWindowTheme({
9871 id: entry.id,
9872 label: entry.label,
9873 tokens: entry.tokens,
9874 priority: entry.priority,
9875 match: () => true,
9876 owner: entry.scriptHandle || void 0
9877 });
9878 shellRegistered.add(entry.id);
9879 } catch (err) {
9880 doAction(HOOKS.SHELL_ERROR, {
9881 scope: "window-theme-shell-register",
9882 id: entry.id,
9883 error: err
9884 });
9885 }
9886 }
9887 };
9888 return async (scripts, themes) => {
9889 const incomingHandles = /* @__PURE__ */ new Set();
9890 for (const entry of scripts) {
9891 if (entry.handle) {
9892 incomingHandles.add(entry.handle);
9893 }
9894 }
9895 for (const handle of Array.from(loadedHandles)) {
9896 if (incomingHandles.has(handle)) {
9897 continue;
9898 }
9899 const ids = collectIdsToRemove(handle);
9900 for (const id of ids) {
9901 unregisterWindowTheme(id);
9902 shellRegistered.delete(id);
9903 }
9904 unregisterWindowThemesByOwner(handle);
9905 loadedHandles.delete(handle);
9906 }
9907 applyMetadata(themes);
9908 for (const entry of scripts) {
9909 if (!entry.handle || loadedHandles.has(entry.handle)) {
9910 continue;
9911 }
9912 await ensureScript(entry);
9913 }
9914 prevIdsByHandle = idsByHandleFrom(themes);
9915 };
9916 }
9917 const store$7 = createSharedStore(
9918 "desktop-mode/window-controls-registry",
9919 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9920 );
9921 const registry$2 = store$7.state.registry;
9922 const listeners$5 = store$7.state.listeners;
9923 const WINDOW_CONTROL_ID = /^[a-z0-9_/-]+$/;
9924 function registerWindowControl(def) {
9925 const errors = [];
9926 if (!def || typeof def !== "object") {
9927 errors.push("def (not an object)");
9928 } else {
9929 if (typeof def.id !== "string" || def.id.trim() === "") {
9930 errors.push("id (missing)");
9931 } else if (!WINDOW_CONTROL_ID.test(def.id.trim().toLowerCase())) {
9932 errors.push(
9933 `id (must match ${WINDOW_CONTROL_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9934 );
9935 }
9936 if (typeof def.label !== "string" || def.label.trim() === "") {
9937 errors.push("label (missing)");
9938 }
9939 if (typeof def.onClick !== "function" && typeof def.render !== "function") {
9940 errors.push("onClick|render (at least one must be a function)");
9941 }
9942 if (typeof def.render !== "function") {
9943 if (typeof def.icon !== "string" || def.icon.trim() === "") {
9944 errors.push("icon (required when render is omitted)");
9945 }
9946 }
9947 if (typeof def.match !== "function") {
9948 errors.push("match (must be a function)");
9949 }
9950 if (def.placement !== void 0 && def.placement !== "left" && def.placement !== "right" && def.placement !== "controls") {
9951 errors.push('placement (must be "left", "right", or "controls")');
9952 }
9953 }
9954 throwOnRegistrationErrors("WindowControl", errors, def);
9955 const id = def.id.trim().toLowerCase();
9956 registry$2.set(id, { ...def, id });
9957 notify$7();
9958 }
9959 function unregisterWindowControl(id) {
9960 if (registry$2.delete(id.toLowerCase())) {
9961 notify$7();
9962 }
9963 }
9964 function unregisterWindowControlsByOwner(owner) {
9965 if (!owner) {
9966 return 0;
9967 }
9968 let removed = 0;
9969 for (const [id, def] of Array.from(registry$2.entries())) {
9970 if (def.owner === owner) {
9971 registry$2.delete(id);
9972 removed++;
9973 }
9974 }
9975 if (removed > 0) {
9976 notify$7();
9977 }
9978 return removed;
9979 }
9980 function listWindowControls() {
9981 return Array.from(registry$2.values()).sort((a, b) => {
9982 const oa = a.order ?? 100;
9983 const ob = b.order ?? 100;
9984 if (oa !== ob) {
9985 return oa - ob;
9986 }
9987 return a.id.localeCompare(b.id);
9988 });
9989 }
9990 function notify$7() {
9991 const snapshot = Array.from(listeners$5);
9992 for (const cb of snapshot) {
9993 try {
9994 cb();
9995 } catch (err) {
9996 if (typeof console !== "undefined") {
9997 console.error(
9998 "[desktop-mode] window-control registry listener threw:",
9999 err
10000 );
10001 }
10002 }
10003 }
10004 }
10005 function registerBuiltInControls() {
10006 registerWindowControl({
10007 id: "core/minimize",
10008 label: __("Minimize"),
10009 icon: "minimize",
10010 placement: "controls",
10011 order: 10,
10012 core: true,
10013 match: () => true,
10014 onClick: (win) => {
10015 win.minimize();
10016 }
10017 });
10018 registerWindowControl({
10019 id: "core/maximize",
10020 label: __("Maximize"),
10021 icon: "maximize",
10022 placement: "controls",
10023 order: 20,
10024 core: true,
10025 match: () => true,
10026 onClick: (win) => {
10027 win.toggleMaximize();
10028 }
10029 });
10030 registerWindowControl({
10031 id: "core/focus-tab",
10032 label: __("Enter fullscreen"),
10033 icon: "fullscreen",
10034 placement: "controls",
10035 order: 30,
10036 core: true,
10037 match: () => true,
10038 onClick: (win) => {
10039 win.toggleFullscreen();
10040 }
10041 });
10042 registerWindowControl({
10043 id: "core/close",
10044 label: __("Close"),
10045 icon: "close",
10046 placement: "controls",
10047 order: 50,
10048 core: true,
10049 match: () => true,
10050 onClick: (win) => {
10051 win.close();
10052 }
10053 });
10054 }
10055 function createWindowControlRegistrySync() {
10056 const loadedHandles = /* @__PURE__ */ new Set();
10057 const loadedUrls = /* @__PURE__ */ new Set();
10058 let prevIdsByHandle = /* @__PURE__ */ new Map();
10059 const ensureScript = async (entry) => {
10060 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
10061 loadedHandles.add(entry.handle);
10062 return;
10063 }
10064 try {
10065 await loadVendorScript(entry.scriptUrl, {
10066 translations: entry.scriptTranslations,
10067 l10n: entry.scriptL10n,
10068 before: entry.scriptBefore,
10069 after: entry.scriptAfter
10070 });
10071 } catch (err) {
10072 doAction(HOOKS.SHELL_ERROR, {
10073 scope: "window-control-script-load",
10074 handle: entry.handle,
10075 url: entry.scriptUrl,
10076 error: err
10077 });
10078 return;
10079 }
10080 loadedUrls.add(entry.scriptUrl);
10081 loadedHandles.add(entry.handle);
10082 };
10083 const idsByHandleFrom = (controls) => {
10084 const map = /* @__PURE__ */ new Map();
10085 if (!controls) {
10086 return map;
10087 }
10088 for (const entry of controls) {
10089 if (!entry.scriptHandle || !entry.id) {
10090 continue;
10091 }
10092 let set = map.get(entry.scriptHandle);
10093 if (!set) {
10094 set = /* @__PURE__ */ new Set();
10095 map.set(entry.scriptHandle, set);
10096 }
10097 set.add(entry.id);
10098 }
10099 return map;
10100 };
10101 const collectIdsToRemove = (handle) => {
10102 const ids = /* @__PURE__ */ new Set();
10103 for (const def of listWindowControls()) {
10104 if (def.owner === handle) {
10105 ids.add(def.id);
10106 }
10107 }
10108 const declared = prevIdsByHandle.get(handle);
10109 if (declared) {
10110 for (const id of declared) {
10111 ids.add(id);
10112 }
10113 }
10114 return ids;
10115 };
10116 return async (scripts, controls) => {
10117 const incomingHandles = /* @__PURE__ */ new Set();
10118 for (const entry of scripts) {
10119 if (entry.handle) {
10120 incomingHandles.add(entry.handle);
10121 }
10122 }
10123 for (const handle of Array.from(loadedHandles)) {
10124 if (incomingHandles.has(handle)) {
10125 continue;
10126 }
10127 for (const id of collectIdsToRemove(handle)) {
10128 unregisterWindowControl(id);
10129 }
10130 unregisterWindowControlsByOwner(handle);
10131 loadedHandles.delete(handle);
10132 }
10133 for (const entry of scripts) {
10134 if (!entry.handle || loadedHandles.has(entry.handle)) {
10135 continue;
10136 }
10137 await ensureScript(entry);
10138 }
10139 prevIdsByHandle = idsByHandleFrom(controls);
10140 };
10141 }
10142 const store$6 = createSharedStore(
10143 "desktop-mode/window-slots-registry",
10144 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
10145 );
10146 const registry$1 = store$6.state.registry;
10147 const listeners$4 = store$6.state.listeners;
10148 const WINDOW_SLOT_ID = /^[a-z0-9_/-]+$/;
10149 const KNOWN_SLOTS = /* @__PURE__ */ new Set([
10150 "before-titlebar",
10151 "before-icon",
10152 "icon",
10153 "title",
10154 "after-title",
10155 "before-controls",
10156 "controls",
10157 "after-controls",
10158 "after-titlebar"
10159 ]);
10160 function registerWindowSlot(def) {
10161 const errors = [];
10162 if (!def || typeof def !== "object") {
10163 errors.push("def (not an object)");
10164 } else {
10165 if (typeof def.id !== "string" || def.id.trim() === "") {
10166 errors.push("id (missing)");
10167 } else if (!WINDOW_SLOT_ID.test(def.id.trim().toLowerCase())) {
10168 errors.push(
10169 `id (must match ${WINDOW_SLOT_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
10170 );
10171 }
10172 if (typeof def.slot !== "string" || def.slot.trim() === "") {
10173 errors.push("slot (missing)");
10174 } else if (!KNOWN_SLOTS.has(def.slot)) {
10175 errors.push(
10176 `slot (must be one of ${Array.from(KNOWN_SLOTS).join(", ")})`
10177 );
10178 }
10179 if (typeof def.match !== "function") {
10180 errors.push("match (must be a function)");
10181 }
10182 if (typeof def.render !== "function") {
10183 errors.push("render (must be a function)");
10184 }
10185 }
10186 throwOnRegistrationErrors("WindowSlot", errors, def);
10187 const id = def.id.trim().toLowerCase();
10188 registry$1.set(id, { ...def, id });
10189 notify$6();
10190 }
10191 function unregisterWindowSlot(id) {
10192 if (registry$1.delete(id.toLowerCase())) {
10193 notify$6();
10194 }
10195 }
10196 function unregisterWindowSlotsByOwner(owner) {
10197 if (!owner) {
10198 return 0;
10199 }
10200 let removed = 0;
10201 for (const [id, def] of Array.from(registry$1.entries())) {
10202 if (def.owner === owner) {
10203 registry$1.delete(id);
10204 removed++;
10205 }
10206 }
10207 if (removed > 0) {
10208 notify$6();
10209 }
10210 return removed;
10211 }
10212 function listWindowSlots() {
10213 return Array.from(registry$1.values()).sort((a, b) => {
10214 const oa = a.order ?? 100;
10215 const ob = b.order ?? 100;
10216 if (oa !== ob) {
10217 return oa - ob;
10218 }
10219 return a.id.localeCompare(b.id);
10220 });
10221 }
10222 function notify$6() {
10223 const snapshot = Array.from(listeners$4);
10224 for (const cb of snapshot) {
10225 try {
10226 cb();
10227 } catch (err) {
10228 if (typeof console !== "undefined") {
10229 console.error(
10230 "[desktop-mode] window-slot registry listener threw:",
10231 err
10232 );
10233 }
10234 }
10235 }
10236 }
10237 function createWindowSlotRegistrySync() {
10238 const loadedHandles = /* @__PURE__ */ new Set();
10239 const loadedUrls = /* @__PURE__ */ new Set();
10240 let prevIdsByHandle = /* @__PURE__ */ new Map();
10241 const ensureScript = async (entry) => {
10242 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
10243 loadedHandles.add(entry.handle);
10244 return;
10245 }
10246 try {
10247 await loadVendorScript(entry.scriptUrl, {
10248 translations: entry.scriptTranslations,
10249 l10n: entry.scriptL10n,
10250 before: entry.scriptBefore,
10251 after: entry.scriptAfter
10252 });
10253 } catch (err) {
10254 doAction(HOOKS.SHELL_ERROR, {
10255 scope: "window-slot-script-load",
10256 handle: entry.handle,
10257 url: entry.scriptUrl,
10258 error: err
10259 });
10260 return;
10261 }
10262 loadedUrls.add(entry.scriptUrl);
10263 loadedHandles.add(entry.handle);
10264 };
10265 const idsByHandleFrom = (slots) => {
10266 const map = /* @__PURE__ */ new Map();
10267 if (!slots) {
10268 return map;
10269 }
10270 for (const entry of slots) {
10271 if (!entry.scriptHandle || !entry.id) {
10272 continue;
10273 }
10274 let set = map.get(entry.scriptHandle);
10275 if (!set) {
10276 set = /* @__PURE__ */ new Set();
10277 map.set(entry.scriptHandle, set);
10278 }
10279 set.add(entry.id);
10280 }
10281 return map;
10282 };
10283 const collectIdsToRemove = (handle) => {
10284 const ids = /* @__PURE__ */ new Set();
10285 for (const def of listWindowSlots()) {
10286 if (def.owner === handle) {
10287 ids.add(def.id);
10288 }
10289 }
10290 const declared = prevIdsByHandle.get(handle);
10291 if (declared) {
10292 for (const id of declared) {
10293 ids.add(id);
10294 }
10295 }
10296 return ids;
10297 };
10298 return async (scripts, slots) => {
10299 const incomingHandles = /* @__PURE__ */ new Set();
10300 for (const entry of scripts) {
10301 if (entry.handle) {
10302 incomingHandles.add(entry.handle);
10303 }
10304 }
10305 for (const handle of Array.from(loadedHandles)) {
10306 if (incomingHandles.has(handle)) {
10307 continue;
10308 }
10309 for (const id of collectIdsToRemove(handle)) {
10310 unregisterWindowSlot(id);
10311 }
10312 unregisterWindowSlotsByOwner(handle);
10313 loadedHandles.delete(handle);
10314 }
10315 for (const entry of scripts) {
10316 if (!entry.handle || loadedHandles.has(entry.handle)) {
10317 continue;
10318 }
10319 await ensureScript(entry);
10320 }
10321 prevIdsByHandle = idsByHandleFrom(slots);
10322 };
10323 }
10324 const KEY_PREFIX = "desktop-mode-notice-dismissed";
10325 function currentUserSuffix() {
10326 const w = window.wp;
10327 const uid = w?.desktop?.config?.currentUserId;
10328 if (typeof uid === "number" && uid > 0) {
10329 return String(uid);
10330 }
10331 return "anon";
10332 }
10333 function storageKey() {
10334 return `${KEY_PREFIX}:${currentUserSuffix()}`;
10335 }
10336 function readMap() {
10337 try {
10338 const raw = window.localStorage.getItem(storageKey());
10339 if (!raw) {
10340 return {};
10341 }
10342 const parsed = JSON.parse(raw);
10343 if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
10344 return parsed;
10345 }
10346 } catch {
10347 }
10348 return {};
10349 }
10350 function writeMap(map) {
10351 try {
10352 window.localStorage.setItem(storageKey(), JSON.stringify(map));
10353 } catch {
10354 }
10355 }
10356 function isNoticeDismissed(id) {
10357 if (!id) {
10358 return false;
10359 }
10360 return readMap()[id] === true;
10361 }
10362 function markNoticeDismissed(id) {
10363 if (!id) {
10364 return;
10365 }
10366 const map = readMap();
10367 map[id] = true;
10368 writeMap(map);
10369 }
10370 function clearNoticeDismissed(id) {
10371 if (!id) {
10372 return;
10373 }
10374 const map = readMap();
10375 if (map[id]) {
10376 delete map[id];
10377 writeMap(map);
10378 }
10379 }
10380 const styles$6 = css`:host{display:flex;align-items:flex-start;gap:10px;width:100%;box-sizing:border-box;padding:10px 14px;font:var( --wpd-notice-font,13px/1.5 var( --desktop-mode-font,system-ui ) );color:var( --wpd-notice-color,var( --desktop-mode-text,#1d2327 ) );background:var( --wpd-notice-bg,rgba( 0,0,0,0.04 ) );border-block-end:1px solid var( --wpd-notice-border,rgba( 0,0,0,0.08 ) );border-inline-start:4px solid var( --wpd-notice-accent,#646970 )}:host( [ hidden ] ){display:none}.wpd-notice__icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;color:var( --wpd-notice-accent,#646970 )}.wpd-notice__icon[ hidden ]{display:none}.wpd-notice__label{flex:1;min-width:0;word-wrap:break-word}::slotted( a ){color:var( --wpd-notice-link,var( --wp-admin-theme-color,#2271b1 ) )}::slotted( p:first-child ){margin-block-start:0}::slotted( p:last-child ){margin-block-end:0}.wpd-notice__close{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;background:transparent;color:inherit;opacity:0.6;cursor:pointer;border-radius:4px;transition:opacity 0.12s ease,background-color 0.12s ease}.wpd-notice__close:hover{opacity:1;background:rgba( 0,0,0,0.06 )}.wpd-notice__close:focus-visible{opacity:1;outline:2px solid var( --wp-admin-theme-color,#2271b1 );outline-offset:1px}.wpd-notice__close[ hidden ]{display:none}.wpd-notice__close svg{width:14px;height:14px}:host( [ tone='info' ] ){--wpd-notice-accent:var( --wpd-notice-info,#0969da );--wpd-notice-bg:var( --wpd-notice-info-bg,rgba( 9,105,218,0.08 ) );--wpd-notice-border:var( --wpd-notice-info-border,rgba( 9,105,218,0.16 ) )}:host( [ tone='success' ] ){--wpd-notice-accent:var( --wpd-notice-success,#1a7f37 );--wpd-notice-bg:var( --wpd-notice-success-bg,rgba( 26,127,55,0.08 ) );--wpd-notice-border:var( --wpd-notice-success-border,rgba( 26,127,55,0.16 ) )}:host( [ tone='warning' ] ){--wpd-notice-accent:var( --wpd-notice-warning,#9a6700 );--wpd-notice-bg:var( --wpd-notice-warning-bg,rgba( 154,103,0,0.08 ) );--wpd-notice-border:var( --wpd-notice-warning-border,rgba( 154,103,0,0.16 ) )}:host( [ tone='error' ] ),:host( [ tone='danger' ] ){--wpd-notice-accent:var( --wpd-notice-error,#cf222e );--wpd-notice-bg:var( --wpd-notice-error-bg,rgba( 207,34,46,0.08 ) );--wpd-notice-border:var( --wpd-notice-error-border,rgba( 207,34,46,0.16 ) )}:host( [ tone='neutral' ] ){--wpd-notice-accent:var( --wpd-notice-neutral,#57606a );--wpd-notice-bg:var( --wpd-notice-neutral-bg,rgba( 87,96,106,0.08 ) );--wpd-notice-border:var( --wpd-notice-neutral-border,rgba( 87,96,106,0.16 ) )}`;
10381 const _WpdNotice = class _WpdNotice extends Component {
10382 connectedCallback() {
10383 super.connectedCallback();
10384 if (!this.hasAttribute("role")) {
10385 this.setAttribute("role", "status");
10386 }
10387 if (!this.hasAttribute("tone")) {
10388 this.setAttribute("tone", "info");
10389 }
10390 const id = this.getAttribute("notice-id");
10391 if (id && isNoticeDismissed(id)) {
10392 this.hidden = true;
10393 }
10394 }
10395 /**
10396 * Imperatively dismiss the notice — hides the host and records
10397 * the dismissal in localStorage when `notice-id` is set.
10398 */
10399 dismiss() {
10400 this.hidden = true;
10401 const id = this.getAttribute("notice-id");
10402 if (id) {
10403 markNoticeDismissed(id);
10404 }
10405 this.emit("wpd-notice-dismiss", { noticeId: id ?? void 0 });
10406 }
10407 /**
10408 * Clear a previously recorded dismissal and re-show the notice.
10409 * Useful in tests and for "Show again" affordances.
10410 */
10411 undismiss() {
10412 const id = this.getAttribute("notice-id");
10413 if (id) {
10414 clearNoticeDismissed(id);
10415 }
10416 this.hidden = false;
10417 }
10418 render() {
10419 const icon = this.getAttribute("icon");
10420 const dismissible = !this.hasAttribute("not-dismissible");
10421 return html`
10422 <span
10423 class="wpd-notice__icon dashicons ${icon ?? ""}"
10424 ?hidden=${!icon}
10425 aria-hidden="true"
10426 ></span>
10427 <span class="wpd-notice__label"><slot></slot></span>
10428 <button
10429 type="button"
10430 class="wpd-notice__close"
10431 ?hidden=${!dismissible}
10432 aria-label=${__("Dismiss notice")}
10433 @click=${(e) => this._onDismiss(e)}
10434 >
10435 <svg viewBox="0 0 14 14" aria-hidden="true">
10436 <path
10437 d="M3 3 L11 11 M11 3 L3 11"
10438 stroke="currentColor"
10439 stroke-width="1.6"
10440 stroke-linecap="round"
10441 fill="none"
10442 ></path>
10443 </svg>
10444 </button>
10445 `;
10446 }
10447 _onDismiss(e) {
10448 e.preventDefault();
10449 e.stopPropagation();
10450 this.dismiss();
10451 }
10452 };
10453 _WpdNotice.props = ["tone", "notDismissible", "icon", "noticeId"];
10454 _WpdNotice.styles = [styles$6];
10455 _WpdNotice.help = {
10456 title: "Notice",
10457 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.",
10458 status: "experimental",
10459 since: "0.22.0",
10460 props: [
10461 {
10462 name: "tone",
10463 type: '"info" | "success" | "warning" | "error" | "danger" | "neutral"',
10464 description: "Color palette. Defaults to `info`. `error` and `danger` are aliases."
10465 },
10466 {
10467 name: "not-dismissible",
10468 type: "boolean",
10469 description: "Suppress the trailing close button. Defaults to dismissible."
10470 },
10471 {
10472 name: "icon",
10473 type: "string",
10474 description: "Optional Dashicons class for a leading glyph (e.g. `dashicons-info`)."
10475 },
10476 {
10477 name: "notice-id",
10478 type: "string",
10479 description: "Persistence key. When set, the notice records its dismissed state in localStorage so it stays closed across reloads for the same user."
10480 }
10481 ],
10482 slots: [
10483 {
10484 name: "(default)",
10485 description: "Message HTML. Links, `<strong>`, `<em>`, and other inline formatting are allowed."
10486 }
10487 ],
10488 events: [
10489 {
10490 name: "wpd-notice-dismiss",
10491 description: "Fires after the user clicks the close button.",
10492 detail: "{ noticeId?: string }"
10493 }
10494 ],
10495 cssProps: [
10496 { name: "--wpd-notice-bg", description: "Background color." },
10497 { name: "--wpd-notice-accent", description: "Left-edge stripe + icon color." },
10498 { name: "--wpd-notice-color", description: "Text color." },
10499 { name: "--wpd-notice-border", description: "Bottom border color." },
10500 { name: "--wpd-notice-link", description: "Color for slotted <a> elements." }
10501 ],
10502 example: html`
10503 <wpd-notice tone="warning" notice-id="docs/example">
10504 Heads up — this is a demo notice.
10505 <a href="#">Learn more</a>.
10506 </wpd-notice>
10507 `
10508 };
10509 let WpdNotice = _WpdNotice;
10510 defineComponent("wpd-notice", WpdNotice);
10511 const store$5 = createSharedStore(
10512 "desktop-mode/window-notices",
10513 () => ({ entries: /* @__PURE__ */ new Map() })
10514 );
10515 const ID_PATTERN = /^[a-z0-9_/-]+$/;
10516 function slotIdFor(id) {
10517 return `desktop-mode-notice/${id.toLowerCase()}`;
10518 }
10519 function buildNoticeElement(entry) {
10520 const el = document.createElement("wpd-notice");
10521 el.setAttribute("tone", entry.tone ?? "info");
10522 el.setAttribute("notice-id", entry.id);
10523 if (entry.dismissible === false) {
10524 el.setAttribute("not-dismissible", "");
10525 }
10526 if (entry.icon) {
10527 el.setAttribute("icon", entry.icon);
10528 }
10529 el.innerHTML = entry.message;
10530 return el;
10531 }
10532 function registerWindowNotice(entry) {
10533 if (!entry || typeof entry !== "object") {
10534 return () => {
10535 };
10536 }
10537 const id = String(entry.id ?? "").trim().toLowerCase();
10538 if (!id || !ID_PATTERN.test(id)) {
10539 return () => {
10540 };
10541 }
10542 if (typeof entry.message !== "string" || entry.message === "") {
10543 return () => {
10544 };
10545 }
10546 const normalised = { ...entry, id };
10547 store$5.state.entries.set(id, normalised);
10548 const slotId = slotIdFor(id);
10549 registerWindowSlot({
10550 id: slotId,
10551 slot: "after-titlebar",
10552 order: normalised.order ?? 100,
10553 // Append rather than clear — every notice slot entry appends
10554 // its own `<wpd-notice>` so multiple notices stack.
10555 replace: false,
10556 owner: normalised.owner,
10557 match: (win) => {
10558 const def = store$5.state.entries.get(id);
10559 if (!def) {
10560 return false;
10561 }
10562 if (typeof def.match !== "function") {
10563 return true;
10564 }
10565 try {
10566 return def.match(win) === true;
10567 } catch {
10568 return false;
10569 }
10570 },
10571 render: (host) => {
10572 const def = store$5.state.entries.get(id);
10573 if (!def) {
10574 return;
10575 }
10576 host.appendChild(buildNoticeElement(def));
10577 }
10578 });
10579 return () => unregisterWindowNotice(id);
10580 }
10581 function unregisterWindowNotice(id) {
10582 const key = String(id ?? "").trim().toLowerCase();
10583 if (!key) {
10584 return;
10585 }
10586 if (store$5.state.entries.delete(key)) {
10587 unregisterWindowSlot(slotIdFor(key));
10588 }
10589 }
10590 function listWindowNotices() {
10591 return Array.from(store$5.state.entries.values()).sort((a, b) => {
10592 const oa = a.order ?? 100;
10593 const ob = b.order ?? 100;
10594 if (oa !== ob) {
10595 return oa - ob;
10596 }
10597 return a.id.localeCompare(b.id);
10598 });
10599 }
10600 function dismissWindowNotice(id) {
10601 const key = String(id ?? "").trim().toLowerCase();
10602 if (!key) {
10603 return;
10604 }
10605 markNoticeDismissed(key);
10606 }
10607 function undismissWindowNotice(id) {
10608 const key = String(id ?? "").trim().toLowerCase();
10609 if (!key) {
10610 return;
10611 }
10612 clearNoticeDismissed(key);
10613 }
10614 function buildMatcher(match) {
10615 if (!match) {
10616 return void 0;
10617 }
10618 const ids = /* @__PURE__ */ new Set();
10619 if (typeof match.window === "string" && match.window !== "") {
10620 ids.add(match.window);
10621 }
10622 if (Array.isArray(match.windows)) {
10623 for (const id of match.windows) {
10624 if (typeof id === "string" && id !== "") {
10625 ids.add(id);
10626 }
10627 }
10628 }
10629 const needle = typeof match.urlContains === "string" && match.urlContains !== "" ? match.urlContains.toLowerCase() : null;
10630 if (ids.size === 0 && needle === null) {
10631 return void 0;
10632 }
10633 return (w) => {
10634 if (ids.size > 0 && !ids.has(w.id)) {
10635 return false;
10636 }
10637 if (needle !== null) {
10638 const url = typeof w.config.url === "string" ? w.config.url.toLowerCase() : "";
10639 if (!url.includes(needle)) {
10640 return false;
10641 }
10642 }
10643 return true;
10644 };
10645 }
10646 function applyServerWindowNotices(entries) {
10647 const wanted = /* @__PURE__ */ new Set();
10648 for (const entry of entries) {
10649 if (!entry || typeof entry.id !== "string" || !entry.id) {
10650 continue;
10651 }
10652 wanted.add(entry.id.toLowerCase());
10653 registerWindowNotice({
10654 id: entry.id,
10655 message: entry.message,
10656 tone: entry.tone,
10657 dismissible: entry.dismissible !== false,
10658 icon: entry.icon,
10659 match: buildMatcher(entry.match),
10660 order: typeof entry.order === "number" ? entry.order : void 0,
10661 // `owner` tag marks every server-shipped notice so a
10662 // targeted cleanup is trivial if/when we surface a sweep
10663 // helper later. Matches the convention used by the
10664 // command / settings-tab sync modules.
10665 owner: "__server__"
10666 });
10667 }
10668 for (const existing of listWindowNotices()) {
10669 if (existing.owner !== "__server__") {
10670 continue;
10671 }
10672 if (!wanted.has(existing.id)) {
10673 unregisterWindowNotice(existing.id);
10674 }
10675 }
10676 }
10677 const store$4 = createSharedStore(
10678 "desktop-mode/window-chrome-registry",
10679 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
10680 );
10681 const registry = store$4.state.registry;
10682 const listeners$3 = store$4.state.listeners;
10683 const WINDOW_CHROME_ID = /^[a-z0-9_/-]+$/;
10684 function registerWindowChrome(def) {
10685 const errors = [];
10686 if (!def || typeof def !== "object") {
10687 errors.push("def (not an object)");
10688 } else {
10689 if (typeof def.id !== "string" || def.id.trim() === "") {
10690 errors.push("id (missing)");
10691 } else if (!WINDOW_CHROME_ID.test(def.id.trim().toLowerCase())) {
10692 errors.push(
10693 `id (must match ${WINDOW_CHROME_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
10694 );
10695 }
10696 if (typeof def.match !== "function") {
10697 errors.push("match (must be a function)");
10698 }
10699 if (typeof def.render !== "function") {
10700 errors.push("render (must be a function)");
10701 }
10702 }
10703 throwOnRegistrationErrors("WindowChrome", errors, def);
10704 const id = def.id.trim().toLowerCase();
10705 registry.set(id, { ...def, id });
10706 notify$5();
10707 }
10708 function unregisterWindowChrome(id) {
10709 if (registry.delete(id.toLowerCase())) {
10710 notify$5();
10711 }
10712 }
10713 function unregisterWindowChromesByOwner(owner) {
10714 if (!owner) {
10715 return 0;
10716 }
10717 let removed = 0;
10718 for (const [id, def] of Array.from(registry.entries())) {
10719 if (def.owner === owner) {
10720 registry.delete(id);
10721 removed++;
10722 }
10723 }
10724 if (removed > 0) {
10725 notify$5();
10726 }
10727 return removed;
10728 }
10729 function listWindowChromes() {
10730 return Array.from(registry.values()).sort(
10731 (a, b) => a.id.localeCompare(b.id)
10732 );
10733 }
10734 function notify$5() {
10735 const snapshot = Array.from(listeners$3);
10736 for (const cb of snapshot) {
10737 try {
10738 cb();
10739 } catch (err) {
10740 if (typeof console !== "undefined") {
10741 console.error(
10742 "[desktop-mode] window-chrome registry listener threw:",
10743 err
10744 );
10745 }
10746 }
10747 }
10748 }
10749 function createWindowChromeRegistrySync() {
10750 const loadedHandles = /* @__PURE__ */ new Set();
10751 const loadedUrls = /* @__PURE__ */ new Set();
10752 let prevIdsByHandle = /* @__PURE__ */ new Map();
10753 const ensureScript = async (entry) => {
10754 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
10755 loadedHandles.add(entry.handle);
10756 return;
10757 }
10758 try {
10759 await loadVendorScript(entry.scriptUrl, {
10760 translations: entry.scriptTranslations,
10761 l10n: entry.scriptL10n,
10762 before: entry.scriptBefore,
10763 after: entry.scriptAfter
10764 });
10765 } catch (err) {
10766 doAction(HOOKS.SHELL_ERROR, {
10767 scope: "window-chrome-script-load",
10768 handle: entry.handle,
10769 url: entry.scriptUrl,
10770 error: err
10771 });
10772 return;
10773 }
10774 loadedUrls.add(entry.scriptUrl);
10775 loadedHandles.add(entry.handle);
10776 };
10777 const idsByHandleFrom = (chromes) => {
10778 const map = /* @__PURE__ */ new Map();
10779 if (!chromes) {
10780 return map;
10781 }
10782 for (const entry of chromes) {
10783 if (!entry.scriptHandle || !entry.id) {
10784 continue;
10785 }
10786 let set = map.get(entry.scriptHandle);
10787 if (!set) {
10788 set = /* @__PURE__ */ new Set();
10789 map.set(entry.scriptHandle, set);
10790 }
10791 set.add(entry.id);
10792 }
10793 return map;
10794 };
10795 const collectIdsToRemove = (handle) => {
10796 const ids = /* @__PURE__ */ new Set();
10797 for (const def of listWindowChromes()) {
10798 if (def.owner === handle) {
10799 ids.add(def.id);
10800 }
10801 }
10802 const declared = prevIdsByHandle.get(handle);
10803 if (declared) {
10804 for (const id of declared) {
10805 ids.add(id);
10806 }
10807 }
10808 return ids;
10809 };
10810 return async (scripts, chromes) => {
10811 const incomingHandles = /* @__PURE__ */ new Set();
10812 for (const entry of scripts) {
10813 if (entry.handle) {
10814 incomingHandles.add(entry.handle);
10815 }
10816 }
10817 for (const handle of Array.from(loadedHandles)) {
10818 if (incomingHandles.has(handle)) {
10819 continue;
10820 }
10821 for (const id of collectIdsToRemove(handle)) {
10822 unregisterWindowChrome(id);
10823 }
10824 unregisterWindowChromesByOwner(handle);
10825 loadedHandles.delete(handle);
10826 }
10827 for (const entry of scripts) {
10828 if (!entry.handle || loadedHandles.has(entry.handle)) {
10829 continue;
10830 }
10831 await ensureScript(entry);
10832 }
10833 prevIdsByHandle = idsByHandleFrom(chromes);
10834 };
10835 }
10836 const INITIAL_ORIGIN$2 = window.location.origin;
10837 let _connSeq = 0;
10838 const _connections = /* @__PURE__ */ new Map();
10839 const _connectionsByTarget = /* @__PURE__ */ new Map();
10840 const _syntheticIframes = /* @__PURE__ */ new Map();
10841 function registerSyntheticIframe(windowId, iframe) {
10842 _syntheticIframes.set(windowId, iframe);
10843 return () => {
10844 if (_syntheticIframes.get(windowId) === iframe) {
10845 _syntheticIframes.delete(windowId);
10846 }
10847 };
10848 }
10849 function nextId() {
10850 return `desktop-mode-conn-${++_connSeq}`;
10851 }
10852 function createConnectionBridge(manager) {
10853 const sendToIframe = (win, message) => {
10854 try {
10855 win.contentWindow?.postMessage(message, INITIAL_ORIGIN$2);
10856 } catch (err) {
10857 if (typeof console !== "undefined") {
10858 console.error(
10859 "[desktop-mode] connection: postMessage failed",
10860 err
10861 );
10862 }
10863 }
10864 };
10865 const connect = (targetWindowId, opts = {}) => {
10866 const id = nextId();
10867 const topics = Array.isArray(opts.topics) ? [...opts.topics] : [];
10868 const subs = /* @__PURE__ */ new Map();
10869 const queue = [];
10870 let isOpen = false;
10871 let destroyed = false;
10872 const targetIframe = () => {
10873 const synth = _syntheticIframes.get(targetWindowId);
10874 if (synth) {
10875 return synth;
10876 }
10877 const w = manager.getById(targetWindowId);
10878 return w?.iframe ?? null;
10879 };
10880 const isNativeTarget = () => {
10881 if (targetIframe()) {
10882 return false;
10883 }
10884 const w = manager.getById(targetWindowId);
10885 return !!w && w.config?.native === true;
10886 };
10887 const nativeSubUnsubs = [];
10888 const flushQueue = () => {
10889 const iframe2 = targetIframe();
10890 if (!iframe2) {
10891 return;
10892 }
10893 while (queue.length) {
10894 const msg = queue.shift();
10895 sendToIframe(iframe2, {
10896 type: "desktop-mode-bridge-publish",
10897 connectionId: id,
10898 topic: msg.topic,
10899 payload: msg.payload
10900 });
10901 }
10902 };
10903 const conn = {
10904 id,
10905 target: targetWindowId,
10906 isOpen: () => isOpen,
10907 subscribe(topic, cb) {
10908 const wrapped = cb;
10909 if (isNativeTarget()) {
10910 const off = addParentSubscriber(
10911 targetWindowId,
10912 topic,
10913 (payload, meta) => {
10914 doAction(HOOKS.CONNECTION_MESSAGE, {
10915 connectionId: id,
10916 topic: meta.channel,
10917 direction: "in"
10918 });
10919 try {
10920 wrapped(payload, { topic: meta.channel });
10921 } catch (err) {
10922 if (typeof console !== "undefined") {
10923 console.error(
10924 "[desktop-mode] connection subscriber threw:",
10925 err
10926 );
10927 }
10928 }
10929 }
10930 );
10931 nativeSubUnsubs.push(off);
10932 return off;
10933 }
10934 let bucket22 = subs.get(topic);
10935 if (!bucket22) {
10936 bucket22 = /* @__PURE__ */ new Set();
10937 subs.set(topic, bucket22);
10938 }
10939 bucket22.add(wrapped);
10940 return () => {
10941 bucket22?.delete(wrapped);
10942 };
10943 },
10944 send(topic, payload) {
10945 if (destroyed) {
10946 return;
10947 }
10948 doAction(HOOKS.CONNECTION_MESSAGE, {
10949 connectionId: id,
10950 topic,
10951 direction: "out"
10952 });
10953 if (isNativeTarget()) {
10954 dispatchToNative(targetWindowId, topic, payload);
10955 return;
10956 }
10957 if (!isOpen) {
10958 queue.push({ topic, payload });
10959 return;
10960 }
10961 const iframe2 = targetIframe();
10962 if (!iframe2) {
10963 return;
10964 }
10965 sendToIframe(iframe2, {
10966 type: "desktop-mode-bridge-publish",
10967 connectionId: id,
10968 topic,
10969 payload
10970 });
10971 },
10972 disconnect() {
10973 conn._destroy("disconnect");
10974 },
10975 _targetWindow: targetIframe,
10976 _handleIframeMessage(data) {
10977 if (!data || typeof data !== "object") {
10978 return;
10979 }
10980 const msg = data;
10981 if (msg.type === "desktop-mode-bridge-handshake-ack") {
10982 if (isOpen) {
10983 return;
10984 }
10985 isOpen = true;
10986 doAction(HOOKS.CONNECTION_OPENED, {
10987 connectionId: id,
10988 targetWindowId,
10989 topics,
10990 // Ship the live Connection alongside the id so
10991 // iframe-initiated connections can be subscribed
10992 // to directly from the hook handler — without
10993 // `wp.desktop.getConnection(id)` plumbing the
10994 // payload would carry the id but no way to call
10995 // `.subscribe()` against it.
10996 connection: conn
10997 });
10998 try {
10999 opts.onOpen?.();
11000 } catch (err) {
11001 if (typeof console !== "undefined") {
11002 console.error(
11003 "[desktop-mode] connection.onOpen threw:",
11004 err
11005 );
11006 }
11007 }
11008 flushQueue();
11009 return;
11010 }
11011 if (msg.type === "desktop-mode-bridge-publish") {
11012 const m = data;
11013 const topic = typeof m.topic === "string" ? m.topic : "";
11014 if (!topic) {
11015 return;
11016 }
11017 doAction(HOOKS.CONNECTION_MESSAGE, {
11018 connectionId: id,
11019 topic,
11020 direction: "in"
11021 });
11022 const exact = subs.get(topic);
11023 if (exact) {
11024 for (const cb of Array.from(exact)) {
11025 try {
11026 cb(m.payload, { topic });
11027 } catch (err) {
11028 if (typeof console !== "undefined") {
11029 console.error(
11030 "[desktop-mode] connection subscriber threw:",
11031 err
11032 );
11033 }
11034 }
11035 }
11036 }
11037 const wildcard = subs.get("*");
11038 if (wildcard) {
11039 for (const cb of Array.from(wildcard)) {
11040 try {
11041 cb(m.payload, { topic });
11042 } catch (err) {
11043 if (typeof console !== "undefined") {
11044 console.error(
11045 "[desktop-mode] connection wildcard subscriber threw:",
11046 err
11047 );
11048 }
11049 }
11050 }
11051 }
11052 return;
11053 }
11054 if (msg.type === "desktop-mode-bridge-disconnect") {
11055 conn._destroy("disconnect");
11056 }
11057 },
11058 _destroy(reason) {
11059 if (destroyed) {
11060 return;
11061 }
11062 destroyed = true;
11063 const wasOpen = isOpen;
11064 isOpen = false;
11065 _connections.delete(id);
11066 const targetSet = _connectionsByTarget.get(targetWindowId);
11067 if (targetSet) {
11068 targetSet.delete(id);
11069 if (targetSet.size === 0) {
11070 _connectionsByTarget.delete(targetWindowId);
11071 }
11072 }
11073 for (const off of nativeSubUnsubs.splice(0)) {
11074 try {
11075 off();
11076 } catch {
11077 }
11078 }
11079 if (wasOpen) {
11080 const iframe2 = targetIframe();
11081 if (iframe2) {
11082 sendToIframe(iframe2, {
11083 type: "desktop-mode-bridge-disconnect",
11084 connectionId: id
11085 });
11086 }
11087 }
11088 doAction(HOOKS.CONNECTION_CLOSED, {
11089 connectionId: id,
11090 reason
11091 });
11092 try {
11093 opts.onClose?.(reason);
11094 } catch (err) {
11095 if (typeof console !== "undefined") {
11096 console.error(
11097 "[desktop-mode] connection.onClose threw:",
11098 err
11099 );
11100 }
11101 }
11102 }
11103 };
11104 _connections.set(id, conn);
11105 let bucket2 = _connectionsByTarget.get(targetWindowId);
11106 if (!bucket2) {
11107 bucket2 = /* @__PURE__ */ new Set();
11108 _connectionsByTarget.set(targetWindowId, bucket2);
11109 }
11110 bucket2.add(id);
11111 if (isNativeTarget()) {
11112 Promise.resolve().then(() => {
11113 if (destroyed || isOpen) {
11114 return;
11115 }
11116 isOpen = true;
11117 doAction(HOOKS.CONNECTION_OPENED, {
11118 connectionId: id,
11119 targetWindowId,
11120 topics
11121 });
11122 try {
11123 opts.onOpen?.();
11124 } catch (err) {
11125 if (typeof console !== "undefined") {
11126 console.error(
11127 "[desktop-mode] connection.onOpen threw:",
11128 err
11129 );
11130 }
11131 }
11132 });
11133 return conn;
11134 }
11135 const iframe = targetIframe();
11136 if (iframe) {
11137 sendToIframe(iframe, {
11138 type: "desktop-mode-bridge-handshake",
11139 connectionId: id,
11140 targetWindowId,
11141 topics
11142 });
11143 }
11144 return conn;
11145 };
11146 const routeIncomingFromIframe = (data, windowId) => {
11147 if (!data || typeof data !== "object") {
11148 return;
11149 }
11150 const msg = data;
11151 if (typeof msg.type !== "string" || !msg.type.startsWith("desktop-mode-bridge-")) {
11152 return;
11153 }
11154 if (msg.type === "desktop-mode-bridge-connection-request" && typeof msg.requestId === "string" && typeof windowId === "string" && windowId !== "") {
11155 handleConnectionRequest(windowId, msg.requestId, Array.isArray(msg.topics) ? msg.topics : []);
11156 return;
11157 }
11158 if (typeof msg.connectionId !== "string") {
11159 return;
11160 }
11161 const conn = _connections.get(msg.connectionId);
11162 conn?._handleIframeMessage(data);
11163 };
11164 const handleConnectionRequest = (windowId, requestId, topics) => {
11165 const synth = _syntheticIframes.get(windowId);
11166 const iframe = synth ?? manager.getById(windowId)?.iframe ?? null;
11167 if (!iframe) {
11168 return;
11169 }
11170 const decision = applyFilters(
11171 HOOKS.IFRAME_CONNECTION_REQUEST,
11172 true,
11173 { windowId, requestId, topics: topics.slice() }
11174 );
11175 if (decision === false) {
11176 try {
11177 iframe.contentWindow?.postMessage({
11178 type: "desktop-mode-bridge-connection-ack",
11179 requestId,
11180 accepted: false,
11181 reason: "rejected"
11182 }, INITIAL_ORIGIN$2);
11183 } catch {
11184 }
11185 return;
11186 }
11187 const finalTopics = decision && typeof decision === "object" && Array.isArray(decision.topics) ? decision.topics : topics;
11188 const conn = connect(windowId, { topics: finalTopics });
11189 try {
11190 iframe.contentWindow?.postMessage({
11191 type: "desktop-mode-bridge-connection-ack",
11192 requestId,
11193 accepted: true,
11194 connectionId: conn.id
11195 }, INITIAL_ORIGIN$2);
11196 } catch {
11197 }
11198 };
11199 const onIframeReady = (windowId) => {
11200 const bucket2 = _connectionsByTarget.get(windowId);
11201 if (!bucket2) {
11202 return;
11203 }
11204 for (const connId of Array.from(bucket2)) {
11205 const conn = _connections.get(connId);
11206 if (!conn || conn.isOpen()) {
11207 continue;
11208 }
11209 const iframe = conn._targetWindow();
11210 if (!iframe) {
11211 continue;
11212 }
11213 sendToIframe(iframe, {
11214 type: "desktop-mode-bridge-handshake",
11215 connectionId: conn.id,
11216 targetWindowId: conn.target,
11217 topics: []
11218 // already negotiated client-side; iframe re-uses
11219 });
11220 }
11221 };
11222 const onWindowClosed = (windowId) => {
11223 const bucket2 = _connectionsByTarget.get(windowId);
11224 if (!bucket2) {
11225 return;
11226 }
11227 for (const connId of Array.from(bucket2)) {
11228 const conn = _connections.get(connId);
11229 conn?._destroy("window-closed");
11230 }
11231 };
11232 const getConnection = (connectionId) => {
11233 const conn = _connections.get(connectionId);
11234 return conn ?? null;
11235 };
11236 return {
11237 connect,
11238 getConnection,
11239 routeIncomingFromIframe,
11240 onIframeReady,
11241 onWindowClosed
11242 };
11243 }
11244 const __vite_import_meta_env__ = {};
11245 function devLog(...args) {
11246 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;
11247 if (mode !== "production") {
11248 console.log(...args);
11249 }
11250 }
11251 const OWNER_PREFIX = "iframe:";
11252 function ownerFor(windowId) {
11253 return OWNER_PREFIX + windowId;
11254 }
11255 function iconFor(harvested) {
11256 if (harvested.icon && typeof harvested.icon === "string" && harvested.icon.startsWith("dashicons-")) {
11257 return harvested.icon;
11258 }
11259 return harvested.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
11260 }
11261 function slugFor(windowId, name) {
11262 const safeName = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
11263 const safeWin = windowId.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
11264 return `win-${safeWin}-${safeName}`;
11265 }
11266 class IframeCommandBridge {
11267 constructor(opts) {
11268 this.subscribedWindowId = null;
11269 this.manager = opts.manager;
11270 this.adminUrl = opts.adminUrl;
11271 }
11272 /** Wire up the focus / close / message listeners. Idempotent. */
11273 install() {
11274 document.addEventListener("desktop-mode-window-focused", (e) => {
11275 const detail = e.detail;
11276 if (detail && typeof detail.windowId === "string") {
11277 this.onFocused(detail.windowId);
11278 }
11279 });
11280 document.addEventListener("desktop-mode-window-closed", (e) => {
11281 const detail = e.detail;
11282 if (detail && typeof detail.windowId === "string") {
11283 unregisterByOwner(ownerFor(detail.windowId));
11284 if (this.subscribedWindowId === detail.windowId) {
11285 this.subscribedWindowId = null;
11286 }
11287 }
11288 });
11289 document.addEventListener("desktop-mode-window-changed", (e) => {
11290 const detail = e.detail;
11291 if (!detail || typeof detail.windowId !== "string") {
11292 return;
11293 }
11294 if (detail.reason !== "state") {
11295 return;
11296 }
11297 if (detail.state !== "minimized") {
11298 return;
11299 }
11300 if (this.subscribedWindowId === detail.windowId) {
11301 this.subscribedWindowId = null;
11302 }
11303 });
11304 window.addEventListener("message", (e) => {
11305 if (e.origin !== window.location.origin) {
11306 return;
11307 }
11308 const data = e.data;
11309 if (!data || typeof data.type !== "string") {
11310 return;
11311 }
11312 if (data.type === "desktop-mode-bridge-ready") {
11313 const win2 = this.manager.findByIframeSource(e.source);
11314 if (win2 && win2.id === this.subscribedWindowId) {
11315 this.sendSubscribe(win2.id);
11316 }
11317 return;
11318 }
11319 if (data.type !== "desktop-mode-commands-list") {
11320 return;
11321 }
11322 if (!Array.isArray(data.commands)) {
11323 return;
11324 }
11325 const win = this.manager.findByIframeSource(e.source);
11326 if (!win) {
11327 return;
11328 }
11329 if (win.id !== this.subscribedWindowId) {
11330 return;
11331 }
11332 this.applyList(win.id, data.commands);
11333 });
11334 const focused = this.manager.getFocused();
11335 if (focused) {
11336 this.onFocused(focused.id);
11337 }
11338 }
11339 onFocused(windowId) {
11340 if (this.subscribedWindowId === windowId) {
11341 return;
11342 }
11343 if (this.subscribedWindowId) {
11344 const prev = this.manager.getById(this.subscribedWindowId);
11345 if (prev && prev.iframe && prev.iframe.contentWindow) {
11346 try {
11347 prev.iframe.contentWindow.postMessage(
11348 { type: "desktop-mode-commands-unsubscribe" },
11349 window.location.origin
11350 );
11351 } catch {
11352 }
11353 }
11354 unregisterByOwner(ownerFor(this.subscribedWindowId));
11355 }
11356 this.subscribedWindowId = windowId;
11357 this.sendSubscribe(windowId);
11358 }
11359 sendSubscribe(windowId) {
11360 const win = this.manager.getById(windowId);
11361 if (!win) {
11362 return;
11363 }
11364 if (!win.iframe) {
11365 return;
11366 }
11367 if (!win.iframe.contentWindow) {
11368 return;
11369 }
11370 try {
11371 win.iframe.contentWindow.postMessage(
11372 { type: "desktop-mode-commands-subscribe" },
11373 window.location.origin
11374 );
11375 } catch (err) {
11376 devLog("[wpd-cmd:parent] sendSubscribe: postMessage threw", err);
11377 }
11378 }
11379 applyList(windowId, commands) {
11380 const owner = ownerFor(windowId);
11381 unregisterByOwner(owner);
11382 for (const cmd of commands) {
11383 if (!cmd || !cmd.name || !cmd.label) {
11384 continue;
11385 }
11386 const slug = slugFor(windowId, cmd.name);
11387 const safeSvg = typeof cmd.iconSvg === "string" && cmd.iconSvg !== "" ? sanitizeIconSvg(cmd.iconSvg) : "";
11388 const def = {
11389 slug,
11390 label: cmd.label,
11391 icon: iconFor(cmd),
11392 iconSvg: safeSvg !== "" ? safeSvg : void 0,
11393 owner,
11394 // Harvested commands are contextual by construction —
11395 // they come from whichever window has focus. Surface
11396 // them eagerly so the user sees "Duplicate block" /
11397 // "Toggle distraction free" without having to type `/`
11398 // first.
11399 eager: true,
11400 run: cmd.kind === "navigate" && cmd.url ? this.runNavigate(cmd.url, cmd.label, iconFor(cmd)) : this.runProxy(windowId, cmd.name)
11401 };
11402 try {
11403 registerCommand(def);
11404 } catch (err) {
11405 console.error(
11406 "[desktop-mode] iframe-bridge: dropping bad command",
11407 def,
11408 err
11409 );
11410 }
11411 }
11412 }
11413 runNavigate(url, title, icon) {
11414 return (_args, ctx) => {
11415 ctx.close();
11416 if (tryNativeUrlRemap(url)) {
11417 return;
11418 }
11419 const id = deriveWindowId(url, this.adminUrl);
11420 this.manager.open({ id, baseId: id, url, title, icon });
11421 };
11422 }
11423 runProxy(windowId, name) {
11424 return (_args, ctx) => {
11425 ctx.close();
11426 const win = this.manager.getById(windowId);
11427 if (!win || !win.iframe || !win.iframe.contentWindow) {
11428 return;
11429 }
11430 try {
11431 win.iframe.contentWindow.postMessage(
11432 { type: "desktop-mode-commands-invoke", name },
11433 window.location.origin
11434 );
11435 } catch {
11436 }
11437 this.manager.focus(win);
11438 };
11439 }
11440 }
11441 const OWNER = "global";
11442 const NAV_HREF_LITERAL_RE = /(?:document\.location\.href|window\.location\.href|location\.href)\s*=\s*['"]([^'"$]+?)['"]/;
11443 const NAV_ASSIGN_LITERAL_RE = /(?:document\.location|window\.location|location)\s*=\s*['"]([^'"$]+?)['"]/;
11444 const NAV_CALL_LITERAL_RE = /location\.(?:assign|replace)\s*\(\s*['"]([^'"$]+?)['"]\s*\)/;
11445 const NAV_INTENT_RE = /(?:document\.location|window\.location|location)\s*(?:\.href\s*)?=|location\.(?:assign|replace)\s*\(/;
11446 const SITE_EDITOR_INTENT_RE = /getSiteEditorPage\s*\(|site-editor\.php/;
11447 const SITE_EDITOR_NAME_RE = /^(wp_template_part|wp_template|wp_navigation|wp_block)-(.+)$/;
11448 function lookupMenuCommand(name) {
11449 const list2 = window.__desktopModeMenuCommands;
11450 if (!Array.isArray(list2)) {
11451 return null;
11452 }
11453 for (const entry of list2) {
11454 if (entry && typeof entry === "object" && entry.name === name && typeof entry.url === "string" && entry.url !== "") {
11455 return {
11456 label: typeof entry.label === "string" ? entry.label : "",
11457 url: entry.url
11458 };
11459 }
11460 }
11461 return null;
11462 }
11463 class ShellCommandHarvester {
11464 constructor(opts) {
11465 this.mounted = false;
11466 this.host = null;
11467 this.root = null;
11468 this.kindCache = /* @__PURE__ */ Object.create(null);
11469 this.callbackCache = /* @__PURE__ */ Object.create(null);
11470 this.lastFingerprint = "";
11471 this.manager = opts.manager;
11472 this.adminUrl = opts.adminUrl;
11473 }
11474 /** Mount the harvester. Idempotent. Safe to call before `wp.data` loads. */
11475 install() {
11476 this.tryMount(0);
11477 }
11478 tryMount(attempt) {
11479 if (this.mounted) {
11480 return;
11481 }
11482 const wp = window.wp;
11483 if (!wp || !wp.data || !wp.element || typeof wp.data.subscribe !== "function") {
11484 if (attempt < 40) {
11485 window.setTimeout(() => this.tryMount(attempt + 1), 150);
11486 }
11487 return;
11488 }
11489 this.mount();
11490 }
11491 mount() {
11492 const wp = window.wp;
11493 const el = wp.element;
11494 const data = wp.data;
11495 const createEl = el.createElement;
11496 const useEffect = el.useEffect;
11497 const useRef = el.useRef;
11498 const useMemo = el.useMemo;
11499 const useSelect = data.useSelect;
11500 if (typeof createEl !== "function" || typeof useEffect !== "function" || typeof useRef !== "function" || typeof useMemo !== "function" || typeof useSelect !== "function" || typeof el.createRoot !== "function") {
11501 return;
11502 }
11503 this.mounted = true;
11504 const host = document.createElement("div");
11505 host.setAttribute("aria-hidden", "true");
11506 host.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;left:-9999px;top:-9999px;";
11507 (document.body || document.documentElement).appendChild(host);
11508 this.host = host;
11509 const bucket2 = {
11510 perLoader: {},
11511 statics: [],
11512 loadersList: []
11513 };
11514 const fingerprint2 = (cmds) => {
11515 if (!Array.isArray(cmds) || cmds.length === 0) {
11516 return "";
11517 }
11518 const keys = new Array(cmds.length);
11519 for (let i = 0; i < cmds.length; i++) {
11520 const c = cmds[i];
11521 keys[i] = c && c.name ? c.name : "";
11522 }
11523 return keys.join("|");
11524 };
11525 const mergeAndPublish = () => {
11526 let merged = [];
11527 for (const name of bucket2.loadersList) {
11528 const slice = bucket2.perLoader[name];
11529 if (Array.isArray(slice)) {
11530 merged = merged.concat(slice);
11531 }
11532 }
11533 if (Array.isArray(bucket2.statics)) {
11534 merged = merged.concat(bucket2.statics);
11535 }
11536 this.callbackCache = /* @__PURE__ */ Object.create(null);
11537 for (const cc of merged) {
11538 if (cc && cc.name && typeof cc.callback === "function") {
11539 this.callbackCache[cc.name] = cc.callback;
11540 }
11541 }
11542 this.publish(merged);
11543 };
11544 const LoaderSlot = (props) => {
11545 const loader = props.loader;
11546 let result = null;
11547 try {
11548 result = loader.hook({ search: "" });
11549 } catch {
11550 }
11551 const cmds = result && Array.isArray(result.commands) ? result.commands : [];
11552 const key = useMemo(() => fingerprint2(cmds), [cmds]);
11553 useEffect(() => {
11554 bucket2.perLoader[loader.name] = cmds;
11555 mergeAndPublish();
11556 }, [key]);
11557 useEffect(() => {
11558 return () => {
11559 delete bucket2.perLoader[loader.name];
11560 mergeAndPublish();
11561 };
11562 }, []);
11563 return null;
11564 };
11565 const Harvester = () => {
11566 const loaders = useSelect((s) => {
11567 const ss = s("core/commands");
11568 if (!ss || typeof ss.getCommandLoaders !== "function") {
11569 return [];
11570 }
11571 return [
11572 ...ss.getCommandLoaders(false) || [],
11573 ...ss.getCommandLoaders(true) || []
11574 ];
11575 }, []);
11576 const staticCmds = useSelect((s) => {
11577 const ss = s("core/commands");
11578 if (!ss || typeof ss.getCommands !== "function") {
11579 return [];
11580 }
11581 return [
11582 ...ss.getCommands(false) || [],
11583 ...ss.getCommands(true) || []
11584 ];
11585 }, []);
11586 const loadersNames = useMemo(() => {
11587 return Array.isArray(loaders) ? loaders.map((l) => l ? l.name || "" : "") : [];
11588 }, [loaders]);
11589 const loadersKey = loadersNames.join("|");
11590 useEffect(() => {
11591 bucket2.loadersList = loadersNames;
11592 mergeAndPublish();
11593 }, [loadersKey]);
11594 const staticKey = useMemo(
11595 () => fingerprint2(Array.isArray(staticCmds) ? staticCmds : []),
11596 [staticCmds]
11597 );
11598 useEffect(() => {
11599 bucket2.statics = Array.isArray(staticCmds) ? staticCmds : [];
11600 mergeAndPublish();
11601 }, [staticKey]);
11602 if (!Array.isArray(loaders) || loaders.length === 0) {
11603 return null;
11604 }
11605 const children = [];
11606 for (const loader of loaders) {
11607 if (!loader || typeof loader.hook !== "function") {
11608 continue;
11609 }
11610 children.push(
11611 createEl(LoaderSlot, { key: loader.name, loader })
11612 );
11613 }
11614 return createEl(el.Fragment || "div", null, children);
11615 };
11616 try {
11617 this.root = el.createRoot(host);
11618 this.root.render(createEl(Harvester));
11619 } catch {
11620 this.mounted = false;
11621 this.root = null;
11622 if (this.host && this.host.parentNode) {
11623 this.host.parentNode.removeChild(this.host);
11624 }
11625 this.host = null;
11626 }
11627 }
11628 publish(raw) {
11629 const seen = /* @__PURE__ */ Object.create(null);
11630 const classified = [];
11631 for (const cmd of raw) {
11632 if (!cmd || !cmd.name || !cmd.label) {
11633 continue;
11634 }
11635 if (cmd.disabled) {
11636 continue;
11637 }
11638 if (seen[cmd.name]) {
11639 continue;
11640 }
11641 seen[cmd.name] = true;
11642 classified.push(this.classify(cmd));
11643 }
11644 let key = "";
11645 for (const c of classified) {
11646 key += `${c.name}|${c.kind}|${c.url || ""}
11647 `;
11648 }
11649 if (key === this.lastFingerprint) {
11650 return;
11651 }
11652 this.lastFingerprint = key;
11653 unregisterByOwner(OWNER);
11654 for (const c of classified) {
11655 if (c.kind === "skip") {
11656 continue;
11657 }
11658 const slug = `global-${c.name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-")}`;
11659 const icon = this.iconFor(c);
11660 const def = {
11661 slug,
11662 label: c.label,
11663 icon,
11664 iconSvg: c.iconSvg && c.iconSvg !== "" ? sanitizeIconSvg(c.iconSvg) : void 0,
11665 owner: OWNER,
11666 // NOT eager. The palette splits the registry into two
11667 // disjoint surfaces: `eager` commands show on empty
11668 // input (and are excluded from slash search at
11669 // `src/ai-assistant/impl.ts:494`); non-eager commands
11670 // show when the user types `/<query>`. The WP baseline
11671 // is large (~150 entries) and meant to be searched —
11672 // surfacing it eagerly would drown the iframe-harvested
11673 // contextual shortcuts on every open. Slash-search is
11674 // the right surface for it, matching the native WP
11675 // palette UX (open, type, find).
11676 run: c.kind === "navigate" && c.url ? this.runNavigate(c.url, c.windowTitle || c.label, icon) : this.runInvoke(c.name, c.label, icon)
11677 };
11678 try {
11679 registerCommand(def);
11680 } catch (err) {
11681 console.error(
11682 "[desktop-mode] shell-harvester: dropping bad command",
11683 def,
11684 err
11685 );
11686 }
11687 }
11688 }
11689 classify(cmd) {
11690 const out = {
11691 name: String(cmd.name),
11692 label: String(cmd.label),
11693 icon: typeof cmd.icon === "string" ? cmd.icon : void 0,
11694 iconSvg: void 0,
11695 kind: "action",
11696 url: void 0,
11697 callback: typeof cmd.callback === "function" ? cmd.callback : void 0
11698 };
11699 const cached = this.kindCache[out.name];
11700 if (cached) {
11701 out.kind = cached.kind;
11702 out.url = cached.url;
11703 out.iconSvg = cached.iconSvg;
11704 return out;
11705 }
11706 if (cmd.icon && typeof cmd.icon !== "string") {
11707 out.iconSvg = this.renderIcon(cmd.icon);
11708 }
11709 const menuEntry = lookupMenuCommand(out.name);
11710 if (menuEntry) {
11711 try {
11712 out.url = new URL(menuEntry.url, this.adminUrl).toString();
11713 out.kind = "navigate";
11714 if (menuEntry.label !== "") {
11715 out.windowTitle = menuEntry.label;
11716 }
11717 } catch {
11718 out.kind = "skip";
11719 }
11720 this.kindCache[out.name] = {
11721 kind: out.kind,
11722 url: out.url,
11723 iconSvg: out.iconSvg
11724 };
11725 return out;
11726 }
11727 if (typeof cmd.callback === "function") {
11728 let src = "";
11729 try {
11730 src = Function.prototype.toString.call(cmd.callback);
11731 } catch {
11732 src = "";
11733 }
11734 const literal = src.match(NAV_HREF_LITERAL_RE) || src.match(NAV_ASSIGN_LITERAL_RE) || src.match(NAV_CALL_LITERAL_RE);
11735 if (literal && literal[1]) {
11736 try {
11737 out.url = new URL(literal[1], window.location.href).toString();
11738 out.kind = "navigate";
11739 } catch {
11740 out.kind = "action";
11741 }
11742 } else if (NAV_INTENT_RE.test(src)) {
11743 const isSiteEditorIntent = SITE_EDITOR_INTENT_RE.test(src);
11744 const nameMatch = isSiteEditorIntent ? out.name.match(SITE_EDITOR_NAME_RE) : null;
11745 if (nameMatch) {
11746 const entityType = nameMatch[1];
11747 const entityId = nameMatch[2];
11748 const p = `/${entityType}/${entityId}`;
11749 try {
11750 const siteEditor = new URL("site-editor.php", this.adminUrl);
11751 siteEditor.searchParams.set("p", p);
11752 siteEditor.searchParams.set("canvas", "edit");
11753 out.url = siteEditor.toString();
11754 out.kind = "navigate";
11755 } catch {
11756 out.kind = "skip";
11757 }
11758 } else {
11759 out.kind = "skip";
11760 }
11761 }
11762 }
11763 this.kindCache[out.name] = {
11764 kind: out.kind,
11765 url: out.url,
11766 iconSvg: out.iconSvg
11767 };
11768 return out;
11769 }
11770 renderIcon(icon) {
11771 const wp = window.wp;
11772 if (!wp || !wp.element || typeof wp.element.renderToString !== "function") {
11773 return "";
11774 }
11775 try {
11776 const rendered = wp.element.renderToString(icon);
11777 if (typeof rendered === "string" && rendered.toLowerCase().startsWith("<svg")) {
11778 return rendered;
11779 }
11780 } catch {
11781 }
11782 return "";
11783 }
11784 iconFor(c) {
11785 if (c.icon && c.icon.startsWith("dashicons-")) {
11786 return c.icon;
11787 }
11788 return c.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
11789 }
11790 runNavigate(url, title, icon) {
11791 return (_args, ctx) => {
11792 ctx.close();
11793 if (tryNativeUrlRemap(url)) {
11794 return;
11795 }
11796 const id = deriveWindowId(url, this.adminUrl);
11797 this.manager.open({ id, baseId: id, url, title, icon });
11798 };
11799 }
11800 runInvoke(name, title, icon) {
11801 return (_args, ctx) => {
11802 ctx.close();
11803 const cb = this.callbackCache[name];
11804 if (typeof cb !== "function") {
11805 return;
11806 }
11807 const captured = this.runWithNavCapture(cb);
11808 if (captured) {
11809 const id = deriveWindowId(captured, this.adminUrl);
11810 this.manager.open({ id, baseId: id, url: captured, title, icon });
11811 }
11812 };
11813 }
11814 /**
11815 * Invoke `cb` with navigation sinks (`document.location`,
11816 * `window.location`, `location.assign`, `location.replace`)
11817 * shadowed so any assignment is captured instead of navigating
11818 * the shell. Returns the captured URL or `null` if the callback
11819 * was a pure JS action.
11820 *
11821 * The shadow uses `Object.defineProperty` on the document /
11822 * window instance to override the prototype's accessor for the
11823 * duration of the call. `delete` afterwards unshadows so the
11824 * native setter is restored.
11825 */
11826 runWithNavCapture(cb) {
11827 let captured = null;
11828 const setCaptured = (v) => {
11829 if (captured === null && typeof v === "string" && v !== "") {
11830 captured = v;
11831 }
11832 };
11833 const realLocation = window.location;
11834 const locationProxy = new Proxy(realLocation, {
11835 get(target2, prop) {
11836 const value = target2[prop];
11837 if (prop === "assign" || prop === "replace") {
11838 return (url) => setCaptured(url);
11839 }
11840 if (typeof value === "function") {
11841 return value.bind(target2);
11842 }
11843 return value;
11844 },
11845 set(_target, prop, value) {
11846 if (prop === "href") {
11847 setCaptured(value);
11848 return true;
11849 }
11850 return true;
11851 }
11852 });
11853 const shadowed = [];
11854 const installShadow = (obj) => {
11855 try {
11856 Object.defineProperty(obj, "location", {
11857 configurable: true,
11858 get: () => locationProxy,
11859 set: (v) => setCaptured(v)
11860 });
11861 shadowed.push({ obj, key: "location" });
11862 } catch {
11863 }
11864 };
11865 installShadow(document);
11866 installShadow(window);
11867 try {
11868 cb({ close: () => {
11869 } });
11870 } catch {
11871 } finally {
11872 for (const s of shadowed) {
11873 try {
11874 delete s.obj[s.key];
11875 } catch {
11876 }
11877 }
11878 }
11879 return captured;
11880 }
11881 }
11882 const seed$2 = [];
11883 function register(def) {
11884 throwOnRegistrationErrors(
11885 "Widget",
11886 collectRegistrationErrors(def, WIDGET_CHECKS),
11887 def
11888 );
11889 const idx = seed$2.findIndex((w) => w.id === def.id);
11890 if (idx >= 0) {
11891 seed$2[idx] = def;
11892 } else {
11893 seed$2.push(def);
11894 }
11895 }
11896 function unregister(id) {
11897 const idx = seed$2.findIndex((w) => w.id === id);
11898 if (idx >= 0) {
11899 seed$2.splice(idx, 1);
11900 }
11901 }
11902 function all() {
11903 const copy = seed$2.slice();
11904 const filtered = applyFilters(HOOKS.WIDGETS, copy);
11905 if (!Array.isArray(filtered)) {
11906 if (typeof console !== "undefined") {
11907 console.warn(
11908 "[desktop-mode] `desktop-mode.widgets` filter returned a non-array; falling back to seed list."
11909 );
11910 }
11911 return copy;
11912 }
11913 return filtered.filter(isValidDef);
11914 }
11915 function get(id) {
11916 return all().find((w) => w.id === id);
11917 }
11918 const WIDGET_CHECKS = [
11919 {
11920 field: "id",
11921 message: "missing or not a non-empty string",
11922 valid: (d) => typeof d.id === "string" && d.id !== ""
11923 },
11924 {
11925 field: "label",
11926 message: "missing or not a non-empty string",
11927 valid: (d) => typeof d.label === "string" && d.label !== ""
11928 },
11929 {
11930 field: "description",
11931 message: "not a string",
11932 valid: (d) => typeof d.description === "string"
11933 },
11934 {
11935 field: "icon",
11936 message: "missing or not a non-empty string",
11937 valid: (d) => typeof d.icon === "string" && d.icon !== ""
11938 },
11939 {
11940 field: "mount",
11941 message: "not a function",
11942 valid: (d) => typeof d.mount === "function"
11943 }
11944 ];
11945 function isValidDef(def) {
11946 return collectRegistrationErrors(def, WIDGET_CHECKS).length === 0;
11947 }
11948 let active$2 = null;
11949 function openWidgetPicker(options) {
11950 if (active$2) {
11951 return;
11952 }
11953 const panel2 = document.createElement("div");
11954 panel2.className = "desktop-mode-widget-picker";
11955 panel2.setAttribute("role", "menu");
11956 panel2.setAttribute("aria-label", __("Add widget"));
11957 const title = document.createElement("div");
11958 title.className = "desktop-mode-widget-picker__title";
11959 title.textContent = __("Add widget");
11960 panel2.appendChild(title);
11961 const list2 = document.createElement("div");
11962 list2.className = "desktop-mode-widget-picker__list";
11963 panel2.appendChild(list2);
11964 paintList(list2, options);
11965 document.body.appendChild(panel2);
11966 positionPanel(panel2, options.anchor);
11967 const onOutsidePointerDown = (e) => {
11968 const target2 = e.target;
11969 if (!target2) {
11970 return;
11971 }
11972 if (panel2.contains(target2) || options.anchor.contains(target2)) {
11973 return;
11974 }
11975 closeWidgetPicker();
11976 };
11977 window.setTimeout(() => {
11978 document.addEventListener("pointerdown", onOutsidePointerDown, true);
11979 }, 0);
11980 const onKeyDown = (e) => {
11981 if (e.key === "Escape") {
11982 closeWidgetPicker();
11983 }
11984 };
11985 document.addEventListener("keydown", onKeyDown);
11986 active$2 = { panel: panel2, options, onOutsidePointerDown, onKeyDown };
11987 const first = list2.querySelector(
11988 "button:not([disabled])"
11989 );
11990 first?.focus();
11991 }
11992 function refreshWidgetPicker() {
11993 if (!active$2) {
11994 return;
11995 }
11996 const list2 = active$2.panel.querySelector(
11997 ".desktop-mode-widget-picker__list"
11998 );
11999 if (list2) {
12000 paintList(list2, active$2.options);
12001 }
12002 }
12003 function closeWidgetPicker() {
12004 if (!active$2) {
12005 return;
12006 }
12007 document.removeEventListener(
12008 "pointerdown",
12009 active$2.onOutsidePointerDown,
12010 true
12011 );
12012 document.removeEventListener("keydown", active$2.onKeyDown);
12013 active$2.panel.remove();
12014 active$2 = null;
12015 }
12016 function paintList(list2, options) {
12017 list2.innerHTML = "";
12018 const enabled = new Set(options.enabledIds());
12019 const defs = options.registry();
12020 if (defs.length === 0) {
12021 const empty = document.createElement("div");
12022 empty.className = "desktop-mode-widget-picker__empty";
12023 empty.textContent = __(
12024 "No widgets available. Activate a plugin that registers one, or see the docs for the registerWidget API."
12025 );
12026 list2.appendChild(empty);
12027 return;
12028 }
12029 for (const def of defs) {
12030 const entry = document.createElement("button");
12031 entry.type = "button";
12032 entry.className = "desktop-mode-widget-picker__entry";
12033 const isAdded = enabled.has(def.id);
12034 if (isAdded) {
12035 entry.classList.add(
12036 "desktop-mode-widget-picker__entry--added"
12037 );
12038 entry.disabled = true;
12039 entry.setAttribute("aria-disabled", "true");
12040 }
12041 entry.setAttribute("role", "menuitem");
12042 let ariaLabel;
12043 if (isAdded) {
12044 ariaLabel = sprintf(__("%s (already added)"), def.label);
12045 } else {
12046 ariaLabel = sprintf(__("Add %s"), def.label);
12047 }
12048 entry.setAttribute("aria-label", ariaLabel);
12049 const icon = document.createElement("span");
12050 icon.className = `desktop-mode-widget-picker__entry-icon dashicons ${def.icon}`;
12051 icon.setAttribute("aria-hidden", "true");
12052 entry.appendChild(icon);
12053 const textWrap = document.createElement("span");
12054 textWrap.className = "desktop-mode-widget-picker__entry-text";
12055 const label = document.createElement("span");
12056 label.className = "desktop-mode-widget-picker__entry-label";
12057 label.textContent = def.label;
12058 textWrap.appendChild(label);
12059 if (def.description) {
12060 const desc = document.createElement("span");
12061 desc.className = "desktop-mode-widget-picker__entry-description";
12062 desc.textContent = def.description;
12063 textWrap.appendChild(desc);
12064 }
12065 entry.appendChild(textWrap);
12066 if (isAdded) {
12067 const status = document.createElement("span");
12068 status.className = "desktop-mode-widget-picker__entry-status";
12069 status.textContent = __("Added");
12070 entry.appendChild(status);
12071 }
12072 if (!isAdded) {
12073 entry.addEventListener("click", (e) => {
12074 e.preventDefault();
12075 e.stopPropagation();
12076 options.onAdd(def.id);
12077 });
12078 }
12079 list2.appendChild(entry);
12080 }
12081 }
12082 function positionPanel(panel2, anchor) {
12083 const rect = anchor.getBoundingClientRect();
12084 panel2.style.position = "fixed";
12085 panel2.style.left = "0px";
12086 panel2.style.top = "0px";
12087 panel2.style.visibility = "hidden";
12088 const panelRect = panel2.getBoundingClientRect();
12089 const width = panelRect.width || 320;
12090 const height = panelRect.height || 200;
12091 const gap = 6;
12092 let left = rect.right - width;
12093 let top = rect.top - height - gap;
12094 if (left < 8) {
12095 left = 8;
12096 }
12097 if (top < 8) {
12098 top = rect.bottom + gap;
12099 }
12100 panel2.style.left = `${Math.round(left)}px`;
12101 panel2.style.top = `${Math.round(top)}px`;
12102 panel2.style.visibility = "";
12103 }
12104 const FLOATING_CLASS = "desktop-mode-widgets__card--floating";
12105 const MOVABLE_CLASS = "desktop-mode-widgets__card--movable";
12106 const RESIZABLE_CLASS = "desktop-mode-widgets__card--resizable";
12107 const DRAGGING_CLASS = "desktop-mode-widgets__card--dragging";
12108 const RESIZING_CLASS = "desktop-mode-widgets__card--resizing";
12109 const DEFAULT_MIN_WIDTH = 160;
12110 const DEFAULT_MIN_HEIGHT = 80;
12111 const DEFAULT_WIDTH$1 = 280;
12112 const DEFAULT_HEIGHT$1 = 180;
12113 const VIEWPORT_MARGIN = 20;
12114 const DRAG_THRESHOLD_PX$1 = 5;
12115 const DRAG_THRESHOLD_SQUARED = DRAG_THRESHOLD_PX$1 * DRAG_THRESHOLD_PX$1;
12116 const DRAG_EXCLUDED_SELECTORS = 'input, textarea, select, button, a, [contenteditable="true"]';
12117 function buildFrame(def, ctx, handlers) {
12118 const card = document.createElement("div");
12119 card.className = "desktop-mode-widgets__card";
12120 card.dataset.widgetId = def.id;
12121 const movable = def.movable === true;
12122 const resizable = def.resizable === true;
12123 if (movable) {
12124 card.classList.add(MOVABLE_CLASS);
12125 }
12126 if (resizable) {
12127 card.classList.add(RESIZABLE_CLASS);
12128 }
12129 if (movable) {
12130 card.appendChild(buildChrome(def, handlers.onRemove, handlers.onRedock));
12131 } else {
12132 card.appendChild(buildCornerClose(def, handlers.onRemove));
12133 }
12134 const body = document.createElement("div");
12135 body.className = "desktop-mode-widgets__card-body";
12136 card.appendChild(body);
12137 let isFloating = false;
12138 if (ctx.geometry) {
12139 applyGeometry(card, ctx.geometry);
12140 card.classList.add(FLOATING_CLASS);
12141 isFloating = true;
12142 }
12143 const resizeCleanups = [];
12144 if (resizable) {
12145 for (const dir of allHandleDirs()) {
12146 const handle = document.createElement("div");
12147 handle.className = `desktop-mode-widgets__resize desktop-mode-widgets__resize--${dir}`;
12148 handle.setAttribute("aria-hidden", "true");
12149 handle.dataset.dir = dir;
12150 card.appendChild(handle);
12151 resizeCleanups.push(
12152 attachResize(card, handle, dir, def, ctx, handlers, () => isFloating)
12153 );
12154 }
12155 }
12156 let dragCleanup = null;
12157 if (movable) {
12158 const chrome = card.querySelector(
12159 ".desktop-mode-widgets__chrome"
12160 );
12161 if (chrome) {
12162 dragCleanup = attachDrag(card, chrome, def, ctx, handlers, (next) => {
12163 isFloating = next;
12164 });
12165 }
12166 }
12167 return {
12168 card,
12169 body,
12170 dispose: () => {
12171 for (const fn of resizeCleanups) {
12172 try {
12173 fn();
12174 } catch {
12175 }
12176 }
12177 if (dragCleanup) {
12178 try {
12179 dragCleanup();
12180 } catch {
12181 }
12182 }
12183 card.remove();
12184 }
12185 };
12186 }
12187 function buildChrome(def, onRemove, onRedock) {
12188 const chrome = document.createElement("header");
12189 chrome.className = "desktop-mode-widgets__chrome";
12190 const grip = document.createElement("span");
12191 grip.className = "desktop-mode-widgets__grip";
12192 grip.setAttribute("aria-hidden", "true");
12193 chrome.appendChild(grip);
12194 const title = document.createElement("span");
12195 title.className = "desktop-mode-widgets__title";
12196 title.textContent = def.label;
12197 chrome.appendChild(title);
12198 chrome.appendChild(buildRedockButton(def, onRedock));
12199 const close = buildCloseButton(def, onRemove);
12200 chrome.appendChild(close);
12201 return chrome;
12202 }
12203 function buildRedockButton(def, onRedock) {
12204 const btn = document.createElement("button");
12205 btn.type = "button";
12206 btn.className = "desktop-mode-widgets__card-redock";
12207 btn.setAttribute(
12208 "aria-label",
12209 // translators: %s is the widget label (e.g., "Clock")
12210 sprintf(__("Dock %s back to widget column"), def.label)
12211 );
12212 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>';
12213 btn.addEventListener("click", (e) => {
12214 e.preventDefault();
12215 e.stopPropagation();
12216 onRedock();
12217 });
12218 btn.dataset.noDrag = "true";
12219 return btn;
12220 }
12221 function buildCornerClose(def, onRemove) {
12222 const close = buildCloseButton(def, onRemove);
12223 close.classList.add("desktop-mode-widgets__card-close--corner");
12224 return close;
12225 }
12226 function buildCloseButton(def, onRemove) {
12227 const close = document.createElement("button");
12228 close.type = "button";
12229 close.className = "desktop-mode-widgets__card-close";
12230 close.setAttribute("aria-label", sprintf(__("Remove %s"), def.label));
12231 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>';
12232 close.addEventListener("click", (e) => {
12233 e.preventDefault();
12234 e.stopPropagation();
12235 onRemove();
12236 });
12237 return close;
12238 }
12239 function attachDrag(card, chrome, def, ctx, handlers, setFloating) {
12240 let pointerId = null;
12241 let startX = 0;
12242 let startY = 0;
12243 let initialLeft = 0;
12244 let initialTop = 0;
12245 let committed = false;
12246 const onDown = (e) => {
12247 if (e.button !== 0) {
12248 return;
12249 }
12250 const target2 = e.target;
12251 if (target2 && target2.closest(DRAG_EXCLUDED_SELECTORS)) {
12252 return;
12253 }
12254 e.preventDefault();
12255 pointerId = e.pointerId;
12256 startX = e.clientX;
12257 startY = e.clientY;
12258 committed = false;
12259 initialLeft = parseFloat(card.style.left) || 0;
12260 initialTop = parseFloat(card.style.top) || 0;
12261 chrome.setPointerCapture(pointerId);
12262 };
12263 const commitDrag = () => {
12264 if (!card.classList.contains(FLOATING_CLASS)) {
12265 const parentRect = ctx.floatingParent.getBoundingClientRect();
12266 const rect = card.getBoundingClientRect();
12267 const initial = {
12268 x: rect.left - parentRect.left,
12269 y: rect.top - parentRect.top,
12270 width: rect.width || def.defaultWidth || DEFAULT_WIDTH$1,
12271 height: rect.height || def.defaultHeight || DEFAULT_HEIGHT$1
12272 };
12273 applyGeometry(card, initial);
12274 card.classList.add(FLOATING_CLASS);
12275 setFloating(true);
12276 handlers.onLiberate(initial);
12277 initialLeft = parseFloat(card.style.left) || 0;
12278 initialTop = parseFloat(card.style.top) || 0;
12279 }
12280 card.classList.add(DRAGGING_CLASS);
12281 };
12282 const onMove = (e) => {
12283 if (pointerId === null || e.pointerId !== pointerId) {
12284 return;
12285 }
12286 const dx = e.clientX - startX;
12287 const dy = e.clientY - startY;
12288 if (!committed) {
12289 if (dx * dx + dy * dy < DRAG_THRESHOLD_SQUARED) {
12290 return;
12291 }
12292 committed = true;
12293 commitDrag();
12294 }
12295 const clamped = clampToParent(
12296 initialLeft + dx,
12297 initialTop + dy,
12298 card.offsetWidth,
12299 card.offsetHeight,
12300 ctx.floatingParent
12301 );
12302 card.style.left = `${clamped.x}px`;
12303 card.style.top = `${clamped.y}px`;
12304 };
12305 const onUp = (e) => {
12306 if (pointerId === null || e.pointerId !== pointerId) {
12307 return;
12308 }
12309 try {
12310 chrome.releasePointerCapture(pointerId);
12311 } catch {
12312 }
12313 pointerId = null;
12314 if (!committed) {
12315 return;
12316 }
12317 committed = false;
12318 card.classList.remove(DRAGGING_CLASS);
12319 handlers.onGeometryChanged(currentGeometry(card));
12320 };
12321 chrome.addEventListener("pointerdown", onDown);
12322 chrome.addEventListener("pointermove", onMove);
12323 chrome.addEventListener("pointerup", onUp);
12324 chrome.addEventListener("pointercancel", onUp);
12325 return () => {
12326 chrome.removeEventListener("pointerdown", onDown);
12327 chrome.removeEventListener("pointermove", onMove);
12328 chrome.removeEventListener("pointerup", onUp);
12329 chrome.removeEventListener("pointercancel", onUp);
12330 };
12331 }
12332 function attachResize(card, handle, dir, def, ctx, handlers, isFloating) {
12333 let pointerId = null;
12334 let startX = 0;
12335 let startY = 0;
12336 let startLeft = 0;
12337 let startTop = 0;
12338 let startW = 0;
12339 let startH = 0;
12340 const onDown = (e) => {
12341 if (e.button !== 0) {
12342 return;
12343 }
12344 if (!isFloating() && !isHeightOnlyDir(dir)) {
12345 return;
12346 }
12347 e.preventDefault();
12348 e.stopPropagation();
12349 pointerId = e.pointerId;
12350 startX = e.clientX;
12351 startY = e.clientY;
12352 const rect = card.getBoundingClientRect();
12353 const parentRect = ctx.floatingParent.getBoundingClientRect();
12354 startLeft = rect.left - parentRect.left;
12355 startTop = rect.top - parentRect.top;
12356 startW = rect.width;
12357 startH = rect.height;
12358 handle.setPointerCapture(pointerId);
12359 card.classList.add(RESIZING_CLASS);
12360 };
12361 const onMove = (e) => {
12362 if (pointerId === null || e.pointerId !== pointerId) {
12363 return;
12364 }
12365 const dx = e.clientX - startX;
12366 const dy = e.clientY - startY;
12367 const next = computeResize(
12368 dir,
12369 dx,
12370 dy,
12371 startLeft,
12372 startTop,
12373 startW,
12374 startH,
12375 def,
12376 ctx.floatingParent,
12377 isFloating()
12378 );
12379 if (isFloating()) {
12380 card.style.left = `${next.x}px`;
12381 card.style.top = `${next.y}px`;
12382 card.style.width = `${next.width}px`;
12383 }
12384 card.style.height = `${next.height}px`;
12385 };
12386 const onUp = (e) => {
12387 if (pointerId === null || e.pointerId !== pointerId) {
12388 return;
12389 }
12390 try {
12391 handle.releasePointerCapture(pointerId);
12392 } catch {
12393 }
12394 pointerId = null;
12395 card.classList.remove(RESIZING_CLASS);
12396 handlers.onGeometryChanged(currentGeometry(card));
12397 };
12398 handle.addEventListener("pointerdown", onDown);
12399 handle.addEventListener("pointermove", onMove);
12400 handle.addEventListener("pointerup", onUp);
12401 handle.addEventListener("pointercancel", onUp);
12402 return () => {
12403 handle.removeEventListener("pointerdown", onDown);
12404 handle.removeEventListener("pointermove", onMove);
12405 handle.removeEventListener("pointerup", onUp);
12406 handle.removeEventListener("pointercancel", onUp);
12407 };
12408 }
12409 function allHandleDirs() {
12410 return ["n", "e", "s", "w", "ne", "nw", "se", "sw"];
12411 }
12412 function isHeightOnlyDir(dir) {
12413 return dir === "s";
12414 }
12415 function applyGeometry(card, geometry) {
12416 card.style.left = `${geometry.x}px`;
12417 card.style.top = `${geometry.y}px`;
12418 card.style.width = `${geometry.width}px`;
12419 card.style.height = `${geometry.height}px`;
12420 }
12421 function currentGeometry(card) {
12422 return {
12423 x: parseFloat(card.style.left) || 0,
12424 y: parseFloat(card.style.top) || 0,
12425 width: card.offsetWidth,
12426 height: card.offsetHeight
12427 };
12428 }
12429 function clampToParent(x, y, width, height, parent) {
12430 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
12431 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
12432 const maxX = Math.max(0, parentWidth - width - VIEWPORT_MARGIN);
12433 const maxY = Math.max(0, parentHeight - height - VIEWPORT_MARGIN);
12434 return {
12435 x: Math.min(Math.max(VIEWPORT_MARGIN, x), maxX),
12436 y: Math.min(Math.max(VIEWPORT_MARGIN, y), maxY)
12437 };
12438 }
12439 function computeResize(dir, dx, dy, startLeft, startTop, startW, startH, def, parent, floating) {
12440 const minW = def.minWidth ?? DEFAULT_MIN_WIDTH;
12441 const minH = def.minHeight ?? DEFAULT_MIN_HEIGHT;
12442 const maxW = def.maxWidth ?? Infinity;
12443 const maxH = def.maxHeight ?? Infinity;
12444 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
12445 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
12446 let x = startLeft;
12447 let y = startTop;
12448 let width = startW;
12449 let height = startH;
12450 if (dir === "e" || dir === "ne" || dir === "se") {
12451 width = clamp$1(startW + dx, minW, Math.min(maxW, parentWidth - startLeft));
12452 }
12453 if (dir === "w" || dir === "nw" || dir === "sw") {
12454 const nextWidth = clamp$1(startW - dx, minW, Math.min(maxW, startLeft + startW));
12455 x = startLeft + (startW - nextWidth);
12456 width = nextWidth;
12457 }
12458 if (dir === "s" || dir === "se" || dir === "sw") {
12459 height = clamp$1(
12460 startH + dy,
12461 minH,
12462 Math.min(maxH, parentHeight - startTop)
12463 );
12464 }
12465 if (dir === "n" || dir === "ne" || dir === "nw") {
12466 const nextHeight = clamp$1(startH - dy, minH, Math.min(maxH, startTop + startH));
12467 y = startTop + (startH - nextHeight);
12468 height = nextHeight;
12469 }
12470 if (!floating) {
12471 width = startW;
12472 x = startLeft;
12473 }
12474 return { x, y, width, height };
12475 }
12476 function clamp$1(value, min, max) {
12477 if (max < min) {
12478 return min;
12479 }
12480 return Math.min(Math.max(value, min), max);
12481 }
12482 const IDS_KEY = "desktop-mode-widgets";
12483 const GEOMETRY_KEY$1 = "desktop-mode-widgets-geometry";
12484 function readRawEnabled() {
12485 try {
12486 return window.localStorage.getItem(IDS_KEY);
12487 } catch {
12488 return null;
12489 }
12490 }
12491 function loadEnabledIds() {
12492 const raw = readRawEnabled();
12493 if (raw === null) {
12494 return [];
12495 }
12496 try {
12497 const parsed = JSON.parse(raw);
12498 if (!Array.isArray(parsed)) {
12499 return [];
12500 }
12501 return parsed.filter((x) => typeof x === "string");
12502 } catch {
12503 return [];
12504 }
12505 }
12506 function saveEnabledIds(ids) {
12507 try {
12508 window.localStorage.setItem(IDS_KEY, JSON.stringify(ids));
12509 } catch {
12510 }
12511 }
12512 function loadGeometry$1() {
12513 try {
12514 const raw = window.localStorage.getItem(GEOMETRY_KEY$1);
12515 if (!raw) {
12516 return {};
12517 }
12518 const parsed = JSON.parse(raw);
12519 if (!parsed || typeof parsed !== "object") {
12520 return {};
12521 }
12522 const out = {};
12523 for (const [id, rawEntry] of Object.entries(parsed)) {
12524 const entry = sanitizeGeometry(rawEntry);
12525 if (entry) {
12526 out[id] = entry;
12527 }
12528 }
12529 return out;
12530 } catch {
12531 return {};
12532 }
12533 }
12534 function saveGeometry$1(geometry) {
12535 try {
12536 window.localStorage.setItem(GEOMETRY_KEY$1, JSON.stringify(geometry));
12537 } catch {
12538 }
12539 }
12540 function sanitizeGeometry(raw) {
12541 if (!raw || typeof raw !== "object") {
12542 return null;
12543 }
12544 const { x, y, width, height } = raw;
12545 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) {
12546 return null;
12547 }
12548 return { x, y, width, height };
12549 }
12550 function createWidgetStorage(widgetId) {
12551 const prefix = `desktop-mode.widget.${widgetId}.`;
12552 const safeGet = (key) => {
12553 try {
12554 return localStorage.getItem(prefix + key);
12555 } catch {
12556 return null;
12557 }
12558 };
12559 return {
12560 get(key) {
12561 const raw = safeGet(key);
12562 if (raw === null) {
12563 return null;
12564 }
12565 try {
12566 return JSON.parse(raw);
12567 } catch {
12568 return null;
12569 }
12570 },
12571 set(key, value) {
12572 try {
12573 localStorage.setItem(prefix + key, JSON.stringify(value));
12574 } catch {
12575 }
12576 },
12577 remove(key) {
12578 try {
12579 localStorage.removeItem(prefix + key);
12580 } catch {
12581 }
12582 },
12583 clear() {
12584 try {
12585 for (let i = localStorage.length - 1; i >= 0; i--) {
12586 const key = localStorage.key(i);
12587 if (key && key.startsWith(prefix)) {
12588 localStorage.removeItem(key);
12589 }
12590 }
12591 } catch {
12592 }
12593 }
12594 };
12595 }
12596 const DEFAULT_ENABLED_IDS = ["clock"];
12597 class WidgetLayer {
12598 /**
12599 * @param root The column element (`#desktop-mode-widgets`).
12600 * @param pluginUrl Absolute plugin URL — passed to widget ctx.
12601 * @param floatingHost Parent for liberated (floating) widgets.
12602 * Defaults to the column's parent (the desktop
12603 * area) so floats are bounded by the visible
12604 * desktop, not the 320 px-wide column.
12605 */
12606 constructor(root, pluginUrl, floatingHost) {
12607 this.mounted = /* @__PURE__ */ new Map();
12608 this.generation = 0;
12609 this.root = root;
12610 this.pluginUrl = pluginUrl;
12611 this.enabledIds = loadEnabledIds();
12612 this.geometry = loadGeometry$1();
12613 this.floatingHost = floatingHost ?? root.parentElement ?? root;
12614 this.listEl = document.createElement("div");
12615 this.listEl.className = "desktop-mode-widgets__list";
12616 this.root.appendChild(this.listEl);
12617 this.addTile = this.buildAddTile();
12618 this.root.appendChild(this.addTile);
12619 this.paintEmptyState();
12620 }
12621 /**
12622 * Mount every widget the user has enabled (per localStorage).
12623 * Called once during shell boot, AFTER the registry seed has run
12624 * so built-ins are available. Safe to call multiple times — the
12625 * `mounted` map dedupes.
12626 */
12627 hydrate() {
12628 if (readRawEnabled() === null) {
12629 this.enabledIds = DEFAULT_ENABLED_IDS.filter(
12630 (id) => !!get(id)
12631 );
12632 saveEnabledIds(this.enabledIds);
12633 }
12634 for (const id of this.enabledIds) {
12635 if (this.mounted.has(id)) {
12636 continue;
12637 }
12638 this.mountById(id);
12639 }
12640 this.paintEmptyState();
12641 }
12642 /**
12643 * Add a widget by id — called by the picker after the user
12644 * selects an available entry. Idempotent.
12645 */
12646 add(id) {
12647 if (this.enabledIds.includes(id)) {
12648 return;
12649 }
12650 if (!get(id)) {
12651 return;
12652 }
12653 this.enabledIds.push(id);
12654 saveEnabledIds(this.enabledIds);
12655 this.mountById(id);
12656 this.paintEmptyState();
12657 doAction(HOOKS.WIDGET_ADDED, { id });
12658 refreshWidgetPicker();
12659 }
12660 /**
12661 * Remove a widget by id — called from the card's × button and
12662 * from the picker. Idempotent.
12663 */
12664 remove(id) {
12665 const before = this.enabledIds.length;
12666 this.enabledIds = this.enabledIds.filter((e) => e !== id);
12667 if (this.enabledIds.length === before) {
12668 return;
12669 }
12670 saveEnabledIds(this.enabledIds);
12671 if (this.geometry[id]) {
12672 delete this.geometry[id];
12673 saveGeometry$1(this.geometry);
12674 }
12675 this.unmountById(id);
12676 this.paintEmptyState();
12677 doAction(HOOKS.WIDGET_REMOVED, { id });
12678 refreshWidgetPicker();
12679 }
12680 /** Public read for the picker / external callers. */
12681 getEnabledIds() {
12682 return [...this.enabledIds];
12683 }
12684 /**
12685 * Mount a widget ONLY if it's already in the user's enabled
12686 * list AND not currently mounted. No-op when the widget isn't
12687 * enabled (user never opted in) and no-op when it's already on
12688 * screen. Used by the server-driven sync: when a plugin
12689 * activates mid-session, its widget def registers via the
12690 * sync's path; if the user had previously enabled that widget
12691 * (in a prior session or before the plugin was deactivated),
12692 * we want to bring it back on screen without toggling the
12693 * "enabled" state or firing a `WIDGET_ADDED` action.
12694 *
12695 * The net behaviour is "rehydrate this one widget now that
12696 * its def is finally registered," which is subtly different
12697 * from `ensureMounted` (which OPT-INs the user into enabling
12698 * the widget for the first time).
12699 */
12700 mountIfEnabled(id) {
12701 if (!get(id)) {
12702 return;
12703 }
12704 if (!this.enabledIds.includes(id)) {
12705 return;
12706 }
12707 if (this.mounted.has(id)) {
12708 return;
12709 }
12710 this.mountById(id);
12711 this.paintEmptyState();
12712 }
12713 /**
12714 * Unmount a widget without touching the persisted enablement.
12715 * Used by the server-driven widget-registry sync: when a plugin
12716 * deactivates mid-session, its widget defs disappear from the
12717 * registry and we need to pull any mounted instance off the
12718 * screen — but we deliberately KEEP the id in the user's
12719 * enabled list so re-activating the plugin re-mounts it
12720 * automatically through `hydrate()`.
12721 *
12722 * Idempotent; a no-op when the widget isn't currently mounted.
12723 */
12724 unmount(id) {
12725 if (!this.mounted.has(id)) {
12726 return;
12727 }
12728 this.unmountById(id);
12729 this.paintEmptyState();
12730 }
12731 /**
12732 * Guarantee the widget identified by `id` is currently mounted,
12733 * adding it to the enabled list if it isn't. No-op when the
12734 * widget is already on screen. Intended for companion plugins
12735 * that want to pin their widget programmatically — a monitor
12736 * plugin that auto-pins itself on the first error burst, a
12737 * first-run onboarding flow that ensures the quick-start widget
12738 * is present, etc.
12739 *
12740 * Returns `true` when the widget is mounted (either newly added
12741 * or already present), `false` when the id isn't registered —
12742 * callers can branch on the failure without having to maintain
12743 * their own registry snapshot.
12744 */
12745 ensureMounted(id) {
12746 if (!get(id)) {
12747 return false;
12748 }
12749 if (this.enabledIds.includes(id)) {
12750 return true;
12751 }
12752 this.add(id);
12753 return true;
12754 }
12755 /**
12756 * Tear down every widget. Called on shell unload via `pagehide`
12757 * so intervals / RAF loops stop before the beacon flush.
12758 */
12759 disposeAll() {
12760 for (const id of Array.from(this.mounted.keys())) {
12761 this.unmountById(id);
12762 }
12763 }
12764 // --- Internal ---------------------------------------------------
12765 mountById(id) {
12766 const def = get(id);
12767 if (!def) {
12768 return;
12769 }
12770 const gen = ++this.generation;
12771 const initialGeometry = def.movable === true ? this.geometry[id] : void 0;
12772 const frame = buildFrame(
12773 def,
12774 { floatingParent: this.floatingHost, geometry: initialGeometry },
12775 {
12776 onRemove: () => this.remove(id),
12777 onGeometryChanged: (geom) => this.persistGeometry(id, geom),
12778 onLiberate: (geom) => this.liberate(id, geom),
12779 onRedock: () => this.redock(id)
12780 }
12781 );
12782 const floating = !!initialGeometry;
12783 const record = {
12784 id,
12785 frame,
12786 generation: gen,
12787 teardown: null,
12788 floating
12789 };
12790 this.mounted.set(id, record);
12791 this.placeCard(frame.card, floating);
12792 const ctx = {
12793 id,
12794 pluginUrl: this.pluginUrl,
12795 storage: createWidgetStorage(id)
12796 };
12797 doAction(HOOKS.WIDGET_MOUNTING, { id, container: frame.body, ctx });
12798 const onResolve = (teardown) => {
12799 const current = this.mounted.get(id);
12800 if (!current || current.generation !== gen) {
12801 try {
12802 teardown();
12803 } catch {
12804 }
12805 return;
12806 }
12807 current.teardown = teardown;
12808 doAction(HOOKS.WIDGET_MOUNTED, { id, container: frame.body, ctx });
12809 };
12810 let result;
12811 try {
12812 result = def.mount(frame.body, ctx);
12813 } catch (err) {
12814 this.handleMountFailure(id, err);
12815 return;
12816 }
12817 if (isThenable(result)) {
12818 result.then(onResolve, (err) => {
12819 if (this.mounted.get(id)?.generation === gen) {
12820 this.handleMountFailure(id, err);
12821 }
12822 });
12823 return;
12824 }
12825 onResolve(result);
12826 }
12827 unmountById(id) {
12828 const record = this.mounted.get(id);
12829 if (!record) {
12830 return;
12831 }
12832 doAction(HOOKS.WIDGET_UNMOUNTING, { id });
12833 try {
12834 record.teardown?.();
12835 } catch (err) {
12836 doAction(HOOKS.SHELL_ERROR, { scope: "widget-teardown", id, error: err });
12837 if (typeof console !== "undefined") {
12838 console.error(
12839 `[desktop-mode] Widget "${id}" teardown threw:`,
12840 err
12841 );
12842 }
12843 }
12844 this.generation++;
12845 record.frame.dispose();
12846 this.mounted.delete(id);
12847 }
12848 handleMountFailure(id, err) {
12849 const record = this.mounted.get(id);
12850 if (record) {
12851 record.frame.dispose();
12852 this.mounted.delete(id);
12853 }
12854 doAction(HOOKS.WIDGET_MOUNT_FAILED, { id, error: err });
12855 doAction(HOOKS.SHELL_ERROR, { scope: "widget-mount", id, error: err });
12856 if (typeof console !== "undefined") {
12857 console.error(
12858 `[desktop-mode] Widget "${id}" failed to mount:`,
12859 err
12860 );
12861 }
12862 }
12863 buildAddTile() {
12864 const tile2 = document.createElement("button");
12865 tile2.type = "button";
12866 tile2.className = "desktop-mode-widgets__add";
12867 tile2.setAttribute("aria-label", __("Add widget"));
12868 const plus = document.createElement("span");
12869 plus.className = "desktop-mode-widgets__add-plus";
12870 plus.setAttribute("aria-hidden", "true");
12871 plus.textContent = "+";
12872 const label = document.createElement("span");
12873 label.className = "desktop-mode-widgets__add-label";
12874 label.textContent = __("Add widget");
12875 tile2.appendChild(plus);
12876 tile2.appendChild(label);
12877 tile2.addEventListener("click", (e) => {
12878 e.preventDefault();
12879 e.stopPropagation();
12880 openWidgetPicker({
12881 anchor: tile2,
12882 registry: () => all(),
12883 enabledIds: () => [...this.enabledIds],
12884 onAdd: (id) => this.add(id)
12885 });
12886 });
12887 return tile2;
12888 }
12889 /**
12890 * Drop a card into the right parent based on its floating state.
12891 * Docked cards append to the column list above the `+` tile;
12892 * floating cards append to the desktop-area-level host so they
12893 * sit above the wallpaper and can range across the viewport.
12894 */
12895 placeCard(card, floating) {
12896 if (floating) {
12897 this.floatingHost.appendChild(card);
12898 } else {
12899 this.listEl.appendChild(card);
12900 }
12901 }
12902 /**
12903 * Move a widget from the column into the floating host. Called by
12904 * the frame on the user's first drag of a movable widget.
12905 */
12906 liberate(id, geometry) {
12907 const record = this.mounted.get(id);
12908 if (!record || record.floating) {
12909 return;
12910 }
12911 record.floating = true;
12912 this.floatingHost.appendChild(record.frame.card);
12913 applyGeometry(record.frame.card, geometry);
12914 this.persistGeometry(id, geometry);
12915 this.paintEmptyState();
12916 }
12917 /**
12918 * Inverse of {@link liberate}: move a floating card back into
12919 * the column and drop its persisted geometry so a subsequent
12920 * shell boot brings it up docked. Called when the user clicks
12921 * the re-dock button in the card's chrome header, or
12922 * programmatically by companion plugins via
12923 * `wp.desktop.widgets.redock( id )` /
12924 * `wp.desktop.widgetLayer.redock( id )`.
12925 *
12926 * Idempotent — a docked widget silently no-ops, an unknown id
12927 * silently no-ops. The `--floating` class on the card is
12928 * removed as part of the same write so CSS rules that depend
12929 * on it (re-dock button visibility, absolute positioning) flip
12930 * back in one paint.
12931 *
12932 * @since 0.7.0 (private)
12933 * @since 0.25.0 (public)
12934 */
12935 redock(id) {
12936 const record = this.mounted.get(id);
12937 if (!record || !record.floating) {
12938 return;
12939 }
12940 record.floating = false;
12941 if (this.geometry[id]) {
12942 delete this.geometry[id];
12943 saveGeometry$1(this.geometry);
12944 }
12945 const card = record.frame.card;
12946 card.classList.remove("desktop-mode-widgets__card--floating");
12947 card.style.left = "";
12948 card.style.top = "";
12949 card.style.width = "";
12950 card.style.height = "";
12951 this.listEl.appendChild(card);
12952 this.paintEmptyState();
12953 }
12954 persistGeometry(id, geometry) {
12955 this.geometry[id] = geometry;
12956 saveGeometry$1(this.geometry);
12957 }
12958 /**
12959 * Toggle a `--has-widgets` modifier so CSS can hide the column's
12960 * decorative backdrop when nothing's mounted (keeps the empty
12961 * state clean — just the `+` tile floating in the corner).
12962 *
12963 * Floating widgets don't count toward "has widgets" in the column
12964 * sense — if every enabled widget is floating, the column itself
12965 * shows only the empty state + add tile.
12966 */
12967 paintEmptyState() {
12968 let docked = 0;
12969 for (const record of this.mounted.values()) {
12970 if (!record.floating) {
12971 docked++;
12972 }
12973 }
12974 this.root.classList.toggle(
12975 "desktop-mode-widgets--has-widgets",
12976 docked > 0
12977 );
12978 }
12979 }
12980 function isThenable(x) {
12981 return !!x && (typeof x === "object" || typeof x === "function") && typeof x.then === "function";
12982 }
12983 const DEFAULT_NATIVE_MIN_WIDTH = 280;
12984 const DEFAULT_NATIVE_MIN_HEIGHT = 220;
12985 const DEFAULT_NATIVE_WIDTH = 520;
12986 const DEFAULT_NATIVE_HEIGHT = 400;
12987 function buildIframeContentRender(cfg, cleanups, windowId) {
12988 return (body) => {
12989 const iframe = document.createElement("iframe");
12990 iframe.style.width = "100%";
12991 iframe.style.height = "100%";
12992 iframe.style.border = "0";
12993 iframe.setAttribute("src", cfg.url);
12994 if (typeof cfg.sandbox === "string" && cfg.sandbox !== "") {
12995 iframe.setAttribute("sandbox", cfg.sandbox);
12996 }
12997 body.style.padding = "0";
12998 body.appendChild(iframe);
12999 const unregisterSynth = registerSyntheticIframe(windowId, iframe);
13000 cleanups.push(unregisterSynth);
13001 let targetOrigin;
13002 try {
13003 targetOrigin = new URL(cfg.url, window.location.origin).origin;
13004 } catch {
13005 targetOrigin = window.location.origin;
13006 }
13007 let resolveReady = null;
13008 const readyPromise = new Promise((resolve2) => {
13009 resolveReady = resolve2;
13010 });
13011 const onLoad = () => {
13012 if (cfg.bridge) {
13013 try {
13014 const doc = iframe.contentDocument;
13015 if (doc && !doc.querySelector("script[data-desktop-mode-iframe-bridge]")) {
13016 const bridgeUrl = window.desktopModeConfig?.iframeBridgeUrl;
13017 if (bridgeUrl) {
13018 const s = doc.createElement("script");
13019 s.src = bridgeUrl;
13020 s.setAttribute("data-desktop-mode-iframe-bridge", "1");
13021 doc.head?.appendChild(s);
13022 }
13023 }
13024 } catch {
13025 }
13026 }
13027 markWindowContentReady(windowId);
13028 resolveReady?.();
13029 };
13030 iframe.addEventListener("load", onLoad);
13031 const onMessage = (e) => {
13032 if (!iframe.contentWindow || e.source !== iframe.contentWindow) {
13033 return;
13034 }
13035 if (e.origin !== targetOrigin && e.origin !== window.location.origin) {
13036 return;
13037 }
13038 const data = e.data;
13039 if (data && typeof data === "object" && typeof data.type === "string" && data.type.startsWith("desktop-mode-bridge-")) {
13040 const bridgeRouter = window.__desktopModeConnectionBridge;
13041 bridgeRouter?.routeIncomingFromIframe(data, windowId);
13042 }
13043 if (data && typeof data === "object" && data.type === "desktop-mode-window-publish" && typeof data.channel === "string" && data.channel !== "") {
13044 dispatchFromWindow(
13045 windowId,
13046 data.channel,
13047 data.payload
13048 );
13049 }
13050 try {
13051 cfg.onMessage?.(e.data);
13052 } catch (err) {
13053 if (typeof console !== "undefined") {
13054 console.error(
13055 "[desktop-mode] iframeContent.onMessage threw:",
13056 err
13057 );
13058 }
13059 }
13060 };
13061 window.addEventListener("message", onMessage);
13062 cleanups.push(() => {
13063 window.removeEventListener("message", onMessage);
13064 iframe.removeEventListener("load", onLoad);
13065 });
13066 return readyPromise;
13067 };
13068 }
13069 function createRegisterWindow(manager) {
13070 return async (def) => {
13071 const userRender = def.render;
13072 let render2 = userRender;
13073 const cleanups = [];
13074 if (def.iframeContent) {
13075 if (userRender && typeof console !== "undefined") {
13076 console.warn(
13077 "[desktop-mode] registerWindow: both `render` and `iframeContent` provided — ignoring `render` and using the iframe shorthand. Drop one."
13078 );
13079 }
13080 render2 = buildIframeContentRender(
13081 def.iframeContent,
13082 cleanups,
13083 def.id
13084 );
13085 }
13086 const userOnClose = def.onClose;
13087 const onClose = cleanups.length ? () => {
13088 for (const fn of cleanups) {
13089 try {
13090 fn();
13091 } catch {
13092 }
13093 }
13094 userOnClose?.();
13095 } : userOnClose;
13096 const win = await manager.open({
13097 id: def.id,
13098 baseId: def.baseId || def.id,
13099 native: true,
13100 url: def.url || `#${def.id}`,
13101 title: def.title,
13102 icon: def.icon,
13103 x: def.x ?? 0,
13104 y: def.y ?? 0,
13105 width: def.width ?? DEFAULT_NATIVE_WIDTH,
13106 height: def.height ?? DEFAULT_NATIVE_HEIGHT,
13107 minWidth: def.minWidth ?? DEFAULT_NATIVE_MIN_WIDTH,
13108 minHeight: def.minHeight ?? DEFAULT_NATIVE_MIN_HEIGHT,
13109 render: render2,
13110 onClose,
13111 onResize: def.onResize,
13112 autofocus: def.autofocus,
13113 initialState: def.initialState,
13114 ownerHandle: def.ownerHandle,
13115 multi: def.multi,
13116 desktopId: def.desktopId
13117 });
13118 return win;
13119 };
13120 }
13121 let onWindowInstanceCounter = 0;
13122 function onWindow(id, handlers, options = {}) {
13123 const namespace = `desktop-mode/on-window/${id}/${++onWindowInstanceCounter}`;
13124 const persistent = options.persistent === true;
13125 const bindings = [
13126 ["opened", HOOKS.WINDOW_OPENED],
13127 ["reopened", HOOKS.WINDOW_REOPENED],
13128 ["focused", HOOKS.WINDOW_FOCUSED],
13129 ["blurred", HOOKS.WINDOW_BLURRED],
13130 ["closing", HOOKS.WINDOW_CLOSING],
13131 ["closed", HOOKS.WINDOW_CLOSED],
13132 ["minimized", HOOKS.WINDOW_MINIMIZED],
13133 ["restored", HOOKS.WINDOW_RESTORED],
13134 ["maximized", HOOKS.WINDOW_MAXIMIZED],
13135 ["unmaximized", HOOKS.WINDOW_UNMAXIMIZED],
13136 ["fullscreenEntered", HOOKS.WINDOW_FULLSCREEN_ENTERED],
13137 ["fullscreenExited", HOOKS.WINDOW_FULLSCREEN_EXITED],
13138 ["resized", HOOKS.WINDOW_RESIZED],
13139 ["bodyResized", HOOKS.WINDOW_BODY_RESIZED],
13140 ["boundsChanged", HOOKS.WINDOW_BOUNDS_CHANGED]
13141 ];
13142 const registered = [];
13143 let disposed = false;
13144 const unsubscribe = () => {
13145 if (disposed) {
13146 return;
13147 }
13148 disposed = true;
13149 for (const hookName2 of registered) {
13150 removeAction(hookName2, namespace);
13151 }
13152 };
13153 for (const [key, hookName2] of bindings) {
13154 const handler = handlers[key];
13155 if (!handler) {
13156 continue;
13157 }
13158 registered.push(hookName2);
13159 addAction(hookName2, namespace, (payload) => {
13160 const p = payload;
13161 if (p.windowId !== id) {
13162 return;
13163 }
13164 const { windowId: _w, ...rest } = p;
13165 handler(rest);
13166 if (key === "closed" && !persistent) {
13167 unsubscribe();
13168 }
13169 });
13170 }
13171 return unsubscribe;
13172 }
13173 function createNativeWindowSync(deps2) {
13174 const { manager, appendSystemTile, removeSystemTile } = deps2;
13175 const registered = /* @__PURE__ */ new Set();
13176 const injectedTemplates = /* @__PURE__ */ new Set();
13177 const loadedScripts = /* @__PURE__ */ new Set();
13178 const loadedStyles = /* @__PURE__ */ new Set();
13179 const entriesById = /* @__PURE__ */ new Map();
13180 const resolveSizeForEntry = (entry) => {
13181 const saved = loadNativeWindowGeometry(entry.id);
13182 if (!saved) {
13183 return { width: entry.width, height: entry.height };
13184 }
13185 return {
13186 width: Math.max(saved.width, entry.minWidth),
13187 height: Math.max(saved.height, entry.minHeight)
13188 };
13189 };
13190 const ensureTemplate = (entry) => {
13191 if (injectedTemplates.has(entry.templateId)) {
13192 return;
13193 }
13194 if (document.getElementById(entry.templateId)) {
13195 injectedTemplates.add(entry.templateId);
13196 return;
13197 }
13198 if (!entry.templateHtml) {
13199 return;
13200 }
13201 const tpl = document.createElement("template");
13202 tpl.id = entry.templateId;
13203 tpl.innerHTML = entry.templateHtml;
13204 document.body.appendChild(tpl);
13205 injectedTemplates.add(entry.templateId);
13206 };
13207 const ensureStyle = (entry) => {
13208 const url = entry.styleUrl;
13209 if (!url || loadedStyles.has(url)) {
13210 return;
13211 }
13212 const safeUrl = url.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
13213 const existing = document.head.querySelector(
13214 `link[rel="stylesheet"][href="${safeUrl}"]`
13215 );
13216 if (!existing) {
13217 const link = document.createElement("link");
13218 link.rel = "stylesheet";
13219 link.href = url;
13220 if (entry.styleHandle) {
13221 link.dataset.desktopModeStyleHandle = entry.styleHandle;
13222 }
13223 document.head.appendChild(link);
13224 }
13225 if (Array.isArray(entry.styleInline)) {
13226 for (const css2 of entry.styleInline) {
13227 if (typeof css2 !== "string" || css2 === "") {
13228 continue;
13229 }
13230 const style = document.createElement("style");
13231 if (entry.styleHandle) {
13232 style.dataset.desktopModeStyleHandle = entry.styleHandle;
13233 }
13234 style.textContent = css2;
13235 document.head.appendChild(style);
13236 }
13237 }
13238 loadedStyles.add(url);
13239 };
13240 const ensureScript = async (entry) => {
13241 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
13242 return;
13243 }
13244 try {
13245 await loadVendorScript(entry.scriptUrl, {
13246 translations: entry.scriptTranslations,
13247 l10n: entry.scriptL10n,
13248 before: entry.scriptBefore,
13249 after: entry.scriptAfter
13250 });
13251 } catch (err) {
13252 doAction(HOOKS.SHELL_ERROR, {
13253 scope: "native-window-script-load",
13254 id: entry.id,
13255 error: err
13256 });
13257 }
13258 loadedScripts.add(entry.scriptUrl);
13259 };
13260 const openFromEntry = (entry) => {
13261 const globalRegistry = window.desktopModeNativeWindows || {};
13262 const render2 = globalRegistry[entry.id];
13263 const finalRender = (body, ctx) => {
13264 body.appendChild(cloneTemplate(entry.templateId));
13265 return render2?.(body, ctx);
13266 };
13267 const size = resolveSizeForEntry(entry);
13268 void manager.open({
13269 id: entry.id,
13270 baseId: entry.id,
13271 native: true,
13272 url: `#${entry.id}`,
13273 title: entry.title,
13274 icon: entry.icon,
13275 width: size.width,
13276 height: size.height,
13277 minWidth: entry.minWidth,
13278 minHeight: entry.minHeight,
13279 render: finalRender,
13280 autofocus: entry.autofocus,
13281 ownerHandle: entry.ownerHandle || entry.scriptHandle
13282 });
13283 };
13284 const openNewFromEntry = (entry) => {
13285 const globalRegistry = window.desktopModeNativeWindows || {};
13286 const render2 = globalRegistry[entry.id];
13287 const finalRender = (body, ctx) => {
13288 body.appendChild(cloneTemplate(entry.templateId));
13289 return render2?.(body, ctx);
13290 };
13291 const size = resolveSizeForEntry(entry);
13292 void manager.openNew({
13293 id: entry.id,
13294 baseId: entry.id,
13295 native: true,
13296 url: `#${entry.id}`,
13297 title: entry.title,
13298 icon: entry.icon,
13299 width: size.width,
13300 height: size.height,
13301 minWidth: entry.minWidth,
13302 minHeight: entry.minHeight,
13303 initialState: "normal",
13304 render: finalRender,
13305 autofocus: entry.autofocus,
13306 ownerHandle: entry.ownerHandle || entry.scriptHandle
13307 });
13308 };
13309 const registerTile = async (entry) => {
13310 if (registered.has(entry.id)) {
13311 return;
13312 }
13313 if ("none" === entry.placement) {
13314 ensureTemplate(entry);
13315 ensureStyle(entry);
13316 await ensureScript(entry);
13317 registered.add(entry.id);
13318 return;
13319 }
13320 ensureTemplate(entry);
13321 ensureStyle(entry);
13322 await ensureScript(entry);
13323 appendSystemTile({
13324 id: entry.id,
13325 title: entry.title,
13326 icon: entry.icon,
13327 isOpen: () => !!manager.getById(entry.id),
13328 onOpen: () => openFromEntry(entry)
13329 });
13330 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: entry.id });
13331 registered.add(entry.id);
13332 };
13333 const unregisterTile = (id) => {
13334 if (!registered.has(id)) {
13335 return;
13336 }
13337 removeSystemTile(id);
13338 registered.delete(id);
13339 entriesById.delete(id);
13340 };
13341 const sync = async (list2) => {
13342 const incoming = /* @__PURE__ */ new Set();
13343 for (const entry of list2) {
13344 incoming.add(entry.id);
13345 entriesById.set(entry.id, entry);
13346 }
13347 for (const id of Array.from(registered)) {
13348 if (!incoming.has(id)) {
13349 unregisterTile(id);
13350 }
13351 }
13352 for (const entry of list2) {
13353 if (!registered.has(entry.id)) {
13354 await registerTile(entry);
13355 }
13356 }
13357 };
13358 const openById = (id, opts = {}) => {
13359 const entry = entriesById.get(id);
13360 if (!entry) {
13361 return false;
13362 }
13363 activity.publish("desktop-mode/open-requested", {
13364 windowId: id,
13365 source: opts.source ?? "api"
13366 });
13367 openFromEntry(entry);
13368 return true;
13369 };
13370 const openNewById = (id, opts = {}) => {
13371 const entry = entriesById.get(id);
13372 if (!entry) {
13373 return false;
13374 }
13375 activity.publish("desktop-mode/open-requested", {
13376 windowId: id,
13377 source: opts.source ?? "api"
13378 });
13379 openNewFromEntry(entry);
13380 return true;
13381 };
13382 addAction(
13383 HOOKS.WINDOW_RESIZE_END,
13384 "desktop-mode-native-window-geometry",
13385 (payload) => {
13386 const p = payload;
13387 const windowId = p?.windowId;
13388 const width = p?.width;
13389 const height = p?.height;
13390 if (!windowId || typeof width !== "number" || typeof height !== "number") {
13391 return;
13392 }
13393 const win = manager.getById(windowId);
13394 if (!win) {
13395 return;
13396 }
13397 if (win.state !== "normal") {
13398 return;
13399 }
13400 const baseId = win.config.baseId || win.id;
13401 saveNativeWindowGeometry(baseId, { width, height });
13402 if (win.element) {
13403 saveNativeWindowPosition(baseId, {
13404 x: win.element.offsetLeft,
13405 y: win.element.offsetTop
13406 });
13407 }
13408 }
13409 );
13410 addAction(
13411 HOOKS.WINDOW_DRAG_END,
13412 "desktop-mode-native-window-geometry",
13413 (payload) => {
13414 const windowId = payload?.windowId;
13415 if (!windowId) {
13416 return;
13417 }
13418 const win = manager.getById(windowId);
13419 if (!win) {
13420 return;
13421 }
13422 if (win.state !== "normal") {
13423 return;
13424 }
13425 if (!win.element) {
13426 return;
13427 }
13428 const baseId = win.config.baseId || win.id;
13429 saveNativeWindowGeometry(baseId, {
13430 width: win.element.offsetWidth,
13431 height: win.element.offsetHeight
13432 });
13433 saveNativeWindowPosition(baseId, {
13434 x: win.element.offsetLeft,
13435 y: win.element.offsetTop
13436 });
13437 }
13438 );
13439 addAction(
13440 HOOKS.WINDOW_MAXIMIZED,
13441 "desktop-mode-native-window-geometry",
13442 (payload) => {
13443 const windowId = payload?.windowId;
13444 if (!windowId) {
13445 return;
13446 }
13447 const win = manager.getById(windowId);
13448 if (!win) {
13449 return;
13450 }
13451 const baseId = win.config.baseId || win.id;
13452 const entry = entriesById.get(baseId);
13453 const defaults = entry ? { width: entry.width, height: entry.height } : { width: win.config.width, height: win.config.height };
13454 setNativeWindowSavedState(baseId, "maximized", defaults);
13455 }
13456 );
13457 addAction(
13458 HOOKS.WINDOW_UNMAXIMIZED,
13459 "desktop-mode-native-window-geometry",
13460 (payload) => {
13461 const windowId = payload?.windowId;
13462 if (!windowId) {
13463 return;
13464 }
13465 const win = manager.getById(windowId);
13466 if (!win) {
13467 return;
13468 }
13469 const baseId = win.config.baseId || win.id;
13470 setNativeWindowSavedState(baseId, null);
13471 }
13472 );
13473 return { sync, openById, openNewById };
13474 }
13475 function cloneTemplate(template) {
13476 let tpl = null;
13477 if (typeof template === "string") {
13478 const found = document.getElementById(template);
13479 if (found instanceof HTMLTemplateElement) {
13480 tpl = found;
13481 }
13482 } else {
13483 tpl = template;
13484 }
13485 if (!tpl) {
13486 throw new Error(
13487 `[desktop-mode] cloneTemplate: no <template> found for ${typeof template === "string" ? `#${template}` : "<reference>"}`
13488 );
13489 }
13490 return tpl.content.cloneNode(true);
13491 }
13492 function renderIcon(icon, opts) {
13493 const className = opts.className ?? "";
13494 const title = opts.title ?? "";
13495 if (typeof icon === "string" && icon.startsWith("dashicons-")) {
13496 const el = document.createElement("span");
13497 el.className = `dashicons ${icon} ${className}`.trim();
13498 el.setAttribute("aria-hidden", "true");
13499 return el;
13500 }
13501 if (typeof icon === "string" && icon.startsWith("data:image/svg+xml;base64,")) {
13502 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
13503 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
13504 const el = document.createElement("span");
13505 el.className = className;
13506 el.setAttribute("aria-hidden", "true");
13507 el.style.backgroundImage = `url("${icon}")`;
13508 el.style.backgroundRepeat = "no-repeat";
13509 el.style.backgroundPosition = "center";
13510 el.style.backgroundSize = "contain";
13511 el.style.display = "inline-block";
13512 return el;
13513 }
13514 }
13515 if (typeof icon === "string" && /^data:image\/(png|jpeg|jpg|gif|webp|x-icon|vnd\.microsoft\.icon);base64,/i.test(icon)) {
13516 const commaIdx = icon.indexOf(",");
13517 const payload = commaIdx >= 0 ? icon.slice(commaIdx + 1) : "";
13518 if (/^[A-Za-z0-9+/=]+$/.test(payload)) {
13519 return makeImgIcon(icon, className);
13520 }
13521 }
13522 if (typeof icon === "string" && (icon.startsWith("http://") || icon.startsWith("https://"))) {
13523 return makeImgIcon(icon, className);
13524 }
13525 const span = document.createElement("span");
13526 span.className = `${className} desktop-mode-icon-letter`.trim();
13527 span.setAttribute("aria-hidden", "true");
13528 const letters = letterFromTitle(title);
13529 span.textContent = letters;
13530 const hue = hashTitleToHue(title);
13531 span.style.backgroundColor = `hsl( ${hue}, 60%, 45% )`;
13532 span.style.color = "#fff";
13533 span.style.display = "inline-flex";
13534 span.style.alignItems = "center";
13535 span.style.justifyContent = "center";
13536 span.style.fontWeight = "600";
13537 span.style.borderRadius = "4px";
13538 return span;
13539 }
13540 function makeImgIcon(src, className) {
13541 const img = document.createElement("img");
13542 img.className = className;
13543 img.src = src;
13544 img.alt = "";
13545 img.setAttribute("aria-hidden", "true");
13546 img.draggable = false;
13547 return img;
13548 }
13549 function letterFromTitle(title) {
13550 const trimmed = (title ?? "").trim();
13551 if (trimmed === "") {
13552 return "?";
13553 }
13554 const words = trimmed.split(/\s+/);
13555 if (words.length >= 2) {
13556 return (words[0][0] + words[1][0]).toUpperCase();
13557 }
13558 const first = words[0];
13559 if (first.length >= 2) {
13560 return first.slice(0, 2).toUpperCase();
13561 }
13562 return first.toUpperCase();
13563 }
13564 const BADGE_CLASS = "desktop-mode-icon__badge";
13565 const _badges = /* @__PURE__ */ new Map();
13566 function _safeBadge(count) {
13567 return Math.max(0, Math.floor(Number(count) || 0));
13568 }
13569 function setIconBadge(iconId, count) {
13570 if (!iconId) {
13571 return;
13572 }
13573 const tile2 = _findIconTile(iconId);
13574 if (!tile2) {
13575 return;
13576 }
13577 const safe = _safeBadge(count);
13578 const previous = _badges.get(iconId) ?? 0;
13579 if (safe === previous) {
13580 return;
13581 }
13582 if (safe === 0) {
13583 _badges.delete(iconId);
13584 } else {
13585 _badges.set(iconId, safe);
13586 }
13587 _paintBadgeNode(tile2, safe);
13588 activity.publish("desktop-mode/badge-changed", {
13589 itemId: iconId,
13590 count: safe,
13591 rail: "icon"
13592 });
13593 doAction(HOOKS.ICON_BADGE_CHANGED, {
13594 iconId,
13595 count: safe,
13596 previousCount: previous
13597 });
13598 }
13599 function clearIconBadge(iconId) {
13600 setIconBadge(iconId, 0);
13601 }
13602 function getIconBadge(iconId) {
13603 return _badges.get(iconId) ?? 0;
13604 }
13605 const iconsApi = {
13606 setBadge: setIconBadge,
13607 clearBadge: clearIconBadge,
13608 getBadge: getIconBadge
13609 };
13610 function fingerprintIcons(icons) {
13611 if (!icons || icons.length === 0) {
13612 return "";
13613 }
13614 return icons.map(
13615 (i) => `${i.id}|${i.title}|${i.icon}|${i.window ?? ""}|${i.url ?? ""}|${i.position ?? 0}|${i.pinned ? 1 : 0}`
13616 ).join(";");
13617 }
13618 let _lastFingerprint = "";
13619 function renderDesktopIcons(host, icons, deps2) {
13620 const fp = fingerprintIcons(icons);
13621 if (fp === _lastFingerprint && host.querySelector(":scope > .desktop-mode-icons")) {
13622 return;
13623 }
13624 _lastFingerprint = fp;
13625 const existing = host.querySelector(":scope > .desktop-mode-icons");
13626 if (existing) {
13627 existing.remove();
13628 }
13629 if (!icons || icons.length === 0) {
13630 return;
13631 }
13632 const container = document.createElement("div");
13633 container.className = "desktop-mode-icons";
13634 container.setAttribute("role", "list");
13635 container.setAttribute("aria-label", __("Desktop icons"));
13636 const ordered = [...icons].sort((a, b) => {
13637 const ap = a.pinned ? 0 : 1;
13638 const bp = b.pinned ? 0 : 1;
13639 return ap - bp;
13640 });
13641 const tiles = /* @__PURE__ */ new Map();
13642 for (const entry of ordered) {
13643 const tile2 = buildIcon(entry, deps2);
13644 const stored = _badges.get(entry.id) ?? 0;
13645 if (stored > 0) {
13646 _paintBadgeNode(tile2, stored);
13647 }
13648 container.appendChild(tile2);
13649 tiles.set(entry.id, tile2);
13650 }
13651 host.appendChild(container);
13652 doAction(HOOKS.DESKTOP_ICONS_RENDERED, {
13653 ids: (icons ?? []).map((i) => i.id),
13654 container,
13655 tiles
13656 });
13657 }
13658 function _findIconTile(iconId) {
13659 if (!iconId) {
13660 return null;
13661 }
13662 const container = document.querySelector(
13663 ".desktop-mode-icons"
13664 );
13665 if (!container) {
13666 return null;
13667 }
13668 return container.querySelector(
13669 `[data-icon-id="${_cssEscape(iconId)}"]`
13670 );
13671 }
13672 function _paintBadgeNode(host, count) {
13673 const existing = host.querySelector(
13674 `:scope > .${BADGE_CLASS}`
13675 );
13676 if (count <= 0) {
13677 existing?.remove();
13678 return;
13679 }
13680 const display = count > 99 ? "99+" : String(count);
13681 const ariaLabel = sprintf(
13682 // translators: %d is the number of pending items in a desktop-icon badge.
13683 _n("%d notification", "%d notifications", count),
13684 count
13685 );
13686 if (existing) {
13687 if (existing.textContent !== display) {
13688 existing.textContent = display;
13689 }
13690 existing.setAttribute("aria-label", ariaLabel);
13691 return;
13692 }
13693 const badge = document.createElement("span");
13694 badge.className = BADGE_CLASS;
13695 badge.textContent = display;
13696 badge.setAttribute("aria-label", ariaLabel);
13697 host.appendChild(badge);
13698 }
13699 function _cssEscape(value) {
13700 const c = window.CSS;
13701 return c?.escape ? c.escape(value) : value;
13702 }
13703 function buildIcon(entry, deps2) {
13704 const tile2 = document.createElement("button");
13705 tile2.type = "button";
13706 tile2.className = entry.pinned ? "desktop-mode-icon desktop-mode-icon--pinned" : "desktop-mode-icon";
13707 tile2.dataset.iconId = entry.id;
13708 if (entry.pinned) {
13709 tile2.dataset.pinned = "1";
13710 }
13711 tile2.setAttribute("role", "listitem");
13712 tile2.setAttribute("aria-label", entry.title);
13713 const icon = renderIcon(entry.icon, {
13714 title: entry.title,
13715 className: "desktop-mode-icon__image"
13716 });
13717 tile2.appendChild(icon);
13718 const label = document.createElement("span");
13719 label.className = "desktop-mode-icon__label";
13720 label.textContent = entry.title;
13721 tile2.appendChild(label);
13722 tile2.addEventListener("click", (e) => {
13723 e.stopPropagation();
13724 doAction(HOOKS.DESKTOP_ICON_CLICKED, {
13725 id: entry.id,
13726 target: entry.window ? "window" : "url"
13727 });
13728 openTarget(entry, deps2);
13729 });
13730 tile2.addEventListener("contextmenu", (e) => {
13731 if (entry.pinned) {
13732 return;
13733 }
13734 e.preventDefault();
13735 e.stopPropagation();
13736 openItemVisibilityMenu({
13737 x: e.clientX,
13738 y: e.clientY,
13739 id: entry.id,
13740 title: entry.title,
13741 surface: "desktop"
13742 });
13743 });
13744 return tile2;
13745 }
13746 function openTarget(entry, deps2) {
13747 if (entry.window) {
13748 const opened = deps2.openWindow(entry.window);
13749 if (!opened) {
13750 return;
13751 }
13752 return;
13753 }
13754 if (entry.url) {
13755 if (tryOpenExternalUrl(entry.url)) {
13756 return;
13757 }
13758 try {
13759 const parsed = new URL(entry.url, window.location.origin);
13760 const windowId = deps2.deriveWindowId(parsed.toString());
13761 void deps2.manager.open({
13762 id: windowId,
13763 baseId: windowId,
13764 url: parsed.toString(),
13765 title: entry.title,
13766 icon: entry.icon
13767 });
13768 } catch {
13769 }
13770 }
13771 }
13772 const SIDE_DOCK_ID = "desktop-mode-side-dock";
13773 function coreItemToIconEntry(item, index2) {
13774 return {
13775 id: `dock-core:${item.id}`,
13776 title: item.title,
13777 icon: item.icon,
13778 window: "",
13779 url: item.url,
13780 // Synthesized icons render after server-registered ones; the
13781 // large offset leaves headroom for plugin authors who set
13782 // explicit `position` values.
13783 position: 1e3 + index2
13784 };
13785 }
13786 function createLayoutDispatcher(deps2, initialLayout, initialDockItems, initialServerIcons) {
13787 let layout = initialLayout;
13788 let items = initialDockItems;
13789 let serverIcons = initialServerIcons ?? [];
13790 let primary = null;
13791 let side = null;
13792 let primaryDock = null;
13793 let sideDock = null;
13794 let sideDockEl = null;
13795 const systemTiles = /* @__PURE__ */ new Map();
13796 const railFor = (affinity) => {
13797 if (affinity === "core" && side) {
13798 return side;
13799 }
13800 return primary;
13801 };
13802 const ensureSideDockEl = () => {
13803 const existing = document.getElementById(
13804 SIDE_DOCK_ID
13805 );
13806 if (existing) {
13807 return existing;
13808 }
13809 const el = document.createElement("nav");
13810 el.id = SIDE_DOCK_ID;
13811 el.className = "desktop-mode-dock";
13812 el.setAttribute("role", "toolbar");
13813 el.setAttribute("aria-label", "Core admin navigation");
13814 deps2.shellBody.insertBefore(el, deps2.shellBody.firstChild);
13815 return el;
13816 };
13817 const removeSideDockEl = () => {
13818 if (sideDockEl && sideDockEl.parentNode) {
13819 sideDockEl.parentNode.removeChild(sideDockEl);
13820 }
13821 sideDockEl = null;
13822 };
13823 const readSettings = () => deps2.getSettings?.() ?? { itemVisibility: {}, dockOrder: [] };
13824 const effectiveDockItems = () => {
13825 const dockedNativeWindows = /* @__PURE__ */ new Set();
13826 for (const entry of systemTiles.values()) {
13827 dockedNativeWindows.add(entry.item.id);
13828 }
13829 return applyDockPlacement(
13830 items,
13831 serverIcons,
13832 readSettings(),
13833 dockedNativeWindows
13834 );
13835 };
13836 const partition = () => {
13837 const effective = effectiveDockItems();
13838 const core = [];
13839 const plugin = [];
13840 for (const item of effective) {
13841 if (item.isCore) {
13842 core.push(item);
13843 } else {
13844 plugin.push(item);
13845 }
13846 }
13847 return { core, plugin };
13848 };
13849 const repaintIcons = () => {
13850 const settings = readSettings();
13851 if (layout !== "spatial") {
13852 deps2.renderIcons(
13853 applyDesktopPlacement(serverIcons, items, settings.itemVisibility)
13854 );
13855 return;
13856 }
13857 const { core } = partition();
13858 const synthesized = core.map(coreItemToIconEntry);
13859 const keptServerIcons = serverIcons.filter((icon) => {
13860 const override = settings.itemVisibility[icon.id];
13861 if (override) {
13862 return override === "desktop" || override === "both";
13863 }
13864 return Boolean(icon.pinned);
13865 });
13866 const explicitlyPromoted = [];
13867 let synthIndex = 0;
13868 for (const item of items) {
13869 const placement = settings.itemVisibility[item.id];
13870 if (placement === "desktop" || placement === "both") {
13871 explicitlyPromoted.push({
13872 id: `dock:${item.id}`,
13873 title: item.title,
13874 icon: item.icon,
13875 window: "",
13876 url: item.url || "",
13877 position: 2e3 + synthIndex++
13878 });
13879 }
13880 }
13881 deps2.renderIcons([
13882 ...synthesized,
13883 ...keptServerIcons,
13884 ...explicitlyPromoted
13885 ]);
13886 };
13887 const tearDownDocks = () => {
13888 if (primary) {
13889 try {
13890 primary.destroy();
13891 } catch (err) {
13892 doAction(HOOKS.SHELL_ERROR, {
13893 scope: "dock-rail-renderer/destroy",
13894 error: err
13895 });
13896 }
13897 primary = null;
13898 primaryDock = null;
13899 }
13900 if (side) {
13901 try {
13902 side.destroy();
13903 } catch (err) {
13904 doAction(HOOKS.SHELL_ERROR, {
13905 scope: "dock-rail-renderer/destroy",
13906 error: err
13907 });
13908 }
13909 side = null;
13910 sideDock = null;
13911 }
13912 };
13913 const mountRail = (mountDeps) => {
13914 const renderer = resolveActive();
13915 if (!renderer) {
13916 doAction(HOOKS.SHELL_ERROR, {
13917 scope: "dock-rail-renderer",
13918 message: "No dock rail renderer is registered."
13919 });
13920 return null;
13921 }
13922 try {
13923 return renderer.mount(mountDeps);
13924 } catch (err) {
13925 doAction(HOOKS.SHELL_ERROR, {
13926 scope: "dock-rail-renderer/mount",
13927 rendererId: renderer.id,
13928 error: err
13929 });
13930 if (renderer === defaultDockRailRenderer) {
13931 return null;
13932 }
13933 try {
13934 return defaultDockRailRenderer.mount(mountDeps);
13935 } catch {
13936 return null;
13937 }
13938 }
13939 };
13940 const buildMountDeps = (container, railItems, orientation) => ({
13941 container,
13942 items: railItems,
13943 // `fullMenu` is the complete admin-menu list. Renderers that
13944 // want to ignore the layout's partitioning (e.g., paint
13945 // every menu item in one ring regardless of `isCore`) read
13946 // this instead of `items`. Snapshot per-mount so a renderer
13947 // holding the array sees a stable list; live updates flow
13948 // through `replaceItems`.
13949 fullMenu: items.slice(),
13950 // Same idea for system tiles — OS Settings, plugin-owned
13951 // native-window launchers, etc. Lets a renderer apply
13952 // uniform treatment across menu + system cohorts in one
13953 // pass. Live updates flow through `appendSystemItem` /
13954 // `removeSystemItem`.
13955 fullSystemTiles: Array.from(systemTiles.values()).map(
13956 (entry) => entry.item
13957 ),
13958 orientation,
13959 windowManager: deps2.windowManager,
13960 adminUrl: deps2.adminUrl,
13961 // `openItem` / `openSubmenuPick` / `openSystemItem` /
13962 // `requestSubmenu` are routing callbacks for custom
13963 // renderers. They mirror exactly what the default renderer
13964 // (`Dock.openPage` / `Dock.openSubmenuPick`) does internally
13965 // — same `deriveWindowId(url, adminUrl)` call, same window-
13966 // config shape — so a custom renderer addresses the same
13967 // window with the same id at runtime. Switching renderer
13968 // mid-session doesn't lose the user's open windows.
13969 openItem: (item) => {
13970 const baseId = deriveWindowId(item.url, deps2.adminUrl);
13971 deps2.windowManager.open({
13972 id: baseId,
13973 baseId,
13974 url: item.url,
13975 parentUrl: item.url,
13976 title: item.title,
13977 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
13978 submenu: item.submenu,
13979 multi: !!item.multi
13980 });
13981 },
13982 openSubmenuPick: (item, sub) => {
13983 deps2.windowManager.open({
13984 id: deriveWindowId(sub.url, deps2.adminUrl),
13985 baseId: deriveWindowId(item.url, deps2.adminUrl),
13986 url: sub.url,
13987 // Pin the synthetic parent tab to the dock landing
13988 // page, not to the sub-page the user picked. Without
13989 // this, a submenu-pick (e.g. clicking "Editor" inside
13990 // Appearance's submenu popover) would open at
13991 // site-editor.php with no way back to themes.php.
13992 parentUrl: item.url,
13993 title: item.title,
13994 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
13995 submenu: item.submenu,
13996 multi: !!item.multi
13997 });
13998 },
13999 openSystemItem: (item) => item.onOpen()
14000 });
14001 const buildDocksForCurrentLayout = () => {
14002 tearDownDocks();
14003 const { core, plugin } = partition();
14004 if (layout === "classic") {
14005 sideDockEl = ensureSideDockEl();
14006 side = mountRail(
14007 buildMountDeps(sideDockEl, core, "left")
14008 );
14009 sideDock = unwrapDefaultDock(side);
14010 primary = mountRail(
14011 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
14012 );
14013 primaryDock = unwrapDefaultDock(primary);
14014 } else if (layout === "unified") {
14015 removeSideDockEl();
14016 primary = mountRail(
14017 buildMountDeps(deps2.bottomDockEl, effectiveDockItems(), "bottom")
14018 );
14019 primaryDock = unwrapDefaultDock(primary);
14020 } else {
14021 removeSideDockEl();
14022 primary = mountRail(
14023 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
14024 );
14025 primaryDock = unwrapDefaultDock(primary);
14026 }
14027 for (const entry of systemTiles.values()) {
14028 railFor(entry.affinity)?.appendSystemItem(entry.item);
14029 }
14030 };
14031 const dispatcher = {
14032 getLayout: () => layout,
14033 getPrimary: () => primaryDock,
14034 getSide: () => sideDock,
14035 setLayout: (next) => {
14036 if (next === layout) {
14037 return;
14038 }
14039 layout = next;
14040 deps2.shellRoot.setAttribute("data-desktop-mode-layout", next);
14041 buildDocksForCurrentLayout();
14042 repaintIcons();
14043 document.dispatchEvent(
14044 new CustomEvent("desktop-mode-layout-changed", {
14045 detail: {
14046 layout: next,
14047 primary: primaryDock,
14048 side: sideDock
14049 }
14050 })
14051 );
14052 },
14053 applyDockItems: (nextItems) => {
14054 items = nextItems;
14055 const { core, plugin } = partition();
14056 if (layout === "classic") {
14057 side?.replaceItems(core);
14058 primary?.replaceItems(plugin);
14059 } else if (layout === "unified") {
14060 primary?.replaceItems(effectiveDockItems());
14061 } else {
14062 primary?.replaceItems(plugin);
14063 }
14064 repaintIcons();
14065 },
14066 applyDesktopIcons: (next) => {
14067 serverIcons = next ?? [];
14068 repaintIcons();
14069 },
14070 appendSystemTile: (item, affinity = "plugin") => {
14071 systemTiles.set(item.id, { item, affinity });
14072 railFor(affinity)?.appendSystemItem(item);
14073 },
14074 removeSystemTile: (id) => {
14075 const entry = systemTiles.get(id);
14076 if (!entry) {
14077 return;
14078 }
14079 systemTiles.delete(id);
14080 railFor(entry.affinity)?.removeSystemItem(id);
14081 },
14082 listSystemTiles: () => Array.from(systemTiles.values()).map((entry) => ({
14083 id: entry.item.id,
14084 title: entry.item.title,
14085 icon: entry.item.icon,
14086 affinity: entry.affinity
14087 })),
14088 getSystemTile: (id) => systemTiles.get(id)?.item ?? null,
14089 getMenuItems: () => items.slice(),
14090 refresh: () => {
14091 const { core, plugin } = partition();
14092 if (layout === "classic") {
14093 side?.replaceItems(core);
14094 primary?.replaceItems(plugin);
14095 } else if (layout === "unified") {
14096 primary?.replaceItems(effectiveDockItems());
14097 } else {
14098 primary?.replaceItems(plugin);
14099 }
14100 repaintIcons();
14101 },
14102 destroy: () => {
14103 tearDownDocks();
14104 removeSideDockEl();
14105 }
14106 };
14107 deps2.shellRoot.setAttribute("data-desktop-mode-layout", layout);
14108 buildDocksForCurrentLayout();
14109 repaintIcons();
14110 let lastResolvedId = resolveActive()?.id ?? null;
14111 subscribe$3(() => {
14112 const nextId2 = resolveActive()?.id ?? null;
14113 if (nextId2 === lastResolvedId) {
14114 return;
14115 }
14116 lastResolvedId = nextId2;
14117 buildDocksForCurrentLayout();
14118 repaintIcons();
14119 document.dispatchEvent(
14120 new CustomEvent("desktop-mode-layout-changed", {
14121 detail: {
14122 layout,
14123 primary: primaryDock,
14124 side: sideDock
14125 }
14126 })
14127 );
14128 });
14129 return dispatcher;
14130 }
14131 function loadImpl(scriptUrl) {
14132 if (window.desktopModeCreateAiAssistant) {
14133 return Promise.resolve(window.desktopModeCreateAiAssistant);
14134 }
14135 return new Promise((resolve2, reject) => {
14136 const existing = document.querySelector(
14137 `script[data-desktop-mode-ai="1"]`
14138 );
14139 const finish = () => {
14140 const factory = window.desktopModeCreateAiAssistant;
14141 if (!factory) {
14142 reject(
14143 new Error(
14144 "[desktop-mode] ai-assistant bundle loaded but did not register desktopModeCreateAiAssistant"
14145 )
14146 );
14147 return;
14148 }
14149 resolve2(factory);
14150 };
14151 if (existing) {
14152 if (window.desktopModeCreateAiAssistant) {
14153 finish();
14154 } else {
14155 existing.addEventListener("load", finish);
14156 existing.addEventListener(
14157 "error",
14158 () => reject(new Error("failed to load ai-assistant bundle"))
14159 );
14160 }
14161 return;
14162 }
14163 const s = document.createElement("script");
14164 s.src = scriptUrl;
14165 s.async = true;
14166 s.dataset.desktopModeAi = "1";
14167 s.addEventListener("load", finish);
14168 s.addEventListener(
14169 "error",
14170 () => reject(new Error("failed to load ai-assistant bundle"))
14171 );
14172 document.head.appendChild(s);
14173 });
14174 }
14175 class AiAssistantStub {
14176 constructor(config, scriptUrl) {
14177 this._real = null;
14178 this._loadPromise = null;
14179 this._pendingAsk = null;
14180 this._intendOpen = false;
14181 this.ask = (...args) => {
14182 return this._ensure().then((r) => r.ask(...args));
14183 };
14184 this._config = config;
14185 this._scriptUrl = scriptUrl;
14186 }
14187 _ensure() {
14188 if (this._loadPromise) {
14189 return this._loadPromise;
14190 }
14191 this._loadPromise = loadImpl(this._scriptUrl).then((factory) => {
14192 const real = factory(this._config);
14193 if (this._pendingAsk) {
14194 real.attachAsk(this._pendingAsk);
14195 }
14196 this._real = real;
14197 return real;
14198 });
14199 return this._loadPromise;
14200 }
14201 open() {
14202 this._intendOpen = true;
14203 void this._ensure().then((r) => r.open());
14204 }
14205 close() {
14206 this._intendOpen = false;
14207 if (this._real) {
14208 this._real.close();
14209 }
14210 }
14211 toggle() {
14212 if (this.isOpen) {
14213 this.close();
14214 } else {
14215 this.open();
14216 }
14217 }
14218 get isOpen() {
14219 return this._real ? this._real.isOpen : this._intendOpen;
14220 }
14221 /**
14222 * Late-bind the programmatic `ask` callback. Mirrors the real
14223 * class's `attachAsk` signature so `desktop.ts`'s call site is
14224 * identical whether it's wiring the stub or the impl.
14225 */
14226 attachAsk(fn) {
14227 this._pendingAsk = fn;
14228 if (this._real) {
14229 this._real.attachAsk(fn);
14230 }
14231 }
14232 }
14233 const isAbortError = (err) => {
14234 if (!err || typeof err !== "object") {
14235 return false;
14236 }
14237 return err.name === "AbortError";
14238 };
14239 const normaliseToolsOpt = (tools) => {
14240 if (!tools) {
14241 return [];
14242 }
14243 const all2 = listAiCallableCommands();
14244 if (tools === true || tools === "aiCallable") {
14245 return all2;
14246 }
14247 if (Array.isArray(tools)) {
14248 const allowed = new Set(tools.map((s) => s.toLowerCase()));
14249 return all2.filter((c) => allowed.has(c.slug));
14250 }
14251 if (typeof tools === "function") {
14252 return all2.filter((c) => {
14253 try {
14254 return tools(c.slug) === true;
14255 } catch {
14256 return false;
14257 }
14258 });
14259 }
14260 return [];
14261 };
14262 const normaliseSystemPrompt = (sp) => {
14263 if (!sp) {
14264 return null;
14265 }
14266 if (typeof sp === "string") {
14267 return { text: sp, mode: "append" };
14268 }
14269 if (typeof sp === "object" && typeof sp.text === "string" && sp.text !== "") {
14270 return {
14271 text: sp.text,
14272 mode: sp.mode === "replace" ? "replace" : "append"
14273 };
14274 }
14275 return null;
14276 };
14277 function liftMessage(payloadMessage, result) {
14278 const seed2 = payloadMessage ?? "";
14279 if (seed2 !== "") {
14280 return seed2;
14281 }
14282 if (typeof result === "string" && result !== "") {
14283 return result;
14284 }
14285 if (result && typeof result === "object" && "message" in result && typeof result.message === "string") {
14286 return result.message;
14287 }
14288 return "";
14289 }
14290 function serialiseOutcome(result) {
14291 if (result === void 0) {
14292 return { value: null };
14293 }
14294 if (typeof result === "object" && result !== null) {
14295 return result;
14296 }
14297 return { value: result };
14298 }
14299 function createAsk(deps2) {
14300 const postToSearch = async (body, signal) => {
14301 const config = deps2.config();
14302 const url = config.aiSearchUrl ?? "";
14303 const nonce = config.restNonce ?? "";
14304 if (!url || !nonce) {
14305 throw new Error(
14306 "[desktop-mode] wp.desktop.ai.ask: aiSearchUrl / restNonce missing from config. AI Copilot may not be enabled."
14307 );
14308 }
14309 try {
14310 return await trackedFetch$1(
14311 url,
14312 {
14313 method: "POST",
14314 credentials: "same-origin",
14315 headers: {
14316 "Content-Type": "application/json",
14317 "X-WP-Nonce": nonce
14318 },
14319 body: JSON.stringify(body),
14320 signal
14321 },
14322 { source: "desktop-mode/ai-ask" }
14323 );
14324 } catch (err) {
14325 if (isAbortError(err)) {
14326 throw err;
14327 }
14328 throw new Error(
14329 `[desktop-mode] wp.desktop.ai.ask: network error — ${String(
14330 err?.message ?? err
14331 )}`
14332 );
14333 }
14334 };
14335 const dispatchToolCall = async (payload, opts) => {
14336 const slug = payload.tool?.slug ?? "";
14337 const args = payload.tool?.args ?? "";
14338 const cmd = findCommand(slug);
14339 if (!cmd) {
14340 return {
14341 ok: false,
14342 response: {
14343 answer_type: "tool_call",
14344 message: `Command /${slug} was not registered on this page.`,
14345 entity: null,
14346 admin_links: null,
14347 toolCall: {
14348 slug,
14349 args,
14350 result: { error: "command_not_found" }
14351 },
14352 request_id: payload.request_id
14353 }
14354 };
14355 }
14356 const ctx = opts.commandContext ?? deps2.fallbackContext();
14357 let result;
14358 try {
14359 result = await Promise.resolve(cmd.run(args, ctx));
14360 } catch (err) {
14361 result = { error: String(err?.message ?? err) };
14362 }
14363 return { ok: true, slug, args, result };
14364 };
14365 const composeFollowUp = async (text, slug, args, result, sp, signal) => {
14366 const body = {
14367 query: text,
14368 follow_up: {
14369 tool: { slug, args },
14370 result: serialiseOutcome(result)
14371 }
14372 };
14373 if (sp) {
14374 body.system_prompt_text = sp.text;
14375 body.system_prompt_mode = sp.mode;
14376 }
14377 let res;
14378 try {
14379 res = await postToSearch(body, signal);
14380 } catch (err) {
14381 if (isAbortError(err)) {
14382 throw err;
14383 }
14384 return null;
14385 }
14386 if (!res.ok) {
14387 return null;
14388 }
14389 const payload = await res.json().catch(() => ({}));
14390 const message = typeof payload.message === "string" ? payload.message.trim() : "";
14391 return message !== "" ? payload.message ?? null : null;
14392 };
14393 return async function ask(query, opts = {}) {
14394 const text = (query ?? "").trim();
14395 if (text === "") {
14396 const hasMeaningfulOpts = opts.tools !== void 0 || opts.systemPrompt !== void 0 || opts.followUp === true || opts.resumeTool !== void 0 || opts.commandContext !== void 0;
14397 if (hasMeaningfulOpts) {
14398 throw new Error(
14399 "[desktop-mode] wp.desktop.ai.ask: empty query passed with non-default options — likely a caller bug. Provide a query or call without options."
14400 );
14401 }
14402 return {
14403 answer_type: "chat",
14404 message: "",
14405 entity: null,
14406 admin_links: null
14407 };
14408 }
14409 const commandTools = normaliseToolsOpt(opts.tools);
14410 const sp = normaliseSystemPrompt(opts.systemPrompt);
14411 const body = { query: text };
14412 if (opts.resumeTool) {
14413 body.resume_tool = opts.resumeTool;
14414 }
14415 if (typeof opts.startOffset === "number") {
14416 body.start_offset = opts.startOffset;
14417 }
14418 if (commandTools.length > 0) {
14419 body.command_tools = commandTools;
14420 }
14421 if (sp) {
14422 body.system_prompt_text = sp.text;
14423 body.system_prompt_mode = sp.mode;
14424 }
14425 const res = await postToSearch(body, opts.signal);
14426 if (!res.ok) {
14427 const detail = await res.json().catch(() => ({ message: res.statusText }));
14428 throw new Error(
14429 `[desktop-mode] wp.desktop.ai.ask: HTTP ${res.status} — ${detail.message ?? res.statusText}`
14430 );
14431 }
14432 const payload = await res.json();
14433 if (payload.answer_type !== "tool_call" || !payload.tool) {
14434 return {
14435 answer_type: payload.answer_type,
14436 message: payload.message ?? "",
14437 entity: payload.entity ?? null,
14438 admin_links: payload.admin_links ?? null,
14439 request_id: payload.request_id,
14440 continue: payload.continue ?? null
14441 };
14442 }
14443 const dispatch2 = await dispatchToolCall(payload, opts);
14444 if (!dispatch2.ok) {
14445 return dispatch2.response;
14446 }
14447 const { slug, args, result } = dispatch2;
14448 let message = liftMessage(payload.message, result);
14449 if (opts.followUp === true) {
14450 const composed = await composeFollowUp(
14451 text,
14452 slug,
14453 args,
14454 result,
14455 sp,
14456 opts.signal
14457 );
14458 if (composed !== null) {
14459 message = composed;
14460 }
14461 }
14462 return {
14463 answer_type: "tool_call",
14464 message,
14465 entity: null,
14466 admin_links: null,
14467 toolCall: { slug, args, result },
14468 request_id: payload.request_id
14469 };
14470 };
14471 }
14472 const EVENT_NAME = "desktop-mode-broadcast";
14473 const POSTMESSAGE_TYPE = "desktop-mode-broadcast";
14474 const ORIGIN = window.location.origin;
14475 let _manager = null;
14476 function attachBroadcastBus(manager) {
14477 _manager = manager;
14478 }
14479 function broadcast(topic, payload) {
14480 const filteredTopic = String(
14481 applyFilters("desktop-mode.broadcast.topic", topic, { payload }) ?? topic
14482 );
14483 const filteredPayload = applyFilters(
14484 "desktop-mode.broadcast.payload",
14485 payload,
14486 { topic: filteredTopic }
14487 );
14488 const detail = {
14489 topic: filteredTopic,
14490 payload: filteredPayload
14491 };
14492 document.dispatchEvent(new CustomEvent(EVENT_NAME, { detail }));
14493 doAction(HOOKS.BROADCAST, detail);
14494 activity.publish(
14495 filteredTopic,
14496 filteredPayload
14497 );
14498 if (!_manager) {
14499 return;
14500 }
14501 const message = {
14502 type: POSTMESSAGE_TYPE,
14503 topic: filteredTopic,
14504 payload: filteredPayload
14505 };
14506 for (const win of _manager._stack) {
14507 const target2 = win.iframe?.contentWindow;
14508 if (!target2) {
14509 continue;
14510 }
14511 try {
14512 target2.postMessage(message, ORIGIN);
14513 } catch (err) {
14514 }
14515 }
14516 }
14517 function subscribe$2(topic, cb) {
14518 const handler = (e) => {
14519 const detail = e.detail;
14520 if (!detail) {
14521 return;
14522 }
14523 if (topic !== "*" && detail.topic !== topic) {
14524 return;
14525 }
14526 try {
14527 cb(detail.payload, { topic: detail.topic });
14528 } catch (err) {
14529 doAction(HOOKS.SHELL_ERROR, {
14530 scope: "broadcast-subscriber",
14531 topic: detail.topic,
14532 error: err
14533 });
14534 }
14535 };
14536 document.addEventListener(EVENT_NAME, handler);
14537 return () => document.removeEventListener(EVENT_NAME, handler);
14538 }
14539 function installBroadcastReceiver() {
14540 window.addEventListener("message", (e) => {
14541 if (e.origin !== ORIGIN) {
14542 return;
14543 }
14544 const data = e.data;
14545 if (!data || data.type !== POSTMESSAGE_TYPE) {
14546 return;
14547 }
14548 if (data._fromParent) {
14549 return;
14550 }
14551 if (typeof data.topic !== "string") {
14552 return;
14553 }
14554 broadcast(data.topic, data.payload);
14555 });
14556 }
14557 const LOG_PREFIX = "[desktop-mode-bin badge]";
14558 function log(...args) {
14559 try {
14560 if (window.localStorage?.getItem("desktopModeBinDebug")) {
14561 console.info(LOG_PREFIX, ...args);
14562 }
14563 } catch {
14564 }
14565 }
14566 function warn(...args) {
14567 console.warn(LOG_PREFIX, ...args);
14568 }
14569 const TARGET_ID = "desktop-mode-recycle-bin";
14570 const HEARTBEAT_FIELD$1 = "desktop_mode_recycle_bin_seen_ts";
14571 function getDesktopApi() {
14572 return window.wp?.desktop;
14573 }
14574 const store$3 = createSharedStore(
14575 "desktop-mode/recycle-bin/badge",
14576 () => ({
14577 current: 0,
14578 seenTs: 0,
14579 started: false,
14580 countUrl: ""
14581 })
14582 );
14583 function setRecycleBinBadge(next) {
14584 const safe = Math.max(0, Math.floor(next));
14585 const prev = store$3.state.current;
14586 store$3.state.current = safe;
14587 log("setRecycleBinBadge", { prev, next: safe });
14588 paintBadge(safe);
14589 }
14590 function adjustRecycleBinBadge(delta) {
14591 setRecycleBinBadge(store$3.state.current + delta);
14592 }
14593 function _currentRecycleBinBadge() {
14594 return store$3.state.current;
14595 }
14596 function paintBadge(count) {
14597 const desktop = getDesktopApi();
14598 const active2 = isBinWindowActive();
14599 const visible = active2 ? 0 : count;
14600 log("paintBadge", { count, visible, active: active2 });
14601 desktop?.dock?.setBadge?.(TARGET_ID, visible);
14602 desktop?.taskbar?.setBadge?.(TARGET_ID, visible);
14603 desktop?.icons?.setBadge?.(TARGET_ID, visible);
14604 }
14605 function isBinWindowActive() {
14606 return !!getDesktopApi()?.windowManager?.isActive?.(TARGET_ID);
14607 }
14608 function startRecycleBinBadge(initialRaw, countUrl = "") {
14609 const initial = Number(initialRaw) || 0;
14610 const cfg = window.desktopModeConfig;
14611 const cfgCount = cfg?.recycleBinCount;
14612 const cfgUrl = cfg?.recycleBinCountUrl;
14613 const cfgDebug = cfg?.desktopModeBinDebug;
14614 log("startRecycleBinBadge entry", {
14615 initial,
14616 countUrl,
14617 alreadyStarted: store$3.state.started,
14618 cfgCount,
14619 cfgUrl,
14620 cfgDebug,
14621 readyState: document.readyState
14622 });
14623 const cfgCountNum = Number(cfgCount);
14624 const cfgCountIsHealthy = (typeof cfgCount === "number" || typeof cfgCount === "string") && Number.isFinite(cfgCountNum);
14625 if (!cfgCountIsHealthy) {
14626 warn(
14627 "desktopModeConfig.recycleBinCount is missing — PHP filter `desktop_mode_shell_config` did not deliver. Check your PHP error log for `[desktop-mode-bin debug]` lines.",
14628 { cfg }
14629 );
14630 }
14631 if (store$3.state.started) {
14632 setRecycleBinBadge(initial);
14633 return;
14634 }
14635 store$3.state.started = true;
14636 store$3.state.countUrl = countUrl;
14637 store$3.state.seenTs = Date.now();
14638 setRecycleBinBadge(initial);
14639 wireDockTileSignal();
14640 wireDesktopIconsSignal();
14641 wireBroadcastDeltas();
14642 wirePostMessageFastPath();
14643 wireHeartbeatProbe();
14644 wireWindowLifecycleSignals();
14645 }
14646 function wireWindowLifecycleSignals() {
14647 const ns = "desktop-mode/recycle-bin/badge-lifecycle";
14648 const repaint = (payload) => {
14649 const detail = payload;
14650 if (detail?.windowId !== TARGET_ID) {
14651 return;
14652 }
14653 paintBadge(store$3.state.current);
14654 };
14655 addAction(HOOKS.WINDOW_OPENED, ns, repaint);
14656 addAction(HOOKS.WINDOW_FOCUSED, ns, repaint);
14657 addAction(HOOKS.WINDOW_BLURRED, ns, repaint);
14658 addAction(HOOKS.WINDOW_MINIMIZED, ns, repaint);
14659 addAction(HOOKS.WINDOW_RESTORED, ns, repaint);
14660 addAction(HOOKS.WINDOW_CLOSED, ns, repaint);
14661 addAction(HOOKS.WINDOW_REOPENED, ns, repaint);
14662 }
14663 function wireDockTileSignal() {
14664 addAction(
14665 HOOKS.DOCK_ITEM_APPENDED,
14666 "desktop-mode/recycle-bin/badge",
14667 (payload) => {
14668 if (payload?.id === TARGET_ID) {
14669 paintBadge(store$3.state.current);
14670 }
14671 }
14672 );
14673 }
14674 function wireDesktopIconsSignal() {
14675 addAction(
14676 HOOKS.DESKTOP_ICONS_RENDERED,
14677 "desktop-mode/recycle-bin/badge",
14678 (payload) => {
14679 if (payload?.ids?.includes(TARGET_ID)) {
14680 paintBadge(store$3.state.current);
14681 }
14682 }
14683 );
14684 }
14685 function wireBroadcastDeltas() {
14686 const onDomain = (payload) => {
14687 const detail = payload;
14688 if (!detail) {
14689 return;
14690 }
14691 const ids = Array.isArray(detail.ids) ? detail.ids.length : 0;
14692 switch (detail.action) {
14693 case "trashed":
14694 adjustRecycleBinBadge(+ids);
14695 break;
14696 case "untrashed":
14697 case "deleted":
14698 adjustRecycleBinBadge(-ids);
14699 break;
14700 }
14701 };
14702 subscribe$2("desktop-mode.post.changed", onDomain);
14703 subscribe$2("desktop-mode.page.changed", onDomain);
14704 subscribe$2("desktop-mode.attachment.changed", onDomain);
14705 subscribe$2("desktop-mode.comment.changed", onDomain);
14706 subscribe$2("desktop-mode.placement.changed", onDomain);
14707 subscribe$2("desktop-mode.shortcut.changed", onDomain);
14708 subscribe$2("desktop-mode.folder.changed", onDomain);
14709 }
14710 function wirePostMessageFastPath() {
14711 const expectedOrigin = window.location.origin;
14712 window.addEventListener("message", (e) => {
14713 if (e.origin !== expectedOrigin) {
14714 return;
14715 }
14716 const data = e.data;
14717 if (!data || data.type !== "desktop-mode-recycle-bin-changed") {
14718 return;
14719 }
14720 const ts = typeof data.ts === "number" ? data.ts : Date.now();
14721 if (ts <= store$3.state.seenTs) {
14722 log("postMessage skipped (ts <= seenTs)", { ts, seenTs: store$3.state.seenTs });
14723 return;
14724 }
14725 log("postMessage triggers refetch", { ts, prevSeenTs: store$3.state.seenTs });
14726 store$3.state.seenTs = ts;
14727 void refetchCount();
14728 });
14729 }
14730 function wireHeartbeatProbe() {
14731 const $ = window.jQuery;
14732 if (!$) {
14733 warn("wireHeartbeatProbe: window.jQuery not available — heartbeat path disabled");
14734 return;
14735 }
14736 log("wireHeartbeatProbe: jQuery + heartbeat hooks attached");
14737 $(document).on("heartbeat-send", (...args) => {
14738 const data = args[1];
14739 if (data) {
14740 data[HEARTBEAT_FIELD$1] = store$3.state.seenTs;
14741 }
14742 });
14743 $(document).on("heartbeat-tick", (...args) => {
14744 const response = args[1];
14745 const block = response?.desktop_mode_recycle_bin;
14746 log("heartbeat-tick", { hasBlock: !!block, block });
14747 if (!block) {
14748 return;
14749 }
14750 if (typeof block.ts === "number" && block.ts > store$3.state.seenTs) {
14751 store$3.state.seenTs = block.ts;
14752 }
14753 if (typeof block.count === "number") {
14754 setRecycleBinBadge(block.count);
14755 }
14756 });
14757 }
14758 async function refetchCount() {
14759 if (!store$3.state.countUrl) {
14760 log("refetchCount: no countUrl, skip");
14761 return;
14762 }
14763 log("refetchCount: hitting", store$3.state.countUrl);
14764 try {
14765 const response = await fetch(store$3.state.countUrl, {
14766 credentials: "same-origin",
14767 headers: { Accept: "application/json" }
14768 });
14769 if (!response.ok) {
14770 warn("refetchCount: non-OK", response.status, response.statusText);
14771 return;
14772 }
14773 const json = await response.json();
14774 log("refetchCount: response", json);
14775 if (typeof json.count === "number") {
14776 setRecycleBinBadge(json.count);
14777 }
14778 } catch (err) {
14779 warn("refetchCount: fetch failed", err);
14780 }
14781 }
14782 const OS_SETTINGS_ID = "desktop-mode-os-settings";
14783 const RECYCLE_BIN_ID = "desktop-mode-recycle-bin";
14784 function registerBuiltInPeekRenderers(opts) {
14785 const wpHooks = getWpHooks();
14786 if (!wpHooks) {
14787 return;
14788 }
14789 wpHooks.addFilter(
14790 "desktop-mode.dock.peek-card-content",
14791 "desktop-mode/built-in-peek-renderers",
14792 (body, ctx) => {
14793 const context = ctx;
14794 const id = context.window.id;
14795 if (id === OS_SETTINGS_ID) {
14796 return renderOsSettings();
14797 }
14798 if (id === RECYCLE_BIN_ID) {
14799 return renderRecycleBin(context, opts.getRecycleBinCount);
14800 }
14801 return body;
14802 }
14803 );
14804 }
14805 function renderOsSettings(_ctx) {
14806 const root = document.createElement("span");
14807 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--os-settings";
14808 root.setAttribute("aria-hidden", "true");
14809 const hero = document.createElement("span");
14810 hero.className = "desktop-mode-dock-peek__os-hero dashicons dashicons-admin-generic";
14811 root.appendChild(hero);
14812 const subtitle = document.createElement("span");
14813 subtitle.className = "desktop-mode-dock-peek__os-subtitle";
14814 subtitle.textContent = __("System Preferences");
14815 root.appendChild(subtitle);
14816 const tabs = document.createElement("span");
14817 tabs.className = "desktop-mode-dock-peek__os-tabs";
14818 for (const cls of [
14819 "dashicons-art",
14820 "dashicons-admin-customizer",
14821 "dashicons-editor-help"
14822 ]) {
14823 const tab = document.createElement("span");
14824 tab.className = `desktop-mode-dock-peek__os-tab dashicons ${cls}`;
14825 tabs.appendChild(tab);
14826 }
14827 root.appendChild(tabs);
14828 return root;
14829 }
14830 function renderRecycleBin(_ctx, getCount) {
14831 const root = document.createElement("span");
14832 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--recycle-bin";
14833 root.setAttribute("aria-hidden", "true");
14834 const count = Math.max(0, Math.floor(getCount() || 0));
14835 root.dataset.empty = count === 0 ? "true" : "false";
14836 const stage = document.createElement("span");
14837 stage.className = "desktop-mode-dock-peek__bin-stage";
14838 const stack = document.createElement("span");
14839 stack.className = "desktop-mode-dock-peek__bin-stack";
14840 for (let i = 0; i < 3; i++) {
14841 const slip = document.createElement("span");
14842 slip.className = "desktop-mode-dock-peek__bin-slip";
14843 stack.appendChild(slip);
14844 }
14845 stage.appendChild(stack);
14846 const icon = document.createElement("span");
14847 icon.className = `desktop-mode-dock-peek__bin-icon dashicons ${count === 0 ? "dashicons-trash" : "dashicons-trash"}`;
14848 stage.appendChild(icon);
14849 root.appendChild(stage);
14850 const label = document.createElement("span");
14851 label.className = "desktop-mode-dock-peek__bin-label";
14852 if (count === 0) {
14853 label.textContent = __("Recycle Bin — empty");
14854 } else if (count === 1) {
14855 label.textContent = __("1 item");
14856 } else if (count > 99) {
14857 label.textContent = "99+ items";
14858 } else {
14859 label.textContent = `${count} items`;
14860 }
14861 root.appendChild(label);
14862 return root;
14863 }
14864 function getWpHooks() {
14865 const wp = window.wp;
14866 return wp?.hooks ?? null;
14867 }
14868 const BUG_REPORT_WINDOW_ID = "desktop-mode-bug-report";
14869 const REPO_OWNER = "WordPress";
14870 const REPO_NAME = "desktop-mode";
14871 const MAX_BODY_LENGTH = 6e3;
14872 function renderBugReport(body) {
14873 body.classList.add("desktop-mode-bug-report");
14874 body.replaceChildren();
14875 const form = document.createElement("form");
14876 form.className = "desktop-mode-bug-report__form";
14877 form.setAttribute("novalidate", "");
14878 const intro = document.createElement("p");
14879 intro.className = "desktop-mode-bug-report__intro";
14880 intro.textContent = __(
14881 "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."
14882 );
14883 form.appendChild(intro);
14884 form.appendChild(buildTypeField());
14885 form.appendChild(buildTextField("title", __("Title"), {
14886 placeholder: __("A short summary"),
14887 required: true
14888 }));
14889 form.appendChild(buildTextareaField("description", __("What happened? What did you expect?"), {
14890 placeholder: __("Describe the issue or the feature you have in mind."),
14891 rows: 5,
14892 required: true
14893 }));
14894 form.appendChild(buildTextareaField("steps", __("Steps to reproduce (bug only)"), {
14895 placeholder: __("One step per line"),
14896 rows: 4
14897 }));
14898 const meta = buildMetadataPreview();
14899 form.appendChild(meta);
14900 const actions = document.createElement("div");
14901 actions.className = "desktop-mode-bug-report__actions";
14902 const submit = document.createElement("button");
14903 submit.type = "submit";
14904 submit.className = "desktop-mode-bug-report__submit";
14905 submit.textContent = __("Open issue on GitHub");
14906 actions.appendChild(submit);
14907 const hint = document.createElement("span");
14908 hint.className = "desktop-mode-bug-report__hint";
14909 hint.textContent = __("You will review and submit on GitHub.");
14910 actions.appendChild(hint);
14911 form.appendChild(actions);
14912 form.addEventListener("submit", (e) => {
14913 e.preventDefault();
14914 const state2 = readFormState(form);
14915 if (!state2.title.trim() || !state2.description.trim()) {
14916 showInlineError(form, __("Title and description are both required."));
14917 return;
14918 }
14919 const url = buildGithubIssueUrl(state2);
14920 window.open(url, "_blank", "noopener");
14921 });
14922 body.appendChild(form);
14923 }
14924 function buildTypeField() {
14925 const wrap = document.createElement("div");
14926 wrap.className = "desktop-mode-bug-report__field desktop-mode-bug-report__field--type";
14927 const label = document.createElement("span");
14928 label.className = "desktop-mode-bug-report__label";
14929 label.textContent = __("Type");
14930 wrap.appendChild(label);
14931 const group = document.createElement("div");
14932 group.className = "desktop-mode-bug-report__radio-group";
14933 group.setAttribute("role", "radiogroup");
14934 const options = [
14935 { value: "bug", label: __("Bug"), checked: true },
14936 { value: "feature", label: __("Feature request") },
14937 { value: "question", label: __("Question") }
14938 ];
14939 for (const opt of options) {
14940 const radioLabel = document.createElement("label");
14941 radioLabel.className = "desktop-mode-bug-report__radio";
14942 const input = document.createElement("input");
14943 input.type = "radio";
14944 input.name = "type";
14945 input.value = opt.value;
14946 if (opt.checked) {
14947 input.checked = true;
14948 }
14949 radioLabel.appendChild(input);
14950 const text = document.createElement("span");
14951 text.textContent = opt.label;
14952 radioLabel.appendChild(text);
14953 group.appendChild(radioLabel);
14954 }
14955 wrap.appendChild(group);
14956 return wrap;
14957 }
14958 function buildTextField(name, labelText, opts = {}) {
14959 const wrap = document.createElement("div");
14960 wrap.className = "desktop-mode-bug-report__field";
14961 const label = document.createElement("label");
14962 label.className = "desktop-mode-bug-report__label";
14963 label.textContent = labelText;
14964 wrap.appendChild(label);
14965 const input = document.createElement("input");
14966 input.type = "text";
14967 input.name = name;
14968 input.className = "desktop-mode-bug-report__input";
14969 if (opts.placeholder) {
14970 input.placeholder = opts.placeholder;
14971 }
14972 if (opts.required) {
14973 input.setAttribute("aria-required", "true");
14974 }
14975 label.appendChild(input);
14976 return wrap;
14977 }
14978 function buildTextareaField(name, labelText, opts = {}) {
14979 const wrap = document.createElement("div");
14980 wrap.className = "desktop-mode-bug-report__field";
14981 const label = document.createElement("label");
14982 label.className = "desktop-mode-bug-report__label";
14983 label.textContent = labelText;
14984 wrap.appendChild(label);
14985 const textarea = document.createElement("textarea");
14986 textarea.name = name;
14987 textarea.className = "desktop-mode-bug-report__textarea";
14988 textarea.rows = opts.rows ?? 4;
14989 if (opts.placeholder) {
14990 textarea.placeholder = opts.placeholder;
14991 }
14992 if (opts.required) {
14993 textarea.setAttribute("aria-required", "true");
14994 }
14995 label.appendChild(textarea);
14996 return wrap;
14997 }
14998 function buildMetadataPreview() {
14999 const details = document.createElement("details");
15000 details.className = "desktop-mode-bug-report__metadata";
15001 const summary = document.createElement("summary");
15002 summary.textContent = __("Environment included with the report");
15003 details.appendChild(summary);
15004 const pre = document.createElement("pre");
15005 pre.className = "desktop-mode-bug-report__metadata-body";
15006 pre.textContent = formatMetadata(collectMetadata());
15007 details.appendChild(pre);
15008 return details;
15009 }
15010 function showInlineError(form, msg) {
15011 let banner = form.querySelector(".desktop-mode-bug-report__error");
15012 if (!banner) {
15013 banner = document.createElement("div");
15014 banner.className = "desktop-mode-bug-report__error";
15015 banner.setAttribute("role", "alert");
15016 form.prepend(banner);
15017 }
15018 banner.textContent = msg;
15019 }
15020 function readFormState(form) {
15021 const data = new FormData(form);
15022 return {
15023 type: data.get("type") ?? "bug",
15024 title: data.get("title") ?? "",
15025 description: data.get("description") ?? "",
15026 steps: data.get("steps") ?? ""
15027 };
15028 }
15029 function buildGithubIssueUrl(state2) {
15030 const labels = labelsForType(state2.type);
15031 const body = composeIssueBody(state2);
15032 const params = new URLSearchParams();
15033 params.set("title", state2.title.trim());
15034 params.set("body", body);
15035 if (labels.length) {
15036 params.set("labels", labels.join(","));
15037 }
15038 return `https://github.com/${REPO_OWNER}/${REPO_NAME}/issues/new?${params.toString()}`;
15039 }
15040 function labelsForType(type) {
15041 switch (type) {
15042 case "bug":
15043 return ["bug"];
15044 case "feature":
15045 return ["enhancement"];
15046 case "question":
15047 return ["question"];
15048 default:
15049 return [];
15050 }
15051 }
15052 function composeIssueBody(state2) {
15053 const parts = [];
15054 parts.push(state2.description.trim());
15055 if (state2.type === "bug" && state2.steps.trim()) {
15056 parts.push("");
15057 parts.push("## Steps to reproduce");
15058 parts.push("");
15059 parts.push(state2.steps.trim());
15060 }
15061 parts.push("");
15062 parts.push("<details><summary>Environment</summary>");
15063 parts.push("");
15064 parts.push("```");
15065 parts.push(formatMetadata(collectMetadata()));
15066 parts.push("```");
15067 parts.push("");
15068 parts.push("</details>");
15069 let out = parts.join("\n");
15070 if (out.length > MAX_BODY_LENGTH) {
15071 out = out.slice(0, MAX_BODY_LENGTH) + "\n\n…(truncated to fit GitHub URL length limit)";
15072 }
15073 return out;
15074 }
15075 function collectMetadata() {
15076 const cfg = window.wp?.desktop?.config;
15077 return {
15078 pluginVersion: cfg?.pluginVersion ?? "unknown",
15079 wordpressVersion: cfg?.wordpressVersion ?? "unknown",
15080 userAgent: navigator.userAgent,
15081 viewport: `${window.innerWidth}x${window.innerHeight}`,
15082 platform: navigator.platform || "unknown",
15083 currentUrl: window.location.href
15084 };
15085 }
15086 function formatMetadata(m) {
15087 return [
15088 `Plugin version: ${m.pluginVersion}`,
15089 `WordPress version: ${m.wordpressVersion}`,
15090 `User agent: ${m.userAgent}`,
15091 `Viewport: ${m.viewport}`,
15092 `Platform: ${m.platform}`,
15093 `Current URL: ${m.currentUrl}`
15094 ].join("\n");
15095 }
15096 let _config = null;
15097 let _state = {
15098 installHintDismissed: false,
15099 notificationsEnabled: false
15100 };
15101 const _listeners = /* @__PURE__ */ new Set();
15102 function initPwaState(config) {
15103 if (!config) {
15104 _config = null;
15105 return;
15106 }
15107 _config = config;
15108 _state = { ...config.state };
15109 notify$4();
15110 }
15111 function getPwaState() {
15112 return { ..._state };
15113 }
15114 function updatePwaState(patch) {
15115 _state = { ..._state, ...patch };
15116 notify$4();
15117 if (!_config) {
15118 return getPwaState();
15119 }
15120 const body = JSON.stringify(patch);
15121 const nonce = readRestNonce$2();
15122 void fetch(_config.stateUrl, {
15123 method: "POST",
15124 credentials: "same-origin",
15125 headers: {
15126 "Content-Type": "application/json",
15127 ...nonce ? { "X-WP-Nonce": nonce } : {}
15128 },
15129 body
15130 }).catch((err) => {
15131 if (typeof console !== "undefined") {
15132 console.warn("[desktop-mode] pwa-state write failed:", err);
15133 }
15134 });
15135 return getPwaState();
15136 }
15137 function subscribePwaState(cb) {
15138 _listeners.add(cb);
15139 return () => {
15140 _listeners.delete(cb);
15141 };
15142 }
15143 function notify$4() {
15144 const snapshot = getPwaState();
15145 for (const cb of Array.from(_listeners)) {
15146 try {
15147 cb(snapshot);
15148 } catch (err) {
15149 if (typeof console !== "undefined") {
15150 console.error(
15151 "[desktop-mode] pwa-state listener threw:",
15152 err
15153 );
15154 }
15155 }
15156 }
15157 }
15158 function readRestNonce$2() {
15159 const cfg = window.desktopModeConfig;
15160 return cfg?.restNonce ?? "";
15161 }
15162 const state = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
15163 __proto__: null,
15164 getPwaState,
15165 initPwaState,
15166 subscribePwaState,
15167 updatePwaState
15168 }, Symbol.toStringTag, { value: "Module" }));
15169 let _registration = null;
15170 let _registrationFailed = false;
15171 let _controllerChangeBound = false;
15172 let _reloadingForSwUpdate = false;
15173 let _status = "pending";
15174 function bindControllerChangeReload() {
15175 if (_controllerChangeBound) {
15176 return;
15177 }
15178 _controllerChangeBound = true;
15179 const hadInitialController = !!navigator.serviceWorker.controller;
15180 navigator.serviceWorker.addEventListener("controllerchange", () => {
15181 if (!hadInitialController) {
15182 return;
15183 }
15184 if (_reloadingForSwUpdate) {
15185 return;
15186 }
15187 if (wasRecentlyReloadedForSwUpdate()) {
15188 return;
15189 }
15190 markReloadedForSwUpdate();
15191 _reloadingForSwUpdate = true;
15192 setTimeout(() => window.location.reload(), 0);
15193 });
15194 }
15195 const SW_RELOAD_THROTTLE_KEY = "wpd-sw-reload-ts";
15196 const SW_RELOAD_THROTTLE_MS = 3e4;
15197 function wasRecentlyReloadedForSwUpdate() {
15198 try {
15199 const raw = sessionStorage.getItem(SW_RELOAD_THROTTLE_KEY);
15200 const last = raw ? Number.parseInt(raw, 10) : 0;
15201 if (!Number.isFinite(last) || last <= 0) {
15202 return false;
15203 }
15204 return Date.now() - last < SW_RELOAD_THROTTLE_MS;
15205 } catch {
15206 return false;
15207 }
15208 }
15209 function markReloadedForSwUpdate() {
15210 try {
15211 sessionStorage.setItem(SW_RELOAD_THROTTLE_KEY, String(Date.now()));
15212 } catch {
15213 }
15214 }
15215 async function registerServiceWorker(config, options = {}) {
15216 if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
15217 _status = "unsupported";
15218 return null;
15219 }
15220 if (!config?.swUrl) {
15221 _status = "unsupported";
15222 return null;
15223 }
15224 if (!window.isSecureContext) {
15225 _status = "unsupported";
15226 return null;
15227 }
15228 if (_registration || _registrationFailed) {
15229 return _registration;
15230 }
15231 if (!options.forceReplace) {
15232 const existing = await navigator.serviceWorker.getRegistrations().catch(() => []);
15233 const foreign = existing.find((reg) => {
15234 const url = reg.active?.scriptURL ?? reg.installing?.scriptURL ?? "";
15235 return url !== "" && url !== config.swUrl;
15236 });
15237 if (foreign) {
15238 _status = "foreign-sw";
15239 if (typeof console !== "undefined") {
15240 console.warn(
15241 "[desktop-mode] another service worker is already registered (" + foreign.scope + "); skipping desktop-mode SW. Set desktop_mode_pwa_force_replace_sw=true to override."
15242 );
15243 }
15244 return null;
15245 }
15246 }
15247 try {
15248 _registration = await navigator.serviceWorker.register(config.swUrl, {
15249 scope: "/",
15250 updateViaCache: "none"
15251 });
15252 _status = "registered";
15253 bindControllerChangeReload();
15254 return _registration;
15255 } catch (err) {
15256 _registrationFailed = true;
15257 _status = "failed";
15258 if (typeof console !== "undefined") {
15259 console.warn("[desktop-mode] SW registration failed:", err);
15260 }
15261 return null;
15262 }
15263 }
15264 function getSwRegistrationStatus() {
15265 return _status;
15266 }
15267 const PWA_INSTALL_TILE_ID = "desktop-mode-pwa-install";
15268 function isStandaloneDisplay() {
15269 if (typeof window === "undefined") {
15270 return false;
15271 }
15272 if (window.matchMedia?.("(display-mode: standalone)").matches) {
15273 return true;
15274 }
15275 const nav = window.navigator;
15276 return nav.standalone === true;
15277 }
15278 async function isLikelyInstalled() {
15279 if (isStandaloneDisplay()) {
15280 return true;
15281 }
15282 const nav = window.navigator;
15283 if (typeof nav.getInstalledRelatedApps !== "function") {
15284 return false;
15285 }
15286 try {
15287 const apps = await nav.getInstalledRelatedApps();
15288 return Array.isArray(apps) && apps.length > 0;
15289 } catch {
15290 return false;
15291 }
15292 }
15293 let _deferred = null;
15294 function installPwaInstallAffordance(siteName, showToast2) {
15295 if (typeof window === "undefined") {
15296 return;
15297 }
15298 window.removeEventListener(
15299 "beforeinstallprompt",
15300 _handleBeforeInstall
15301 );
15302 window.addEventListener(
15303 "beforeinstallprompt",
15304 _handleBeforeInstall
15305 );
15306 window.removeEventListener("appinstalled", _handleAppInstalled);
15307 window.addEventListener("appinstalled", _handleAppInstalled);
15308 function _handleBeforeInstall(ev) {
15309 ev.preventDefault();
15310 _deferred = ev;
15311 }
15312 function _handleAppInstalled() {
15313 _deferred = null;
15314 showToast2({
15315 message: sprintf(
15316 /* translators: %s: site name */
15317 __("Installed %s as an app."),
15318 siteName
15319 )
15320 });
15321 }
15322 }
15323 function getInstallTileDef(siteName, showToast2) {
15324 return {
15325 id: PWA_INSTALL_TILE_ID,
15326 title: sprintf(
15327 /* translators: %s: site name */
15328 __("Install %s as an app"),
15329 siteName
15330 ),
15331 // Dashicons class — the dock renderer prefers Dashicons
15332 // strings. `dashicons-download` is the closest match for
15333 // "install" in the WordPress glyph set without shipping
15334 // bespoke artwork.
15335 icon: "dashicons-download",
15336 onOpen: () => {
15337 void onTileClick(siteName, showToast2);
15338 }
15339 };
15340 }
15341 async function onTileClick(siteName, showToast2) {
15342 if (_deferred) {
15343 const event = _deferred;
15344 _deferred = null;
15345 try {
15346 await event.prompt();
15347 const choice = await event.userChoice;
15348 if (choice.outcome === "dismissed") {
15349 showToast2({
15350 message: __("Install cancelled.")
15351 });
15352 }
15353 } catch (err) {
15354 if (typeof console !== "undefined") {
15355 console.warn(
15356 "[desktop-mode] install prompt failed:",
15357 err
15358 );
15359 }
15360 }
15361 return;
15362 }
15363 if (await isLikelyInstalled()) {
15364 showToast2({
15365 message: sprintf(
15366 /* translators: %s: site name */
15367 __(
15368 "%s is already installed. Open it from your apps menu or home screen."
15369 ),
15370 siteName
15371 )
15372 });
15373 return;
15374 }
15375 if (getSwRegistrationStatus() === "foreign-sw") {
15376 showToast2({
15377 message: __(
15378 "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."
15379 )
15380 });
15381 return;
15382 }
15383 showToast2({
15384 message: __(
15385 "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."
15386 )
15387 });
15388 }
15389 async function promptInstall() {
15390 if (!_deferred) {
15391 return "unavailable";
15392 }
15393 const event = _deferred;
15394 _deferred = null;
15395 try {
15396 await event.prompt();
15397 const choice = await event.userChoice;
15398 return choice.outcome;
15399 } catch {
15400 return "unavailable";
15401 }
15402 }
15403 function undismissInstallHint() {
15404 Promise.resolve().then(() => state).then((m) => {
15405 m.updatePwaState({ installHintDismissed: false });
15406 });
15407 }
15408 function notify$3(options) {
15409 const intent = activity.filter(
15410 "desktop-mode/notification-requested",
15411 { ...options }
15412 );
15413 if (!intent || intent.cancel === true || !intent.title) {
15414 return () => void 0;
15415 }
15416 let dismissed = false;
15417 let dismissNative = null;
15418 let dismissToast = null;
15419 const dismiss = () => {
15420 if (dismissed) {
15421 return;
15422 }
15423 dismissed = true;
15424 if (dismissNative) {
15425 dismissNative();
15426 }
15427 if (dismissToast) {
15428 dismissToast();
15429 }
15430 };
15431 const fallback = () => {
15432 dismissToast = showToast({
15433 message: intent.body ? intent.title + " — " + intent.body : intent.title
15434 });
15435 activity.publish("desktop-mode/notification-shown", {
15436 ...intent,
15437 fallback: "toast"
15438 });
15439 };
15440 if (typeof window === "undefined" || typeof Notification === "undefined") {
15441 fallback();
15442 return dismiss;
15443 }
15444 const perm = Notification.permission;
15445 if (perm === "granted") {
15446 dismissNative = renderNative(intent);
15447 return dismiss;
15448 }
15449 if (perm === "denied") {
15450 fallback();
15451 return dismiss;
15452 }
15453 void Notification.requestPermission().then((result) => {
15454 if (dismissed) {
15455 return;
15456 }
15457 if (result === "granted") {
15458 updatePwaState({ notificationsEnabled: true });
15459 dismissNative = renderNative(intent);
15460 return;
15461 }
15462 fallback();
15463 });
15464 return dismiss;
15465 }
15466 function renderNative(intent) {
15467 let n = null;
15468 try {
15469 n = new Notification(intent.title, {
15470 body: intent.body,
15471 icon: intent.icon,
15472 tag: intent.tag,
15473 requireInteraction: intent.requireInteraction
15474 });
15475 } catch (err) {
15476 if (typeof console !== "undefined") {
15477 console.warn("[desktop-mode] Notification ctor threw:", err);
15478 }
15479 return () => void 0;
15480 }
15481 if (intent.onClick) {
15482 const handler = intent.onClick;
15483 n.onclick = () => {
15484 try {
15485 handler(n);
15486 } catch (hErr) {
15487 if (typeof console !== "undefined") {
15488 console.error(
15489 "[desktop-mode] notification onClick threw:",
15490 hErr
15491 );
15492 }
15493 }
15494 };
15495 }
15496 activity.publish("desktop-mode/notification-shown", {
15497 ...intent,
15498 fallback: null
15499 });
15500 return () => {
15501 if (n) {
15502 n.close();
15503 }
15504 };
15505 }
15506 async function requestNotificationPermission() {
15507 if (typeof Notification === "undefined") {
15508 return "unsupported";
15509 }
15510 if (Notification.permission !== "default") {
15511 return Notification.permission;
15512 }
15513 const result = await Notification.requestPermission();
15514 if (result === "granted") {
15515 updatePwaState({ notificationsEnabled: true });
15516 }
15517 return result;
15518 }
15519 function getNotificationPermission() {
15520 if (typeof Notification === "undefined") {
15521 return "unsupported";
15522 }
15523 return Notification.permission;
15524 }
15525 function bootstrapPwa(config, showToast2) {
15526 if (!config.pwa) {
15527 return;
15528 }
15529 initPwaState(config.pwa);
15530 installPwaInstallAffordance(
15531 config.pwa.appName || "WordPress",
15532 showToast2
15533 );
15534 void registerServiceWorker(config.pwa, {
15535 forceReplace: !!config.pwa.forceReplaceSw
15536 });
15537 }
15538 const DRAG_BRIDGE_EVENTS = {
15539 START: "desktop-mode-cross-frame-drag-start",
15540 END: "desktop-mode-cross-frame-drag-end"
15541 };
15542 function isStart(m) {
15543 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-start" && !!m.payload && typeof m.payload === "object";
15544 }
15545 function isEnd(m) {
15546 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-end";
15547 }
15548 function isPayloadRequest(m) {
15549 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-payload-request";
15550 }
15551 function normalizeLegacyPayload(payload) {
15552 const obj = payload;
15553 if (obj.kind !== void 0 && obj.kind !== null) {
15554 return payload;
15555 }
15556 if (typeof obj.id === "number" && typeof obj.url === "string" && typeof obj.mime === "string") {
15557 return {
15558 kind: "attachment",
15559 id: obj.id,
15560 url: obj.url,
15561 title: typeof obj.title === "string" ? obj.title : "",
15562 alt: typeof obj.alt === "string" ? obj.alt : "",
15563 mime: obj.mime,
15564 thumbnailUrl: typeof obj.thumbnailUrl === "string" ? obj.thumbnailUrl : void 0,
15565 sizes: obj.sizes && typeof obj.sizes === "object" ? obj.sizes : void 0
15566 };
15567 }
15568 return payload;
15569 }
15570 class DragBridge {
15571 constructor() {
15572 this._payload = null;
15573 this._onMessage = (e) => {
15574 if (e.origin !== this._origin) {
15575 return;
15576 }
15577 const msg = e.data;
15578 if (isStart(msg)) {
15579 this._startDrag(msg.payload);
15580 return;
15581 }
15582 if (isEnd(msg)) {
15583 this._endDrag();
15584 return;
15585 }
15586 if (isPayloadRequest(msg) && this._payload && e.source) {
15587 try {
15588 e.source.postMessage(
15589 { type: "desktop-mode-drag-payload", payload: this._payload },
15590 this._origin
15591 );
15592 } catch {
15593 }
15594 }
15595 };
15596 this._origin = window.location.origin;
15597 window.addEventListener("message", this._onMessage);
15598 }
15599 getPayload() {
15600 return this._payload;
15601 }
15602 isDragging() {
15603 return this._payload !== null;
15604 }
15605 start(payload) {
15606 if (this._payload === payload) {
15607 return;
15608 }
15609 this._startDrag(payload);
15610 }
15611 end() {
15612 this._endDrag();
15613 }
15614 _startDrag(payload) {
15615 const normalized = normalizeLegacyPayload(payload);
15616 this._payload = normalized;
15617 document.dispatchEvent(
15618 new CustomEvent(DRAG_BRIDGE_EVENTS.START, {
15619 detail: { payload: normalized }
15620 })
15621 );
15622 }
15623 _endDrag() {
15624 if (this._payload === null) {
15625 return;
15626 }
15627 const payload = this._payload;
15628 this._payload = null;
15629 document.dispatchEvent(
15630 new CustomEvent(DRAG_BRIDGE_EVENTS.END, { detail: { payload } })
15631 );
15632 }
15633 }
15634 class DropTargetRegistry {
15635 constructor() {
15636 this._targets = /* @__PURE__ */ new Map();
15637 this._byElement = /* @__PURE__ */ new Map();
15638 }
15639 register(target2) {
15640 const prev = this._targets.get(target2.id);
15641 if (prev) {
15642 this._byElement.delete(prev.element);
15643 }
15644 this._targets.set(target2.id, target2);
15645 this._byElement.set(target2.element, target2);
15646 return () => {
15647 const cur = this._targets.get(target2.id);
15648 if (cur === target2) {
15649 this._targets.delete(target2.id);
15650 this._byElement.delete(target2.element);
15651 }
15652 };
15653 }
15654 list() {
15655 return Array.from(this._targets.values());
15656 }
15657 clear() {
15658 this._targets.clear();
15659 this._byElement.clear();
15660 }
15661 /**
15662 * Find the deepest registered target whose element is `el` or an
15663 * ancestor of `el`. Walks the DOM tree once (O(depth)).
15664 *
15665 * Window claim boundary: if the walk crosses a `.desktop-mode-window`
15666 * element BEFORE finding a registered target, hit-testing stops
15667 * there and returns null. This is the rule that makes "drag over
15668 * a Gutenberg admin window" produce reject feedback instead of
15669 * silently routing the drop to the wallpaper canvas underneath.
15670 *
15671 * A window can opt INTO accepting drops by registering a target
15672 * on its own body (e.g. Recycle Bin's `[data-desktop-mode-recycle-bin-root]`):
15673 * since that element sits inside the window, the walk hits it
15674 * before reaching the window boundary and the body's target wins.
15675 */
15676 hitTest(el) {
15677 let cur = el;
15678 while (cur) {
15679 if (cur instanceof HTMLElement) {
15680 const t = this._byElement.get(cur);
15681 if (t) {
15682 return t;
15683 }
15684 if (cur.classList.contains("desktop-mode-window")) {
15685 return null;
15686 }
15687 }
15688 cur = cur.parentElement;
15689 }
15690 return null;
15691 }
15692 /**
15693 * Convenience: pick the target at viewport `(clientX, clientY)`.
15694 * Caller is responsible for hiding any obscuring ghost element
15695 * before calling — see `GhostHandle.withHidden()`.
15696 */
15697 hitTestPoint(clientX, clientY) {
15698 const el = document.elementFromPoint(clientX, clientY);
15699 const target2 = this.hitTest(el);
15700 return { target: target2, element: el, accepted: false };
15701 }
15702 }
15703 const GHOST_CLASS = "desktop-mode-drag-ghost";
15704 const GHOST_ACCEPT_CLASS = "desktop-mode-drag-ghost--accept";
15705 const GHOST_REJECT_CLASS = "desktop-mode-drag-ghost--reject";
15706 const HINT_CLASS = "desktop-mode-drag-hint";
15707 const HINT_ACCEPT_CLASS = "desktop-mode-drag-hint--accept";
15708 const HINT_REJECT_CLASS = "desktop-mode-drag-hint--reject";
15709 const HINT_NEUTRAL_CLASS = "desktop-mode-drag-hint--neutral";
15710 const HINT_OFFSET_X = 16;
15711 const HINT_OFFSET_Y = 18;
15712 function mountGhost(payload, clientX, clientY) {
15713 const ghost = buildGhost(payload);
15714 const offsetX = payload.ghost?.offsetX ?? defaultOffsetX(payload.source);
15715 const offsetY = payload.ghost?.offsetY ?? defaultOffsetY(payload.source);
15716 ghost.classList.add(GHOST_CLASS);
15717 ghost.setAttribute("aria-hidden", "true");
15718 ghost.style.position = "fixed";
15719 ghost.style.left = "0";
15720 ghost.style.top = "0";
15721 ghost.style.margin = "0";
15722 ghost.style.pointerEvents = "none";
15723 ghost.style.zIndex = "2147483647";
15724 ghost.style.willChange = "transform";
15725 document.body.appendChild(ghost);
15726 const labels = resolveHintLabels(payload);
15727 const hint = labels ? buildHintChip() : null;
15728 if (hint) {
15729 document.body.appendChild(hint);
15730 }
15731 const handle = {
15732 get element() {
15733 return ghost;
15734 },
15735 moveTo(cx, cy) {
15736 ghost.style.transform = `translate3d(${cx - offsetX}px, ${cy - offsetY}px, 0)`;
15737 if (hint) {
15738 hint.style.transform = `translate3d(${cx + HINT_OFFSET_X}px, ${cy + HINT_OFFSET_Y}px, 0)`;
15739 }
15740 },
15741 setMode(mode, overrides) {
15742 ghost.classList.remove(GHOST_ACCEPT_CLASS, GHOST_REJECT_CLASS);
15743 if (mode === "accept") {
15744 ghost.classList.add(GHOST_ACCEPT_CLASS);
15745 } else if (mode === "reject") {
15746 ghost.classList.add(GHOST_REJECT_CLASS);
15747 }
15748 if (hint && labels) {
15749 hint.classList.remove(
15750 HINT_ACCEPT_CLASS,
15751 HINT_REJECT_CLASS,
15752 HINT_NEUTRAL_CLASS
15753 );
15754 if (mode === "accept") {
15755 hint.classList.add(HINT_ACCEPT_CLASS);
15756 hint.textContent = overrides?.acceptLabel ?? labels.accept;
15757 } else if (mode === "reject") {
15758 hint.classList.add(HINT_REJECT_CLASS);
15759 hint.textContent = labels.reject;
15760 } else {
15761 hint.classList.add(HINT_NEUTRAL_CLASS);
15762 hint.textContent = labels.neutral;
15763 }
15764 hint.hidden = !hint.textContent;
15765 }
15766 },
15767 withHidden(fn) {
15768 const prevG = ghost.style.visibility;
15769 const prevH = hint?.style.visibility ?? "";
15770 ghost.style.visibility = "hidden";
15771 if (hint) {
15772 hint.style.visibility = "hidden";
15773 }
15774 try {
15775 return fn();
15776 } finally {
15777 ghost.style.visibility = prevG;
15778 if (hint) {
15779 hint.style.visibility = prevH;
15780 }
15781 }
15782 },
15783 dispose() {
15784 if (ghost.isConnected) {
15785 ghost.remove();
15786 }
15787 if (hint?.isConnected) {
15788 hint.remove();
15789 }
15790 }
15791 };
15792 handle.moveTo(clientX, clientY);
15793 handle.setMode("neutral");
15794 return handle;
15795 }
15796 function buildHintChip() {
15797 const chip = document.createElement("div");
15798 chip.className = HINT_CLASS;
15799 chip.setAttribute("aria-hidden", "true");
15800 chip.setAttribute("role", "presentation");
15801 chip.style.position = "fixed";
15802 chip.style.left = "0";
15803 chip.style.top = "0";
15804 chip.style.margin = "0";
15805 chip.style.pointerEvents = "none";
15806 chip.style.zIndex = "2147483647";
15807 chip.style.willChange = "transform";
15808 return chip;
15809 }
15810 function resolveHintLabels(payload) {
15811 const cfg = payload.ghost?.hint;
15812 if (cfg?.hidden) {
15813 return null;
15814 }
15815 return {
15816 accept: cfg?.accept ?? defaultAcceptLabel(payload),
15817 reject: cfg?.reject ?? defaultRejectLabel(),
15818 neutral: cfg?.neutral ?? defaultNeutralLabel(payload)
15819 };
15820 }
15821 function defaultAcceptLabel(payload) {
15822 if (payload.type === "shortcut") {
15823 return __("Drop here to create shortcut", "desktop-mode");
15824 }
15825 if (payload.type === "desktop-file") {
15826 return __("Drop here to move", "desktop-mode");
15827 }
15828 return __("Drop here", "desktop-mode");
15829 }
15830 function defaultRejectLabel(_payload) {
15831 return __("Can’t drop here", "desktop-mode");
15832 }
15833 function defaultNeutralLabel(payload) {
15834 if (payload.type === "shortcut") {
15835 return __(
15836 "Drop on the desktop or a folder",
15837 "desktop-mode"
15838 );
15839 }
15840 if (payload.type === "desktop-file") {
15841 return __("Drop in a folder", "desktop-mode");
15842 }
15843 return "";
15844 }
15845 function buildGhost(payload) {
15846 if (payload.ghost?.element) {
15847 return payload.ghost.element;
15848 }
15849 const clone = payload.source.cloneNode(true);
15850 clone.removeAttribute("id");
15851 const rect = payload.source.getBoundingClientRect();
15852 clone.style.width = `${rect.width}px`;
15853 clone.style.height = `${rect.height}px`;
15854 return clone;
15855 }
15856 function defaultOffsetX(source) {
15857 return source.offsetWidth / 2;
15858 }
15859 function defaultOffsetY(source) {
15860 return source.offsetHeight / 2;
15861 }
15862 let _installed$2 = false;
15863 function installRecovery(cancelActive) {
15864 if (_installed$2) {
15865 return;
15866 }
15867 _installed$2 = true;
15868 document.addEventListener("keydown", (e) => {
15869 if (e.key === "Escape") {
15870 cancelActive("escape");
15871 }
15872 });
15873 window.addEventListener("blur", () => {
15874 cancelActive("blur");
15875 });
15876 document.addEventListener("visibilitychange", () => {
15877 if (document.hidden) {
15878 cancelActive("visibility");
15879 }
15880 });
15881 }
15882 const DRAG_THRESHOLD_PX = 4;
15883 const DRAG_EVENTS = {
15884 START: "desktop-mode.drag.start",
15885 MOVE: "desktop-mode.drag.move",
15886 ENTER: "desktop-mode.drag.enter",
15887 LEAVE: "desktop-mode.drag.leave",
15888 REJECTED: "desktop-mode.drag.rejected",
15889 COMMIT: "desktop-mode.drag.commit",
15890 CANCEL: "desktop-mode.drag.cancel",
15891 END: "desktop-mode.drag.end"
15892 };
15893 const SOURCE_DRAGGING_CLASS = "desktop-mode-file-tile--dragging";
15894 const TARGET_DROP_ACTIVE_CLASS = "desktop-mode-file-tile--drop-target";
15895 const TRASH_DROP_ACTIVE_ATTR$1 = "data-desktop-mode-trash-drop-active";
15896 const FILES_DROP_ACTIVE_ATTR = "data-files-drop-active";
15897 const BODY_DRAGGING_ATTR = "data-desktop-mode-dragging";
15898 const BODY_DRAG_TYPE_ATTR = "data-desktop-mode-drag-type";
15899 const BODY_DRAG_MODE_ATTR = "data-desktop-mode-drag-mode";
15900 class DragManager {
15901 constructor() {
15902 this._registry = new DropTargetRegistry();
15903 this._active = null;
15904 this._docListenersAttached = false;
15905 this._lastLiftedEndAt = 0;
15906 this._onPointerMove = (e) => {
15907 const session = this._active;
15908 if (!session || session._pointerId !== e.pointerId) {
15909 return;
15910 }
15911 const dx = e.clientX - session._origin.clientX;
15912 const dy = e.clientY - session._origin.clientY;
15913 if (!session._lifted) {
15914 if (Math.abs(dx) < DRAG_THRESHOLD_PX && Math.abs(dy) < DRAG_THRESHOLD_PX) {
15915 return;
15916 }
15917 this._lift(session, e);
15918 }
15919 if (!session._ghost) {
15920 return;
15921 }
15922 session._ghost.moveTo(e.clientX, e.clientY);
15923 this._updateHover(session, e.clientX, e.clientY);
15924 dispatchOnDocument(DRAG_EVENTS.MOVE, {
15925 payload: session.payload,
15926 clientX: e.clientX,
15927 clientY: e.clientY
15928 });
15929 };
15930 this._onPointerUp = (e) => {
15931 const session = this._active;
15932 if (!session || session._pointerId !== e.pointerId) {
15933 return;
15934 }
15935 if (!session._lifted) {
15936 session._finished = true;
15937 this._active = null;
15938 try {
15939 session._callbacks.onClickOnly?.();
15940 } catch (err) {
15941 console.error("[desktop-mode] drag onClickOnly threw:", err);
15942 }
15943 return;
15944 }
15945 const hit = this._hitTestNow(session, e.clientX, e.clientY);
15946 if (hit && hit.accepted && hit.target) {
15947 this._commit(session, hit.target, e.clientX, e.clientY);
15948 return;
15949 }
15950 this._cancel(session, hit && hit.target ? "rejected" : "no-target");
15951 };
15952 this._onPointerCancel = (e) => {
15953 const session = this._active;
15954 if (!session || session._pointerId !== e.pointerId) {
15955 return;
15956 }
15957 this._cancel(session, "pointercancel");
15958 };
15959 }
15960 start(opts) {
15961 if (this._active) {
15962 return null;
15963 }
15964 if (opts.origin.button !== 0) {
15965 return null;
15966 }
15967 const session = {
15968 payload: opts.payload,
15969 isFinished: () => session._finished,
15970 cancel: (reason) => this._cancel(session, reason ?? "caller"),
15971 _origin: opts.origin,
15972 _pointerId: opts.origin.pointerId,
15973 _lifted: false,
15974 _finished: false,
15975 _callbacks: {
15976 onClickOnly: opts.onClickOnly,
15977 onCancel: opts.onCancel,
15978 onCommit: opts.onCommit
15979 },
15980 _ghost: null,
15981 _currentTarget: null,
15982 _currentAccepted: false
15983 };
15984 this._active = session;
15985 this._ensureDocListeners();
15986 installRecovery((reason) => {
15987 if (this._active) {
15988 this._cancel(this._active, reason);
15989 }
15990 });
15991 return session;
15992 }
15993 registerDropTarget(target2) {
15994 return this._registry.register(target2);
15995 }
15996 isDragging() {
15997 return this._active !== null && this._active._lifted;
15998 }
15999 /**
16000 * Whether a real (lifted) drag ended within `withinMs` of now.
16001 * Surfaces that bind plain `click` listeners use this to ignore
16002 * the synthesized click that fires after a drop. 500 ms is a
16003 * generous default — browsers fire the click within 10–50 ms of
16004 * pointerup, but plugins may chain post-drag work into a
16005 * `requestAnimationFrame` and call back into a click-driven API.
16006 *
16007 * @public
16008 * @since 0.18.x
16009 */
16010 recentlyEndedDrag(withinMs = 500) {
16011 if (this._lastLiftedEndAt === 0) {
16012 return false;
16013 }
16014 return Date.now() - this._lastLiftedEndAt < withinMs;
16015 }
16016 getActive() {
16017 return this._active;
16018 }
16019 debug() {
16020 return {
16021 findOrphans: () => findOrphans(),
16022 listTargets: () => this._registry.list()
16023 };
16024 }
16025 // -----------------------------------------------------------------
16026 // Internals
16027 // -----------------------------------------------------------------
16028 _ensureDocListeners() {
16029 if (this._docListenersAttached) {
16030 return;
16031 }
16032 this._docListenersAttached = true;
16033 document.addEventListener("pointermove", this._onPointerMove, true);
16034 document.addEventListener("pointerup", this._onPointerUp, true);
16035 document.addEventListener("pointercancel", this._onPointerCancel, true);
16036 }
16037 _lift(session, e) {
16038 session._lifted = true;
16039 session.payload.source.classList.add(SOURCE_DRAGGING_CLASS);
16040 session._ghost = mountGhost(session.payload, e.clientX, e.clientY);
16041 if (typeof document !== "undefined" && document.body) {
16042 document.body.setAttribute(BODY_DRAGGING_ATTR, "");
16043 document.body.setAttribute(
16044 BODY_DRAG_TYPE_ATTR,
16045 String(session.payload.type)
16046 );
16047 document.body.setAttribute(BODY_DRAG_MODE_ATTR, "neutral");
16048 }
16049 dispatchOnDocument(DRAG_EVENTS.START, { payload: session.payload });
16050 }
16051 _hitTestNow(session, clientX, clientY) {
16052 const run = () => {
16053 const el = document.elementFromPoint(clientX, clientY);
16054 const target2 = this._registry.hitTest(el);
16055 if (!target2) {
16056 return { target: null, accepted: false };
16057 }
16058 let accepted = false;
16059 try {
16060 accepted = target2.accept(session.payload);
16061 } catch (err) {
16062 console.error("[desktop-mode] drop target accept() threw:", target2.id, err);
16063 }
16064 return { target: target2, accepted };
16065 };
16066 if (session._ghost) {
16067 return session._ghost.withHidden(run);
16068 }
16069 return run();
16070 }
16071 _updateHover(session, clientX, clientY) {
16072 const next = this._hitTestNow(session, clientX, clientY);
16073 const prevTarget = session._currentTarget;
16074 if (next.target === prevTarget && next.accepted === session._currentAccepted) {
16075 return;
16076 }
16077 if (prevTarget) {
16078 fireLeave(prevTarget, session);
16079 }
16080 session._currentTarget = next.target;
16081 session._currentAccepted = next.accepted;
16082 let mode;
16083 if (next.target) {
16084 if (next.accepted) {
16085 fireEnter(next.target, session);
16086 session._ghost?.setMode("accept", {
16087 acceptLabel: next.target.acceptLabel
16088 });
16089 mode = "accept";
16090 } else {
16091 session._ghost?.setMode("reject");
16092 dispatchOnDocument(DRAG_EVENTS.REJECTED, {
16093 payload: session.payload,
16094 targetId: next.target.id
16095 });
16096 mode = "reject";
16097 }
16098 } else {
16099 session._ghost?.setMode("reject");
16100 mode = "reject";
16101 }
16102 if (typeof document !== "undefined" && document.body) {
16103 document.body.setAttribute(BODY_DRAG_MODE_ATTR, mode);
16104 }
16105 }
16106 _commit(session, target2, clientX, clientY) {
16107 session._finished = true;
16108 this._lastLiftedEndAt = Date.now();
16109 fireLeave(target2, session);
16110 this._cleanupDom(session);
16111 const prevActive = this._active;
16112 this._active = null;
16113 try {
16114 void target2.onDrop(session, { clientX, clientY });
16115 } catch (err) {
16116 console.error("[desktop-mode] drop target onDrop threw:", target2.id, err);
16117 }
16118 try {
16119 session._callbacks.onCommit?.(target2);
16120 } catch (err) {
16121 console.error("[desktop-mode] drag onCommit threw:", err);
16122 }
16123 dispatchOnDocument(DRAG_EVENTS.COMMIT, {
16124 payload: session.payload,
16125 targetId: target2.id
16126 });
16127 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason: "commit" });
16128 if (this._active === prevActive) {
16129 this._active = null;
16130 }
16131 }
16132 _cancel(session, reason) {
16133 if (session._finished) {
16134 return;
16135 }
16136 session._finished = true;
16137 if (session._lifted) {
16138 this._lastLiftedEndAt = Date.now();
16139 }
16140 if (session._currentTarget) {
16141 fireLeave(session._currentTarget, session);
16142 }
16143 this._cleanupDom(session);
16144 this._active = null;
16145 try {
16146 session._callbacks.onCancel?.(reason);
16147 } catch (err) {
16148 console.error("[desktop-mode] drag onCancel threw:", err);
16149 }
16150 dispatchOnDocument(DRAG_EVENTS.CANCEL, { payload: session.payload, reason });
16151 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason });
16152 }
16153 _cleanupDom(session) {
16154 try {
16155 session.payload.source.classList.remove(SOURCE_DRAGGING_CLASS);
16156 } catch {
16157 }
16158 session._ghost?.dispose();
16159 session._ghost = null;
16160 session._currentTarget = null;
16161 session._currentAccepted = false;
16162 if (typeof document !== "undefined" && document.body) {
16163 document.body.removeAttribute(BODY_DRAGGING_ATTR);
16164 document.body.removeAttribute(BODY_DRAG_TYPE_ATTR);
16165 document.body.removeAttribute(BODY_DRAG_MODE_ATTR);
16166 }
16167 scrubOrphans();
16168 }
16169 }
16170 function dispatchOnDocument(type, detail) {
16171 if (typeof document === "undefined") {
16172 return;
16173 }
16174 document.dispatchEvent(new CustomEvent(type, { detail }));
16175 }
16176 function fireEnter(target2, session) {
16177 try {
16178 target2.onEnter?.(session);
16179 } catch (err) {
16180 console.error("[desktop-mode] drop target onEnter threw:", target2.id, err);
16181 }
16182 dispatchOnDocument(DRAG_EVENTS.ENTER, {
16183 payload: session.payload,
16184 targetId: target2.id
16185 });
16186 }
16187 function fireLeave(target2, session) {
16188 try {
16189 target2.onLeave?.(session);
16190 } catch (err) {
16191 console.error("[desktop-mode] drop target onLeave threw:", target2.id, err);
16192 }
16193 dispatchOnDocument(DRAG_EVENTS.LEAVE, {
16194 payload: session.payload,
16195 targetId: target2.id
16196 });
16197 }
16198 function findOrphans() {
16199 if (typeof document === "undefined") {
16200 return [];
16201 }
16202 const out = [];
16203 for (const sel of [
16204 `.${SOURCE_DRAGGING_CLASS}`,
16205 `.${TARGET_DROP_ACTIVE_CLASS}`,
16206 `[${TRASH_DROP_ACTIVE_ATTR$1}]`,
16207 `[${FILES_DROP_ACTIVE_ATTR}]`
16208 ]) {
16209 document.querySelectorAll(sel).forEach((el) => out.push(el));
16210 }
16211 return out;
16212 }
16213 function scrubOrphans() {
16214 for (const el of findOrphans()) {
16215 el.classList.remove(SOURCE_DRAGGING_CLASS, TARGET_DROP_ACTIVE_CLASS);
16216 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR$1);
16217 el.removeAttribute(FILES_DROP_ACTIVE_ATTR);
16218 }
16219 }
16220 const TARGET_ID_PREFIX = "desktop-mode-iframe-drop-";
16221 const IFRAME_SELECTOR = "iframe.desktop-mode-window__iframe";
16222 const DROP_ACTIVE_ATTR = "data-desktop-mode-iframe-drop-active";
16223 let _installed$1 = false;
16224 let _dragManager = null;
16225 const _suppressedIframes = /* @__PURE__ */ new Map();
16226 const _activeRegistrations = /* @__PURE__ */ new Map();
16227 let _bridgeInterceptPayload = null;
16228 let _lastHoveredBridgeIframe = null;
16229 function suppressIframePointerEventsBridge() {
16230 const iframes = document.querySelectorAll(
16231 IFRAME_SELECTOR
16232 );
16233 iframes.forEach((iframe) => {
16234 if (_suppressedIframes.has(iframe)) {
16235 return;
16236 }
16237 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
16238 iframe.style.pointerEvents = "none";
16239 });
16240 }
16241 function restoreIframePointerEvents() {
16242 _suppressedIframes.forEach((prev, iframe) => {
16243 iframe.style.pointerEvents = prev;
16244 });
16245 _suppressedIframes.clear();
16246 }
16247 function findIframeAtCursor(clientX, clientY) {
16248 const el = document.elementFromPoint(clientX, clientY);
16249 if (!el) {
16250 return null;
16251 }
16252 const win = el.closest(".desktop-mode-window");
16253 if (!(win instanceof HTMLElement)) {
16254 return null;
16255 }
16256 const iframe = win.querySelector(IFRAME_SELECTOR);
16257 return iframe instanceof HTMLIFrameElement ? iframe : null;
16258 }
16259 const onBridgeDragOver = (e) => {
16260 if (!_bridgeInterceptPayload) {
16261 return;
16262 }
16263 e.preventDefault();
16264 if (e.dataTransfer) {
16265 e.dataTransfer.dropEffect = "copy";
16266 }
16267 const iframe = findIframeAtCursor(e.clientX, e.clientY);
16268 if (iframe === _lastHoveredBridgeIframe) {
16269 return;
16270 }
16271 if (_lastHoveredBridgeIframe) {
16272 postIntoIframe(_lastHoveredBridgeIframe, {
16273 type: "desktop-mode-drag-leave"
16274 });
16275 }
16276 _lastHoveredBridgeIframe = iframe;
16277 if (iframe) {
16278 postIntoIframe(iframe, {
16279 type: "desktop-mode-drag-over",
16280 payload: _bridgeInterceptPayload
16281 });
16282 }
16283 };
16284 const onBridgeDrop = (e) => {
16285 if (!_bridgeInterceptPayload) {
16286 return;
16287 }
16288 e.preventDefault();
16289 e.stopPropagation();
16290 if (typeof e.stopImmediatePropagation === "function") {
16291 e.stopImmediatePropagation();
16292 }
16293 const iframe = findIframeAtCursor(e.clientX, e.clientY);
16294 const payload = _bridgeInterceptPayload;
16295 stopBridgeIntercept();
16296 if (!iframe) {
16297 return;
16298 }
16299 const rect = iframe.getBoundingClientRect();
16300 postIntoIframe(iframe, {
16301 type: "desktop-mode-drop",
16302 payload,
16303 position: {
16304 x: e.clientX - rect.left,
16305 y: e.clientY - rect.top
16306 }
16307 });
16308 };
16309 const onBridgeDragEnd = () => {
16310 stopBridgeIntercept();
16311 };
16312 function startBridgeIntercept(payload) {
16313 if (_bridgeInterceptPayload) {
16314 _bridgeInterceptPayload = payload;
16315 return;
16316 }
16317 _bridgeInterceptPayload = payload;
16318 suppressIframePointerEventsBridge();
16319 document.addEventListener("dragover", onBridgeDragOver, true);
16320 document.addEventListener("drop", onBridgeDrop, true);
16321 document.addEventListener("dragend", onBridgeDragEnd, true);
16322 }
16323 function stopBridgeIntercept() {
16324 if (!_bridgeInterceptPayload) {
16325 return;
16326 }
16327 _bridgeInterceptPayload = null;
16328 if (_lastHoveredBridgeIframe) {
16329 postIntoIframe(_lastHoveredBridgeIframe, {
16330 type: "desktop-mode-drag-leave"
16331 });
16332 _lastHoveredBridgeIframe = null;
16333 }
16334 document.removeEventListener("dragover", onBridgeDragOver, true);
16335 document.removeEventListener("drop", onBridgeDrop, true);
16336 document.removeEventListener("dragend", onBridgeDragEnd, true);
16337 restoreIframePointerEvents();
16338 }
16339 function extractBridgePayload(payload) {
16340 if (!payload || typeof payload !== "object") {
16341 return void 0;
16342 }
16343 const obj = payload;
16344 if (obj.type !== "shortcut" && obj.type !== "desktop-file") {
16345 return void 0;
16346 }
16347 const data = obj.data;
16348 return data?.bridgePayload;
16349 }
16350 function postIntoIframe(iframe, msg) {
16351 const w = iframe.contentWindow;
16352 if (!w) {
16353 return;
16354 }
16355 try {
16356 w.postMessage(msg, window.location.origin);
16357 } catch {
16358 }
16359 }
16360 function registerDropTargetFor(dragManager, iframe, target2, windowId) {
16361 return dragManager.registerDropTarget({
16362 id: `${TARGET_ID_PREFIX}${windowId}`,
16363 element: target2,
16364 accept: (payload) => !!extractBridgePayload(payload),
16365 onEnter: (session) => {
16366 const bridge = extractBridgePayload(session.payload);
16367 if (!bridge) {
16368 return;
16369 }
16370 target2.setAttribute(DROP_ACTIVE_ATTR, "");
16371 postIntoIframe(iframe, {
16372 type: "desktop-mode-drag-over",
16373 payload: bridge
16374 });
16375 },
16376 onLeave: () => {
16377 target2.removeAttribute(DROP_ACTIVE_ATTR);
16378 postIntoIframe(iframe, { type: "desktop-mode-drag-leave" });
16379 },
16380 onDrop: (session, ev) => {
16381 target2.removeAttribute(DROP_ACTIVE_ATTR);
16382 const bridge = extractBridgePayload(session.payload);
16383 if (!bridge) {
16384 return;
16385 }
16386 const rect = iframe.getBoundingClientRect();
16387 postIntoIframe(iframe, {
16388 type: "desktop-mode-drop",
16389 payload: bridge,
16390 position: {
16391 x: ev.clientX - rect.left,
16392 y: ev.clientY - rect.top
16393 }
16394 });
16395 }
16396 });
16397 }
16398 function deriveWindowIdFromIframe(iframe) {
16399 let cur = iframe.parentElement;
16400 while (cur) {
16401 if (cur.id.startsWith("wp-window-")) {
16402 return cur.id.slice("wp-window-".length);
16403 }
16404 cur = cur.parentElement;
16405 }
16406 return `unknown-${Math.random().toString(36).slice(2, 10)}`;
16407 }
16408 function onDragStart(payload) {
16409 const dragManager = _dragManager;
16410 if (!dragManager) {
16411 return;
16412 }
16413 const iframes = document.querySelectorAll(IFRAME_SELECTOR);
16414 const isBridgeable = !!extractBridgePayload(payload);
16415 console.info(
16416 "[desktop-mode] drag-start: suppressing %d iframe(s); bridgeable=%s",
16417 iframes.length,
16418 isBridgeable,
16419 payload
16420 );
16421 iframes.forEach((iframe) => {
16422 if (!_suppressedIframes.has(iframe)) {
16423 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
16424 iframe.style.pointerEvents = "none";
16425 }
16426 if (!isBridgeable) {
16427 return;
16428 }
16429 if (_activeRegistrations.has(iframe)) {
16430 return;
16431 }
16432 const dropTargetEl = iframe.parentElement;
16433 if (!dropTargetEl) {
16434 return;
16435 }
16436 const windowId = deriveWindowIdFromIframe(iframe);
16437 const deregister = registerDropTargetFor(
16438 dragManager,
16439 iframe,
16440 dropTargetEl,
16441 windowId
16442 );
16443 _activeRegistrations.set(iframe, deregister);
16444 });
16445 }
16446 function onDragEnd() {
16447 _suppressedIframes.forEach((prev, iframe) => {
16448 iframe.style.pointerEvents = prev;
16449 });
16450 _suppressedIframes.clear();
16451 _activeRegistrations.forEach((deregister) => {
16452 try {
16453 deregister();
16454 } catch {
16455 }
16456 });
16457 _activeRegistrations.clear();
16458 }
16459 function installIframeDropTargets(dragManager) {
16460 if (_installed$1) {
16461 return;
16462 }
16463 _installed$1 = true;
16464 _dragManager = dragManager;
16465 document.addEventListener(DRAG_EVENTS.START, (e) => {
16466 const detail = e.detail;
16467 onDragStart(detail?.payload);
16468 });
16469 document.addEventListener(DRAG_EVENTS.END, () => {
16470 onDragEnd();
16471 });
16472 document.addEventListener(DRAG_BRIDGE_EVENTS.START, (e) => {
16473 const detail = e.detail;
16474 if (!detail?.payload) {
16475 return;
16476 }
16477 startBridgeIntercept(detail.payload);
16478 });
16479 document.addEventListener(DRAG_BRIDGE_EVENTS.END, () => {
16480 stopBridgeIntercept();
16481 });
16482 addAction(
16483 HOOKS.WINDOW_CLOSED,
16484 "desktop-mode/drag/iframe-drop-targets-window-close",
16485 () => {
16486 for (const [iframe] of Array.from(_suppressedIframes)) {
16487 if (!iframe.isConnected) {
16488 _suppressedIframes.delete(iframe);
16489 }
16490 }
16491 for (const [iframe, deregister] of Array.from(_activeRegistrations)) {
16492 if (!iframe.isConnected) {
16493 try {
16494 deregister();
16495 } catch {
16496 }
16497 _activeRegistrations.delete(iframe);
16498 }
16499 }
16500 }
16501 );
16502 window.__desktopModeIframeDropDebug = () => ({
16503 installed: _installed$1,
16504 iframesInDom: document.querySelectorAll(IFRAME_SELECTOR).length,
16505 suppressedCount: _suppressedIframes.size,
16506 registeredCount: _activeRegistrations.size,
16507 suppressedIframeIds: Array.from(_suppressedIframes.keys()).map(
16508 deriveWindowIdFromIframe
16509 )
16510 });
16511 }
16512 function collectOpenables() {
16513 const desktop = window.wp?.desktop;
16514 if (!desktop) {
16515 return [];
16516 }
16517 const wm = desktop.windowManager;
16518 const config = desktop.config;
16519 if (!wm || !config) {
16520 return [];
16521 }
16522 const items = [];
16523 const fromMenu = (item, group) => ({
16524 id: item.id,
16525 label: item.title,
16526 description: group,
16527 icon: item.icon,
16528 open: () => wm.open({
16529 id: item.id,
16530 baseId: item.id,
16531 url: item.url,
16532 title: item.title,
16533 icon: item.icon
16534 })
16535 });
16536 for (const item of config.dockItems ?? []) {
16537 items.push(fromMenu(item, "Admin menu"));
16538 }
16539 const filtered = applyFilters(
16540 "desktop-mode.open-command.items",
16541 items
16542 );
16543 return Array.isArray(filtered) ? filtered : items;
16544 }
16545 const openCommand = {
16546 slug: "open",
16547 label: "Open",
16548 description: "Open an admin page or registered window.",
16549 hint: "[window]",
16550 icon: "dashicons-external",
16551 /**
16552 * Suggest matching windows as the user types args. Simple
16553 * case-insensitive substring match against label AND id so
16554 * "add" finds "Add New Post" and "jorvy" finds Jorvy whether
16555 * the plugin listed it with a friendly label or the slug.
16556 */
16557 suggest(args) {
16558 const q = args.trim().toLowerCase();
16559 const list2 = collectOpenables();
16560 const hits = q === "" ? list2 : list2.filter(
16561 (w) => w.label.toLowerCase().includes(q) || w.id.toLowerCase().includes(q)
16562 );
16563 return hits.slice(0, 12).map((w) => ({
16564 value: w.label,
16565 label: w.label,
16566 description: w.description,
16567 icon: w.icon ?? "dashicons-external"
16568 }));
16569 },
16570 run(args, ctx) {
16571 const q = args.trim();
16572 if (!q) {
16573 return "Type the name of a window to open, for example `/open Posts`.";
16574 }
16575 const list2 = collectOpenables();
16576 const ql = q.toLowerCase();
16577 const match = list2.find((w) => w.label.toLowerCase() === ql || w.id.toLowerCase() === ql) ?? list2.find(
16578 (w) => w.label.toLowerCase().includes(ql) || w.id.toLowerCase().includes(ql)
16579 );
16580 if (!match) {
16581 return `No window matching **${q}** — try \`/open\` alone to see available options.`;
16582 }
16583 match.open();
16584 ctx.close();
16585 }
16586 };
16587 function registerBuiltInCommands() {
16588 registerCommand(openCommand);
16589 }
16590 const palettes = [];
16591 const listeners$2 = /* @__PURE__ */ new Set();
16592 function registerPalette(p) {
16593 if (!p || typeof p.id !== "string" || p.id === "") {
16594 return () => {
16595 };
16596 }
16597 if (typeof p.open !== "function" || typeof p.close !== "function" || typeof p.isOpen !== "function") {
16598 return () => {
16599 };
16600 }
16601 const idx = palettes.findIndex((x) => x.id === p.id);
16602 if (idx >= 0) {
16603 palettes[idx] = p;
16604 } else {
16605 palettes.push(p);
16606 }
16607 notify$2();
16608 return () => {
16609 const i = palettes.findIndex((x) => x.id === p.id);
16610 if (i >= 0) {
16611 palettes.splice(i, 1);
16612 notify$2();
16613 }
16614 };
16615 }
16616 function unregisterPalette(id) {
16617 const idx = palettes.findIndex((x) => x.id === id);
16618 if (idx >= 0) {
16619 palettes.splice(idx, 1);
16620 notify$2();
16621 }
16622 }
16623 function listPalettes() {
16624 return palettes.slice();
16625 }
16626 function notify$2() {
16627 for (const cb of Array.from(listeners$2)) {
16628 try {
16629 cb();
16630 } catch (err) {
16631 if (typeof console !== "undefined") {
16632 console.error("[desktop-mode] palette-registry listener threw:", err);
16633 }
16634 }
16635 }
16636 }
16637 function cyclePalettes() {
16638 if (palettes.length === 0) {
16639 return;
16640 }
16641 const cur = palettes.findIndex((p) => {
16642 try {
16643 return p.isOpen();
16644 } catch {
16645 return false;
16646 }
16647 });
16648 if (cur === -1) {
16649 try {
16650 palettes[0].open();
16651 } catch {
16652 }
16653 return;
16654 }
16655 try {
16656 palettes[cur].close();
16657 } catch {
16658 }
16659 const next = cur + 1;
16660 if (next < palettes.length) {
16661 try {
16662 palettes[next].open();
16663 } catch {
16664 }
16665 }
16666 }
16667 function openPaletteOnly(id) {
16668 const target2 = palettes.find((p) => p.id === id);
16669 if (!target2) {
16670 return;
16671 }
16672 for (const p of palettes) {
16673 if (p.id !== id) {
16674 try {
16675 if (p.isOpen()) {
16676 p.close();
16677 }
16678 } catch {
16679 }
16680 }
16681 }
16682 try {
16683 target2.open();
16684 } catch {
16685 }
16686 }
16687 let installed$1 = false;
16688 function installPaletteShortcut() {
16689 if (installed$1) {
16690 return;
16691 }
16692 installed$1 = true;
16693 document.addEventListener(
16694 "keydown",
16695 (e) => {
16696 if (!(e.metaKey || e.ctrlKey) || e.key !== "k") {
16697 return;
16698 }
16699 if (e.shiftKey || e.altKey) {
16700 return;
16701 }
16702 e.preventDefault();
16703 e.stopImmediatePropagation();
16704 cyclePalettes();
16705 },
16706 true
16707 );
16708 const origin = window.location.origin;
16709 window.addEventListener("message", (e) => {
16710 if (e.origin !== origin) {
16711 return;
16712 }
16713 const data = e.data;
16714 if (data && data.type === "desktop-mode-palette-cycle") {
16715 cyclePalettes();
16716 }
16717 });
16718 }
16719 const suppliers = /* @__PURE__ */ new Map();
16720 const subscribers = /* @__PURE__ */ new Map();
16721 let booted$2 = false;
16722 const heartbeat = {
16723 contribute(field, supplier) {
16724 suppliers.set(field, supplier);
16725 return () => {
16726 if (suppliers.get(field) === supplier) {
16727 suppliers.delete(field);
16728 }
16729 };
16730 },
16731 subscribe(field, cb) {
16732 let set = subscribers.get(field);
16733 if (!set) {
16734 set = /* @__PURE__ */ new Set();
16735 subscribers.set(field, set);
16736 }
16737 set.add(cb);
16738 return () => {
16739 set.delete(cb);
16740 };
16741 }
16742 };
16743 function bootHeartbeatBus() {
16744 if (booted$2) {
16745 return;
16746 }
16747 booted$2 = true;
16748 const $ = window.jQuery;
16749 if (!$) {
16750 console.warn(
16751 "[desktop-mode/heartbeat] jQuery missing — Heartbeat bus disabled."
16752 );
16753 return;
16754 }
16755 $(document).on("heartbeat-send", (...args) => {
16756 const data = args[1];
16757 if (!data) {
16758 return;
16759 }
16760 for (const [field, supplier] of suppliers) {
16761 try {
16762 data[field] = supplier();
16763 } catch (err) {
16764 console.error(
16765 `[desktop-mode/heartbeat] supplier for "${field}" threw:`,
16766 err
16767 );
16768 }
16769 }
16770 });
16771 $(document).on("heartbeat-tick", (...args) => {
16772 const response = args[1];
16773 if (!response) {
16774 return;
16775 }
16776 for (const [field, set] of subscribers) {
16777 const value = response[field];
16778 if (value === void 0) {
16779 continue;
16780 }
16781 for (const cb of set) {
16782 try {
16783 cb(value);
16784 } catch (err) {
16785 console.error(
16786 `[desktop-mode/heartbeat] subscriber for "${field}" threw:`,
16787 err
16788 );
16789 }
16790 }
16791 }
16792 });
16793 }
16794 const store$2 = createSharedStore(
16795 "desktop-mode/presence",
16796 () => ({ byUser: /* @__PURE__ */ new Map(), serverTimeMs: 0 })
16797 );
16798 const ACTIVE_THRESHOLD_MS = 5 * 60 * 1e3;
16799 let lastInputMs = Date.now();
16800 let booted$1 = false;
16801 function noteUserActivity() {
16802 lastInputMs = Date.now();
16803 }
16804 function applySnapshot(block) {
16805 if (!block || !block.snapshot) {
16806 return;
16807 }
16808 const previous = store$2.state.byUser;
16809 const next = new Map(previous);
16810 const transitions = [];
16811 for (const [rawId, raw] of Object.entries(block.snapshot)) {
16812 const userId = Number(rawId);
16813 if (!Number.isFinite(userId) || userId <= 0) {
16814 continue;
16815 }
16816 const status = raw?.status ?? "offline";
16817 const entry = {
16818 status,
16819 lastSeenMs: Number(raw?.lastSeenMs ?? 0) || 0,
16820 lastActiveMs: Number(raw?.lastActiveMs ?? 0) || 0
16821 };
16822 const old = previous.get(userId);
16823 next.set(userId, entry);
16824 if (!old || old.status !== entry.status) {
16825 transitions.push({
16826 userId,
16827 oldStatus: old ? old.status : null,
16828 newStatus: entry.status,
16829 entry
16830 });
16831 }
16832 }
16833 store$2.state.byUser = next;
16834 if (typeof block.serverTimeMs === "number") {
16835 store$2.state.serverTimeMs = block.serverTimeMs;
16836 }
16837 store$2.notify();
16838 for (const t of transitions) {
16839 const detail = {
16840 userId: t.userId,
16841 oldStatus: t.oldStatus,
16842 newStatus: t.newStatus,
16843 lastSeenMs: t.entry.lastSeenMs,
16844 lastActiveMs: t.entry.lastActiveMs
16845 };
16846 document.dispatchEvent(
16847 new CustomEvent("desktop-mode-presence-changed", { detail })
16848 );
16849 activity.publish("desktop-mode/presence-changed", detail);
16850 }
16851 activity.publish("desktop-mode/presence-snapshot-applied", {
16852 applied: Object.keys(block.snapshot).length,
16853 transitions: transitions.length
16854 });
16855 }
16856 function bootPresenceProbe() {
16857 if (booted$1) {
16858 return;
16859 }
16860 booted$1 = true;
16861 document.addEventListener("pointerdown", noteUserActivity, {
16862 capture: true,
16863 passive: true
16864 });
16865 document.addEventListener("keydown", noteUserActivity, {
16866 capture: true,
16867 passive: true
16868 });
16869 document.addEventListener("visibilitychange", () => {
16870 if (!document.hidden) {
16871 noteUserActivity();
16872 }
16873 });
16874 heartbeat.contribute("desktop_mode_presence_active", () => true);
16875 heartbeat.contribute(
16876 "desktop_mode_user_active",
16877 () => Date.now() - lastInputMs < ACTIVE_THRESHOLD_MS
16878 );
16879 heartbeat.subscribe("desktop_mode_presence", (block) => {
16880 applySnapshot(block);
16881 });
16882 }
16883 function getStatus(userId) {
16884 const entry = store$2.state.byUser.get(userId);
16885 return entry ? entry.status : "offline";
16886 }
16887 function getAll() {
16888 return new Map(store$2.state.byUser);
16889 }
16890 function getEntry(userId) {
16891 return store$2.state.byUser.get(userId) ?? null;
16892 }
16893 function subscribe$1(cb) {
16894 return store$2.subscribe((s) => cb(s));
16895 }
16896 function markActive() {
16897 noteUserActivity();
16898 }
16899 function applyPresenceBatch(updates) {
16900 if (!Array.isArray(updates) || updates.length === 0) {
16901 return;
16902 }
16903 const previous = store$2.state.byUser;
16904 const next = new Map(previous);
16905 const transitions = [];
16906 for (const u of updates) {
16907 const userId = Number(u.userId);
16908 if (!Number.isFinite(userId) || userId <= 0) {
16909 continue;
16910 }
16911 const old = previous.get(userId);
16912 const entry = {
16913 status: u.status,
16914 lastSeenMs: typeof u.lastSeenMs === "number" ? u.lastSeenMs : old?.lastSeenMs ?? 0,
16915 lastActiveMs: typeof u.lastActiveMs === "number" ? u.lastActiveMs : old?.lastActiveMs ?? 0
16916 };
16917 next.set(userId, entry);
16918 if (!old || old.status !== entry.status) {
16919 transitions.push({
16920 userId,
16921 oldStatus: old ? old.status : null,
16922 newStatus: entry.status,
16923 entry
16924 });
16925 }
16926 }
16927 if (transitions.length === 0 && next.size === previous.size) {
16928 return;
16929 }
16930 store$2.state.byUser = next;
16931 store$2.notify();
16932 for (const t of transitions) {
16933 const detail = {
16934 userId: t.userId,
16935 oldStatus: t.oldStatus,
16936 newStatus: t.newStatus,
16937 lastSeenMs: t.entry.lastSeenMs,
16938 lastActiveMs: t.entry.lastActiveMs
16939 };
16940 document.dispatchEvent(
16941 new CustomEvent("desktop-mode-presence-changed", { detail })
16942 );
16943 activity.publish("desktop-mode/presence-changed", detail);
16944 }
16945 activity.publish("desktop-mode/presence-snapshot-applied", {
16946 applied: updates.length,
16947 transitions: transitions.length
16948 });
16949 }
16950 const presenceApi = Object.freeze({
16951 getStatus,
16952 getAll,
16953 getEntry,
16954 subscribe: subscribe$1,
16955 markActive,
16956 applyBatch: applyPresenceBatch
16957 });
16958 const HEARTBEAT_FIELD = "desktop_mode_nonces";
16959 const targets = /* @__PURE__ */ new Map();
16960 let booted = false;
16961 function registerNonceTarget(action, updater) {
16962 if (typeof action !== "string" || action === "") {
16963 return () => {
16964 };
16965 }
16966 let set = targets.get(action);
16967 if (!set) {
16968 set = /* @__PURE__ */ new Set();
16969 targets.set(action, set);
16970 }
16971 set.add(updater);
16972 return () => {
16973 set.delete(updater);
16974 };
16975 }
16976 function bootNonceRefresh() {
16977 if (booted) {
16978 return;
16979 }
16980 booted = true;
16981 heartbeat.subscribe(HEARTBEAT_FIELD, (payload) => {
16982 if (!payload || typeof payload !== "object") {
16983 return;
16984 }
16985 for (const [action, value] of Object.entries(payload)) {
16986 if (typeof value !== "string" || value === "") {
16987 continue;
16988 }
16989 const set = targets.get(action);
16990 if (!set) {
16991 continue;
16992 }
16993 for (const updater of set) {
16994 try {
16995 updater(value);
16996 } catch (err) {
16997 console.error(
16998 `[desktop-mode/nonce-refresh] updater for "${action}" threw:`,
16999 err
17000 );
17001 }
17002 }
17003 }
17004 });
17005 registerShellAndPluginsWindowTargets();
17006 }
17007 function registerShellAndPluginsWindowTargets() {
17008 registerNonceTarget("wp_rest", updateAllRestNonces);
17009 registerNonceTarget("desktop-mode-plugins", (fresh) => {
17010 writeWindowConfigField("desktop-mode-plugins", "ajaxNonce", fresh);
17011 });
17012 registerNonceTarget("updates", (fresh) => {
17013 writeWindowConfigField("desktop-mode-plugins", "updatesNonce", fresh);
17014 });
17015 }
17016 function updateAllRestNonces(fresh) {
17017 const cfg = readShellConfig();
17018 if (cfg && typeof cfg.restNonce === "string") {
17019 cfg.restNonce = fresh;
17020 }
17021 const windowConfigs = readWindowConfigs();
17022 if (!windowConfigs) {
17023 return;
17024 }
17025 for (const blob of Object.values(windowConfigs)) {
17026 if (blob && typeof blob === "object" && typeof blob.restNonce === "string") {
17027 blob.restNonce = fresh;
17028 }
17029 }
17030 }
17031 function writeWindowConfigField(windowId, field, value) {
17032 const blobs = readWindowConfigs();
17033 const blob = blobs?.[windowId];
17034 if (blob && typeof blob === "object") {
17035 blob[field] = value;
17036 }
17037 }
17038 function readShellConfig() {
17039 if (typeof window === "undefined") {
17040 return void 0;
17041 }
17042 return window.desktopModeConfig;
17043 }
17044 function readWindowConfigs() {
17045 if (typeof window === "undefined") {
17046 return void 0;
17047 }
17048 return window.desktopModeWindowConfig;
17049 }
17050 const VIEWPORT_CLAMP_MARGIN = 12;
17051 function findDockEntryForUrl(url, config) {
17052 const windowId = deriveWindowId(url, config.adminUrl);
17053 return (config.dockItems || []).find(
17054 (i) => deriveWindowId(i.url, config.adminUrl) === windowId || (i.submenu || []).some(
17055 (s) => deriveWindowId(s.url, config.adminUrl) === windowId
17056 )
17057 );
17058 }
17059 function clampGeometryToViewport(win, rect) {
17060 const maxW = Math.max(200, rect.width - VIEWPORT_CLAMP_MARGIN * 2);
17061 const maxH = Math.max(200, rect.height - VIEWPORT_CLAMP_MARGIN * 2);
17062 const width = Math.min(win.width, maxW);
17063 const height = Math.min(win.height, maxH);
17064 const maxX = Math.max(0, rect.width - width - VIEWPORT_CLAMP_MARGIN);
17065 const maxY = Math.max(0, rect.height - height - VIEWPORT_CLAMP_MARGIN);
17066 const x = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.x, maxX));
17067 const y = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.y, maxY));
17068 return { x, y, width, height };
17069 }
17070 const INITIAL_ORIGIN$1 = window.location.origin;
17071 function bindTopWindowLinkInterceptor(manager, config) {
17072 document.addEventListener(
17073 "click",
17074 (e) => {
17075 if (e.defaultPrevented) {
17076 return;
17077 }
17078 if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
17079 return;
17080 }
17081 const target2 = e.target;
17082 const link = target2 && target2.closest ? target2.closest("a[href]") : null;
17083 if (!link) {
17084 return;
17085 }
17086 const anchor = link;
17087 const linkTarget = anchor.getAttribute("target");
17088 if (linkTarget && linkTarget !== "" && linkTarget !== "_self") {
17089 return;
17090 }
17091 if (anchor.hasAttribute("download")) {
17092 return;
17093 }
17094 const rawHref = anchor.getAttribute("href");
17095 if (!rawHref || rawHref.charAt(0) === "#") {
17096 return;
17097 }
17098 if (/^(mailto:|tel:|javascript:|data:)/i.test(rawHref)) {
17099 return;
17100 }
17101 let url;
17102 try {
17103 url = new URL(rawHref, window.location.href);
17104 } catch (err) {
17105 if (typeof console !== "undefined") {
17106 console.warn(
17107 "[desktop-mode] Couldn’t parse href; letting the browser handle the click:",
17108 rawHref,
17109 err
17110 );
17111 }
17112 return;
17113 }
17114 if (url.origin !== INITIAL_ORIGIN$1) {
17115 return;
17116 }
17117 let adminPath;
17118 try {
17119 adminPath = new URL(config.adminUrl).pathname;
17120 } catch (err) {
17121 if (typeof console !== "undefined") {
17122 console.error(
17123 "[desktop-mode] config.adminUrl is not a valid URL; falling back to /wp-admin/:",
17124 config.adminUrl,
17125 err
17126 );
17127 }
17128 adminPath = "/wp-admin/";
17129 }
17130 if (!url.pathname.startsWith(adminPath)) {
17131 return;
17132 }
17133 if (/\/(admin-post|admin-ajax)\.php$/.test(url.pathname)) {
17134 return;
17135 }
17136 if (url.searchParams.has("action") && url.searchParams.get("action") === "logout") {
17137 return;
17138 }
17139 if (url.searchParams.has("desktop_mode_classic")) {
17140 return;
17141 }
17142 e.preventDefault();
17143 e.stopPropagation();
17144 if (tryNativeUrlRemap(url.href)) {
17145 return;
17146 }
17147 const windowId = deriveWindowId(url.href, config.adminUrl);
17148 const dockEntry = findDockEntryForUrl(url.href, config);
17149 const fallbackTitle = (anchor.textContent || "").trim() || dockEntry?.title || "";
17150 const isAdminBarNew = !!anchor.closest("#wp-admin-bar-new-content");
17151 const openOpts = {
17152 id: windowId,
17153 baseId: windowId,
17154 multi: !!dockEntry?.multi || isAdminBarNew,
17155 url: url.href,
17156 parentUrl: dockEntry?.url ?? url.href,
17157 title: dockEntry?.title || fallbackTitle,
17158 icon: dockEntry?.icon || "dashicons-admin-generic",
17159 submenu: dockEntry?.submenu
17160 };
17161 if (isAdminBarNew) {
17162 void manager.openNew(openOpts);
17163 return;
17164 }
17165 void manager.open(openOpts);
17166 },
17167 true
17168 );
17169 }
17170 const REGISTRY_CHANGED_EVENT = "desktop-mode-registry-changed";
17171 function diffIds(prev, next) {
17172 const prevIds = /* @__PURE__ */ new Set();
17173 if (Array.isArray(prev)) {
17174 for (const item of prev) {
17175 if (item && typeof item.id === "string") {
17176 prevIds.add(item.id);
17177 }
17178 }
17179 }
17180 const nextIds = /* @__PURE__ */ new Set();
17181 for (const item of next) {
17182 if (item && typeof item.id === "string") {
17183 nextIds.add(item.id);
17184 }
17185 }
17186 const added = [];
17187 for (const id of nextIds) {
17188 if (!prevIds.has(id)) {
17189 added.push(id);
17190 }
17191 }
17192 const removed = [];
17193 for (const id of prevIds) {
17194 if (!nextIds.has(id)) {
17195 removed.push(id);
17196 }
17197 }
17198 return { added, removed };
17199 }
17200 function emitRegistryChanged(registry2, prev, next) {
17201 const { added, removed } = diffIds(prev, next);
17202 if (added.length === 0 && removed.length === 0) {
17203 return;
17204 }
17205 if (typeof document === "undefined") {
17206 return;
17207 }
17208 const detail = { registry: registry2, added, removed };
17209 document.dispatchEvent(
17210 new CustomEvent(REGISTRY_CHANGED_EVENT, { detail })
17211 );
17212 }
17213 function createApplyPayload(deps2) {
17214 const {
17215 applyDockItems,
17216 config,
17217 syncNativeWindows,
17218 syncServerWidgets,
17219 syncServerWallpapers,
17220 syncServerCommands,
17221 syncServerSettingsTabs,
17222 syncServerTitleBarButtons,
17223 syncServerUnfocusEffects,
17224 syncServerDockRailRenderers,
17225 renderIcons
17226 } = deps2;
17227 return function applyPayload(payload) {
17228 const dockItems = payload.dockItems;
17229 const nativeWindows = payload.nativeWindows;
17230 const serverWidgets = payload.serverWidgets;
17231 const serverWallpapers = payload.serverWallpapers;
17232 const serverCommandScripts = payload.serverCommandScripts;
17233 const serverCommands = payload.serverCommands;
17234 const serverSettingsTabScripts = payload.serverSettingsTabScripts;
17235 const serverSettingsTabs = payload.serverSettingsTabs;
17236 const serverDockRailRendererScripts = payload.serverDockRailRendererScripts;
17237 const serverTitleBarButtonScripts = payload.serverTitleBarButtonScripts;
17238 const serverUnfocusEffectScripts = payload.serverUnfocusEffectScripts;
17239 const serverWindowNotices = payload.serverWindowNotices;
17240 const desktopIcons = payload.desktopIcons;
17241 if (!Array.isArray(dockItems) || dockItems.length === 0) {
17242 return;
17243 }
17244 const prevDockItems = config.dockItems;
17245 applyDockItems(dockItems);
17246 config.dockItems = dockItems;
17247 emitRegistryChanged(
17248 "dock-items",
17249 prevDockItems,
17250 dockItems
17251 );
17252 if (Array.isArray(nativeWindows)) {
17253 const prevNativeWindows = config.nativeWindows;
17254 void syncNativeWindows(
17255 nativeWindows
17256 );
17257 config.nativeWindows = nativeWindows;
17258 emitRegistryChanged(
17259 "native-windows",
17260 prevNativeWindows,
17261 nativeWindows
17262 );
17263 }
17264 if (Array.isArray(serverWidgets)) {
17265 void syncServerWidgets(
17266 serverWidgets
17267 );
17268 config.serverWidgets = serverWidgets;
17269 }
17270 if (Array.isArray(serverWallpapers)) {
17271 void syncServerWallpapers(
17272 serverWallpapers
17273 );
17274 config.serverWallpapers = serverWallpapers;
17275 }
17276 if (Array.isArray(serverCommandScripts)) {
17277 void syncServerCommands(
17278 serverCommandScripts,
17279 Array.isArray(serverCommands) ? serverCommands : void 0
17280 );
17281 config.serverCommandScripts = serverCommandScripts;
17282 if (Array.isArray(serverCommands)) {
17283 config.serverCommands = serverCommands;
17284 }
17285 }
17286 if (Array.isArray(serverSettingsTabScripts)) {
17287 void syncServerSettingsTabs(
17288 serverSettingsTabScripts,
17289 Array.isArray(serverSettingsTabs) ? serverSettingsTabs : void 0
17290 );
17291 config.serverSettingsTabScripts = serverSettingsTabScripts;
17292 if (Array.isArray(serverSettingsTabs)) {
17293 config.serverSettingsTabs = serverSettingsTabs;
17294 }
17295 }
17296 if (Array.isArray(serverTitleBarButtonScripts)) {
17297 void syncServerTitleBarButtons(
17298 serverTitleBarButtonScripts
17299 );
17300 config.serverTitleBarButtonScripts = serverTitleBarButtonScripts;
17301 }
17302 if (Array.isArray(serverUnfocusEffectScripts)) {
17303 void syncServerUnfocusEffects(
17304 serverUnfocusEffectScripts
17305 );
17306 config.serverUnfocusEffectScripts = serverUnfocusEffectScripts;
17307 }
17308 if (Array.isArray(serverDockRailRendererScripts)) {
17309 void syncServerDockRailRenderers(
17310 serverDockRailRendererScripts
17311 );
17312 config.serverDockRailRendererScripts = serverDockRailRendererScripts;
17313 }
17314 if (Array.isArray(serverWindowNotices)) {
17315 applyServerWindowNotices(
17316 serverWindowNotices
17317 );
17318 config.serverWindowNotices = serverWindowNotices;
17319 }
17320 if (Array.isArray(desktopIcons)) {
17321 const prevDesktopIcons = config.desktopIcons;
17322 renderIcons(desktopIcons);
17323 config.desktopIcons = desktopIcons;
17324 emitRegistryChanged(
17325 "desktop-icons",
17326 prevDesktopIcons,
17327 desktopIcons
17328 );
17329 }
17330 };
17331 }
17332 const MENU_REFRESH_TIMEOUT_MS = 8e3;
17333 function bindMenuRefresh(deps2) {
17334 const {
17335 layoutDispatcher,
17336 desktopArea,
17337 config,
17338 syncNativeWindows,
17339 syncServerWidgets,
17340 syncServerWallpapers,
17341 syncServerCommands,
17342 syncServerSettingsTabs,
17343 syncServerTitleBarButtons,
17344 syncServerUnfocusEffects,
17345 syncServerDockRailRenderers,
17346 renderIcons
17347 } = deps2;
17348 const applyPayload = createApplyPayload({
17349 applyDockItems: (items) => layoutDispatcher?.applyDockItems(items),
17350 config,
17351 syncNativeWindows,
17352 syncServerWidgets,
17353 syncServerWallpapers,
17354 syncServerCommands,
17355 syncServerSettingsTabs,
17356 syncServerTitleBarButtons,
17357 syncServerUnfocusEffects,
17358 syncServerDockRailRenderers,
17359 renderIcons
17360 });
17361 window.addEventListener("message", (e) => {
17362 if (e.origin !== INITIAL_ORIGIN$1) {
17363 return;
17364 }
17365 const data = e.data;
17366 if (!data || data.type !== "desktop-mode-plugins-changed") {
17367 return;
17368 }
17369 if (data.payload) {
17370 applyPayload(data.payload);
17371 }
17372 });
17373 const refresh = () => {
17374 if (!config.adminUrl) {
17375 return Promise.resolve();
17376 }
17377 const probeUrl = (() => {
17378 try {
17379 const url = new URL("admin.php", config.adminUrl);
17380 url.searchParams.set("desktop_mode_chromeless", "1");
17381 url.searchParams.set("desktop_mode_menu_refresh", "1");
17382 return url.toString();
17383 } catch (_err) {
17384 return null;
17385 }
17386 })();
17387 if (!probeUrl) {
17388 return Promise.resolve();
17389 }
17390 return new Promise((resolve2) => {
17391 const iframe = document.createElement("iframe");
17392 iframe.setAttribute("aria-hidden", "true");
17393 iframe.tabIndex = -1;
17394 iframe.style.cssText = "position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;border:0;opacity:0;pointer-events:none;";
17395 iframe.src = probeUrl;
17396 let done = false;
17397 const cleanup = () => {
17398 if (done) {
17399 return;
17400 }
17401 done = true;
17402 window.clearTimeout(timeoutId);
17403 window.removeEventListener("message", onMessage);
17404 if (iframe.parentNode) {
17405 iframe.parentNode.removeChild(iframe);
17406 }
17407 resolve2();
17408 };
17409 const onMessage = (e) => {
17410 if (e.source !== iframe.contentWindow) {
17411 return;
17412 }
17413 const data = e.data;
17414 if (!data || data.type !== "desktop-mode-plugins-changed") {
17415 return;
17416 }
17417 cleanup();
17418 };
17419 const timeoutId = window.setTimeout(() => {
17420 doAction(HOOKS.SHELL_ERROR, {
17421 scope: "menu-refresh",
17422 error: new Error("menu refresh probe timed out")
17423 });
17424 cleanup();
17425 }, MENU_REFRESH_TIMEOUT_MS);
17426 window.addEventListener("message", onMessage);
17427 document.body.appendChild(iframe);
17428 });
17429 };
17430 return refresh;
17431 }
17432 function hasRestorableSession(session) {
17433 if (!session) {
17434 return false;
17435 }
17436 if (Array.isArray(session.windows) && session.windows.length > 0) {
17437 return true;
17438 }
17439 if (typeof session.updated !== "number" || session.updated <= 0 || !Array.isArray(session.desktops) || session.desktops.length === 0) {
17440 return false;
17441 }
17442 if (session.desktops.length > 1) {
17443 return true;
17444 }
17445 const onlyDesktop = session.desktops[0];
17446 if (onlyDesktop?.id && onlyDesktop.id !== "desktop-1") {
17447 return true;
17448 }
17449 return !!session.activeDesktop && session.activeDesktop !== "desktop-1";
17450 }
17451 async function restoreSession(manager, config, desktopArea) {
17452 const rect = desktopArea.getBoundingClientRect();
17453 if (Array.isArray(config.session.desktops) && config.session.desktops.length > 0) {
17454 manager.seedDesktops(
17455 config.session.desktops,
17456 config.session.activeDesktop || config.session.desktops[0].id
17457 );
17458 }
17459 for (const win of config.session.windows) {
17460 const clamped = clampGeometryToViewport(win, rect);
17461 const dockEntry = findDockEntryForUrl(win.url, config);
17462 const opened = await manager.open({
17463 id: win.id,
17464 baseId: win.baseId || win.id,
17465 desktopId: win.desktopId,
17466 multi: !!dockEntry?.multi,
17467 url: win.url,
17468 // `dockEntry?.url` is the parent menu's landing page —
17469 // recover it so the synthetic "back to parent" tab in
17470 // the in-window strip points at the dock URL even when
17471 // the saved `win.url` is a sub-page (e.g. theme-install.php
17472 // under Appearance, or a deep wc-admin route under
17473 // WooCommerce). Without this the dedup check in
17474 // `dom.ts` sees the iframe URL match a submenu entry
17475 // and suppresses the parent tab — losing the only
17476 // affordance to navigate back.
17477 parentUrl: dockEntry?.url ?? win.url,
17478 title: win.title,
17479 icon: win.icon || "dashicons-admin-generic",
17480 x: clamped.x,
17481 y: clamped.y,
17482 width: clamped.width,
17483 height: clamped.height,
17484 initialState: win.state,
17485 submenu: dockEntry?.submenu
17486 });
17487 if (Array.isArray(win.externalTabs)) {
17488 for (const ext of win.externalTabs) {
17489 if (ext && typeof ext.url === "string" && ext.url !== "") {
17490 opened.addExternalTab(
17491 ext.url,
17492 typeof ext.label === "string" && ext.label !== "" ? ext.label : ext.url
17493 );
17494 }
17495 }
17496 }
17497 }
17498 if (config.session.focused) {
17499 const focused = manager.getById(config.session.focused);
17500 if (focused) {
17501 manager.focus(focused);
17502 }
17503 }
17504 }
17505 async function openCurrentPage(manager, config) {
17506 if (tryNativeUrlRemap(config.currentPage)) {
17507 return;
17508 }
17509 const windowId = deriveWindowId(config.currentPage, config.adminUrl);
17510 const dockEntry = findDockEntryForUrl(config.currentPage, config);
17511 await manager.open({
17512 id: windowId,
17513 baseId: windowId,
17514 multi: !!dockEntry?.multi,
17515 url: config.currentPage,
17516 parentUrl: dockEntry?.url ?? config.currentPage,
17517 title: config.currentTitle,
17518 icon: config.currentIcon,
17519 submenu: dockEntry?.submenu
17520 });
17521 }
17522 function shouldAutoOpenCurrentPage(inputs) {
17523 const suppress = inputs.fromPortal && !inputs.fromPortalIntent && (inputs.hasSession || !inputs.defaultEnabled || inputs.isNativeDefault);
17524 return !suppress;
17525 }
17526 function trackedFetch(manager, input, requestInit, opts) {
17527 const finalInit = injectRestNonce(input, requestInit);
17528 const promise = window.fetch(input, finalInit);
17529 if (opts?.silent) {
17530 return promise;
17531 }
17532 let target2 = opts?.window;
17533 if (!target2 && opts?.windowId) {
17534 target2 = manager.getById(opts.windowId) ?? null;
17535 }
17536 if (!target2) {
17537 target2 = manager.getFocused();
17538 }
17539 if (target2 && typeof target2.trackActivity === "function") {
17540 void target2.trackActivity(promise).catch(() => {
17541 });
17542 }
17543 return promise;
17544 }
17545 const SESSION_SAVE_DEBOUNCE_MS = 500;
17546 function createSessionSaver(manager, config) {
17547 let debounceTimer = null;
17548 let inFlight = false;
17549 const doSave = async () => {
17550 if (inFlight) {
17551 return;
17552 }
17553 const payload = manager.snapshot();
17554 inFlight = true;
17555 try {
17556 await trackedFetch(
17557 manager,
17558 config.sessionUrl,
17559 {
17560 method: "POST",
17561 credentials: "same-origin",
17562 headers: {
17563 "Content-Type": "application/json",
17564 "X-WP-Nonce": config.restNonce
17565 },
17566 body: JSON.stringify({ session: payload }),
17567 // Best-effort: we don't block the UI on persistence.
17568 keepalive: true
17569 },
17570 { silent: true }
17571 );
17572 } catch (err) {
17573 doAction(HOOKS.SHELL_ERROR, { scope: "session-save", error: err });
17574 } finally {
17575 inFlight = false;
17576 }
17577 };
17578 const flushImmediately = () => {
17579 if (debounceTimer !== null) {
17580 clearTimeout(debounceTimer);
17581 debounceTimer = null;
17582 }
17583 const payload = manager.snapshot();
17584 const body = new Blob(
17585 [JSON.stringify({ session: payload })],
17586 { type: "application/json" }
17587 );
17588 const beaconUrl = config.sessionUrl + (config.sessionUrl.includes("?") ? "&" : "?") + "_wpnonce=" + encodeURIComponent(config.restNonce);
17589 if (navigator.sendBeacon && navigator.sendBeacon(beaconUrl, body)) {
17590 return;
17591 }
17592 void doSave();
17593 };
17594 const schedule = () => {
17595 if (debounceTimer !== null) {
17596 clearTimeout(debounceTimer);
17597 }
17598 debounceTimer = window.setTimeout(() => {
17599 debounceTimer = null;
17600 void doSave();
17601 }, SESSION_SAVE_DEBOUNCE_MS);
17602 };
17603 window.addEventListener("pagehide", flushImmediately);
17604 document.addEventListener("visibilitychange", () => {
17605 if (document.visibilityState === "hidden") {
17606 flushImmediately();
17607 }
17608 });
17609 return schedule;
17610 }
17611 const SHELL_RESIZE_DEBOUNCE_MS = 120;
17612 function wireSessionEvents(save) {
17613 document.addEventListener("desktop-mode-window-opened", save);
17614 document.addEventListener("desktop-mode-window-closed", save);
17615 document.addEventListener("desktop-mode-window-focused", save);
17616 document.addEventListener("desktop-mode-window-changed", save);
17617 addAction(HOOKS.DESKTOP_CREATED, "desktop-mode/session-save", save);
17618 addAction(HOOKS.DESKTOP_CLOSED, "desktop-mode/session-save", save);
17619 addAction(HOOKS.DESKTOP_SWITCHED, "desktop-mode/session-save", save);
17620 }
17621 function bindShellLifecycle() {
17622 const shellEl = document.getElementById("desktop-mode-shell");
17623 let resizeTimer = null;
17624 const fireShellResize = () => {
17625 resizeTimer = null;
17626 const rect = shellEl ? shellEl.getBoundingClientRect() : null;
17627 doAction(HOOKS.SHELL_RESIZED, {
17628 width: rect ? Math.round(rect.width) : window.innerWidth,
17629 height: rect ? Math.round(rect.height) : window.innerHeight
17630 });
17631 };
17632 window.addEventListener("resize", () => {
17633 if (resizeTimer !== null) {
17634 window.clearTimeout(resizeTimer);
17635 }
17636 resizeTimer = window.setTimeout(
17637 fireShellResize,
17638 SHELL_RESIZE_DEBOUNCE_MS
17639 );
17640 });
17641 document.addEventListener("visibilitychange", () => {
17642 doAction(HOOKS.SHELL_VISIBILITY, {
17643 state: document.hidden ? "hidden" : "visible"
17644 });
17645 });
17646 }
17647 function applyTileClasses(baseClasses, item, ctx) {
17648 const fullCtx = {
17649 rail: ctx.rail ?? "dock",
17650 orientation: ctx.orientation,
17651 dockId: ctx.dockId,
17652 container: ctx.container ?? document.body,
17653 item,
17654 isSystem: ctx.isSystem
17655 };
17656 return applyFilters(
17657 HOOKS.DOCK_TILE_CLASS,
17658 baseClasses,
17659 fullCtx
17660 );
17661 }
17662 function applyTileElement(tile2, item, ctx) {
17663 const fullCtx = {
17664 rail: ctx.rail ?? "dock",
17665 orientation: ctx.orientation,
17666 dockId: ctx.dockId,
17667 container: ctx.container ?? document.body,
17668 item,
17669 isSystem: ctx.isSystem
17670 };
17671 return applyFilters(
17672 HOOKS.DOCK_TILE_ELEMENT,
17673 tile2,
17674 fullCtx
17675 );
17676 }
17677 function applyTileTooltip(label, item, ctx) {
17678 const fullCtx = {
17679 rail: ctx.rail ?? "dock",
17680 orientation: ctx.orientation,
17681 dockId: ctx.dockId,
17682 container: ctx.container ?? document.body,
17683 item,
17684 isSystem: ctx.isSystem
17685 };
17686 return applyFilters(
17687 HOOKS.DOCK_TILE_TOOLTIP,
17688 label,
17689 fullCtx
17690 );
17691 }
17692 function dispatchTileRendered(el, item, ctx) {
17693 const fullCtx = {
17694 rail: ctx.rail ?? "dock",
17695 orientation: ctx.orientation,
17696 dockId: ctx.dockId,
17697 container: ctx.container ?? document.body,
17698 item,
17699 isSystem: ctx.isSystem
17700 };
17701 doAction(HOOKS.DOCK_TILE_RENDERED, { ...fullCtx, el });
17702 }
17703 const DEFAULT_DOCK_SELECTOR = [
17704 ".desktop-mode-dock",
17705 "#desktop-mode-dock",
17706 "#desktop-mode-side-dock",
17707 ".desktop-mode-dock__tooltip",
17708 ".desktop-mode-dock-submenu"
17709 ].join(",");
17710 const customSelectors = /* @__PURE__ */ new Set();
17711 function isDockElement(target2) {
17712 if (!target2 || typeof target2.closest !== "function") {
17713 return false;
17714 }
17715 const el = target2;
17716 if (el.closest(DEFAULT_DOCK_SELECTOR)) {
17717 return true;
17718 }
17719 for (const selector of customSelectors) {
17720 if (el.closest(selector)) {
17721 return true;
17722 }
17723 }
17724 return false;
17725 }
17726 function registerDockSelector(selector) {
17727 if (typeof selector !== "string" || selector.trim() === "") {
17728 return () => void 0;
17729 }
17730 customSelectors.add(selector);
17731 return () => {
17732 customSelectors.delete(selector);
17733 };
17734 }
17735 const states = /* @__PURE__ */ new Map();
17736 const INITIAL_ORIGIN = window.location.origin;
17737 function ensureState(windowId) {
17738 let s = states.get(windowId);
17739 if (!s) {
17740 s = {
17741 headers: /* @__PURE__ */ new Map(),
17742 observers: /* @__PURE__ */ new Set(),
17743 observeCount: 0,
17744 loadHandler: null,
17745 loadHandlerTarget: null
17746 };
17747 states.set(windowId, s);
17748 }
17749 ensureLoadHandler(windowId, s);
17750 return s;
17751 }
17752 function ensureLoadHandler(windowId, s) {
17753 const iframe = findIframe(windowId);
17754 if (!iframe) {
17755 return;
17756 }
17757 if (s.loadHandlerTarget === iframe && s.loadHandler) {
17758 return;
17759 }
17760 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
17761 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
17762 }
17763 if (typeof iframe.addEventListener !== "function") {
17764 return;
17765 }
17766 const handler = () => {
17767 queueMicrotask(() => pushInstrumentation(windowId));
17768 };
17769 iframe.addEventListener("load", handler);
17770 s.loadHandler = handler;
17771 s.loadHandlerTarget = iframe;
17772 }
17773 function detachLoadHandler(s) {
17774 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
17775 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
17776 }
17777 s.loadHandler = null;
17778 s.loadHandlerTarget = null;
17779 }
17780 function findIframe(windowId) {
17781 const wpd = window.wp?.desktop?.windowManager;
17782 if (wpd && typeof wpd.getById === "function") {
17783 const win = wpd.getById(windowId);
17784 if (win?.iframe) {
17785 return win.iframe;
17786 }
17787 if (win?.element) {
17788 const synth = win.element.querySelector("iframe");
17789 if (synth) {
17790 return synth;
17791 }
17792 }
17793 }
17794 const fallback = document.getElementById(`wp-window-${windowId}`);
17795 return fallback?.querySelector("iframe") ?? null;
17796 }
17797 function snapshotHeaders(s) {
17798 const out = {};
17799 for (const [name, contributions] of s.headers) {
17800 const parts = [];
17801 for (const c of contributions) {
17802 let v;
17803 try {
17804 v = typeof c.value === "function" ? c.value() : c.value;
17805 } catch {
17806 continue;
17807 }
17808 if (typeof v === "string" && v !== "") {
17809 parts.push(v);
17810 }
17811 }
17812 if (parts.length > 0) {
17813 out[name] = parts.join(", ");
17814 }
17815 }
17816 return out;
17817 }
17818 function pushInstrumentation(windowId) {
17819 const iframe = findIframe(windowId);
17820 if (!iframe || !iframe.contentWindow) {
17821 return;
17822 }
17823 const s = states.get(windowId);
17824 const headers = s ? snapshotHeaders(s) : {};
17825 const observe = !!s && s.observeCount > 0;
17826 try {
17827 iframe.contentWindow.postMessage(
17828 {
17829 type: "desktop-mode-instrument-set",
17830 headers,
17831 observe
17832 },
17833 INITIAL_ORIGIN
17834 );
17835 } catch {
17836 }
17837 }
17838 addAction(HOOKS.IFRAME_READY, "desktop-mode/devtools/replay", (payload) => {
17839 const p = payload;
17840 if (p && typeof p.windowId === "string" && states.has(p.windowId)) {
17841 pushInstrumentation(p.windowId);
17842 }
17843 });
17844 addAction(
17845 HOOKS.IFRAME_NETWORK_COMPLETED,
17846 "desktop-mode/devtools/dispatch",
17847 (payload) => {
17848 const p = payload;
17849 if (!p || typeof p.windowId !== "string") {
17850 return;
17851 }
17852 const s = states.get(p.windowId);
17853 if (!s) {
17854 return;
17855 }
17856 for (const cb of s.observers) {
17857 try {
17858 cb(p);
17859 } catch {
17860 }
17861 }
17862 }
17863 );
17864 const sessions = /* @__PURE__ */ new Map();
17865 const POLL_INTERVAL_MS = 1e3;
17866 function pollOnce(sessionId, restUrl2, restNonce) {
17867 const sp = sessions.get(sessionId);
17868 if (!sp || sp.inflight) {
17869 return;
17870 }
17871 sp.inflight = true;
17872 const u = new URL(restUrl2 + "desktop-mode/v1/debug", window.location.origin);
17873 u.searchParams.set("sessionId", sessionId);
17874 u.searchParams.set("since", String(sp.cursor));
17875 for (const ch of sp.channels.keys()) {
17876 u.searchParams.append("channels[]", ch);
17877 }
17878 const url = u.toString();
17879 fetch(url, {
17880 credentials: "same-origin",
17881 headers: { "X-WP-Nonce": restNonce }
17882 }).then((r) => r.ok ? r.json() : { events: [], cursor: sp.cursor }).then((body) => {
17883 sp.inflight = false;
17884 if (!sessions.has(sessionId)) {
17885 return;
17886 }
17887 if (typeof body.cursor === "number") {
17888 sp.cursor = body.cursor;
17889 }
17890 for (const ev of body.events || []) {
17891 const bucket2 = sp.channels.get(ev.channel);
17892 if (!bucket2) {
17893 continue;
17894 }
17895 for (const cb of bucket2) {
17896 try {
17897 cb(ev);
17898 } catch {
17899 }
17900 }
17901 }
17902 }).catch(() => {
17903 sp.inflight = false;
17904 }).finally(() => {
17905 const stillThere = sessions.get(sessionId);
17906 if (stillThere && stillThere.channels.size > 0) {
17907 stillThere.timer = setTimeout(
17908 () => pollOnce(sessionId, restUrl2, restNonce),
17909 POLL_INTERVAL_MS
17910 );
17911 }
17912 });
17913 }
17914 function getRestEndpoint() {
17915 const cfg = window.desktopModeConfig;
17916 if (!cfg || !cfg.restUrl || !cfg.restNonce) {
17917 return null;
17918 }
17919 return { restUrl: cfg.restUrl, restNonce: cfg.restNonce };
17920 }
17921 function dispatchLocal(sessionId, ev) {
17922 const sp = sessions.get(sessionId);
17923 if (!sp) {
17924 return;
17925 }
17926 const bucket2 = sp.channels.get(ev.channel);
17927 if (!bucket2) {
17928 return;
17929 }
17930 for (const cb of bucket2) {
17931 try {
17932 cb(ev);
17933 } catch {
17934 }
17935 }
17936 }
17937 let _localEventCounter = 0;
17938 const debugBus = {
17939 startSession() {
17940 const cryptoApi = window.crypto;
17941 if (cryptoApi && typeof cryptoApi.randomUUID === "function") {
17942 return cryptoApi.randomUUID();
17943 }
17944 return "wpdbg-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
17945 },
17946 publish(sessionId, channel, payload) {
17947 dispatchLocal(sessionId, {
17948 id: ++_localEventCounter,
17949 t: Date.now(),
17950 channel,
17951 payload
17952 });
17953 },
17954 subscribe(sessionId, channel, cb) {
17955 let sp = sessions.get(sessionId);
17956 const startedFresh = !sp;
17957 if (!sp) {
17958 sp = {
17959 channels: /* @__PURE__ */ new Map(),
17960 cursor: 0,
17961 timer: null,
17962 inflight: false
17963 };
17964 sessions.set(sessionId, sp);
17965 }
17966 let bucket2 = sp.channels.get(channel);
17967 if (!bucket2) {
17968 bucket2 = /* @__PURE__ */ new Set();
17969 sp.channels.set(channel, bucket2);
17970 }
17971 bucket2.add(cb);
17972 if (startedFresh) {
17973 const ep = getRestEndpoint();
17974 if (ep) {
17975 pollOnce(sessionId, ep.restUrl, ep.restNonce);
17976 }
17977 }
17978 return () => {
17979 const cur = sessions.get(sessionId);
17980 if (!cur) {
17981 return;
17982 }
17983 const b = cur.channels.get(channel);
17984 if (b) {
17985 b.delete(cb);
17986 if (b.size === 0) {
17987 cur.channels.delete(channel);
17988 }
17989 }
17990 if (cur.channels.size === 0) {
17991 if (cur.timer) {
17992 clearTimeout(cur.timer);
17993 }
17994 sessions.delete(sessionId);
17995 }
17996 };
17997 }
17998 };
17999 const devtools = {
18000 addRequestHeader(windowId, name, value) {
18001 if (typeof windowId !== "string" || windowId === "") {
18002 return () => {
18003 };
18004 }
18005 if (typeof name !== "string" || name === "") {
18006 return () => {
18007 };
18008 }
18009 const s = ensureState(windowId);
18010 const contribution = { value };
18011 let bucket2 = s.headers.get(name);
18012 if (!bucket2) {
18013 bucket2 = [];
18014 s.headers.set(name, bucket2);
18015 }
18016 bucket2.push(contribution);
18017 pushInstrumentation(windowId);
18018 return () => {
18019 const cur = states.get(windowId);
18020 if (!cur) {
18021 return;
18022 }
18023 const b = cur.headers.get(name);
18024 if (!b) {
18025 return;
18026 }
18027 const i = b.indexOf(contribution);
18028 if (i >= 0) {
18029 b.splice(i, 1);
18030 }
18031 if (b.length === 0) {
18032 cur.headers.delete(name);
18033 }
18034 pushInstrumentation(windowId);
18035 gcWindowState(windowId);
18036 };
18037 },
18038 onRequest(windowId, cb, opts) {
18039 if (typeof windowId !== "string" || windowId === "") {
18040 return () => {
18041 };
18042 }
18043 if (typeof cb !== "function") {
18044 return () => {
18045 };
18046 }
18047 const s = ensureState(windowId);
18048 s.observers.add(cb);
18049 const wantsObserve = !!opts?.observe;
18050 if (wantsObserve) {
18051 s.observeCount++;
18052 pushInstrumentation(windowId);
18053 }
18054 return () => {
18055 const cur = states.get(windowId);
18056 if (!cur) {
18057 return;
18058 }
18059 cur.observers.delete(cb);
18060 if (wantsObserve) {
18061 cur.observeCount = Math.max(0, cur.observeCount - 1);
18062 pushInstrumentation(windowId);
18063 }
18064 gcWindowState(windowId);
18065 };
18066 },
18067 reloadWithDebugSession(windowId, sessionId, opts) {
18068 if (typeof windowId !== "string" || windowId === "" || typeof sessionId !== "string" || sessionId === "") {
18069 return null;
18070 }
18071 const iframe = findIframe(windowId);
18072 if (!iframe) {
18073 return null;
18074 }
18075 const headerName = opts?.headerName || "X-WP-Debug-Session";
18076 const queryArg = opts?.queryArg || "wp_debug_session";
18077 const stopHeader = devtools.addRequestHeader(windowId, headerName, sessionId);
18078 try {
18079 const currentSrc = iframe.getAttribute("src") || iframe.src || "";
18080 const u = new URL(currentSrc, window.location.origin);
18081 u.searchParams.set(queryArg, sessionId);
18082 iframe.src = u.toString();
18083 } catch {
18084 }
18085 return {
18086 dispose: () => {
18087 stopHeader();
18088 }
18089 };
18090 },
18091 debug: debugBus
18092 };
18093 function gcWindowState(windowId) {
18094 const s = states.get(windowId);
18095 if (!s) {
18096 return;
18097 }
18098 if (s.headers.size === 0 && s.observers.size === 0) {
18099 detachLoadHandler(s);
18100 states.delete(windowId);
18101 }
18102 }
18103 async function wpdConfirm(options) {
18104 await ensureShellOverlaysLoaded(shellOverlaysBundleUrl());
18105 return new Promise((resolve2) => {
18106 const dialog2 = document.createElement("wpd-confirm-dialog");
18107 dialog2.setAttribute("open", "");
18108 if (options.title) {
18109 dialog2.setAttribute("title", options.title);
18110 }
18111 dialog2.setAttribute("message", options.message);
18112 if (options.confirmLabel) {
18113 dialog2.setAttribute("confirm-label", options.confirmLabel);
18114 }
18115 if (options.cancelLabel) {
18116 dialog2.setAttribute("cancel-label", options.cancelLabel);
18117 }
18118 if (options.danger) {
18119 dialog2.setAttribute("danger", "");
18120 }
18121 if (options.hideCancel) {
18122 dialog2.setAttribute("hide-cancel", "");
18123 }
18124 if (options.dismissable) {
18125 dialog2.setAttribute("dismissable", "");
18126 }
18127 const cleanup = (ok) => {
18128 dialog2.remove();
18129 resolve2(ok);
18130 };
18131 dialog2.addEventListener("wpd-confirm", () => cleanup(true));
18132 dialog2.addEventListener("wpd-cancel", () => cleanup(false));
18133 document.body.appendChild(dialog2);
18134 const inner = dialog2.shadowRoot?.querySelector(".dialog");
18135 (inner ?? dialog2).focus?.();
18136 });
18137 }
18138 function collectWallpaperSurfaces(manager) {
18139 const seed2 = [];
18140 for (const w of manager.getVisibleRects()) {
18141 if (w.state === "minimized") {
18142 continue;
18143 }
18144 if (w.element.offsetParent === null) {
18145 continue;
18146 }
18147 const r = w.element.getBoundingClientRect();
18148 seed2.push({
18149 id: `window:${w.windowId}`,
18150 kind: "window",
18151 rect: rectFromDom(r),
18152 face: "top",
18153 element: w.element
18154 });
18155 }
18156 const shellEl = document.getElementById("desktop-mode-shell");
18157 if (shellEl) {
18158 const r = shellEl.getBoundingClientRect();
18159 seed2.push({
18160 id: "shell:floor",
18161 kind: "shell",
18162 rect: {
18163 x: r.left,
18164 y: r.bottom - 1,
18165 width: r.width,
18166 height: 1
18167 },
18168 face: "top",
18169 element: shellEl
18170 });
18171 }
18172 const dockEls = document.querySelectorAll(
18173 ".desktop-mode-dock"
18174 );
18175 let dockIndex = 0;
18176 for (const dockEl of Array.from(dockEls)) {
18177 const r = dockEl.getBoundingClientRect();
18178 if (r.width <= 0 || r.height <= 0) {
18179 continue;
18180 }
18181 const placement = dockEl.getAttribute("data-desktop-mode-dock-placement") ?? "bottom";
18182 const id = dockIndex === 0 ? "dock:edge" : `dock:edge:${dockIndex}`;
18183 dockIndex++;
18184 if (placement === "bottom") {
18185 seed2.push({
18186 id,
18187 kind: "dock",
18188 rect: { x: r.left, y: r.top, width: r.width, height: 1 },
18189 face: "top",
18190 element: dockEl
18191 });
18192 } else if (placement === "right") {
18193 seed2.push({
18194 id,
18195 kind: "dock",
18196 rect: { x: r.left, y: r.top, width: 1, height: r.height },
18197 face: "left",
18198 element: dockEl
18199 });
18200 } else {
18201 seed2.push({
18202 id,
18203 kind: "dock",
18204 rect: {
18205 x: r.right - 1,
18206 y: r.top,
18207 width: 1,
18208 height: r.height
18209 },
18210 face: "right",
18211 element: dockEl
18212 });
18213 }
18214 }
18215 const widgetCards = document.querySelectorAll(
18216 ".desktop-mode-widgets__card"
18217 );
18218 let widgetIndex = 0;
18219 widgetCards.forEach((card) => {
18220 const r = card.getBoundingClientRect();
18221 if (r.width === 0 && r.height === 0) {
18222 return;
18223 }
18224 const id = card.dataset.widgetId ?? String(widgetIndex++);
18225 seed2.push({
18226 id: `widget:${id}`,
18227 kind: "widget",
18228 rect: rectFromDom(r),
18229 face: "top",
18230 element: card
18231 });
18232 });
18233 const filtered = applyFilters(HOOKS.WALLPAPER_SURFACES, seed2);
18234 return Array.isArray(filtered) ? filtered : seed2;
18235 }
18236 function rectFromDom(r) {
18237 return {
18238 x: r.left,
18239 y: r.top,
18240 width: r.width,
18241 height: r.height
18242 };
18243 }
18244 const NODE_KEY_PROP = "__desktop_modeKeyedListKey";
18245 const NODE_DATA_PROP = "__desktop_modeKeyedListData";
18246 function getHostState(host) {
18247 const cached = host.__desktop_modeKeyedList;
18248 if (cached) {
18249 return cached;
18250 }
18251 const fresh = { byKey: /* @__PURE__ */ new Map() };
18252 host.__desktop_modeKeyedList = fresh;
18253 return fresh;
18254 }
18255 function renderKeyedList(host, items, opts) {
18256 const state2 = getHostState(host);
18257 const prev = state2.byKey;
18258 const next = /* @__PURE__ */ new Map();
18259 const ordered = [];
18260 const seenKeys = /* @__PURE__ */ new Set();
18261 for (const item of items) {
18262 const key = String(opts.keyOf(item));
18263 if (seenKeys.has(key)) {
18264 console.warn(
18265 "[desktop-mode/keyed-list] duplicate key — only the last item with this key will render:",
18266 key
18267 );
18268 }
18269 seenKeys.add(key);
18270 const reused = prev.get(key);
18271 if (reused) {
18272 const prevData = reused.data;
18273 opts.updateItem?.(reused.el, item, prevData);
18274 reused.data = item;
18275 next.set(key, reused);
18276 ordered.push(reused.el);
18277 continue;
18278 }
18279 const el = opts.buildItem(item);
18280 el[NODE_KEY_PROP] = key;
18281 el[NODE_DATA_PROP] = item;
18282 next.set(key, { el, data: item });
18283 ordered.push(el);
18284 }
18285 for (const [key, entry] of prev) {
18286 if (!next.has(key)) {
18287 entry.el.remove();
18288 }
18289 }
18290 for (let i = 0; i < ordered.length; i++) {
18291 const desired = ordered[i];
18292 const live = host.children[i];
18293 if (live === desired) {
18294 continue;
18295 }
18296 host.insertBefore(desired, live ?? null);
18297 }
18298 state2.byKey = next;
18299 }
18300 function clearKeyedList(host) {
18301 const cached = host.__desktop_modeKeyedList;
18302 if (!cached) {
18303 return;
18304 }
18305 for (const entry of cached.byKey.values()) {
18306 entry.el.remove();
18307 }
18308 cached.byKey.clear();
18309 delete host.__desktop_modeKeyedList;
18310 }
18311 function createInfiniteList(options) {
18312 const {
18313 root,
18314 fetchPage,
18315 getId,
18316 renderItem,
18317 rootMargin = "200px",
18318 initialCursor = null,
18319 onLoadingChange = () => void 0,
18320 onError = (err) => {
18321 if (typeof console !== "undefined") {
18322 console.error("[desktop-mode] createInfiniteList:", err);
18323 }
18324 }
18325 } = options;
18326 let sentinel = options.sentinel ?? null;
18327 if (!sentinel) {
18328 sentinel = document.createElement("div");
18329 sentinel.dataset.wpdInfiniteListSentinel = "";
18330 sentinel.style.height = "1px";
18331 root.appendChild(sentinel);
18332 }
18333 const seen = /* @__PURE__ */ new Set();
18334 let cursor = initialCursor;
18335 let hasMoreInternal = true;
18336 let loading = false;
18337 let controller = null;
18338 let renderedCount = 0;
18339 let destroyed = false;
18340 let observer = null;
18341 const setLoading = (next) => {
18342 if (loading === next) {
18343 return;
18344 }
18345 loading = next;
18346 try {
18347 onLoadingChange(next);
18348 } catch (err) {
18349 onError(err);
18350 }
18351 };
18352 const detachObserver = () => {
18353 if (observer) {
18354 observer.disconnect();
18355 observer = null;
18356 }
18357 };
18358 const ensureObserver = () => {
18359 if (observer || !sentinel || destroyed) {
18360 return;
18361 }
18362 observer = new IntersectionObserver(
18363 (entries) => {
18364 for (const entry of entries) {
18365 if (entry.isIntersecting) {
18366 void loadMore();
18367 }
18368 }
18369 },
18370 { rootMargin }
18371 );
18372 observer.observe(sentinel);
18373 };
18374 const loadMore = async () => {
18375 if (destroyed || loading || !hasMoreInternal) {
18376 return;
18377 }
18378 setLoading(true);
18379 controller = new AbortController();
18380 const localController = controller;
18381 try {
18382 const page = await fetchPage(cursor, localController.signal);
18383 if (destroyed || localController !== controller) {
18384 return;
18385 }
18386 let appended = 0;
18387 const frag = document.createDocumentFragment();
18388 for (const item of page.items ?? []) {
18389 const key = String(getId(item));
18390 if (seen.has(key)) {
18391 continue;
18392 }
18393 seen.add(key);
18394 const el = renderItem(item, renderedCount + appended);
18395 frag.appendChild(el);
18396 appended++;
18397 }
18398 if (appended > 0) {
18399 if (sentinel && sentinel.parentNode === root) {
18400 root.insertBefore(frag, sentinel);
18401 } else {
18402 root.appendChild(frag);
18403 }
18404 renderedCount += appended;
18405 }
18406 cursor = page.nextCursor ?? null;
18407 if (!cursor) {
18408 hasMoreInternal = false;
18409 detachObserver();
18410 }
18411 } catch (err) {
18412 if (err?.name === "AbortError") {
18413 return;
18414 }
18415 onError(err);
18416 } finally {
18417 if (localController === controller) {
18418 setLoading(false);
18419 controller = null;
18420 }
18421 }
18422 };
18423 const reset = () => {
18424 if (destroyed) {
18425 return;
18426 }
18427 controller?.abort();
18428 controller = null;
18429 seen.clear();
18430 cursor = initialCursor;
18431 hasMoreInternal = true;
18432 renderedCount = 0;
18433 const sentinelInRoot = sentinel && sentinel.parentNode === root;
18434 while (root.firstChild) {
18435 root.removeChild(root.firstChild);
18436 }
18437 if (sentinelInRoot && sentinel) {
18438 root.appendChild(sentinel);
18439 }
18440 setLoading(false);
18441 ensureObserver();
18442 void loadMore();
18443 };
18444 const destroy = () => {
18445 if (destroyed) {
18446 return;
18447 }
18448 destroyed = true;
18449 detachObserver();
18450 controller?.abort();
18451 controller = null;
18452 if (!options.sentinel && sentinel && sentinel.parentNode === root) {
18453 root.removeChild(sentinel);
18454 }
18455 sentinel = null;
18456 setLoading(false);
18457 };
18458 ensureObserver();
18459 void loadMore();
18460 return {
18461 reset,
18462 loadMore,
18463 hasMore: () => hasMoreInternal,
18464 isLoading: () => loading,
18465 destroy
18466 };
18467 }
18468 const POPUP_DEFAULT_WIDTH = 520;
18469 const POPUP_DEFAULT_HEIGHT = 720;
18470 const POPUP_CLOSE_POLL_MS = 500;
18471 function startOAuth(service, options = {}) {
18472 if (typeof service !== "string" || service === "") {
18473 return Promise.reject(
18474 new Error("[desktop-mode] startOAuth requires a non-empty service slug.")
18475 );
18476 }
18477 const restRoot2 = readRestRoot$1();
18478 const restNonce = readRestNonce$1();
18479 return trackedFetch$1(
18480 joinRestUrl(restRoot2, "desktop-mode/v1/oauth/start"),
18481 {
18482 method: "POST",
18483 headers: {
18484 "Content-Type": "application/json",
18485 "X-WP-Nonce": restNonce ?? ""
18486 },
18487 body: JSON.stringify({ service })
18488 },
18489 { source: "desktop-mode/oauth-start" }
18490 ).then(async (res) => {
18491 if (!res.ok) {
18492 const text = await res.text().catch(() => "");
18493 throw new Error(
18494 `[desktop-mode] OAuth start failed (${res.status}): ${text}`
18495 );
18496 }
18497 return await res.json();
18498 }).then((startBody) => openPopupAndWait(startBody, service, options));
18499 }
18500 function openPopupAndWait(body, service, options) {
18501 return new Promise((resolve2, reject) => {
18502 const width = options.width ?? POPUP_DEFAULT_WIDTH;
18503 const height = options.height ?? POPUP_DEFAULT_HEIGHT;
18504 const left = Math.max(0, Math.floor((window.screen.width - width) / 2));
18505 const top = Math.max(0, Math.floor((window.screen.height - height) / 2));
18506 const features = [
18507 `width=${width}`,
18508 `height=${height}`,
18509 `left=${left}`,
18510 `top=${top}`,
18511 "menubar=no",
18512 "toolbar=no",
18513 "location=yes",
18514 "status=no",
18515 "resizable=yes",
18516 "scrollbars=yes"
18517 ].join(",");
18518 const popup = window.open(
18519 body.authorize_url,
18520 `desktop-mode-oauth-${service}`,
18521 features
18522 );
18523 if (!popup) {
18524 reject(
18525 new Error(
18526 "[desktop-mode] OAuth popup blocked. Tell users to allow popups for this site."
18527 )
18528 );
18529 return;
18530 }
18531 const expectedOrigin = window.location.origin;
18532 let pollTimer = null;
18533 let detached = false;
18534 const cleanup = () => {
18535 if (detached) {
18536 return;
18537 }
18538 detached = true;
18539 window.removeEventListener("message", onMessage);
18540 if (pollTimer !== null) {
18541 window.clearInterval(pollTimer);
18542 pollTimer = null;
18543 }
18544 };
18545 const onMessage = (e) => {
18546 if (e.origin !== expectedOrigin) {
18547 return;
18548 }
18549 const data = e.data;
18550 if (!data || data.type !== "desktop-mode-oauth-callback") {
18551 return;
18552 }
18553 const payload = data.payload;
18554 cleanup();
18555 if (payload && payload.ok) {
18556 resolve2(payload);
18557 } else {
18558 const reason = payload?.reason ?? "unknown";
18559 const message = payload?.message ?? "OAuth flow failed";
18560 const err = new Error(
18561 `[desktop-mode] startOAuth(${service}) failed: ${reason} — ${message}`
18562 );
18563 err.cause = payload;
18564 reject(err);
18565 }
18566 };
18567 window.addEventListener("message", onMessage);
18568 pollTimer = window.setInterval(() => {
18569 if (popup.closed) {
18570 cleanup();
18571 reject(
18572 new Error(
18573 `[desktop-mode] startOAuth(${service}) cancelled — popup closed before completing.`
18574 )
18575 );
18576 }
18577 }, POPUP_CLOSE_POLL_MS);
18578 });
18579 }
18580 function readDesktopConfig() {
18581 return window.desktopModeConfig ?? {};
18582 }
18583 function readRestRoot$1() {
18584 const root = readDesktopConfig().restRoot;
18585 if (typeof root === "string" && root !== "") {
18586 return root;
18587 }
18588 return `${window.location.origin}/wp-json/`;
18589 }
18590 function readRestNonce$1() {
18591 const nonce = readDesktopConfig().restNonce;
18592 return typeof nonce === "string" && nonce !== "" ? nonce : null;
18593 }
18594 const RESERVED_NAMESPACE_KEYS = /* @__PURE__ */ new Set([
18595 "windowManager",
18596 "dock",
18597 "taskbar",
18598 "icons",
18599 "saveSession",
18600 "hooks",
18601 "HOOKS",
18602 "isActive",
18603 "registerWallpaper",
18604 "registerWidget",
18605 "widgetLayer",
18606 "widgets",
18607 "registerSystemTile",
18608 "registerWindow",
18609 "openWindow",
18610 "cloneTemplate",
18611 "onWindow",
18612 "loadVendorScript",
18613 "getWallpaperSurfaces",
18614 "registerModule",
18615 "loadModules",
18616 "whenReady",
18617 "ready",
18618 "isReady",
18619 "setDefaultWindow",
18620 "refreshMenu",
18621 "config",
18622 "ai",
18623 "dragBridge",
18624 "dragManager",
18625 "registerCommand",
18626 "unregisterCommand",
18627 "listCommands",
18628 "registerDestructiveAdminAction",
18629 "unregisterDestructiveAdminAction",
18630 "listDestructiveAdminActions",
18631 "registerSettingsTab",
18632 "unregisterSettingsTab",
18633 "listSettingsTabs",
18634 "registerDockRailRenderer",
18635 "unregisterDockRailRenderer",
18636 "listDockRailRenderers",
18637 "openOsSettings",
18638 "getOsSettings",
18639 "subscribeOsSettings",
18640 "updateOsSettings",
18641 "deriveWindowId",
18642 "listSystemTiles",
18643 "getSystemTile",
18644 "getMenuItems",
18645 "renderIcon",
18646 "applyTileClasses",
18647 "applyTileElement",
18648 "applyTileTooltip",
18649 "dispatchTileRendered",
18650 "isDockElement",
18651 "registerDockSelector",
18652 "registerTitleBarButton",
18653 "unregisterTitleBarButton",
18654 "listTitleBarButtons",
18655 "registerUnfocusEffect",
18656 "unregisterUnfocusEffect",
18657 "listUnfocusEffects",
18658 "registerWindowTheme",
18659 "unregisterWindowTheme",
18660 "listWindowThemes",
18661 "applyWindowTheme",
18662 "registerWindowControl",
18663 "unregisterWindowControl",
18664 "listWindowControls",
18665 "applyWindowControls",
18666 "registerWindowSlot",
18667 "unregisterWindowSlot",
18668 "listWindowSlots",
18669 "applyWindowSlot",
18670 "registerWindowNotice",
18671 "unregisterWindowNotice",
18672 "listWindowNotices",
18673 "dismissWindowNotice",
18674 "undismissWindowNotice",
18675 "registerWindowChrome",
18676 "unregisterWindowChrome",
18677 "listWindowChromes",
18678 "applyWindowChrome",
18679 "connect",
18680 "broadcast",
18681 "subscribe",
18682 "registerPalette",
18683 "unregisterPalette",
18684 "listPalettes",
18685 "openPalette",
18686 "devtools",
18687 "createSharedStore",
18688 "presence",
18689 "activity",
18690 "heartbeat",
18691 "showToast",
18692 "renderKeyedList",
18693 "clearKeyedList",
18694 "registerNamespace",
18695 "notify",
18696 "pwa",
18697 "getWindowConfig",
18698 "debug",
18699 "fetch"
18700 ]);
18701 function buildPublicApi(deps2) {
18702 const {
18703 manager,
18704 dock,
18705 layoutDispatcher,
18706 osSettings,
18707 iconsApi: iconsApi2,
18708 filesApi: filesApi2,
18709 saveSession,
18710 widgetLayer,
18711 registerWindow,
18712 openWindowById,
18713 openNewWindowById,
18714 placeSystemTile,
18715 setDefaultWindow,
18716 refreshMenu,
18717 openOsSettings,
18718 aiAssistant,
18719 dragBridge,
18720 dragManager,
18721 connect,
18722 getConnection,
18723 config
18724 } = deps2;
18725 const desktopApi = {
18726 windowManager: manager,
18727 dock,
18728 sideDock: layoutDispatcher?.getSide() ?? null,
18729 desktopLayout: osSettings.getOsSettingsSnapshot().desktopLayout,
18730 icons: iconsApi2,
18731 files: filesApi2,
18732 confirm: wpdConfirm,
18733 saveSession,
18734 hooks: rawHooks(),
18735 HOOKS,
18736 isActive: () => !!document.getElementById("desktop-mode-shell"),
18737 registerWallpaper: (def) => {
18738 register$2(def);
18739 osSettings.apply();
18740 },
18741 registerWidget: (def) => {
18742 register(def);
18743 },
18744 widgetLayer,
18745 widgets: {
18746 redock: (id) => {
18747 widgetLayer?.redock(id);
18748 }
18749 },
18750 loadVendorScript,
18751 getWallpaperSurfaces: () => collectWallpaperSurfaces(manager),
18752 registerWindow,
18753 openWindow: openWindowById,
18754 openNewWindow: openNewWindowById,
18755 fetch: (input, requestInit, opts) => trackedFetch(manager, input, requestInit, opts),
18756 repaintLoadingOverlays,
18757 cloneTemplate,
18758 onWindow,
18759 createInfiniteList,
18760 startOAuth,
18761 registerSystemTile: (item) => {
18762 placeSystemTile(item);
18763 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: item.id });
18764 },
18765 registerModule,
18766 loadModules,
18767 whenReady,
18768 ready: whenReady,
18769 isReady,
18770 setDefaultWindow,
18771 refreshMenu,
18772 config,
18773 ai: aiAssistant,
18774 dragBridge,
18775 dragManager,
18776 registerCommand,
18777 unregisterCommand,
18778 listCommands,
18779 registerDestructiveAdminAction,
18780 unregisterDestructiveAdminAction,
18781 listDestructiveAdminActions,
18782 registerSettingsTab,
18783 unregisterSettingsTab,
18784 listSettingsTabs,
18785 registerDockRailRenderer: register$1,
18786 unregisterDockRailRenderer: unregister$1,
18787 listDockRailRenderers: list,
18788 openOsSettings,
18789 getOsSettings: () => osSettings.getOsSettingsSnapshot(),
18790 subscribeOsSettings: (cb) => osSettings.subscribeOsSettings(cb),
18791 updateOsSettings: (patch, opts = {}) => {
18792 if (typeof patch.wallpaper === "string") {
18793 osSettings.state.wallpaper = patch.wallpaper;
18794 }
18795 if (typeof patch.accent === "string") {
18796 osSettings.state.accent = patch.accent;
18797 }
18798 if (typeof patch.dockSize === "string") {
18799 osSettings.state.dockSize = patch.dockSize;
18800 }
18801 if (typeof patch.desktopLayout === "string") {
18802 osSettings.state.desktopLayout = patch.desktopLayout;
18803 }
18804 if (typeof patch.dockRailRenderer === "string") {
18805 osSettings.state.dockRailRenderer = patch.dockRailRenderer;
18806 }
18807 if (patch.ai && typeof patch.ai === "object") {
18808 osSettings.state.ai = { ...osSettings.state.ai, ...patch.ai };
18809 }
18810 if (typeof patch.nativePostsEnabled === "boolean") {
18811 osSettings.state.nativePostsEnabled = patch.nativePostsEnabled;
18812 }
18813 if (typeof patch.nativePagesEnabled === "boolean") {
18814 osSettings.state.nativePagesEnabled = patch.nativePagesEnabled;
18815 }
18816 if (typeof patch.nativeUsersEnabled === "boolean") {
18817 osSettings.state.nativeUsersEnabled = patch.nativeUsersEnabled;
18818 }
18819 if (typeof patch.nativePluginsEnabled === "boolean") {
18820 osSettings.state.nativePluginsEnabled = patch.nativePluginsEnabled;
18821 }
18822 if (typeof patch.nativeCommentsEnabled === "boolean") {
18823 osSettings.state.nativeCommentsEnabled = patch.nativeCommentsEnabled;
18824 }
18825 if (typeof patch.foldersSharingEnabled === "boolean") {
18826 osSettings.state.foldersSharingEnabled = patch.foldersSharingEnabled;
18827 }
18828 if (Array.isArray(patch.nativePostsHiddenColumns)) {
18829 osSettings.state.nativePostsHiddenColumns = patch.nativePostsHiddenColumns.filter(
18830 (v) => typeof v === "string" && v !== ""
18831 ).slice(0, 32);
18832 }
18833 if (patch.itemVisibility && typeof patch.itemVisibility === "object") {
18834 const allowed = ["both", "dock", "desktop", "hidden"];
18835 const next = {};
18836 for (const [k, v] of Object.entries(
18837 patch.itemVisibility
18838 )) {
18839 if (typeof k !== "string" || k === "") {
18840 continue;
18841 }
18842 if (typeof v !== "string" || !allowed.includes(v)) {
18843 continue;
18844 }
18845 next[k] = v;
18846 }
18847 osSettings.state.itemVisibility = next;
18848 }
18849 if (Array.isArray(patch.dockOrder)) {
18850 osSettings.state.dockOrder = patch.dockOrder.filter(
18851 (v) => typeof v === "string" && v !== ""
18852 ).slice(0, 256);
18853 }
18854 if (patch.dockPromotedPositions && typeof patch.dockPromotedPositions === "object") {
18855 const MAX_COORD = 1e5;
18856 const next = {};
18857 for (const [k, v] of Object.entries(
18858 patch.dockPromotedPositions
18859 )) {
18860 if (typeof k !== "string" || k === "") {
18861 continue;
18862 }
18863 if (!v || typeof v !== "object") {
18864 continue;
18865 }
18866 const pos = v;
18867 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) {
18868 continue;
18869 }
18870 next[k] = { x: pos.x, y: pos.y };
18871 if (Object.keys(next).length >= 256) {
18872 break;
18873 }
18874 }
18875 osSettings.state.dockPromotedPositions = next;
18876 }
18877 osSettings.save(opts);
18878 if (patch.itemVisibility || patch.dockOrder) {
18879 layoutDispatcher?.refresh();
18880 }
18881 },
18882 deriveWindowId: (url, overrideAdminUrl) => deriveWindowId(url, overrideAdminUrl ?? config.adminUrl),
18883 listSystemTiles: () => layoutDispatcher?.listSystemTiles() ?? [],
18884 getSystemTile: (id) => layoutDispatcher?.getSystemTile(id) ?? null,
18885 getMenuItems: () => layoutDispatcher?.getMenuItems() ?? [],
18886 renderIcon,
18887 applyTileClasses,
18888 applyTileElement,
18889 applyTileTooltip,
18890 dispatchTileRendered,
18891 isDockElement,
18892 registerDockSelector,
18893 registerTitleBarButton,
18894 unregisterTitleBarButton,
18895 listTitleBarButtons,
18896 registerUnfocusEffect,
18897 unregisterUnfocusEffect,
18898 listUnfocusEffects,
18899 registerWindowTheme,
18900 unregisterWindowTheme,
18901 listWindowThemes,
18902 applyWindowTheme: (windowId, override) => {
18903 const win = manager.getById(windowId);
18904 if (!win) {
18905 return;
18906 }
18907 win.setAppearanceTheme(override);
18908 },
18909 registerWindowControl,
18910 unregisterWindowControl,
18911 listWindowControls,
18912 applyWindowControls: (windowId, override) => {
18913 const win = manager.getById(windowId);
18914 if (!win) {
18915 return;
18916 }
18917 win.setAppearanceControls(override);
18918 },
18919 registerWindowSlot,
18920 unregisterWindowSlot,
18921 listWindowSlots,
18922 applyWindowSlot: (windowId, slot, slotConfig) => {
18923 const win = manager.getById(windowId);
18924 if (!win) {
18925 return;
18926 }
18927 win.setAppearanceSlot(slot, slotConfig);
18928 },
18929 registerWindowNotice,
18930 unregisterWindowNotice,
18931 listWindowNotices,
18932 dismissWindowNotice,
18933 undismissWindowNotice,
18934 registerWindowChrome,
18935 unregisterWindowChrome,
18936 listWindowChromes,
18937 applyWindowChrome: (windowId, chromeId) => {
18938 const win = manager.getById(windowId);
18939 if (!win) {
18940 return;
18941 }
18942 win.setAppearanceChrome(chromeId);
18943 },
18944 connect,
18945 getConnection,
18946 broadcast,
18947 subscribe: subscribe$2,
18948 registerPalette,
18949 unregisterPalette,
18950 listPalettes,
18951 openPalette: openPaletteOnly,
18952 devtools,
18953 createSharedStore,
18954 presence: presenceApi,
18955 activity,
18956 heartbeat,
18957 showToast,
18958 notify: notify$3,
18959 pwa: {
18960 promptInstall,
18961 undismissInstallHint,
18962 getState: getPwaState,
18963 subscribe: subscribePwaState,
18964 requestNotificationPermission,
18965 getNotificationPermission
18966 },
18967 renderKeyedList,
18968 clearKeyedList,
18969 registerNamespace: (name, api) => {
18970 if (typeof name !== "string" || name === "") {
18971 console.warn(
18972 "[desktop-mode] registerNamespace: name must be a non-empty string"
18973 );
18974 return;
18975 }
18976 if (!api || typeof api !== "object") {
18977 console.warn(
18978 `[desktop-mode] registerNamespace("${name}"): api must be an object`
18979 );
18980 return;
18981 }
18982 if (RESERVED_NAMESPACE_KEYS.has(name)) {
18983 console.warn(
18984 `[desktop-mode] registerNamespace("${name}"): name is reserved by the shell — pick a plugin-specific key`
18985 );
18986 return;
18987 }
18988 desktopApi[name] = api;
18989 },
18990 getWindowConfig: (id) => {
18991 const store2 = window.desktopModeWindowConfig;
18992 if (!store2 || typeof store2 !== "object") {
18993 return void 0;
18994 }
18995 const value = store2[id];
18996 return value === void 0 ? void 0 : value;
18997 },
18998 debug: {
18999 window: (id) => {
19000 const entry = (config.nativeWindows ?? []).find(
19001 (e) => e.id === id
19002 );
19003 if (!entry) {
19004 return null;
19005 }
19006 const url = entry.scriptUrl || "";
19007 let loadPath = "unknown";
19008 let tagInDom = false;
19009 if (url) {
19010 const lazyTag = document.querySelector(
19011 `script[data-desktop-mode-vendor="${url.replace(/"/g, '\\"')}"]`
19012 );
19013 if (lazyTag) {
19014 loadPath = "lazy";
19015 tagInDom = true;
19016 } else {
19017 const eagerTag = Array.from(
19018 document.querySelectorAll(
19019 "script[src]"
19020 )
19021 ).find((s) => s.src === url);
19022 if (eagerTag) {
19023 loadPath = "eager";
19024 tagInDom = true;
19025 }
19026 }
19027 }
19028 const cfgStore = window.desktopModeWindowConfig;
19029 const configPresent = !!(cfgStore && typeof cfgStore === "object" && Object.prototype.hasOwnProperty.call(cfgStore, id));
19030 return {
19031 id,
19032 scriptHandle: entry.scriptHandle || "",
19033 scriptUrl: url,
19034 loadPath,
19035 tagInDom,
19036 configPresent,
19037 extras: {
19038 hasTranslations: !!entry.scriptTranslations,
19039 l10nCount: (entry.scriptL10n ?? []).length,
19040 beforeCount: (entry.scriptBefore ?? []).length,
19041 afterCount: (entry.scriptAfter ?? []).length
19042 }
19043 };
19044 }
19045 }
19046 };
19047 return desktopApi;
19048 }
19049 function installPublicApi(api) {
19050 if (!window.wp) {
19051 window.wp = {};
19052 }
19053 if (!window.wp.desktop) {
19054 window.wp.desktop = api;
19055 return;
19056 }
19057 Object.assign(
19058 window.wp.desktop,
19059 api
19060 );
19061 }
19062 const store$1 = createSharedStore("desktop-mode/layout", () => ({
19063 // Default mirrors the OsSettingsSnapshot default; the shell
19064 // re-publishes the persisted value as soon as it boots.
19065 layout: "classic"
19066 }));
19067 function setCurrentLayout(layout) {
19068 if (store$1.state.layout === layout) {
19069 return;
19070 }
19071 store$1.state.layout = layout;
19072 store$1.notify();
19073 }
19074 class DesktopFile {
19075 constructor(shape) {
19076 this.shape = shape;
19077 }
19078 /** Title shown under the tile. Defaults to `shape.title`. */
19079 title() {
19080 return this.shape.title;
19081 }
19082 /** Dashicon class or data URI. Defaults to `shape.icon`. */
19083 icon() {
19084 return this.shape.icon;
19085 }
19086 /** Optional preview-image URL. Defaults to `shape.previewUrl`. */
19087 previewUrl() {
19088 return this.shape.previewUrl;
19089 }
19090 /** Reference (id, URL, …). */
19091 ref() {
19092 return this.shape.ref;
19093 }
19094 /** Whether the underlying entity still exists. */
19095 exists() {
19096 return this.shape.exists;
19097 }
19098 }
19099 class DefaultDesktopFile extends DesktopFile {
19100 constructor(shape, typeSlug) {
19101 super(shape);
19102 this.typeSlug = typeSlug;
19103 }
19104 type() {
19105 return this.typeSlug;
19106 }
19107 }
19108 const seed$1 = /* @__PURE__ */ new Map();
19109 const listeners$1 = /* @__PURE__ */ new Set();
19110 function registerType(def) {
19111 if (!def.type) {
19112 throw new Error("[desktop-mode] registerType: `type` is required.");
19113 }
19114 if (!def.label) {
19115 throw new Error("[desktop-mode] registerType: `label` is required.");
19116 }
19117 seed$1.set(def.type, {
19118 type: def.type,
19119 label: def.label,
19120 sort: typeof def.sort === "number" ? def.sort : 100,
19121 DesktopFile: def.DesktopFile
19122 });
19123 doAction("desktop-mode.files.type-registered", def.type, def);
19124 notify$1();
19125 }
19126 function unregisterType(typeSlug) {
19127 if (seed$1.delete(typeSlug)) {
19128 doAction("desktop-mode.files.type-unregistered", typeSlug);
19129 notify$1();
19130 }
19131 }
19132 function getType(typeSlug) {
19133 const entry = seed$1.get(typeSlug);
19134 return entry ? entry : null;
19135 }
19136 function getTypes() {
19137 const list2 = Array.from(seed$1.values()).slice();
19138 const filtered = applyFilters(
19139 "desktop-mode.files.types",
19140 list2
19141 );
19142 const arr = Array.isArray(filtered) ? filtered : list2;
19143 arr.sort((a, b) => {
19144 if (a.sort !== b.sort) {
19145 return a.sort - b.sort;
19146 }
19147 return a.label.localeCompare(b.label);
19148 });
19149 return arr;
19150 }
19151 function resolve(shape) {
19152 const entry = seed$1.get(shape.type);
19153 if (entry?.DesktopFile) {
19154 return new entry.DesktopFile(shape);
19155 }
19156 return new DefaultDesktopFile(shape, shape.type);
19157 }
19158 function subscribe(cb) {
19159 listeners$1.add(cb);
19160 return () => listeners$1.delete(cb);
19161 }
19162 function notify$1() {
19163 for (const cb of listeners$1) {
19164 try {
19165 cb();
19166 } catch (err) {
19167 console.error("[desktop-mode] files registry subscriber threw:", err);
19168 }
19169 }
19170 }
19171 const seed = /* @__PURE__ */ new Map();
19172 const listeners = /* @__PURE__ */ new Set();
19173 let userAssociations = {};
19174 function setUserAssociations(map) {
19175 userAssociations = { ...map };
19176 notify();
19177 }
19178 function getUserAssociations() {
19179 return { ...userAssociations };
19180 }
19181 function registerOpener(def) {
19182 if (!def.id) {
19183 throw new Error("[desktop-mode] registerOpener: `id` is required.");
19184 }
19185 if (!def.label) {
19186 throw new Error("[desktop-mode] registerOpener: `label` is required.");
19187 }
19188 if (!Array.isArray(def.types) || def.types.length === 0) {
19189 throw new Error("[desktop-mode] registerOpener: `types` must be a non-empty array.");
19190 }
19191 if (!def.handler || typeof def.handler !== "object") {
19192 throw new Error("[desktop-mode] registerOpener: `handler` is required.");
19193 }
19194 seed.set(def.id, {
19195 id: def.id,
19196 label: def.label,
19197 types: def.types.slice(),
19198 isDefault: !!def.isDefault,
19199 sort: typeof def.sort === "number" ? def.sort : 100,
19200 handler: def.handler
19201 });
19202 doAction("desktop-mode.files.opener-registered", def.id, def);
19203 notify();
19204 }
19205 function unregisterOpener(id) {
19206 if (seed.delete(id)) {
19207 doAction("desktop-mode.files.opener-unregistered", id);
19208 notify();
19209 }
19210 }
19211 function getOpener(id) {
19212 return seed.get(id) ?? null;
19213 }
19214 function getOpeners() {
19215 const list2 = Array.from(seed.values()).slice();
19216 const filtered = applyFilters(
19217 "desktop-mode.files.openers",
19218 list2
19219 );
19220 const arr = Array.isArray(filtered) ? filtered : list2;
19221 arr.sort((a, b) => {
19222 const sa = typeof a.sort === "number" ? a.sort : 100;
19223 const sb = typeof b.sort === "number" ? b.sort : 100;
19224 if (sa !== sb) {
19225 return sa - sb;
19226 }
19227 return a.label.localeCompare(b.label);
19228 });
19229 return arr;
19230 }
19231 function getOpenersForType(type) {
19232 return getOpeners().filter((e) => e.types.includes(type));
19233 }
19234 function resolveOpener(type) {
19235 const candidates = getOpenersForType(type);
19236 if (candidates.length === 0) {
19237 return null;
19238 }
19239 const override = userAssociations[type];
19240 let resolved = null;
19241 if (override) {
19242 resolved = candidates.find((e) => e.id === override) ?? null;
19243 }
19244 if (!resolved) {
19245 resolved = candidates.find((e) => e.isDefault) ?? null;
19246 }
19247 if (!resolved) {
19248 resolved = candidates[0];
19249 }
19250 const filtered = applyFilters(
19251 "desktop-mode.files.resolve-opener",
19252 resolved,
19253 type
19254 );
19255 return filtered ?? null;
19256 }
19257 function subscribeOpeners(cb) {
19258 listeners.add(cb);
19259 return () => listeners.delete(cb);
19260 }
19261 function notify() {
19262 for (const cb of listeners) {
19263 try {
19264 cb();
19265 } catch (err) {
19266 console.error("[desktop-mode] openers subscriber threw:", err);
19267 }
19268 }
19269 }
19270 let deps$1 = null;
19271 function installOpenDeps(next) {
19272 deps$1 = next;
19273 }
19274 async function openFile(file, ctx) {
19275 if (!deps$1) {
19276 console.warn(
19277 "[desktop-mode] wp.desktop.files.open() called before the shell installed open deps. The file will not open."
19278 );
19279 return false;
19280 }
19281 const opener = resolveOpener(file.type());
19282 if (!opener) {
19283 doAction("desktop-mode.files.open-failed", {
19284 reason: "no-opener",
19285 type: file.type(),
19286 ref: file.ref()
19287 });
19288 return false;
19289 }
19290 doAction("desktop-mode.files.opening", { file, openerId: opener.id });
19291 try {
19292 const handler = opener.handler;
19293 if (handler.kind === "url") {
19294 const url = await handler.url(file);
19295 if (!url) {
19296 return false;
19297 }
19298 const id = handler.windowId ? handler.windowId(file) : deps$1.deriveWindowId(url);
19299 const title = handler.title ? handler.title(file) : file.title();
19300 const icon = file.icon();
19301 const opened = deps$1.openUrl({ id, url, title, icon });
19302 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "url" });
19303 return opened;
19304 }
19305 if (handler.kind === "window") {
19306 const config = handler.config ? handler.config(file) : void 0;
19307 const opened = deps$1.openNativeWindow(handler.windowId, config);
19308 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "window" });
19309 return opened;
19310 }
19311 await handler.open(file, ctx);
19312 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "js" });
19313 return true;
19314 } catch (err) {
19315 doAction("desktop-mode.files.open-failed", {
19316 reason: "handler-threw",
19317 type: file.type(),
19318 ref: file.ref(),
19319 openerId: opener.id,
19320 error: err
19321 });
19322 console.error("[desktop-mode] file opener threw:", err);
19323 return false;
19324 }
19325 }
19326 function registerBuiltInFileTypes() {
19327 registerType({ type: "shortcut", label: "Plugin shortcut", sort: 1 });
19328 registerType({ type: "folder", label: "Folder", sort: 5 });
19329 registerType({ type: "post", label: "Post", sort: 10 });
19330 registerType({ type: "attachment", label: "Media", sort: 20 });
19331 registerType({ type: "user", label: "User", sort: 30 });
19332 registerType({ type: "term", label: "Taxonomy term", sort: 40 });
19333 registerType({ type: "comment", label: "Comment", sort: 50 });
19334 registerType({ type: "bookmark", label: "Bookmark", sort: 60 });
19335 registerType({ type: "link", label: "Web link", sort: 70 });
19336 registerType({ type: "embed", label: "Embedded web window", sort: 80 });
19337 }
19338 let deps = null;
19339 function installRestDeps(next) {
19340 deps = next;
19341 }
19342 function ensureDeps() {
19343 if (!deps) {
19344 throw new Error("[desktop-mode] files REST client called before installRestDeps().");
19345 }
19346 return deps;
19347 }
19348 class FilesConflictError extends Error {
19349 constructor(detail) {
19350 super(
19351 `Row was changed by ${detail.actor.name || "another session"} (parent="${detail.current.parentName}")`
19352 );
19353 this.name = "FilesConflictError";
19354 this.status = 409;
19355 this.detail = detail;
19356 }
19357 }
19358 async function call(path, init2) {
19359 const { baseUrl, nonce } = ensureDeps();
19360 const url = joinRestUrl(baseUrl, path);
19361 const headers = new Headers(init2.headers ?? {});
19362 headers.set("X-WP-Nonce", nonce);
19363 if (init2.body && !headers.has("Content-Type")) {
19364 headers.set("Content-Type", "application/json");
19365 }
19366 const res = await trackedFetch$1(
19367 url,
19368 { ...init2, headers, credentials: "same-origin" },
19369 { source: "desktop-mode/files" }
19370 );
19371 const text = await res.text();
19372 let body = null;
19373 let parseError = null;
19374 if (text) {
19375 try {
19376 body = JSON.parse(text);
19377 } catch (e) {
19378 body = null;
19379 parseError = e;
19380 }
19381 }
19382 if (!res.ok) {
19383 if (res.status === 409) {
19384 const data = body?.data?.data ?? body?.data;
19385 if (data && typeof data === "object") {
19386 throw new FilesConflictError(data);
19387 }
19388 }
19389 const err = body;
19390 throw new Error(
19391 `[desktop-mode] files REST ${res.status}: ${err?.code ?? ""} ${err?.message ?? ""}`.trim()
19392 );
19393 }
19394 if (null === body) {
19395 if (parseError && text) {
19396 const head = text.slice(0, 120).replace(/\s+/g, " ");
19397 throw new Error(
19398 `[desktop-mode] files REST ${res.status} returned non-JSON body — ${parseError.message}. First 120 chars: ${head}`
19399 );
19400 }
19401 throw new Error(
19402 `[desktop-mode] files REST ${res.status}: empty or unparseable body.`
19403 );
19404 }
19405 return body;
19406 }
19407 function listPlacements(folderId = 0) {
19408 return call(
19409 `/placements?folder=${encodeURIComponent(String(folderId))}`,
19410 { method: "GET" }
19411 );
19412 }
19413 function createPlacement(body) {
19414 return call("/placements", {
19415 method: "POST",
19416 body: JSON.stringify(body)
19417 });
19418 }
19419 function updatePlacement(id, body, ifMatchMs) {
19420 const headers = {};
19421 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
19422 headers["If-Match"] = String(ifMatchMs);
19423 }
19424 return call(`/placements/${id}`, {
19425 method: "PATCH",
19426 body: JSON.stringify(body),
19427 headers
19428 });
19429 }
19430 function deletePlacement(id) {
19431 return call(`/placements/${id}`, { method: "DELETE" });
19432 }
19433 async function restoreTrashedItem(id, type) {
19434 const { baseUrl, nonce } = ensureDeps();
19435 const root = baseUrl.replace(/\/files\/?$/, "");
19436 const url = `${root}/recycle-bin/restore`;
19437 const res = await trackedFetch$1(
19438 url,
19439 {
19440 method: "POST",
19441 headers: {
19442 "Content-Type": "application/json",
19443 "X-WP-Nonce": nonce
19444 },
19445 credentials: "same-origin",
19446 body: JSON.stringify({ items: [{ id, type }] })
19447 },
19448 { source: "desktop-mode/files" }
19449 );
19450 if (!res.ok) {
19451 throw new Error(`[desktop-mode] restore ${res.status}`);
19452 }
19453 return await res.json();
19454 }
19455 function listFolders() {
19456 return call("/folders", { method: "GET" });
19457 }
19458 function createFolder(body) {
19459 return call("/folders", {
19460 method: "POST",
19461 body: JSON.stringify(body)
19462 });
19463 }
19464 function updateFolder(id, body, ifMatchMs) {
19465 const headers = {};
19466 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
19467 headers["If-Match"] = String(ifMatchMs);
19468 }
19469 return call(`/folders/${id}`, {
19470 method: "PATCH",
19471 body: JSON.stringify(body),
19472 headers
19473 });
19474 }
19475 function deleteFolder(id) {
19476 return call(`/folders/${id}`, { method: "DELETE" });
19477 }
19478 function saveAssociations(associations) {
19479 return call("/associations", {
19480 method: "PUT",
19481 body: JSON.stringify({ associations })
19482 });
19483 }
19484 function listShares(folderId) {
19485 return call(`/folders/${folderId}/shares`, { method: "GET" });
19486 }
19487 function inviteShare(folderId, body) {
19488 return call(`/folders/${folderId}/shares`, {
19489 method: "POST",
19490 body: JSON.stringify(body)
19491 });
19492 }
19493 function updateShareCapability(folderId, shareId, capability) {
19494 return call(`/folders/${folderId}/shares/${shareId}`, {
19495 method: "PATCH",
19496 body: JSON.stringify({ capability })
19497 });
19498 }
19499 function revokeShare(folderId, shareId) {
19500 return call(`/folders/${folderId}/shares/${shareId}`, {
19501 method: "DELETE"
19502 });
19503 }
19504 function acceptShare(folderId, shareId) {
19505 return call(`/folders/${folderId}/shares/${shareId}/accept`, {
19506 method: "POST"
19507 });
19508 }
19509 function denyShare(folderId, shareId) {
19510 return call(`/folders/${folderId}/shares/${shareId}/deny`, {
19511 method: "POST"
19512 });
19513 }
19514 function leaveShare(folderId) {
19515 return call(`/folders/${folderId}/leave`, {
19516 method: "POST"
19517 });
19518 }
19519 function purgeFolderSharingTables() {
19520 return call(
19521 "/folder-sharing-tables/purge",
19522 { method: "POST" }
19523 );
19524 }
19525 const filesRest = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
19526 __proto__: null,
19527 FilesConflictError,
19528 acceptShare,
19529 createFolder,
19530 createPlacement,
19531 deleteFolder,
19532 deletePlacement,
19533 denyShare,
19534 installRestDeps,
19535 inviteShare,
19536 leaveShare,
19537 listFolders,
19538 listPlacements,
19539 listShares,
19540 purgeFolderSharingTables,
19541 restoreTrashedItem,
19542 revokeShare,
19543 saveAssociations,
19544 updateFolder,
19545 updatePlacement,
19546 updateShareCapability
19547 }, Symbol.toStringTag, { value: "Module" }));
19548 const STORE_KEY = "desktop-mode/files";
19549 function getFilesStore() {
19550 return createSharedStore(STORE_KEY, () => ({
19551 placementsByFolder: /* @__PURE__ */ new Map(),
19552 folders: /* @__PURE__ */ new Map(),
19553 hydratedFolders: /* @__PURE__ */ new Set()
19554 }));
19555 }
19556 function fireChanged(detail) {
19557 if (typeof document === "undefined") {
19558 return;
19559 }
19560 document.dispatchEvent(
19561 new CustomEvent("desktop-mode-files-changed", {
19562 detail: { source: "local", ...detail }
19563 })
19564 );
19565 }
19566 function setFolderPlacements(folderId, placements) {
19567 const store2 = getFilesStore();
19568 const next = new Map(store2.state.placementsByFolder);
19569 next.set(folderId, placements.slice());
19570 const hydrated = new Set(store2.state.hydratedFolders);
19571 hydrated.add(folderId);
19572 store2.state = { ...store2.state, placementsByFolder: next, hydratedFolders: hydrated };
19573 store2.notify();
19574 fireChanged({ kind: "placements-set", folderId });
19575 }
19576 function upsertPlacement(placement, source = "local") {
19577 if (!placement || typeof placement.id !== "number") {
19578 console.warn(
19579 "[desktop-mode] upsertPlacement called with a non-placement value; ignoring.",
19580 placement
19581 );
19582 return;
19583 }
19584 const store2 = getFilesStore();
19585 const next = new Map(store2.state.placementsByFolder);
19586 for (const [folderId, list2] of next) {
19587 const idx2 = list2.findIndex((p) => p && p.id === placement.id);
19588 if (idx2 >= 0 && folderId !== placement.parentId) {
19589 const copy = list2.filter(Boolean);
19590 const removeAt = copy.findIndex((p) => p.id === placement.id);
19591 if (removeAt >= 0) {
19592 copy.splice(removeAt, 1);
19593 }
19594 next.set(folderId, copy);
19595 }
19596 }
19597 const rawTarget = next.get(placement.parentId)?.slice() ?? [];
19598 const target2 = rawTarget.filter(Boolean);
19599 const idx = target2.findIndex((p) => p.id === placement.id);
19600 if (idx >= 0) {
19601 target2[idx] = placement;
19602 } else {
19603 target2.push(placement);
19604 }
19605 next.set(placement.parentId, target2);
19606 store2.state = { ...store2.state, placementsByFolder: next };
19607 store2.notify();
19608 fireChanged({ kind: "placement-upserted", placementId: placement.id, folderId: placement.parentId, source });
19609 }
19610 function removePlacement(placementId, source = "local") {
19611 const store2 = getFilesStore();
19612 const next = new Map(store2.state.placementsByFolder);
19613 let touchedFolder;
19614 for (const [folderId, list2] of next) {
19615 const idx = list2.findIndex((p) => p && p.id === placementId);
19616 if (idx >= 0) {
19617 const copy = list2.filter(Boolean).filter(
19618 (p) => p.id !== placementId
19619 );
19620 next.set(folderId, copy);
19621 touchedFolder = folderId;
19622 }
19623 }
19624 if (touchedFolder === void 0) {
19625 return;
19626 }
19627 store2.state = { ...store2.state, placementsByFolder: next };
19628 store2.notify();
19629 fireChanged({ kind: "placement-removed", placementId, folderId: touchedFolder, source });
19630 }
19631 function setFolders(folders) {
19632 const store2 = getFilesStore();
19633 const next = /* @__PURE__ */ new Map();
19634 for (const f of folders) {
19635 next.set(f.id, f);
19636 }
19637 store2.state = { ...store2.state, folders: next };
19638 store2.notify();
19639 fireChanged({ kind: "folders-set" });
19640 }
19641 function upsertFolder(folder, source = "local") {
19642 const store2 = getFilesStore();
19643 const next = new Map(store2.state.folders);
19644 next.set(folder.id, folder);
19645 store2.state = { ...store2.state, folders: next };
19646 store2.notify();
19647 fireChanged({ kind: "folder-upserted", folderRowId: folder.id, source });
19648 }
19649 function removeFolder(folderId, source = "local") {
19650 const store2 = getFilesStore();
19651 const folders = new Map(store2.state.folders);
19652 folders.delete(folderId);
19653 const placements = new Map(store2.state.placementsByFolder);
19654 placements.delete(folderId);
19655 store2.state = { ...store2.state, folders, placementsByFolder: placements };
19656 store2.notify();
19657 fireChanged({ kind: "folder-removed", folderRowId: folderId, source });
19658 }
19659 function subscribeFilesStore(cb) {
19660 const store2 = getFilesStore();
19661 const off = store2.subscribe(cb);
19662 return off;
19663 }
19664 function getFilesState() {
19665 return getFilesStore().getState();
19666 }
19667 const store = {
19668 getState: getFilesState,
19669 subscribe: subscribeFilesStore,
19670 setFolderPlacements,
19671 upsertPlacement,
19672 upsertFolder,
19673 removePlacement,
19674 removeFolder
19675 };
19676 const styles$5 = css`:host{display:inline-block}`;
19677 const styles$4 = css`:host{position:absolute;width:var( --wpd-ribbon-size,90px );height:var( --wpd-ribbon-size,90px );overflow:hidden;pointer-events:none;z-index:var( --wpd-ribbon-z,2 )}:host( [ hidden ] ){display:none}.banner{position:absolute;display:block;width:var( --wpd-ribbon-banner-width,140px );padding:var( --wpd-ribbon-padding,4px 0 );text-align:center;font:var( --wpd-ribbon-font,700 10px/1.4 var( --desktop-mode-font,system-ui ) );letter-spacing:var( --wpd-ribbon-tracking,0.06em );text-transform:uppercase;color:var( --wpd-ribbon-fg,#fff );background:var( --wpd-ribbon-bg,var( --wp-admin-theme-color,#2271b1 ) );box-shadow:var( --wpd-ribbon-shadow,0 2px 4px rgba( 0,0,0,0.2 ) )}:host(:not( [ placement ] ) ),:host( [ placement='top-end' ] ){inset-block-start:0;inset-inline-end:0}:host(:not( [ placement ] ) ) .banner,:host( [ placement='top-end' ] ) .banner{inset-block-start:var( --wpd-ribbon-banner-offset,20px );inset-inline-end:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( 45deg )}:host( [ placement='top-start' ] ){inset-block-start:0;inset-inline-start:0}:host( [ placement='top-start' ] ) .banner{inset-block-start:var( --wpd-ribbon-banner-offset,20px );inset-inline-start:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( -45deg )}:host( [ placement='bottom-end' ] ){inset-block-end:0;inset-inline-end:0}:host( [ placement='bottom-end' ] ) .banner{inset-block-end:var( --wpd-ribbon-banner-offset,20px );inset-inline-end:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( -45deg )}:host( [ placement='bottom-start' ] ){inset-block-end:0;inset-inline-start:0}:host( [ placement='bottom-start' ] ) .banner{inset-block-end:var( --wpd-ribbon-banner-offset,20px );inset-inline-start:var( --wpd-ribbon-banner-pull,-36px );transform:rotate( 45deg )}:host-context( [ dir='rtl' ] ):host(:not( [ placement ] ) ) .banner,:host-context( [ dir='rtl' ] ):host( [ placement='top-end' ] ) .banner{transform:rotate( -45deg )}:host-context( [ dir='rtl' ] ):host( [ placement='top-start' ] ) .banner{transform:rotate( 45deg )}:host-context( [ dir='rtl' ] ):host( [ placement='bottom-end' ] ) .banner{transform:rotate( 45deg )}:host-context( [ dir='rtl' ] ):host( [ placement='bottom-start' ] ) .banner{transform:rotate( -45deg )}:host( [ tone='success' ] ) .banner{background:var( --wpd-ribbon-success,#1a7f37 )}:host( [ tone='warning' ] ) .banner{background:var( --wpd-ribbon-warning,#9a6700 )}:host( [ tone='danger' ] ) .banner{background:var( --wpd-ribbon-danger,#cf222e )}:host( [ tone='info' ] ) .banner{background:var( --wpd-ribbon-info,#0969da )}:host( [ tone='neutral' ] ) .banner{background:var( --wpd-ribbon-neutral,#57606a )}`;
19678 const _WpdRibbon = class _WpdRibbon extends Component {
19679 render() {
19680 return html`<span class="banner" part="banner"><slot></slot></span>`;
19681 }
19682 };
19683 _WpdRibbon.props = ["placement", "tone"];
19684 _WpdRibbon.styles = [styles$4];
19685 _WpdRibbon.help = {
19686 title: "Ribbon",
19687 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.",
19688 status: "experimental",
19689 since: "0.20.0",
19690 props: [
19691 {
19692 name: "placement",
19693 type: '"top-end" | "top-start" | "bottom-end" | "bottom-start"',
19694 description: "Which corner of the parent the ribbon hugs. Defaults to `top-end` (logical right in LTR, left in RTL)."
19695 },
19696 {
19697 name: "tone",
19698 type: '"primary" | "success" | "warning" | "danger" | "info" | "neutral"',
19699 description: "Background color tone. Defaults to `primary` (the admin theme accent)."
19700 }
19701 ],
19702 slots: [{ name: "(default)", description: "Ribbon label text. Keep short." }],
19703 cssProps: [
19704 { name: "--wpd-ribbon-size", default: "90px", description: "Square clipping window edge." },
19705 { name: "--wpd-ribbon-banner-width", default: "140px", description: "Width of the rotated strip." },
19706 { name: "--wpd-ribbon-banner-offset", default: "20px", description: "Distance from corner to strip center." },
19707 { name: "--wpd-ribbon-banner-pull", default: "-36px", description: "How far the strip overhangs the clip edge." },
19708 { name: "--wpd-ribbon-bg", default: "var(--wp-admin-theme-color, #2271b1)" },
19709 { name: "--wpd-ribbon-fg", default: "#fff" },
19710 { name: "--wpd-ribbon-shadow", default: "0 2px 4px rgba(0,0,0,0.2)" },
19711 { name: "--wpd-ribbon-padding", default: "4px 0" },
19712 { name: "--wpd-ribbon-font", default: "700 10px/1.4 system-ui" },
19713 { name: "--wpd-ribbon-tracking", default: "0.06em" },
19714 { name: "--wpd-ribbon-z", default: "2" }
19715 ],
19716 example: html`
19717 <div
19718 style="position: relative; width: 240px; height: 120px;
19719 border: 1px solid #ccc; border-radius: 8px;
19720 padding: 16px; box-sizing: border-box;"
19721 >
19722 <wpd-ribbon>Featured</wpd-ribbon>
19723 Card body…
19724 </div>
19725 `
19726 };
19727 let WpdRibbon = _WpdRibbon;
19728 defineComponent("wpd-ribbon", WpdRibbon);
19729 const TILE_CLASS = "desktop-mode-file-tile";
19730 const STATUS_LABEL = {
19731 draft: "Draft",
19732 pending: "Pending",
19733 private: "Private",
19734 future: "Scheduled"
19735 };
19736 function statusRibbonsEnabled() {
19737 const get2 = window.wp?.desktop?.getOsSettings;
19738 if (typeof get2 !== "function") {
19739 return true;
19740 }
19741 try {
19742 return get2()?.showPostStatusRibbons !== false;
19743 } catch {
19744 return true;
19745 }
19746 }
19747 function getDragManager$1() {
19748 const api = window.wp?.desktop?.dragManager;
19749 return api ?? null;
19750 }
19751 const REACTIVE_PROPS = [
19752 "type",
19753 "ref",
19754 "label",
19755 "icon",
19756 "thumbnail",
19757 "kind",
19758 "status",
19759 "selected",
19760 "missing",
19761 "access-gated",
19762 "drag-kind",
19763 "drag-title",
19764 "drag-icon"
19765 ];
19766 const _WpdTile = class _WpdTile extends Component {
19767 constructor() {
19768 super(...arguments);
19769 this._pointerdownHandler = null;
19770 this._keydownHandler = null;
19771 }
19772 connectedCallback() {
19773 super.connectedCallback();
19774 if (!this._keydownHandler) {
19775 this._keydownHandler = (e) => {
19776 if (e.key === "Enter" || e.key === " ") {
19777 e.preventDefault();
19778 this.click();
19779 }
19780 };
19781 this.addEventListener("keydown", this._keydownHandler);
19782 }
19783 this._paint();
19784 }
19785 disconnectedCallback() {
19786 if (this._pointerdownHandler) {
19787 this.removeEventListener(
19788 "pointerdown",
19789 this._pointerdownHandler
19790 );
19791 this._pointerdownHandler = null;
19792 }
19793 if (this._keydownHandler) {
19794 this.removeEventListener(
19795 "keydown",
19796 this._keydownHandler
19797 );
19798 this._keydownHandler = null;
19799 }
19800 }
19801 /**
19802 * Bypass the templated render loop. Lit-html's `render(template,
19803 * root)` would wipe the host's light-DOM children every tick —
19804 * including the visual / label / ribbon `_paint()` just
19805 * inserted. We override `requestUpdate` directly so attribute
19806 * changes call `_paint` (idempotent) without lit-html getting
19807 * involved.
19808 */
19809 requestUpdate() {
19810 if (!this.isConnected) {
19811 return;
19812 }
19813 this._paint();
19814 }
19815 render() {
19816 return html``;
19817 }
19818 _paint() {
19819 const type = this.getAttribute("type") ?? "";
19820 const ref = this.getAttribute("ref") ?? "";
19821 const label = this.getAttribute("label") ?? "";
19822 const icon = this.getAttribute("icon") ?? "";
19823 const thumbnail = this.getAttribute("thumbnail") ?? "";
19824 const kind = this.getAttribute("kind") ?? "entry";
19825 const status = this.getAttribute("status") ?? "";
19826 const selected = this.hasAttribute("selected");
19827 const missing = this.hasAttribute("missing");
19828 const accessGated = this.hasAttribute("access-gated");
19829 const ownedClasses = [
19830 TILE_CLASS,
19831 `${TILE_CLASS}--folder`,
19832 `${TILE_CLASS}--missing`,
19833 `${TILE_CLASS}--access-gated`,
19834 `${TILE_CLASS}--selected`
19835 ];
19836 for (const c of ownedClasses) {
19837 this.classList.remove(c);
19838 }
19839 this.classList.add(TILE_CLASS);
19840 if (kind === "folder") {
19841 this.classList.add(`${TILE_CLASS}--folder`);
19842 }
19843 if (missing) {
19844 this.classList.add(`${TILE_CLASS}--missing`);
19845 }
19846 if (accessGated) {
19847 this.classList.add(`${TILE_CLASS}--access-gated`);
19848 }
19849 if (selected) {
19850 this.classList.add(`${TILE_CLASS}--selected`);
19851 }
19852 this.dataset.fileType = type;
19853 this.dataset.fileRef = ref;
19854 if (kind) {
19855 this.dataset.role = kind;
19856 }
19857 this.setAttribute("role", "listitem");
19858 this.setAttribute("aria-label", label);
19859 if (!this.hasAttribute("tabindex")) {
19860 this.setAttribute("tabindex", "0");
19861 }
19862 const accessGatedTitle = "You don’t have permission to open this — ask the folder owner for access.";
19863 if (accessGated) {
19864 this.title = accessGatedTitle;
19865 this.setAttribute("aria-disabled", "true");
19866 } else {
19867 this.removeAttribute("aria-disabled");
19868 if (this.title === accessGatedTitle) {
19869 this.removeAttribute("title");
19870 }
19871 }
19872 const SLOTS = [
19873 `${TILE_CLASS}__visual`,
19874 `${TILE_CLASS}__label`,
19875 `${TILE_CLASS}__lock`
19876 ];
19877 for (const cls of SLOTS) {
19878 this.querySelectorAll(`:scope > .${cls}`).forEach(
19879 (n) => n.remove()
19880 );
19881 }
19882 this.querySelectorAll(":scope > wpd-ribbon").forEach(
19883 (n) => n.remove()
19884 );
19885 const visual = document.createElement("span");
19886 visual.className = `${TILE_CLASS}__visual`;
19887 if (thumbnail) {
19888 const img = document.createElement("img");
19889 img.src = thumbnail;
19890 img.alt = "";
19891 img.loading = "lazy";
19892 img.decoding = "async";
19893 img.className = `${TILE_CLASS}__preview`;
19894 img.draggable = false;
19895 visual.appendChild(img);
19896 } else if (icon) {
19897 const iconNode = renderIcon(icon, {
19898 title: label,
19899 className: `${TILE_CLASS}__icon`
19900 });
19901 visual.appendChild(iconNode);
19902 }
19903 this.appendChild(visual);
19904 const labelNode = document.createElement("span");
19905 labelNode.className = `${TILE_CLASS}__label`;
19906 labelNode.textContent = label;
19907 this.appendChild(labelNode);
19908 if (accessGated) {
19909 const lock = document.createElement("span");
19910 lock.className = `${TILE_CLASS}__lock dashicons dashicons-lock`;
19911 lock.setAttribute("aria-hidden", "true");
19912 this.appendChild(lock);
19913 }
19914 if (status && status !== "publish" && STATUS_LABEL[status] && statusRibbonsEnabled()) {
19915 const ribbon = document.createElement("wpd-ribbon");
19916 ribbon.setAttribute("placement", "top-end");
19917 ribbon.setAttribute("tone", ribbonToneFor(status));
19918 ribbon.textContent = STATUS_LABEL[status];
19919 this.appendChild(ribbon);
19920 }
19921 applyTileEntryStagger(this);
19922 doAction("desktop-mode.tile.rendered", { tile: this });
19923 this._wireDragOut();
19924 }
19925 _wireDragOut() {
19926 if (this._pointerdownHandler) {
19927 this.removeEventListener(
19928 "pointerdown",
19929 this._pointerdownHandler
19930 );
19931 this._pointerdownHandler = null;
19932 }
19933 const dragKind = this.getAttribute("drag-kind");
19934 if (!dragKind) {
19935 return;
19936 }
19937 const handler = (e) => {
19938 if (e.button !== 0) {
19939 return;
19940 }
19941 const dragManager = getDragManager$1();
19942 if (!dragManager) {
19943 return;
19944 }
19945 const ref = this.getAttribute("ref") ?? "";
19946 const title = this.getAttribute("drag-title") ?? this.getAttribute("label") ?? void 0;
19947 const icon = this.getAttribute("drag-icon") ?? this.getAttribute("icon") ?? void 0;
19948 const rect = this.getBoundingClientRect();
19949 dragManager.start({
19950 payload: {
19951 type: "shortcut",
19952 source: this,
19953 data: {
19954 kind: dragKind,
19955 ref,
19956 title,
19957 icon
19958 },
19959 ghost: {
19960 offsetX: e.clientX - rect.left,
19961 offsetY: e.clientY - rect.top
19962 }
19963 },
19964 origin: e
19965 });
19966 };
19967 this._pointerdownHandler = handler;
19968 this.addEventListener("pointerdown", handler);
19969 }
19970 };
19971 _WpdTile.shadow = false;
19972 _WpdTile.props = REACTIVE_PROPS;
19973 _WpdTile.styles = [styles$5];
19974 _WpdTile.help = {
19975 title: "Tile",
19976 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.",
19977 status: "experimental",
19978 since: "0.21.0",
19979 props: [
19980 { name: "type", type: "string" },
19981 { name: "ref", type: "string" },
19982 { name: "label", type: "string" },
19983 { name: "icon", type: "string", description: "Dashicon class / URL / data URI. Ignored when `thumbnail` is set." },
19984 { name: "thumbnail", type: "string", description: "Preview image URL. Renders as `<img>` and wins over `icon`." },
19985 { name: "kind", type: "`entry` | `folder`" },
19986 { name: "status", type: "`draft` | `pending` | `private` | `future` | `publish`" },
19987 { name: "selected", type: "boolean" },
19988 { name: "missing", type: "boolean" },
19989 { name: "access-gated", type: "boolean" },
19990 { name: "drag-kind", type: "string", description: "When set, the component wires pointerdown → DragManager." },
19991 { name: "drag-title", type: "string" },
19992 { name: "drag-icon", type: "string" }
19993 ]
19994 };
19995 let WpdTile = _WpdTile;
19996 function ribbonToneFor(status) {
19997 switch (status) {
19998 case "draft":
19999 return "warning";
20000 case "pending":
20001 return "info";
20002 case "private":
20003 return "danger";
20004 case "future":
20005 return "primary";
20006 default:
20007 return "primary";
20008 }
20009 }
20010 defineComponent("wpd-tile", WpdTile);
20011 function buildTileFromSpec(spec) {
20012 const tile2 = document.createElement("wpd-tile");
20013 tile2.setAttribute("type", spec.type);
20014 tile2.setAttribute("ref", spec.ref);
20015 tile2.setAttribute("label", spec.label);
20016 if (spec.icon) {
20017 tile2.setAttribute("icon", spec.icon);
20018 }
20019 if (spec.thumbnail) {
20020 tile2.setAttribute("thumbnail", spec.thumbnail);
20021 }
20022 if (spec.role) {
20023 tile2.setAttribute("kind", spec.role);
20024 }
20025 if (spec.status) {
20026 tile2.setAttribute("status", spec.status);
20027 }
20028 if (spec.missing) {
20029 tile2.setAttribute("missing", "");
20030 }
20031 if (spec.accessGated) {
20032 tile2.setAttribute("access-gated", "");
20033 }
20034 if (spec.dataset) {
20035 for (const [key, raw] of Object.entries(spec.dataset)) {
20036 if (raw === void 0 || raw === null) {
20037 continue;
20038 }
20039 tile2.dataset[key] = String(raw);
20040 }
20041 }
20042 if (Array.isArray(spec.extraClasses)) {
20043 for (const c of spec.extraClasses) {
20044 if (c) {
20045 tile2.classList.add(c);
20046 }
20047 }
20048 }
20049 const classFiltered = applyFilters(
20050 "desktop-mode.tile.class",
20051 tile2.className,
20052 spec
20053 );
20054 if (classFiltered && classFiltered !== tile2.className) {
20055 tile2.className = classFiltered;
20056 }
20057 if (typeof spec.x === "number" && typeof spec.y === "number") {
20058 tile2.style.position = "absolute";
20059 tile2.style.left = `${spec.x}px`;
20060 tile2.style.top = `${spec.y}px`;
20061 }
20062 return tile2;
20063 }
20064 function placementToSpec(placement, folderId) {
20065 const file = resolve(placement.file);
20066 const previewUrl = file.previewUrl();
20067 const metaName = placement.meta && typeof placement.meta.name === "string" ? placement.meta.name.trim() : "";
20068 const label = metaName !== "" ? metaName : file.title();
20069 const metaIconUrl = placement.meta && typeof placement.meta.iconUrl === "string" ? placement.meta.iconUrl.trim() : "";
20070 return {
20071 type: placement.file.type,
20072 ref: placement.file.ref,
20073 label,
20074 // Preview wins over icon (matches the previous behavior).
20075 thumbnail: previewUrl || void 0,
20076 icon: previewUrl ? void 0 : metaIconUrl || file.icon(),
20077 x: placement.x,
20078 y: placement.y,
20079 dataset: {
20080 placementId: placement.id,
20081 folderId
20082 },
20083 meta: placement.meta,
20084 missing: !placement.file.exists,
20085 accessGated: Boolean(placement.accessGated),
20086 ariaLabel: label
20087 };
20088 }
20089 function buildTile(placement, folderId) {
20090 const file = resolve(placement.file);
20091 const tile2 = buildTileFromSpec(placementToSpec(placement, folderId));
20092 const classFiltered = applyFilters(
20093 "desktop-mode.files.tile-class",
20094 TILE_CLASS,
20095 placement
20096 );
20097 if (classFiltered && classFiltered !== TILE_CLASS) {
20098 tile2.className = classFiltered;
20099 }
20100 const extra = applyFilters(
20101 "desktop-mode.files.tile-element",
20102 null,
20103 placement
20104 );
20105 if (extra instanceof Element) {
20106 tile2.appendChild(extra);
20107 }
20108 tile2.addEventListener("dblclick", (e) => {
20109 e.preventDefault();
20110 e.stopPropagation();
20111 if (placement.accessGated) {
20112 showToast({
20113 message: `You don’t have permission to open "${placement.file.title || file.title()}". Ask the folder owner if you need access to this item.`,
20114 duration: 6e3
20115 });
20116 return;
20117 }
20118 void openFile(file, {
20119 placement: {
20120 id: placement.id,
20121 x: placement.x,
20122 y: placement.y,
20123 meta: placement.meta
20124 }
20125 });
20126 });
20127 doAction("desktop-mode.files.tile-rendered", { tile: tile2, placement });
20128 return tile2;
20129 }
20130 function setTilePosition(tile2, x, y) {
20131 tile2.style.left = `${x}px`;
20132 tile2.style.top = `${y}px`;
20133 }
20134 function attachDismissable(host, options) {
20135 const onAway = (e) => {
20136 if (e.target instanceof Node && host.contains(e.target)) {
20137 return;
20138 }
20139 if (e.target instanceof Node) {
20140 for (const sel of options.siblingSelectors ?? []) {
20141 const matches = Array.from(
20142 document.querySelectorAll(sel)
20143 );
20144 for (const m of matches) {
20145 if (m.contains(e.target)) {
20146 return;
20147 }
20148 }
20149 }
20150 }
20151 if (options.excludeOutsideTarget && e.target instanceof Node && options.excludeOutsideTarget.contains(e.target)) {
20152 return;
20153 }
20154 options.close();
20155 };
20156 const onKey = (e) => {
20157 if (e.key === "Escape") {
20158 options.close();
20159 }
20160 };
20161 document.addEventListener("mousedown", onAway, { capture: true });
20162 document.addEventListener("keydown", onKey);
20163 return () => {
20164 document.removeEventListener("mousedown", onAway, { capture: true });
20165 document.removeEventListener("keydown", onKey);
20166 };
20167 }
20168 const MENU_CLASS$2 = "desktop-mode-wallpaper-menu";
20169 let activeMenu$2 = null;
20170 function closeTileMenu() {
20171 if (!activeMenu$2) {
20172 return;
20173 }
20174 activeMenu$2.dispatchEvent(new CustomEvent("tile-menu-closed"));
20175 activeMenu$2.remove();
20176 activeMenu$2 = null;
20177 doAction("desktop-mode.files.tile-menu.closed", {});
20178 }
20179 let openGeneration$1 = 0;
20180 function openTileMenu(pos, opts) {
20181 closeTileMenu();
20182 const myGen = ++openGeneration$1;
20183 openWithShellOverlays(
20184 () => myGen === openGeneration$1,
20185 () => openTileMenuImmediate(pos, opts)
20186 );
20187 }
20188 function openTileMenuImmediate(pos, { placement, items }) {
20189 const list2 = applyFilters(
20190 "desktop-mode.files.tile-menu",
20191 items.slice(),
20192 placement
20193 );
20194 const sorted = (Array.isArray(list2) ? list2 : items).slice().sort((a, b) => {
20195 const sa = typeof a.sort === "number" ? a.sort : 100;
20196 const sb = typeof b.sort === "number" ? b.sort : 100;
20197 if (sa !== sb) {
20198 return sa - sb;
20199 }
20200 return a.label.localeCompare(b.label);
20201 });
20202 if (sorted.length === 0) {
20203 return;
20204 }
20205 const menu = document.createElement("wpd-context-menu");
20206 menu.setAttribute("open", "");
20207 menu.classList.add(MENU_CLASS$2);
20208 menu.dataset.placementId = String(placement.id);
20209 menu.style.left = `${pos.x}px`;
20210 menu.style.top = `${pos.y}px`;
20211 const itemById = /* @__PURE__ */ new Map();
20212 for (const item of sorted) {
20213 itemById.set(item.id, item);
20214 const opt = document.createElement("wpd-context-menu-option");
20215 opt.dataset.menuItemId = item.id;
20216 opt.setAttribute("value", item.id);
20217 if (item.danger) {
20218 opt.setAttribute("danger", "");
20219 }
20220 if (item.disabled) {
20221 opt.setAttribute("disabled", "");
20222 }
20223 if (item.icon) {
20224 opt.setAttribute("icon", sanitizeClass$2(item.icon));
20225 }
20226 opt.textContent = item.label;
20227 menu.appendChild(opt);
20228 }
20229 menu.addEventListener("wpd-context-menu-pick", (e) => {
20230 const detail = e.detail;
20231 const item = itemById.get(detail.id);
20232 if (!item) {
20233 return;
20234 }
20235 closeTileMenu();
20236 void item.onClick(new MouseEvent("click"));
20237 });
20238 document.body.appendChild(menu);
20239 activeMenu$2 = menu;
20240 const rect = menu.getBoundingClientRect();
20241 if (rect.right > window.innerWidth) {
20242 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
20243 }
20244 if (rect.bottom > window.innerHeight) {
20245 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
20246 }
20247 const detach = attachDismissable(menu, {
20248 close: () => closeTileMenu()
20249 });
20250 menu.addEventListener("tile-menu-closed", detach);
20251 doAction("desktop-mode.files.tile-menu.opened", {
20252 placementId: placement.id,
20253 items: sorted.map((i) => i.id)
20254 });
20255 }
20256 function sanitizeClass$2(raw) {
20257 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
20258 }
20259 const ROOT_CLASS$3 = "desktop-mode-create-folder-dialog";
20260 let active$1 = null;
20261 function closeCreateFolderDialog() {
20262 if (!active$1) {
20263 return;
20264 }
20265 active$1.dispatchEvent(new CustomEvent("create-folder-dialog-closed"));
20266 active$1.remove();
20267 active$1 = null;
20268 doAction("desktop-mode.files.create-folder.closed", {});
20269 }
20270 function openCreateFolderDialog(options) {
20271 closeCreateFolderDialog();
20272 const decision = applyFilters(
20273 "desktop-mode.files.create-folder.dialog",
20274 null,
20275 options
20276 );
20277 if (decision === false) {
20278 return;
20279 }
20280 const initial = (options.initialName ?? "Untitled folder").trim();
20281 const overlay = document.createElement("div");
20282 overlay.className = `${ROOT_CLASS$3}__overlay`;
20283 overlay.setAttribute("role", "presentation");
20284 const dialog2 = document.createElement("div");
20285 dialog2.className = ROOT_CLASS$3;
20286 dialog2.setAttribute("role", "dialog");
20287 dialog2.setAttribute("aria-modal", "true");
20288 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS$3}-title`);
20289 const title = document.createElement("h2");
20290 title.id = `${ROOT_CLASS$3}-title`;
20291 title.className = `${ROOT_CLASS$3}__title`;
20292 title.textContent = options.title ?? "New folder";
20293 dialog2.appendChild(title);
20294 const label = document.createElement("label");
20295 label.className = `${ROOT_CLASS$3}__label`;
20296 label.htmlFor = `${ROOT_CLASS$3}-input`;
20297 label.textContent = options.label ?? "Folder name";
20298 dialog2.appendChild(label);
20299 const input = document.createElement("input");
20300 input.type = "text";
20301 input.id = `${ROOT_CLASS$3}-input`;
20302 input.className = `${ROOT_CLASS$3}__input`;
20303 input.value = initial;
20304 input.setAttribute("autocomplete", "off");
20305 input.setAttribute("spellcheck", "false");
20306 dialog2.appendChild(input);
20307 const error = document.createElement("p");
20308 error.className = `${ROOT_CLASS$3}__error`;
20309 error.hidden = true;
20310 error.setAttribute("role", "alert");
20311 dialog2.appendChild(error);
20312 const actions = document.createElement("div");
20313 actions.className = `${ROOT_CLASS$3}__actions`;
20314 const cancel = document.createElement("button");
20315 cancel.type = "button";
20316 cancel.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--secondary`;
20317 cancel.textContent = "Cancel";
20318 const submit = document.createElement("button");
20319 submit.type = "button";
20320 submit.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--primary`;
20321 submit.textContent = options.submitLabel ?? "Create";
20322 actions.appendChild(cancel);
20323 actions.appendChild(submit);
20324 dialog2.appendChild(actions);
20325 overlay.appendChild(dialog2);
20326 document.body.appendChild(overlay);
20327 active$1 = overlay;
20328 input.focus();
20329 input.select();
20330 doAction("desktop-mode.files.create-folder.opened", {});
20331 const setBusy = (busy) => {
20332 input.disabled = busy;
20333 cancel.disabled = busy;
20334 submit.disabled = busy;
20335 dialog2.classList.toggle(`${ROOT_CLASS$3}--busy`, busy);
20336 };
20337 const showError = (msg) => {
20338 error.textContent = msg;
20339 error.hidden = false;
20340 };
20341 const doCancel = () => {
20342 closeCreateFolderDialog();
20343 options.onCancel?.();
20344 };
20345 const doSubmit = async () => {
20346 const name = input.value.trim();
20347 if (!name) {
20348 showError("Please enter a name.");
20349 input.focus();
20350 return;
20351 }
20352 error.hidden = true;
20353 setBusy(true);
20354 try {
20355 await options.onSubmit(name);
20356 closeCreateFolderDialog();
20357 } catch (err) {
20358 setBusy(false);
20359 showError(
20360 err instanceof Error ? err.message : "Could not create the folder."
20361 );
20362 input.focus();
20363 input.select();
20364 }
20365 };
20366 cancel.addEventListener("click", () => doCancel());
20367 submit.addEventListener("click", () => void doSubmit());
20368 overlay.addEventListener("click", (e) => {
20369 if (e.target === overlay) {
20370 doCancel();
20371 }
20372 });
20373 const onKey = (e) => {
20374 if (e.key === "Escape") {
20375 e.preventDefault();
20376 doCancel();
20377 } else if (e.key === "Enter" && !e.isComposing) {
20378 e.preventDefault();
20379 void doSubmit();
20380 }
20381 };
20382 dialog2.addEventListener("keydown", onKey);
20383 overlay.addEventListener("create-folder-dialog-closed", () => {
20384 dialog2.removeEventListener("keydown", onKey);
20385 });
20386 }
20387 const GRID_PADDING = 16;
20388 const GRID_CELL_W = 96;
20389 const GRID_CELL_H = 110;
20390 function pointToCell(x, y) {
20391 const col = Math.max(0, Math.round((x - GRID_PADDING) / GRID_CELL_W));
20392 const row = Math.max(0, Math.round((y - GRID_PADDING) / GRID_CELL_H));
20393 return cellToPos(col, row);
20394 }
20395 function cellToPos(col, row) {
20396 return {
20397 col,
20398 row,
20399 x: GRID_PADDING + col * GRID_CELL_W,
20400 y: GRID_PADDING + row * GRID_CELL_H
20401 };
20402 }
20403 function snapToEmptyCell(x, y, occupied, host) {
20404 const target2 = pointToCell(x, y);
20405 if (!occupied.has(cellKey(target2.col, target2.row))) {
20406 return target2;
20407 }
20408 const maxRows = host ? Math.max(1, Math.floor((host.clientHeight - GRID_PADDING) / GRID_CELL_H)) : 999;
20409 for (let col = 0; col < 999; col++) {
20410 for (let row = 0; row < maxRows; row++) {
20411 if (!occupied.has(cellKey(col, row))) {
20412 return cellToPos(col, row);
20413 }
20414 }
20415 }
20416 return target2;
20417 }
20418 function nextRowMajorCell(occupied, host) {
20419 const cols = host ? Math.max(
20420 1,
20421 Math.floor((host.clientWidth - GRID_PADDING) / GRID_CELL_W)
20422 ) : 4;
20423 const maxCols = Math.max(1, cols);
20424 for (let row = 0; row < 999; row++) {
20425 for (let col = 0; col < maxCols; col++) {
20426 if (!occupied.has(cellKey(col, row))) {
20427 return cellToPos(col, row);
20428 }
20429 }
20430 }
20431 return cellToPos(0, 0);
20432 }
20433 function buildOccupiedSet(placements, excludeId) {
20434 const out = /* @__PURE__ */ new Set();
20435 for (const p of placements) {
20436 const cell = pointToCell(p.x, p.y);
20437 out.add(cellKey(cell.col, cell.row));
20438 }
20439 return out;
20440 }
20441 function cellKey(col, row) {
20442 return `${col},${row}`;
20443 }
20444 function isConflict(err) {
20445 return err instanceof FilesConflictError;
20446 }
20447 function buildReason(err) {
20448 const actor = err.detail.actor.name || "Someone else";
20449 const where = err.detail.current.parentName || "another folder";
20450 if (err.detail.reason === "trashed") {
20451 return "This item is in the recycle bin.";
20452 }
20453 if (err.detail.reason === "forbidden") {
20454 return "You no longer have access.";
20455 }
20456 if (err.detail.reason === "gone") {
20457 return "This item was deleted.";
20458 }
20459 return `${actor} moved this to "${where}".`;
20460 }
20461 function showConflictToast(err) {
20462 const reason = buildReason(err);
20463 const targetParentId = err.detail.current.parentId;
20464 let action;
20465 if (targetParentId > 0) {
20466 action = {
20467 label: "View folder",
20468 onClick: () => {
20469 const winId = `desktop-mode-folder-${targetParentId}`;
20470 const mgr = window.desktopMode?.windowManager;
20471 if (mgr?.focus) {
20472 const w = mgr.focus(winId);
20473 if (w) {
20474 return;
20475 }
20476 }
20477 if (mgr?.open) {
20478 void mgr.open(winId);
20479 }
20480 }
20481 };
20482 }
20483 showToast({
20484 message: reason,
20485 action,
20486 duration: 7e3
20487 });
20488 }
20489 function broadcastFilesChange(kind, action, ids) {
20490 const api = window.wp?.desktop;
20491 api?.broadcast?.(`desktop-mode.${kind}.changed`, {
20492 source: "desktop-files",
20493 action,
20494 ids
20495 });
20496 }
20497 function showTrashErrorToast(err) {
20498 const api = window.wp?.desktop;
20499 if (!api?.showToast) {
20500 return;
20501 }
20502 const raw = err instanceof Error ? err.message : String(err);
20503 const friendly = raw.replace(/^\[desktop-mode\][^:]*:\s*/, "").replace(/^desktop_mode_files_[a-z_]+\s*/, "");
20504 api.showToast({
20505 message: friendly || "Could not move this item to the recycle bin.",
20506 duration: 5e3
20507 });
20508 }
20509 function showTrashedToast(message, onUndo) {
20510 const api = window.wp?.desktop;
20511 if (!api?.showToast) {
20512 return;
20513 }
20514 api.showToast({
20515 message,
20516 duration: 6e3,
20517 action: {
20518 label: "Undo",
20519 onClick: onUndo
20520 }
20521 });
20522 }
20523 async function trashPlacementWithUndo(placement) {
20524 const placementId = placement.id;
20525 const parentId = placement.parentId;
20526 const title = placement.file?.title ?? "Item";
20527 const kind = placement.file?.type === "shortcut" ? "shortcut" : "placement";
20528 store.removePlacement(placementId);
20529 try {
20530 await deletePlacement(placementId);
20531 broadcastFilesChange(kind, "trashed", [placementId]);
20532 showTrashedToast(`"${title}" moved to Trash`, async () => {
20533 try {
20534 await restoreTrashedItem(placementId, "placement");
20535 const res = await listPlacements(parentId);
20536 store.setFolderPlacements(parentId, res.placements);
20537 broadcastFilesChange(kind, "untrashed", [placementId]);
20538 } catch (err) {
20539 console.error("[desktop-mode] restore failed:", err);
20540 }
20541 });
20542 } catch (err) {
20543 console.error("[desktop-mode] deletePlacement failed:", err);
20544 showTrashErrorToast(err);
20545 void listPlacements(parentId).then((res) => {
20546 store.setFolderPlacements(parentId, res.placements);
20547 });
20548 }
20549 }
20550 async function trashFolderWithUndo(placement) {
20551 const folderId = parseInt(placement.file.ref, 10);
20552 if (!folderId) {
20553 return;
20554 }
20555 const placementId = placement.id;
20556 const parentId = placement.parentId;
20557 const title = placement.file?.title ?? "Folder";
20558 store.removePlacement(placementId);
20559 store.removeFolder(folderId);
20560 try {
20561 await deleteFolder(folderId);
20562 broadcastFilesChange("folder", "trashed", [folderId]);
20563 showTrashedToast(`"${title}" moved to Trash`, async () => {
20564 try {
20565 await restoreTrashedItem(folderId, "folder");
20566 const res = await listPlacements(parentId);
20567 store.setFolderPlacements(parentId, res.placements);
20568 broadcastFilesChange("folder", "untrashed", [folderId]);
20569 } catch (err) {
20570 console.error("[desktop-mode] restore folder failed:", err);
20571 }
20572 });
20573 } catch (err) {
20574 console.error("[desktop-mode] deleteFolder failed:", err);
20575 showTrashErrorToast(err);
20576 void listPlacements(parentId).then((res) => {
20577 store.setFolderPlacements(parentId, res.placements);
20578 });
20579 }
20580 }
20581 function trashByFileType(placement) {
20582 if (placement.file?.type === "folder") {
20583 return trashFolderWithUndo(placement);
20584 }
20585 return trashPlacementWithUndo(placement);
20586 }
20587 function buildBridgePayloadFromPlacement(placement) {
20588 const file = placement.file;
20589 if (!file) {
20590 return void 0;
20591 }
20592 const id = parseInt(String(file.ref ?? ""), 10);
20593 if (!Number.isFinite(id) || id <= 0) {
20594 return void 0;
20595 }
20596 const title = String(file.title ?? "");
20597 if (file.type === "attachment") {
20598 const url = String(file.sourceUrl ?? file.previewUrl ?? "");
20599 return {
20600 kind: "attachment",
20601 id,
20602 url,
20603 title,
20604 alt: String(file.alt ?? ""),
20605 mime: String(file.mime ?? ""),
20606 thumbnailUrl: file.previewUrl ? String(file.previewUrl) : void 0
20607 };
20608 }
20609 if (file.type === "post") {
20610 return {
20611 kind: "post",
20612 id,
20613 postType: String(file.postType ?? "post"),
20614 url: String(file.link ?? ""),
20615 title
20616 };
20617 }
20618 if (file.type === "user") {
20619 return {
20620 kind: "user",
20621 id,
20622 url: String(file.link ?? ""),
20623 title
20624 };
20625 }
20626 return void 0;
20627 }
20628 function getDragManager() {
20629 const api = window.wp?.desktop?.dragManager;
20630 return api ?? null;
20631 }
20632 const LAYER_CLASS = "desktop-mode-files-layer";
20633 function mountFilesLayer(host, folderId = 0) {
20634 const container = document.createElement("div");
20635 container.className = LAYER_CLASS;
20636 container.setAttribute("role", "list");
20637 container.dataset.folderId = String(folderId);
20638 host.appendChild(container);
20639 let lastFingerprint = "";
20640 let selectedId = null;
20641 const selectionListeners = /* @__PURE__ */ new Set();
20642 const notifySelection = (placement) => {
20643 for (const cb of selectionListeners) {
20644 try {
20645 cb(placement);
20646 } catch (err) {
20647 console.error(
20648 "[desktop-mode] files: selection listener threw:",
20649 err
20650 );
20651 }
20652 }
20653 };
20654 const setSelected = (placement) => {
20655 const newId = placement ? placement.id : null;
20656 if (newId === selectedId) {
20657 return;
20658 }
20659 container.querySelectorAll(`.${TILE_CLASS}--selected`).forEach((n) => n.removeAttribute("selected"));
20660 if (placement) {
20661 const tile2 = container.querySelector(
20662 `[data-placement-id="${placement.id}"]`
20663 );
20664 tile2?.setAttribute("selected", "");
20665 }
20666 selectedId = newId;
20667 notifySelection(placement);
20668 };
20669 const computeLayout = (list2) => {
20670 const pinnedSlots = /* @__PURE__ */ new Map();
20671 const occupiedCells = /* @__PURE__ */ new Set();
20672 let pinnedIdx = 0;
20673 for (const placement of list2) {
20674 if (!isPinned(placement)) {
20675 continue;
20676 }
20677 const slot = cellToPos(0, pinnedIdx);
20678 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
20679 occupiedCells.add(cellKey(slot.col, slot.row));
20680 pinnedIdx += 1;
20681 }
20682 const displaced = /* @__PURE__ */ new Map();
20683 for (const placement of list2) {
20684 if (pinnedSlots.has(placement.id)) {
20685 continue;
20686 }
20687 const target2 = pointToCell(placement.x, placement.y);
20688 const key = cellKey(target2.col, target2.row);
20689 if (!occupiedCells.has(key)) {
20690 occupiedCells.add(key);
20691 continue;
20692 }
20693 const free = snapToEmptyCell(
20694 placement.x,
20695 placement.y,
20696 occupiedCells,
20697 host
20698 );
20699 occupiedCells.add(cellKey(free.col, free.row));
20700 displaced.set(placement.id, { x: free.x, y: free.y });
20701 }
20702 return { pinnedSlots, displaced };
20703 };
20704 const applyTilePosition = (tile2, placement, pinnedSlots, displaced) => {
20705 const pinned = pinnedSlots.get(placement.id);
20706 const moved = displaced.get(placement.id);
20707 if (pinned) {
20708 setTilePosition(tile2, pinned.x, pinned.y);
20709 } else if (moved) {
20710 setTilePosition(tile2, moved.x, moved.y);
20711 } else {
20712 setTilePosition(tile2, placement.x, placement.y);
20713 }
20714 };
20715 const wireTile = (placement, pinnedSlots, displaced) => {
20716 const tile2 = buildTile(placement, folderId);
20717 const pinnedSlot = pinnedSlots.get(placement.id);
20718 if (pinnedSlot) {
20719 setTilePosition(tile2, pinnedSlot.x, pinnedSlot.y);
20720 tile2.classList.add(`${TILE_CLASS}--pinned`);
20721 attachContextMenu(tile2, placement);
20722 attachSelectOnClick(tile2, placement);
20723 if (shouldRejectTileDrops(placement)) {
20724 const dragManager = getDragManager();
20725 if (dragManager) {
20726 const deregister = dragManager.registerDropTarget({
20727 id: `desktop-mode-files-tile-${placement.id}-reject`,
20728 element: tile2,
20729 accept: () => false,
20730 onDrop: () => {
20731 }
20732 });
20733 tileRejectDeregisters.set(placement.id, deregister);
20734 }
20735 }
20736 return tile2;
20737 }
20738 const moved = displaced.get(placement.id);
20739 if (moved) {
20740 setTilePosition(tile2, moved.x, moved.y);
20741 }
20742 attachTileDrag(tile2, placement, folderId);
20743 attachContextMenu(tile2, placement);
20744 attachSelectOnClick(tile2, placement);
20745 if (placement.file.type === "folder") {
20746 const targetFolderId = parseInt(placement.file.ref, 10);
20747 if (targetFolderId > 0) {
20748 const dragManager = getDragManager();
20749 if (dragManager) {
20750 const deregister = registerFolderDropTarget(
20751 dragManager,
20752 tile2,
20753 targetFolderId
20754 );
20755 folderDropDeregisters.set(placement.id, deregister);
20756 }
20757 }
20758 } else if (shouldRejectTileDrops(placement)) {
20759 const dragManager = getDragManager();
20760 if (dragManager) {
20761 const deregister = dragManager.registerDropTarget({
20762 id: `desktop-mode-files-tile-${placement.id}-reject`,
20763 element: tile2,
20764 accept: () => false,
20765 onDrop: () => {
20766 }
20767 });
20768 tileRejectDeregisters.set(placement.id, deregister);
20769 }
20770 }
20771 return tile2;
20772 };
20773 const tryPatchIncremental = (list2) => {
20774 const existing = /* @__PURE__ */ new Map();
20775 for (const tile2 of container.querySelectorAll(
20776 "[data-placement-id]"
20777 )) {
20778 const raw = tile2.dataset.placementId ?? "";
20779 const id = parseInt(raw, 10);
20780 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
20781 return false;
20782 }
20783 existing.set(id, tile2);
20784 }
20785 const wantIds = /* @__PURE__ */ new Set();
20786 for (const placement of list2) {
20787 wantIds.add(placement.id);
20788 }
20789 for (const placement of list2) {
20790 const tile2 = existing.get(placement.id);
20791 if (!tile2) {
20792 continue;
20793 }
20794 if (tile2.dataset.fileType !== placement.file.type) {
20795 return false;
20796 }
20797 if (tile2.dataset.fileRef !== placement.file.ref) {
20798 return false;
20799 }
20800 const wasPinned = tile2.classList.contains(
20801 `${TILE_CLASS}--pinned`
20802 );
20803 if (wasPinned !== isPinned(placement)) {
20804 return false;
20805 }
20806 }
20807 for (const [id, tile2] of existing) {
20808 if (wantIds.has(id)) {
20809 continue;
20810 }
20811 const folderDereg = folderDropDeregisters.get(id);
20812 if (folderDereg) {
20813 try {
20814 folderDereg();
20815 } catch {
20816 }
20817 folderDropDeregisters.delete(id);
20818 }
20819 const rejectDereg = tileRejectDeregisters.get(id);
20820 if (rejectDereg) {
20821 try {
20822 rejectDereg();
20823 } catch {
20824 }
20825 tileRejectDeregisters.delete(id);
20826 }
20827 tile2.remove();
20828 }
20829 const { pinnedSlots, displaced } = computeLayout(list2);
20830 for (const placement of list2) {
20831 const tile2 = existing.get(placement.id);
20832 if (tile2) {
20833 applyTilePosition(tile2, placement, pinnedSlots, displaced);
20834 continue;
20835 }
20836 container.appendChild(
20837 wireTile(placement, pinnedSlots, displaced)
20838 );
20839 }
20840 if (selectedId !== null && !container.querySelector(
20841 `[data-placement-id="${selectedId}"]`
20842 )) {
20843 selectedId = null;
20844 notifySelection(null);
20845 }
20846 doAction("desktop-mode.files.grid-rendered", {
20847 folderId,
20848 count: list2.length
20849 });
20850 return true;
20851 };
20852 const repaint = (state2) => {
20853 const raw = state2.placementsByFolder.get(folderId) ?? [];
20854 const list2 = raw.slice().sort((a, b) => {
20855 const ap = isPinned(a) ? 0 : 1;
20856 const bp = isPinned(b) ? 0 : 1;
20857 return ap - bp;
20858 });
20859 const fp = fingerprint(list2);
20860 if (fp === lastFingerprint) {
20861 return;
20862 }
20863 lastFingerprint = fp;
20864 if (tryPatchPositions(list2, container, host)) {
20865 return;
20866 }
20867 if (tryPatchIncremental(list2)) {
20868 return;
20869 }
20870 container.replaceChildren();
20871 for (const [, deregister] of folderDropDeregisters) {
20872 try {
20873 deregister();
20874 } catch {
20875 }
20876 }
20877 folderDropDeregisters.clear();
20878 for (const [, deregister] of tileRejectDeregisters) {
20879 try {
20880 deregister();
20881 } catch {
20882 }
20883 }
20884 tileRejectDeregisters.clear();
20885 const { pinnedSlots, displaced } = computeLayout(list2);
20886 for (const placement of list2) {
20887 container.appendChild(
20888 wireTile(placement, pinnedSlots, displaced)
20889 );
20890 }
20891 if (selectedId !== null && !container.querySelector(`[data-placement-id="${selectedId}"]`)) {
20892 selectedId = null;
20893 notifySelection(null);
20894 } else if (selectedId !== null) {
20895 const tile2 = container.querySelector(
20896 `[data-placement-id="${selectedId}"]`
20897 );
20898 tile2?.setAttribute("selected", "");
20899 }
20900 doAction("desktop-mode.files.grid-rendered", {
20901 folderId,
20902 count: list2.length
20903 });
20904 };
20905 const dropTargetDeregisters = [];
20906 const folderDropDeregisters = /* @__PURE__ */ new Map();
20907 const tileRejectDeregisters = /* @__PURE__ */ new Map();
20908 let dropPreviewEl = null;
20909 let dropPreviewMoveHandler = null;
20910 const installCanvasDropPreview = (session) => {
20911 if (dropPreviewEl) {
20912 return;
20913 }
20914 if (session.payload.type !== "desktop-file") {
20915 return;
20916 }
20917 const previewEl = document.createElement("div");
20918 previewEl.className = "desktop-mode-files-drop-preview";
20919 previewEl.setAttribute("aria-hidden", "true");
20920 container.appendChild(previewEl);
20921 dropPreviewEl = previewEl;
20922 const ghost = session.payload.ghost;
20923 const offsetX = ghost?.offsetX ?? 0;
20924 const offsetY = ghost?.offsetY ?? 0;
20925 const data = session.payload.data;
20926 const movingId = data?.placement?.id;
20927 const updatePreview = (clientX, clientY) => {
20928 const rect = container.getBoundingClientRect();
20929 const rawX = Math.max(0, clientX - rect.left - offsetX);
20930 const rawY = Math.max(0, clientY - rect.top - offsetY);
20931 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
20932 const occupied = buildVisualOccupiedSet(peers, movingId);
20933 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
20934 previewEl.style.transform = `translate3d(${cell.x}px, ${cell.y}px, 0)`;
20935 };
20936 const sourceRect = session.payload.source.getBoundingClientRect();
20937 updatePreview(
20938 sourceRect.left + offsetX,
20939 sourceRect.top + offsetY
20940 );
20941 const moveHandler = (ev) => {
20942 updatePreview(ev.clientX, ev.clientY);
20943 };
20944 document.addEventListener("pointermove", moveHandler);
20945 dropPreviewMoveHandler = moveHandler;
20946 };
20947 const teardownCanvasDropPreview = () => {
20948 if (dropPreviewMoveHandler) {
20949 document.removeEventListener("pointermove", dropPreviewMoveHandler);
20950 dropPreviewMoveHandler = null;
20951 }
20952 if (dropPreviewEl) {
20953 dropPreviewEl.remove();
20954 dropPreviewEl = null;
20955 }
20956 };
20957 const canvasDropTarget = {
20958 id: `desktop-mode-files-canvas-${folderId}`,
20959 element: host,
20960 accept: (payload) => {
20961 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
20962 return false;
20963 }
20964 if (folderId > 0 && payload.type === "desktop-file") {
20965 const data = payload.data;
20966 if (data.placement.file?.type === "folder") {
20967 const movingFolderId = parseInt(data.placement.file.ref, 10);
20968 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, folderId)) {
20969 return false;
20970 }
20971 }
20972 }
20973 return true;
20974 },
20975 onEnter: (session) => {
20976 host.setAttribute("data-files-drop-active", "");
20977 installCanvasDropPreview(session);
20978 },
20979 onLeave: () => {
20980 host.removeAttribute("data-files-drop-active");
20981 teardownCanvasDropPreview();
20982 },
20983 onDrop: (session, ev) => {
20984 host.removeAttribute("data-files-drop-active");
20985 teardownCanvasDropPreview();
20986 const rect = container.getBoundingClientRect();
20987 const ghost = session.payload.ghost;
20988 const offsetX = ghost?.offsetX ?? 0;
20989 const offsetY = ghost?.offsetY ?? 0;
20990 const rawX = Math.max(0, ev.clientX - rect.left - offsetX);
20991 const rawY = Math.max(0, ev.clientY - rect.top - offsetY);
20992 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
20993 if (session.payload.type === "desktop-file") {
20994 const data = session.payload.data;
20995 const occupied = buildVisualOccupiedSet(peers, data.placement.id);
20996 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
20997 const next = {
20998 ...data.placement,
20999 x: cell.x,
21000 y: cell.y,
21001 parentId: folderId
21002 };
21003 store.upsertPlacement(next);
21004 doAction("desktop-mode.files.tile-manually-placed", {
21005 folderId,
21006 placementId: data.placement.id
21007 });
21008 if (isSyntheticPlacement(data.placement)) {
21009 const dockItemId = readSynthSource(data.placement);
21010 if (dockItemId) {
21011 persistDockPromotedPosition(
21012 dockItemId,
21013 cell.x,
21014 cell.y
21015 );
21016 }
21017 return;
21018 }
21019 void updatePlacement(
21020 data.placement.id,
21021 {
21022 x: cell.x,
21023 y: cell.y,
21024 parentId: folderId
21025 },
21026 data.placement.updatedAtMs
21027 ).then((server) => {
21028 store.upsertPlacement(server, "remote");
21029 }).catch((err) => {
21030 if (isConflict(err)) {
21031 showConflictToast(err);
21032 } else {
21033 console.error(
21034 "[desktop-mode] files: drag persist failed",
21035 err
21036 );
21037 }
21038 store.upsertPlacement(data.placement);
21039 });
21040 return;
21041 }
21042 if (session.payload.type === "shortcut") {
21043 const data = session.payload.data;
21044 const occupied = buildVisualOccupiedSet(peers);
21045 const cell = nextRowMajorCell(occupied, host);
21046 void createPlacement({
21047 parentId: folderId,
21048 type: data.kind,
21049 ref: data.ref,
21050 x: cell.x,
21051 y: cell.y
21052 }).then((placement) => {
21053 store.upsertPlacement(placement);
21054 doAction("desktop-mode.files.shortcut-dropped", {
21055 folderId,
21056 placement
21057 });
21058 }).catch((err) => {
21059 console.error(
21060 "[desktop-mode] shortcut drop failed:",
21061 err
21062 );
21063 });
21064 }
21065 }
21066 };
21067 const dragManagerForLayer = getDragManager();
21068 if (dragManagerForLayer) {
21069 dropTargetDeregisters.push(
21070 dragManagerForLayer.registerDropTarget(canvasDropTarget)
21071 );
21072 }
21073 const onCanvasClick = (e) => {
21074 if (e.target instanceof Element && e.target.closest(`.${TILE_CLASS}`)) {
21075 return;
21076 }
21077 setSelected(null);
21078 };
21079 host.addEventListener("click", onCanvasClick);
21080 function attachSelectOnClick(tile2, placement) {
21081 tile2.addEventListener("click", (e) => {
21082 e.stopPropagation();
21083 setSelected(placement);
21084 });
21085 }
21086 repaint(store.getState());
21087 const off = store.subscribe(repaint);
21088 let resolveHydrated = () => void 0;
21089 const hydrated = new Promise((resolve2) => {
21090 resolveHydrated = resolve2;
21091 });
21092 if (!store.getState().hydratedFolders.has(folderId)) {
21093 void listPlacements(folderId).then((res) => {
21094 store.setFolderPlacements(folderId, res.placements);
21095 }).catch((err) => {
21096 console.error("[desktop-mode] files: failed to hydrate folder", folderId, err);
21097 }).finally(() => {
21098 resolveHydrated();
21099 });
21100 } else {
21101 queueMicrotask(resolveHydrated);
21102 }
21103 const colsForWidth = () => {
21104 const w = host.clientWidth > 0 ? host.clientWidth : 4 * GRID_CELL_W;
21105 return Math.max(1, Math.floor((w - GRID_PADDING) / GRID_CELL_W));
21106 };
21107 const sortPlacements = (list2, mode) => {
21108 const sorted = list2.slice();
21109 switch (mode) {
21110 case "name-asc":
21111 sorted.sort(
21112 (a, b) => a.file.title.localeCompare(b.file.title)
21113 );
21114 break;
21115 case "name-desc":
21116 sorted.sort(
21117 (a, b) => b.file.title.localeCompare(a.file.title)
21118 );
21119 break;
21120 case "date-asc":
21121 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
21122 break;
21123 case "date-desc":
21124 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
21125 break;
21126 }
21127 return sorted;
21128 };
21129 const sort = (mode) => {
21130 const live = store.getState().placementsByFolder.get(folderId);
21131 if (!live || live.length === 0) {
21132 return;
21133 }
21134 const pinned = live.filter((p) => isPinned(p));
21135 const draggable = live.filter((p) => !isPinned(p));
21136 const sorted = sortPlacements(draggable, mode);
21137 const cols = colsForWidth();
21138 const occupied = /* @__PURE__ */ new Set();
21139 for (let i = 0; i < pinned.length; i += 1) {
21140 occupied.add(cellKey(0, i));
21141 }
21142 let idx = 0;
21143 const nextCell = () => {
21144 while (true) {
21145 const row = Math.floor(idx / cols);
21146 const col = idx % cols;
21147 idx += 1;
21148 if (!occupied.has(cellKey(col, row))) {
21149 return { col, row };
21150 }
21151 }
21152 };
21153 sorted.forEach((p, i) => {
21154 const cell = nextCell();
21155 const x = GRID_PADDING + cell.col * GRID_CELL_W;
21156 const y = GRID_PADDING + cell.row * GRID_CELL_H;
21157 const next = {
21158 ...p,
21159 x,
21160 y,
21161 sortOrder: i
21162 };
21163 store.upsertPlacement(next);
21164 if (isSyntheticPlacement(p)) {
21165 return;
21166 }
21167 void updatePlacement(p.id, { x, y, sortOrder: i }).catch((err) => {
21168 console.error(
21169 "[desktop-mode] files: sort persist failed",
21170 err
21171 );
21172 });
21173 });
21174 };
21175 const reflow = () => {
21176 const live = store.getState().placementsByFolder.get(folderId);
21177 if (!live || live.length === 0) {
21178 return;
21179 }
21180 const w = host.clientWidth > 0 ? host.clientWidth : Infinity;
21181 const overflowing = live.some((p) => {
21182 const right = p.x + GRID_CELL_W;
21183 return right > w;
21184 });
21185 if (!overflowing) {
21186 return;
21187 }
21188 const cols = colsForWidth();
21189 const pinned = live.filter((p) => isPinned(p));
21190 const draggable = live.filter((p) => !isPinned(p));
21191 const occupied = /* @__PURE__ */ new Set();
21192 for (let i = 0; i < pinned.length; i += 1) {
21193 occupied.add(cellKey(0, i));
21194 }
21195 let idx = 0;
21196 const nextCell = () => {
21197 while (true) {
21198 const row = Math.floor(idx / cols);
21199 const col = idx % cols;
21200 idx += 1;
21201 if (!occupied.has(cellKey(col, row))) {
21202 return { col, row };
21203 }
21204 }
21205 };
21206 for (const p of draggable) {
21207 const cell = nextCell();
21208 const x = GRID_PADDING + cell.col * GRID_CELL_W;
21209 const y = GRID_PADDING + cell.row * GRID_CELL_H;
21210 const tile2 = container.querySelector(
21211 `[data-placement-id="${p.id}"]`
21212 );
21213 if (tile2) {
21214 setTilePosition(tile2, x, y);
21215 }
21216 }
21217 };
21218 let lastWidth = host.clientWidth;
21219 let resizeObserver = null;
21220 if (typeof ResizeObserver !== "undefined") {
21221 resizeObserver = new ResizeObserver(() => {
21222 const w = host.clientWidth;
21223 if (w === lastWidth) {
21224 return;
21225 }
21226 lastWidth = w;
21227 reflow();
21228 });
21229 resizeObserver.observe(host);
21230 }
21231 return {
21232 host,
21233 folderId,
21234 onSelectionChange(cb) {
21235 selectionListeners.add(cb);
21236 return () => {
21237 selectionListeners.delete(cb);
21238 };
21239 },
21240 sort,
21241 reflow,
21242 hydrated,
21243 dispose() {
21244 off();
21245 resizeObserver?.disconnect();
21246 resizeObserver = null;
21247 for (const deregister of dropTargetDeregisters) {
21248 try {
21249 deregister();
21250 } catch {
21251 }
21252 }
21253 dropTargetDeregisters.length = 0;
21254 for (const deregister of folderDropDeregisters.values()) {
21255 try {
21256 deregister();
21257 } catch {
21258 }
21259 }
21260 folderDropDeregisters.clear();
21261 for (const deregister of tileRejectDeregisters.values()) {
21262 try {
21263 deregister();
21264 } catch {
21265 }
21266 }
21267 tileRejectDeregisters.clear();
21268 host.removeEventListener("click", onCanvasClick);
21269 selectionListeners.clear();
21270 container.remove();
21271 }
21272 };
21273 }
21274 function fingerprint(list2) {
21275 if (list2.length === 0) {
21276 return "0";
21277 }
21278 const parts = [];
21279 for (const p of list2) {
21280 parts.push(
21281 `${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}`
21282 );
21283 }
21284 return parts.join("|");
21285 }
21286 function isPinned(placement) {
21287 return Boolean(placement.file.pinned);
21288 }
21289 function readSynthSource(placement) {
21290 const meta = placement.meta;
21291 if (!meta || typeof meta !== "object") {
21292 return null;
21293 }
21294 const v = meta.__synthFromDockItem;
21295 return typeof v === "string" && v !== "" ? v : null;
21296 }
21297 function isSyntheticPlacement(placement) {
21298 return placement.id <= 0 || readSynthSource(placement) !== null;
21299 }
21300 const RECYCLE_BIN_REF = "desktop-mode-recycle-bin";
21301 function shouldRejectTileDrops(placement) {
21302 if (placement.file?.type === "folder") {
21303 return false;
21304 }
21305 if (placement.file?.ref === RECYCLE_BIN_REF) {
21306 return false;
21307 }
21308 return true;
21309 }
21310 function buildVisualOccupiedSet(placements, excludeId) {
21311 const sorted = placements.slice().sort((a, b) => {
21312 const ap = isPinned(a) ? 0 : 1;
21313 const bp = isPinned(b) ? 0 : 1;
21314 return ap - bp;
21315 });
21316 const set = /* @__PURE__ */ new Set();
21317 let pinnedIdx = 0;
21318 for (const p of sorted) {
21319 if (excludeId !== void 0 && p.id === excludeId) {
21320 continue;
21321 }
21322 if (isPinned(p)) {
21323 set.add(cellKey(0, pinnedIdx));
21324 pinnedIdx += 1;
21325 } else {
21326 const cell = pointToCell(p.x, p.y);
21327 set.add(cellKey(cell.col, cell.row));
21328 }
21329 }
21330 return set;
21331 }
21332 function wouldCreateFolderCycle(movingFolderId, targetParentId) {
21333 if (targetParentId <= 0 || movingFolderId <= 0) {
21334 return false;
21335 }
21336 if (movingFolderId === targetParentId) {
21337 return true;
21338 }
21339 const parentByFolderId = /* @__PURE__ */ new Map();
21340 const state2 = store.getState();
21341 for (const bucket2 of state2.placementsByFolder.values()) {
21342 for (const p of bucket2) {
21343 if (p.file?.type !== "folder") {
21344 continue;
21345 }
21346 const fid = parseInt(p.file.ref, 10);
21347 if (Number.isNaN(fid) || fid <= 0) {
21348 continue;
21349 }
21350 if (!parentByFolderId.has(fid)) {
21351 parentByFolderId.set(fid, p.parentId);
21352 }
21353 }
21354 }
21355 const visited = /* @__PURE__ */ new Set();
21356 let cursor = targetParentId;
21357 let maxDepth = 256;
21358 while (cursor > 0 && maxDepth-- > 0) {
21359 if (cursor === movingFolderId) {
21360 return true;
21361 }
21362 if (visited.has(cursor)) {
21363 return true;
21364 }
21365 visited.add(cursor);
21366 const next = parentByFolderId.get(cursor);
21367 if (next === void 0) {
21368 return false;
21369 }
21370 cursor = next;
21371 }
21372 return false;
21373 }
21374 function persistDockPromotedPosition(dockItemId, x, y) {
21375 const api = window.wp?.desktop;
21376 if (!api?.getOsSettings || !api?.updateOsSettings) {
21377 return;
21378 }
21379 const current = api.getOsSettings().dockPromotedPositions ?? {};
21380 api.updateOsSettings({
21381 dockPromotedPositions: {
21382 ...current,
21383 [dockItemId]: { x, y }
21384 }
21385 });
21386 }
21387 function tryPatchPositions(list2, container, host) {
21388 const tiles = Array.from(
21389 container.querySelectorAll("[data-placement-id]")
21390 );
21391 if (tiles.length !== list2.length) {
21392 return false;
21393 }
21394 const byId = /* @__PURE__ */ new Map();
21395 for (const tile2 of tiles) {
21396 const raw = tile2.dataset.placementId ?? "";
21397 const id = parseInt(raw, 10);
21398 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
21399 return false;
21400 }
21401 byId.set(id, tile2);
21402 }
21403 for (const placement of list2) {
21404 const tile2 = byId.get(placement.id);
21405 if (!tile2) {
21406 return false;
21407 }
21408 if (tile2.dataset.fileType !== placement.file.type) {
21409 return false;
21410 }
21411 if (tile2.dataset.fileRef !== placement.file.ref) {
21412 return false;
21413 }
21414 const wasPinned = tile2.classList.contains(`${TILE_CLASS}--pinned`);
21415 if (wasPinned !== isPinned(placement)) {
21416 return false;
21417 }
21418 }
21419 const pinnedSlots = /* @__PURE__ */ new Map();
21420 const occupiedCells = /* @__PURE__ */ new Set();
21421 let pinnedIdx = 0;
21422 for (const placement of list2) {
21423 if (!isPinned(placement)) {
21424 continue;
21425 }
21426 const slot = cellToPos(0, pinnedIdx);
21427 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
21428 occupiedCells.add(cellKey(slot.col, slot.row));
21429 pinnedIdx += 1;
21430 }
21431 const displaced = /* @__PURE__ */ new Map();
21432 for (const placement of list2) {
21433 if (pinnedSlots.has(placement.id)) {
21434 continue;
21435 }
21436 const target2 = pointToCell(placement.x, placement.y);
21437 const key = cellKey(target2.col, target2.row);
21438 if (!occupiedCells.has(key)) {
21439 occupiedCells.add(key);
21440 continue;
21441 }
21442 const free = snapToEmptyCell(
21443 placement.x,
21444 placement.y,
21445 occupiedCells,
21446 host
21447 );
21448 occupiedCells.add(cellKey(free.col, free.row));
21449 displaced.set(placement.id, { x: free.x, y: free.y });
21450 }
21451 for (const placement of list2) {
21452 const tile2 = byId.get(placement.id);
21453 if (!tile2) {
21454 continue;
21455 }
21456 const pinned = pinnedSlots.get(placement.id);
21457 const disp = displaced.get(placement.id);
21458 if (pinned) {
21459 setTilePosition(tile2, pinned.x, pinned.y);
21460 } else if (disp) {
21461 setTilePosition(tile2, disp.x, disp.y);
21462 } else {
21463 setTilePosition(tile2, placement.x, placement.y);
21464 }
21465 }
21466 return true;
21467 }
21468 function hidePromotedDockItem(dockItemId) {
21469 const api = window.wp?.desktop;
21470 if (!api?.getOsSettings || !api?.updateOsSettings) {
21471 return;
21472 }
21473 const current = api.getOsSettings().itemVisibility ?? {};
21474 const next = { ...current, [dockItemId]: "dock" };
21475 api.updateOsSettings({ itemVisibility: next });
21476 }
21477 function registerFolderDropTarget(dragManager, tile2, targetFolderId, currentFolderId) {
21478 const target2 = {
21479 id: `desktop-mode-files-folder-${targetFolderId}-tile-${tile2.dataset.placementId ?? "?"}`,
21480 element: tile2,
21481 accept: (payload) => {
21482 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
21483 return false;
21484 }
21485 if (payload.type === "desktop-file") {
21486 const data = payload.data;
21487 if (data.placement.file.type === "folder" && parseInt(data.placement.file.ref, 10) === targetFolderId) {
21488 return false;
21489 }
21490 if (data.placement.parentId === targetFolderId) {
21491 return false;
21492 }
21493 if (isSyntheticPlacement(data.placement)) {
21494 return false;
21495 }
21496 if (data.placement.file.type === "folder") {
21497 const movingFolderId = parseInt(data.placement.file.ref, 10);
21498 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, targetFolderId)) {
21499 return false;
21500 }
21501 }
21502 }
21503 return true;
21504 },
21505 onEnter: () => {
21506 tile2.classList.add(`${TILE_CLASS}--drop-target`);
21507 },
21508 onLeave: () => {
21509 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
21510 },
21511 onDrop: (session) => {
21512 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
21513 if (session.payload.type === "desktop-file") {
21514 const data = session.payload.data;
21515 const next = {
21516 ...data.placement,
21517 parentId: targetFolderId
21518 };
21519 store.upsertPlacement(next);
21520 void updatePlacement(
21521 data.placement.id,
21522 { parentId: targetFolderId },
21523 data.placement.updatedAtMs
21524 ).then((server) => {
21525 store.upsertPlacement(server, "remote");
21526 }).catch((err) => {
21527 if (isConflict(err)) {
21528 showConflictToast(err);
21529 } else {
21530 console.error(
21531 "[desktop-mode] files: move-into-folder persist failed",
21532 err
21533 );
21534 }
21535 store.upsertPlacement(data.placement);
21536 });
21537 return;
21538 }
21539 if (session.payload.type === "shortcut") {
21540 const data = session.payload.data;
21541 const peers = store.getState().placementsByFolder.get(targetFolderId) ?? [];
21542 const cell = nextRowMajorCell(buildVisualOccupiedSet(peers));
21543 void createPlacement({
21544 parentId: targetFolderId,
21545 type: data.kind,
21546 ref: data.ref,
21547 x: cell.x,
21548 y: cell.y
21549 }).then((placement) => {
21550 store.upsertPlacement(placement);
21551 doAction("desktop-mode.files.shortcut-dropped", {
21552 folderId: targetFolderId,
21553 placement
21554 });
21555 }).catch((err) => {
21556 console.error(
21557 "[desktop-mode] shortcut drop into folder failed:",
21558 err
21559 );
21560 });
21561 }
21562 }
21563 };
21564 return dragManager.registerDropTarget(target2);
21565 }
21566 function attachTileDrag(tile2, placement, folderId) {
21567 tile2.addEventListener("pointerdown", (e) => {
21568 if (e.button !== 0) {
21569 return;
21570 }
21571 const dragManager = getDragManager();
21572 if (!dragManager) {
21573 return;
21574 }
21575 const liveBucket = store.getState().placementsByFolder.get(folderId);
21576 const livePlacement = liveBucket?.find((p) => p.id === placement.id) ?? placement;
21577 parseFloat(tile2.style.left) || livePlacement.x;
21578 parseFloat(tile2.style.top) || livePlacement.y;
21579 dragManager.start({
21580 payload: {
21581 type: "desktop-file",
21582 source: tile2,
21583 data: {
21584 placement: livePlacement,
21585 sourceFolderId: folderId,
21586 // Synthesize a cross-frame bridge payload from the
21587 // placement's file shape so a wallpaper-placed
21588 // shortcut can be dropped into an open Gutenberg
21589 // iframe and inserted as the matching block. The
21590 // PHP serialize() methods (`Desktop_Mode_Post_File`,
21591 // `Desktop_Mode_User_File`, `Desktop_Mode_Attachment_File`)
21592 // surface the URL fields this needs.
21593 bridgePayload: buildBridgePayloadFromPlacement(livePlacement)
21594 },
21595 ghost: {
21596 offsetX: e.clientX - tile2.getBoundingClientRect().left,
21597 offsetY: e.clientY - tile2.getBoundingClientRect().top
21598 }
21599 },
21600 origin: e
21601 // `onClickOnly` intentionally empty — a tile click is
21602 // handled by the dedicated `attachSelectOnClick` listener
21603 // below, which fires from the regular `click` event after
21604 // a sub-threshold pointerup. The manager won't fire a
21605 // `click` itself; the browser does.
21606 });
21607 });
21608 }
21609 function attachContextMenu(tile2, placement) {
21610 tile2.addEventListener("contextmenu", (e) => {
21611 e.preventDefault();
21612 e.stopPropagation();
21613 const items = [
21614 {
21615 id: "open",
21616 label: "Open",
21617 icon: "dashicons-external",
21618 sort: 10,
21619 onClick: () => {
21620 const file = resolve(placement.file);
21621 void openFile(file);
21622 }
21623 }
21624 ];
21625 if (placement.file.type === "post") {
21626 items.push({
21627 id: "navigate-into",
21628 label: "Navigate into",
21629 icon: "dashicons-category",
21630 sort: 20,
21631 onClick: () => {
21632 const postId = parseInt(placement.file.ref, 10);
21633 if (!postId) {
21634 return;
21635 }
21636 const api = window.wp?.desktop?.myWordpress;
21637 const postType = typeof placement.file.postType === "string" ? placement.file.postType : "post";
21638 const entityId = postType === "page" ? "pages" : "posts";
21639 api?.openDetail({
21640 entityId,
21641 postId,
21642 postTitle: placement.file.title || `#${postId}`
21643 });
21644 }
21645 });
21646 }
21647 const isFolder = placement.file.type === "folder";
21648 if (isFolder) {
21649 items.push({
21650 id: "rename-folder",
21651 label: "Rename…",
21652 icon: "dashicons-edit",
21653 sort: 30,
21654 onClick: () => {
21655 const folderId = parseInt(placement.file.ref, 10);
21656 if (!folderId) {
21657 return;
21658 }
21659 openCreateFolderDialog({
21660 title: "Rename folder",
21661 label: "New name",
21662 submitLabel: "Rename",
21663 initialName: placement.file.title,
21664 onSubmit: async (name) => {
21665 const trimmed = name.trim();
21666 if (!trimmed || trimmed === placement.file.title) {
21667 return;
21668 }
21669 const previousTitle = placement.file.title;
21670 const optimistic = {
21671 ...placement,
21672 file: { ...placement.file, title: trimmed }
21673 };
21674 store.upsertPlacement(optimistic);
21675 try {
21676 const folderUpdatedAtMs = store.getState().folders.get(folderId)?.updatedAtMs ?? 0;
21677 const updated = await updateFolder(
21678 folderId,
21679 { name: trimmed },
21680 folderUpdatedAtMs
21681 );
21682 store.upsertFolder(updated);
21683 const refreshed = await listPlacements(
21684 placement.parentId
21685 );
21686 store.setFolderPlacements(
21687 placement.parentId,
21688 refreshed.placements
21689 );
21690 } catch (err) {
21691 console.error(
21692 "[desktop-mode] rename folder failed:",
21693 err
21694 );
21695 store.upsertPlacement({
21696 ...placement,
21697 file: {
21698 ...placement.file,
21699 title: previousTitle
21700 }
21701 });
21702 }
21703 }
21704 });
21705 }
21706 });
21707 if (placement.canTrash !== false) {
21708 items.push({
21709 id: "delete-folder",
21710 label: "Move folder to Trash",
21711 icon: "dashicons-trash",
21712 sort: 90,
21713 danger: true,
21714 onClick: () => trashFolderWithUndo(placement)
21715 });
21716 }
21717 } else {
21718 const synthFromDockItem = readSynthSource(placement);
21719 const isRegisteredIcon = placement.file.type === "shortcut";
21720 if (synthFromDockItem || isRegisteredIcon) {
21721 const hideId = synthFromDockItem ?? placement.file.ref;
21722 items.push({
21723 id: "hide-from-desktop",
21724 label: "Hide from desktop",
21725 icon: "dashicons-hidden",
21726 sort: 90,
21727 onClick: () => hidePromotedDockItem(hideId)
21728 });
21729 } else if (placement.canTrash !== false) {
21730 items.push({
21731 id: "remove",
21732 label: "Move to Trash",
21733 icon: "dashicons-trash",
21734 sort: 90,
21735 danger: true,
21736 onClick: () => trashPlacementWithUndo(placement)
21737 });
21738 }
21739 }
21740 openTileMenu({ x: e.clientX, y: e.clientY }, { placement, items });
21741 });
21742 }
21743 const STATUS_BAR_CLASS = "desktop-mode-folder-status-bar";
21744 const ROOT_CLASS$2 = STATUS_BAR_CLASS;
21745 function mountFolderStatusBar(host, folderId) {
21746 const bar = document.createElement("div");
21747 bar.className = ROOT_CLASS$2;
21748 bar.setAttribute("role", "status");
21749 bar.dataset.folderId = String(folderId);
21750 host.appendChild(bar);
21751 const repaint = () => {
21752 const list2 = getFilesState().placementsByFolder.get(folderId) ?? [];
21753 const folders = list2.filter((p) => p.file.type === "folder").length;
21754 const files = list2.length - folders;
21755 const ctx = {
21756 folderId,
21757 totals: { files, folders, total: list2.length }
21758 };
21759 const segments = computeSegments(ctx);
21760 render(bar, segments);
21761 };
21762 repaint();
21763 const off = subscribeFilesStore(() => repaint());
21764 return {
21765 dispose() {
21766 off();
21767 bar.remove();
21768 }
21769 };
21770 }
21771 function computeSegments(ctx) {
21772 const { folders, files } = ctx.totals;
21773 const builtIns = [
21774 {
21775 id: "count",
21776 label: pluralize(files, "file", "files") + (folders > 0 ? `, ${pluralize(folders, "folder", "folders")}` : ""),
21777 align: "start",
21778 sort: 10
21779 }
21780 ];
21781 const filtered = applyFilters(
21782 "desktop-mode.files.folder-window.status-bar",
21783 builtIns,
21784 ctx
21785 );
21786 return Array.isArray(filtered) ? filtered : builtIns;
21787 }
21788 function render(bar, segments) {
21789 const sort = (a, b) => {
21790 const sa = typeof a.sort === "number" ? a.sort : 100;
21791 const sb = typeof b.sort === "number" ? b.sort : 100;
21792 if (sa !== sb) {
21793 return sa - sb;
21794 }
21795 return a.label.localeCompare(b.label);
21796 };
21797 const start = segments.filter((s) => (s.align ?? "start") === "start").sort(sort);
21798 const end = segments.filter((s) => s.align === "end").sort(sort);
21799 bar.replaceChildren();
21800 bar.appendChild(buildCluster("start", start));
21801 bar.appendChild(buildCluster("end", end));
21802 }
21803 function buildCluster(align, segs) {
21804 const cluster = document.createElement("div");
21805 cluster.className = `${ROOT_CLASS$2}__cluster ${ROOT_CLASS$2}__cluster--${align}`;
21806 for (const seg of segs) {
21807 cluster.appendChild(buildSegment(seg));
21808 }
21809 return cluster;
21810 }
21811 function buildSegment(seg) {
21812 const interactive = typeof seg.onClick === "function";
21813 const el = document.createElement(interactive ? "button" : "span");
21814 el.className = `${ROOT_CLASS$2}__segment`;
21815 el.dataset.segmentId = seg.id;
21816 if (interactive) {
21817 el.type = "button";
21818 el.addEventListener("click", (e) => seg.onClick(e));
21819 }
21820 if (seg.icon) {
21821 const icon = document.createElement("span");
21822 icon.className = `${ROOT_CLASS$2}__icon dashicons ${seg.icon.replace(/[^a-zA-Z0-9_-]/g, "")}`;
21823 icon.setAttribute("aria-hidden", "true");
21824 el.appendChild(icon);
21825 }
21826 const label = document.createElement("span");
21827 label.className = `${ROOT_CLASS$2}__label`;
21828 label.textContent = seg.label;
21829 el.appendChild(label);
21830 return el;
21831 }
21832 function pluralize(n, singular, plural) {
21833 return `${n} ${n === 1 ? singular : plural}`;
21834 }
21835 const MENU_CLASS$1 = "desktop-mode-icon-canvas-menu";
21836 let activeMenu$1 = null;
21837 let activeFlyout = null;
21838 let activeCanvas = null;
21839 let outsideHandler = null;
21840 let escHandler = null;
21841 function attachIconCanvasMenu(canvas, deps2) {
21842 deps2.openOnBackgroundClick !== false;
21843 const onContextMenu = (e) => {
21844 if (isInsideTile(e.target) || isInsideMenu(e.target)) {
21845 return;
21846 }
21847 e.preventDefault();
21848 toggle(e.clientX, e.clientY);
21849 };
21850 let toggleGen = 0;
21851 const toggle = (x, y) => {
21852 if (activeCanvas === canvas && activeMenu$1) {
21853 closeMenu();
21854 return;
21855 }
21856 const items = buildItems(deps2);
21857 const filtered = applyFilters(
21858 "desktop-mode.icon-canvas.menu",
21859 items,
21860 deps2.scope
21861 );
21862 const finalItems = Array.isArray(filtered) ? filtered : items;
21863 const myGen = ++toggleGen;
21864 openWithShellOverlays(
21865 () => myGen === toggleGen,
21866 () => openMenu(finalItems, { x, y }, canvas)
21867 );
21868 };
21869 canvas.addEventListener("contextmenu", onContextMenu);
21870 return {
21871 dispose: () => {
21872 canvas.removeEventListener("contextmenu", onContextMenu);
21873 closeMenu();
21874 }
21875 };
21876 }
21877 function isInsideTile(target2) {
21878 if (!(target2 instanceof Element)) {
21879 return false;
21880 }
21881 return target2.closest(".desktop-mode-file-tile") !== null;
21882 }
21883 function isInsideMenu(target2) {
21884 if (!(target2 instanceof Element)) {
21885 return false;
21886 }
21887 return target2.closest(`.${MENU_CLASS$1}`) !== null;
21888 }
21889 function buildItems(deps2) {
21890 const sortItem = {
21891 id: "sort-by",
21892 label: __("Sort by", "desktop-mode"),
21893 icon: "dashicons-sort",
21894 sort: 10,
21895 children: [
21896 {
21897 id: "sort-name-asc",
21898 label: __("Name (A → Z)", "desktop-mode"),
21899 sort: 10,
21900 onClick: () => deps2.onSort("name-asc")
21901 },
21902 {
21903 id: "sort-name-desc",
21904 label: __("Name (Z → A)", "desktop-mode"),
21905 sort: 20,
21906 onClick: () => deps2.onSort("name-desc")
21907 },
21908 {
21909 id: "sort-date-desc",
21910 label: __("Newest first", "desktop-mode"),
21911 sort: 30,
21912 onClick: () => deps2.onSort("date-desc")
21913 },
21914 {
21915 id: "sort-date-asc",
21916 label: __("Oldest first", "desktop-mode"),
21917 sort: 40,
21918 onClick: () => deps2.onSort("date-asc")
21919 }
21920 ]
21921 };
21922 const items = [sortItem];
21923 if (Array.isArray(deps2.extraItems)) {
21924 items.push(...deps2.extraItems);
21925 }
21926 return items;
21927 }
21928 function sortItems(items) {
21929 return items.slice().sort((a, b) => {
21930 const sa = typeof a.sort === "number" ? a.sort : 100;
21931 const sb = typeof b.sort === "number" ? b.sort : 100;
21932 if (sa !== sb) {
21933 return sa - sb;
21934 }
21935 return a.label.localeCompare(b.label);
21936 });
21937 }
21938 function openMenu(items, pos, canvas) {
21939 closeMenu();
21940 if (items.length === 0) {
21941 return;
21942 }
21943 activeCanvas = canvas;
21944 const sorted = sortItems(items);
21945 const menu = document.createElement("wpd-context-menu");
21946 menu.setAttribute("open", "");
21947 menu.classList.add(MENU_CLASS$1);
21948 menu.style.left = `${pos.x}px`;
21949 menu.style.top = `${pos.y}px`;
21950 const itemById = /* @__PURE__ */ new Map();
21951 for (const item of sorted) {
21952 itemById.set(item.id, item);
21953 const opt = appendOption(menu, item);
21954 if (hasChildren(item)) {
21955 opt.addEventListener("mouseenter", () => {
21956 openFlyout(item, opt);
21957 });
21958 }
21959 }
21960 menu.addEventListener("wpd-context-menu-pick", (e) => {
21961 const detail = e.detail;
21962 const item = itemById.get(detail.id);
21963 if (!item) {
21964 return;
21965 }
21966 if (hasChildren(item)) {
21967 e.stopPropagation();
21968 const anchor = menu.querySelector(
21969 `[data-menu-item-id="${item.id}"]`
21970 );
21971 if (anchor) {
21972 openFlyout(item, anchor);
21973 }
21974 return;
21975 }
21976 closeMenu();
21977 item.onClick?.();
21978 });
21979 document.body.appendChild(menu);
21980 activeMenu$1 = menu;
21981 clampToViewport(menu);
21982 queueMicrotask(() => {
21983 outsideHandler = (e) => {
21984 if (isInsideMenu(e.target)) {
21985 return;
21986 }
21987 closeMenu();
21988 };
21989 escHandler = (e) => {
21990 if (e.key === "Escape") {
21991 closeMenu();
21992 }
21993 };
21994 document.addEventListener("mousedown", outsideHandler);
21995 document.addEventListener("keydown", escHandler);
21996 });
21997 }
21998 function appendOption(host, item) {
21999 const opt = document.createElement("wpd-context-menu-option");
22000 opt.dataset.menuItemId = item.id;
22001 opt.setAttribute("value", item.id);
22002 if (item.heading) {
22003 opt.setAttribute("heading", "");
22004 }
22005 if (item.disabled) {
22006 opt.setAttribute("disabled", "");
22007 }
22008 if (item.icon) {
22009 opt.setAttribute("icon", sanitizeClass$1(item.icon));
22010 }
22011 if (hasChildren(item)) {
22012 opt.setAttribute("has-children", "");
22013 }
22014 opt.textContent = item.label;
22015 host.appendChild(opt);
22016 return opt;
22017 }
22018 function openFlyout(parent, anchor) {
22019 closeFlyout();
22020 if (!hasChildren(parent)) {
22021 return;
22022 }
22023 const fly = document.createElement("wpd-context-menu");
22024 fly.setAttribute("open", "");
22025 fly.classList.add(MENU_CLASS$1, `${MENU_CLASS$1}--flyout`);
22026 const childById = /* @__PURE__ */ new Map();
22027 for (const child of sortItems(parent.children ?? [])) {
22028 childById.set(child.id, child);
22029 appendOption(fly, child);
22030 }
22031 fly.addEventListener("wpd-context-menu-pick", (e) => {
22032 const detail = e.detail;
22033 const child = childById.get(detail.id);
22034 if (!child) {
22035 return;
22036 }
22037 e.stopPropagation();
22038 closeMenu();
22039 child.onClick?.();
22040 });
22041 document.body.appendChild(fly);
22042 activeFlyout = fly;
22043 positionFlyout(fly, anchor);
22044 }
22045 function positionFlyout(fly, anchor) {
22046 const ar = anchor.getBoundingClientRect();
22047 fly.style.position = "fixed";
22048 fly.style.left = `${ar.right}px`;
22049 fly.style.top = `${ar.top}px`;
22050 const fr = fly.getBoundingClientRect();
22051 if (fr.right > window.innerWidth) {
22052 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
22053 }
22054 if (fr.bottom > window.innerHeight) {
22055 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
22056 }
22057 }
22058 function clampToViewport(menu) {
22059 const rect = menu.getBoundingClientRect();
22060 if (rect.right > window.innerWidth) {
22061 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
22062 }
22063 if (rect.bottom > window.innerHeight) {
22064 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
22065 }
22066 }
22067 function hasChildren(item) {
22068 return Array.isArray(item.children) && item.children.length > 0;
22069 }
22070 function closeFlyout() {
22071 if (activeFlyout) {
22072 activeFlyout.remove();
22073 activeFlyout = null;
22074 }
22075 }
22076 function closeMenu() {
22077 closeFlyout();
22078 if (activeMenu$1) {
22079 activeMenu$1.remove();
22080 activeMenu$1 = null;
22081 }
22082 activeCanvas = null;
22083 if (outsideHandler) {
22084 document.removeEventListener("mousedown", outsideHandler);
22085 outsideHandler = null;
22086 }
22087 if (escHandler) {
22088 document.removeEventListener("keydown", escHandler);
22089 escHandler = null;
22090 }
22091 }
22092 function sanitizeClass$1(raw) {
22093 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
22094 }
22095 const ROOT_CLASS$1 = "desktop-mode-breadcrumbs";
22096 function renderBreadcrumbs(host, segments, opts = {}) {
22097 host.replaceChildren();
22098 host.classList.add(ROOT_CLASS$1);
22099 if (opts.onBack) {
22100 const back = document.createElement("button");
22101 back.type = "button";
22102 back.className = `${ROOT_CLASS$1}__back`;
22103 back.setAttribute("aria-label", __("Back", "desktop-mode"));
22104 back.title = __("Back", "desktop-mode");
22105 const arrow = document.createElement("span");
22106 arrow.className = "dashicons dashicons-arrow-left-alt2";
22107 arrow.setAttribute("aria-hidden", "true");
22108 back.appendChild(arrow);
22109 if (opts.backDisabled) {
22110 back.disabled = true;
22111 }
22112 const onBack = opts.onBack;
22113 back.addEventListener("click", () => {
22114 if (back.disabled) {
22115 return;
22116 }
22117 onBack();
22118 });
22119 host.appendChild(back);
22120 }
22121 const nav = document.createElement("nav");
22122 nav.className = `${ROOT_CLASS$1}__crumbs`;
22123 nav.setAttribute("aria-label", __("Breadcrumb", "desktop-mode"));
22124 segments.forEach((seg, idx) => {
22125 if (idx > 0) {
22126 const sep = document.createElement("span");
22127 sep.className = `${ROOT_CLASS$1}__sep`;
22128 sep.setAttribute("aria-hidden", "true");
22129 sep.textContent = "›";
22130 nav.appendChild(sep);
22131 }
22132 if (!seg.onClick) {
22133 const here = document.createElement("span");
22134 here.className = `${ROOT_CLASS$1}__crumb ${ROOT_CLASS$1}__crumb--current`;
22135 here.setAttribute("aria-current", "page");
22136 here.textContent = seg.label;
22137 nav.appendChild(here);
22138 return;
22139 }
22140 const btn = document.createElement("button");
22141 btn.type = "button";
22142 btn.className = `${ROOT_CLASS$1}__crumb`;
22143 btn.textContent = seg.label;
22144 const onClick = seg.onClick;
22145 btn.addEventListener("click", () => {
22146 onClick();
22147 });
22148 nav.appendChild(btn);
22149 });
22150 host.appendChild(nav);
22151 }
22152 async function getJson(url, init2 = {}) {
22153 const response = await trackedFetch$1(url, {
22154 credentials: "same-origin",
22155 headers: {
22156 Accept: "application/json",
22157 "X-WP-Nonce": readRestNonce(),
22158 ...init2.headers ?? {}
22159 },
22160 ...init2
22161 });
22162 if (!response.ok) {
22163 throw new Error(`${response.status} ${response.statusText}`);
22164 }
22165 return await response.json();
22166 }
22167 function readRestNonce() {
22168 const cfg = window.wp?.desktop?.config;
22169 return cfg?.restNonce ?? "";
22170 }
22171 function readRestRoot() {
22172 const cfg = window.wp?.desktop?.config;
22173 if (cfg?.restUrl) {
22174 return cfg.restUrl.endsWith("/") ? cfg.restUrl : cfg.restUrl + "/";
22175 }
22176 return `${window.location.origin}/wp-json/`;
22177 }
22178 function restUrl(path) {
22179 return joinRestUrl(readRestRoot(), path);
22180 }
22181 function renderPlacementPreview(placement, host) {
22182 const filtered = applyFilters(
22183 "desktop-mode.files.preview",
22184 null,
22185 placement
22186 );
22187 if (filtered instanceof HTMLElement) {
22188 host.replaceChildren(filtered);
22189 return;
22190 }
22191 if (placement.accessGated) {
22192 host.replaceChildren(renderAccessGated(placement));
22193 return;
22194 }
22195 host.replaceChildren(renderLoading());
22196 void renderByType(placement).then((node) => {
22197 host.replaceChildren(node);
22198 }).catch((err) => {
22199 host.replaceChildren(renderError(err));
22200 });
22201 }
22202 function renderAccessGated(placement) {
22203 const wrap = document.createElement("div");
22204 wrap.className = "desktop-mode-files__access-gated";
22205 const ring = document.createElement("div");
22206 ring.className = "desktop-mode-files__access-gated-ring";
22207 const glyph = document.createElement("span");
22208 glyph.className = "dashicons dashicons-lock desktop-mode-files__access-gated-glyph";
22209 glyph.setAttribute("aria-hidden", "true");
22210 ring.appendChild(glyph);
22211 wrap.appendChild(ring);
22212 const title = document.createElement("h2");
22213 title.className = "desktop-mode-files__access-gated-title";
22214 title.textContent = "No permission to view";
22215 wrap.appendChild(title);
22216 const sub = document.createElement("p");
22217 sub.className = "desktop-mode-files__access-gated-sub";
22218 const target2 = placement.file.title || placement.file.type;
22219 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.`;
22220 wrap.appendChild(sub);
22221 const hint = document.createElement("p");
22222 hint.className = "desktop-mode-files__access-gated-hint";
22223 hint.textContent = "Ask the owner to grant access on the underlying item, or to remove it from the shared folder.";
22224 wrap.appendChild(hint);
22225 return wrap;
22226 }
22227 async function renderByType(placement) {
22228 const file = placement.file;
22229 switch (file.type) {
22230 case "post":
22231 return renderPostPreview(file.ref, file);
22232 case "folder":
22233 return renderFolderPreview(file);
22234 case "shortcut":
22235 return renderShortcutPreview(file);
22236 case "attachment":
22237 return renderAttachmentPreview(file.ref, file);
22238 case "user":
22239 return renderUserSummary(file.ref, file);
22240 case "term":
22241 return renderTermSummary(file);
22242 case "comment":
22243 return renderCommentSummary(file.ref, file);
22244 case "bookmark":
22245 return renderBookmarkPreview(file);
22246 default:
22247 return renderGenericPreview(file);
22248 }
22249 }
22250 async function renderPostPreview(ref, file) {
22251 const id = parseInt(ref, 10);
22252 if (!id) {
22253 return renderGenericPreview(file);
22254 }
22255 let data = null;
22256 for (const path of ["wp/v2/posts", "wp/v2/pages"]) {
22257 try {
22258 data = await getJson(
22259 restUrl(
22260 `${path}/${id}?_fields=id,title,content,date,link,status`
22261 )
22262 );
22263 break;
22264 } catch {
22265 }
22266 }
22267 if (!data) {
22268 return renderGenericPreview(file);
22269 }
22270 const wrap = articleShell();
22271 const h = document.createElement("h2");
22272 h.className = "desktop-mode-my-wordpress__article-title";
22273 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
22274 wrap.appendChild(h);
22275 const meta = document.createElement("p");
22276 meta.className = "desktop-mode-my-wordpress__article-meta";
22277 const parts = [];
22278 parts.push(formatDate(data.date));
22279 if (data.status && data.status !== "publish") {
22280 parts.push(data.status);
22281 }
22282 meta.textContent = parts.join(" · ");
22283 wrap.appendChild(meta);
22284 if (data.content?.rendered) {
22285 const body = document.createElement("div");
22286 body.className = "desktop-mode-my-wordpress__article-content";
22287 body.innerHTML = data.content.rendered;
22288 wrap.appendChild(body);
22289 }
22290 const footer = document.createElement("footer");
22291 footer.className = "desktop-mode-my-wordpress__article-footer";
22292 const myWordpressApi = window.wp?.desktop?.myWordpress;
22293 if (myWordpressApi) {
22294 const exploreBtn = document.createElement("wpd-button");
22295 exploreBtn.setAttribute("variant", "secondary");
22296 exploreBtn.textContent = __("Explore details", "desktop-mode");
22297 exploreBtn.title = __(
22298 "See author, comments, categories, tags, attached media, and revisions for this entry.",
22299 "desktop-mode"
22300 );
22301 exploreBtn.addEventListener("click", () => {
22302 const postType = typeof file.postType === "string" ? file.postType : "post";
22303 myWordpressApi.openDetail({
22304 entityId: postType === "page" ? "pages" : "posts",
22305 postId: id,
22306 postTitle: stripTags(data.title.rendered) || `#${id}`
22307 });
22308 });
22309 footer.appendChild(exploreBtn);
22310 }
22311 const editBtn = document.createElement("wpd-button");
22312 editBtn.setAttribute("variant", "primary");
22313 editBtn.textContent = __("Open in editor", "desktop-mode");
22314 editBtn.addEventListener("click", () => {
22315 const adminUrl = window.wp?.desktop?.config?.adminUrl;
22316 if (!adminUrl) {
22317 return;
22318 }
22319 const editUrl = `${adminUrl}post.php?post=${id}&action=edit`;
22320 const wm = window.wp?.desktop?.windowManager;
22321 const postType = typeof file.postType === "string" ? file.postType : "post";
22322 const entityId = postType === "page" ? "pages" : "posts";
22323 wm?.open({
22324 id: `${entityId}-edit-${id}`,
22325 url: editUrl,
22326 title: stripTags(data.title.rendered),
22327 icon: file.icon
22328 });
22329 });
22330 footer.appendChild(editBtn);
22331 wrap.appendChild(footer);
22332 return wrap;
22333 }
22334 async function renderUserSummary(ref, file) {
22335 const id = parseInt(ref, 10);
22336 if (!id) {
22337 return renderGenericPreview(file);
22338 }
22339 let data = null;
22340 try {
22341 data = await getJson(
22342 restUrl(`desktop-mode/v1/user-stats/${id}`)
22343 );
22344 } catch {
22345 return renderGenericPreview(file);
22346 }
22347 const wrap = articleShell("desktop-mode-my-wordpress__user");
22348 const header = document.createElement("header");
22349 header.className = "desktop-mode-my-wordpress__user-header";
22350 if (data.profile.avatarUrl) {
22351 const img = document.createElement("img");
22352 img.className = "desktop-mode-my-wordpress__user-avatar";
22353 img.src = data.profile.avatarUrl;
22354 img.alt = "";
22355 header.appendChild(img);
22356 }
22357 const head = document.createElement("div");
22358 head.className = "desktop-mode-my-wordpress__user-headline";
22359 const h = document.createElement("h2");
22360 h.className = "desktop-mode-my-wordpress__article-title";
22361 h.textContent = data.profile.name || file.title || `#${id}`;
22362 head.appendChild(h);
22363 if (data.profile.roleLabels && data.profile.roleLabels.length > 0) {
22364 const roles = document.createElement("div");
22365 roles.className = "desktop-mode-my-wordpress__user-roles";
22366 for (const r of data.profile.roleLabels) {
22367 const badge = document.createElement("span");
22368 badge.className = "desktop-mode-my-wordpress__user-role";
22369 badge.textContent = r;
22370 roles.appendChild(badge);
22371 }
22372 head.appendChild(roles);
22373 }
22374 header.appendChild(head);
22375 wrap.appendChild(header);
22376 if (data.profile.description) {
22377 const bio = document.createElement("div");
22378 bio.className = "desktop-mode-my-wordpress__user-bio";
22379 bio.textContent = data.profile.description;
22380 wrap.appendChild(bio);
22381 }
22382 const cards = document.createElement("div");
22383 cards.className = "desktop-mode-my-wordpress__user-stats";
22384 cards.appendChild(
22385 statCard(
22386 data.counts.posts.total.toLocaleString(),
22387 __("Posts", "desktop-mode")
22388 )
22389 );
22390 cards.appendChild(
22391 statCard(
22392 data.counts.pages.total.toLocaleString(),
22393 __("Pages", "desktop-mode")
22394 )
22395 );
22396 cards.appendChild(
22397 statCard(
22398 data.counts.commentsReceived.toLocaleString(),
22399 __("Comments received", "desktop-mode")
22400 )
22401 );
22402 wrap.appendChild(cards);
22403 return wrap;
22404 }
22405 async function renderTermSummary(file) {
22406 const id = parseInt(file.ref, 10);
22407 const taxonomy = typeof file.taxonomy === "string" && file.taxonomy ? file.taxonomy : "category";
22408 if (!id) {
22409 return renderGenericPreview(file);
22410 }
22411 let data = null;
22412 try {
22413 data = await getJson(
22414 restUrl(`desktop-mode/v1/term-stats/${taxonomy}/${id}`)
22415 );
22416 } catch {
22417 return renderGenericPreview(file);
22418 }
22419 const wrap = articleShell();
22420 const h = document.createElement("h2");
22421 h.className = "desktop-mode-my-wordpress__article-title";
22422 h.textContent = data.profile.name || file.title || `#${id}`;
22423 wrap.appendChild(h);
22424 const meta = document.createElement("p");
22425 meta.className = "desktop-mode-my-wordpress__article-meta";
22426 meta.textContent = data.profile.taxonomyLabel || data.profile.taxonomy;
22427 wrap.appendChild(meta);
22428 if (data.profile.description) {
22429 const desc = document.createElement("div");
22430 desc.className = "desktop-mode-my-wordpress__article-content";
22431 desc.innerHTML = data.profile.description;
22432 wrap.appendChild(desc);
22433 }
22434 const cards = document.createElement("div");
22435 cards.className = "desktop-mode-my-wordpress__user-stats";
22436 cards.appendChild(
22437 statCard(
22438 data.counts.posts.total.toLocaleString(),
22439 __("Posts", "desktop-mode")
22440 )
22441 );
22442 cards.appendChild(
22443 statCard(
22444 data.counts.commentsReceived.toLocaleString(),
22445 __("Comments", "desktop-mode")
22446 )
22447 );
22448 cards.appendChild(
22449 statCard(
22450 data.counts.distinctAuthors.toLocaleString(),
22451 __("Authors", "desktop-mode")
22452 )
22453 );
22454 wrap.appendChild(cards);
22455 return wrap;
22456 }
22457 async function renderCommentSummary(ref, file) {
22458 const id = parseInt(ref, 10);
22459 if (!id) {
22460 return renderGenericPreview(file);
22461 }
22462 let data = null;
22463 try {
22464 data = await getJson(
22465 restUrl(`desktop-mode/v1/comment-stats/${id}`)
22466 );
22467 } catch {
22468 return renderGenericPreview(file);
22469 }
22470 const wrap = articleShell();
22471 const header = document.createElement("header");
22472 header.className = "desktop-mode-my-wordpress__user-header";
22473 if (data.author.avatarUrl) {
22474 const img = document.createElement("img");
22475 img.className = "desktop-mode-my-wordpress__user-avatar";
22476 img.src = data.author.avatarUrl;
22477 img.alt = "";
22478 header.appendChild(img);
22479 }
22480 const head = document.createElement("div");
22481 head.className = "desktop-mode-my-wordpress__user-headline";
22482 const h = document.createElement("h2");
22483 h.className = "desktop-mode-my-wordpress__article-title";
22484 h.textContent = data.author.name;
22485 head.appendChild(h);
22486 const sub = document.createElement("p");
22487 sub.className = "desktop-mode-my-wordpress__article-meta";
22488 sub.textContent = `${formatDate(data.comment.date)} · ${data.comment.status}`;
22489 head.appendChild(sub);
22490 header.appendChild(head);
22491 wrap.appendChild(header);
22492 const body = document.createElement("div");
22493 body.className = "desktop-mode-my-wordpress__article-content";
22494 body.innerHTML = data.comment.rendered;
22495 wrap.appendChild(body);
22496 if (data.post) {
22497 const card = document.createElement("div");
22498 card.className = "desktop-mode-my-wordpress__comment-post";
22499 const link = document.createElement("a");
22500 link.className = "desktop-mode-my-wordpress__comment-post-title";
22501 link.href = data.post.link;
22502 link.target = "_blank";
22503 link.rel = "noopener noreferrer";
22504 link.textContent = data.post.title;
22505 card.appendChild(link);
22506 wrap.appendChild(card);
22507 }
22508 return wrap;
22509 }
22510 async function renderAttachmentPreview(ref, file) {
22511 const id = parseInt(ref, 10);
22512 if (!id) {
22513 return renderGenericPreview(file);
22514 }
22515 let data = null;
22516 try {
22517 data = await getJson(
22518 restUrl(
22519 `wp/v2/media/${id}?_fields=id,title,source_url,mime_type,alt_text,media_details`
22520 )
22521 );
22522 } catch {
22523 return renderGenericPreview(file);
22524 }
22525 const wrap = articleShell();
22526 const h = document.createElement("h2");
22527 h.className = "desktop-mode-my-wordpress__article-title";
22528 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
22529 wrap.appendChild(h);
22530 const meta = document.createElement("p");
22531 meta.className = "desktop-mode-my-wordpress__article-meta";
22532 meta.textContent = data.mime_type;
22533 wrap.appendChild(meta);
22534 if (data.mime_type.startsWith("image/")) {
22535 const img = document.createElement("img");
22536 img.className = "desktop-mode-my-wordpress__article-hero";
22537 const sizes = data.media_details?.sizes;
22538 img.src = sizes?.large?.source_url ?? sizes?.medium?.source_url ?? data.source_url;
22539 img.alt = data.alt_text ?? "";
22540 wrap.appendChild(img);
22541 } else {
22542 const p = document.createElement("p");
22543 const a = document.createElement("a");
22544 a.href = data.source_url;
22545 a.textContent = data.source_url;
22546 a.target = "_blank";
22547 a.rel = "noopener noreferrer";
22548 p.appendChild(a);
22549 wrap.appendChild(p);
22550 }
22551 return wrap;
22552 }
22553 function renderFolderPreview(file) {
22554 const wrap = articleShell();
22555 const h = document.createElement("h2");
22556 h.className = "desktop-mode-my-wordpress__article-title";
22557 h.textContent = file.title || __("(folder)", "desktop-mode");
22558 wrap.appendChild(h);
22559 const meta = document.createElement("p");
22560 meta.className = "desktop-mode-my-wordpress__article-meta";
22561 meta.textContent = __("Double-click to open.", "desktop-mode");
22562 wrap.appendChild(meta);
22563 return wrap;
22564 }
22565 function renderShortcutPreview(file) {
22566 const wrap = articleShell();
22567 const h = document.createElement("h2");
22568 h.className = "desktop-mode-my-wordpress__article-title";
22569 h.textContent = file.title || __("Shortcut", "desktop-mode");
22570 wrap.appendChild(h);
22571 const meta = document.createElement("p");
22572 meta.className = "desktop-mode-my-wordpress__article-meta";
22573 meta.textContent = __("Plugin shortcut. Double-click to open.", "desktop-mode");
22574 wrap.appendChild(meta);
22575 return wrap;
22576 }
22577 function renderBookmarkPreview(file) {
22578 const wrap = articleShell();
22579 const h = document.createElement("h2");
22580 h.className = "desktop-mode-my-wordpress__article-title";
22581 h.textContent = file.title || __("Bookmark", "desktop-mode");
22582 wrap.appendChild(h);
22583 const url = typeof file.url === "string" ? file.url : "";
22584 if (url) {
22585 const a = document.createElement("a");
22586 a.href = url;
22587 a.textContent = url;
22588 a.target = "_blank";
22589 a.rel = "noopener noreferrer";
22590 wrap.appendChild(a);
22591 }
22592 return wrap;
22593 }
22594 function renderGenericPreview(file) {
22595 const wrap = articleShell();
22596 const h = document.createElement("h2");
22597 h.className = "desktop-mode-my-wordpress__article-title";
22598 h.textContent = file.title || file.type;
22599 wrap.appendChild(h);
22600 const meta = document.createElement("p");
22601 meta.className = "desktop-mode-my-wordpress__article-meta";
22602 meta.textContent = sprintf(
22603 // translators: %s is a file-type slug.
22604 __("Type: %s", "desktop-mode"),
22605 file.type
22606 );
22607 wrap.appendChild(meta);
22608 if (!file.exists) {
22609 const warn2 = document.createElement("p");
22610 warn2.className = "desktop-mode-my-wordpress__article-meta";
22611 warn2.textContent = __(
22612 "The underlying entity is no longer available.",
22613 "desktop-mode"
22614 );
22615 wrap.appendChild(warn2);
22616 }
22617 return wrap;
22618 }
22619 function articleShell(extraClass = "") {
22620 const article = document.createElement("article");
22621 article.className = "desktop-mode-my-wordpress__article" + (extraClass ? " " + extraClass : "");
22622 return article;
22623 }
22624 function statCard(value, label) {
22625 const card = document.createElement("div");
22626 card.className = "desktop-mode-my-wordpress__user-stat";
22627 const v = document.createElement("span");
22628 v.className = "desktop-mode-my-wordpress__user-stat-value";
22629 v.textContent = value;
22630 card.appendChild(v);
22631 const l = document.createElement("span");
22632 l.className = "desktop-mode-my-wordpress__user-stat-label";
22633 l.textContent = label;
22634 card.appendChild(l);
22635 return card;
22636 }
22637 function renderLoading() {
22638 const wrap = document.createElement("div");
22639 wrap.className = "desktop-mode-my-wordpress__preview-loading";
22640 const spinner = document.createElement("wpd-spinner");
22641 wrap.appendChild(spinner);
22642 return wrap;
22643 }
22644 function renderError(err) {
22645 const wrap = document.createElement("div");
22646 wrap.className = "desktop-mode-my-wordpress__error";
22647 wrap.textContent = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
22648 return wrap;
22649 }
22650 function stripTags(html2) {
22651 const div = document.createElement("div");
22652 div.innerHTML = html2;
22653 return (div.textContent ?? "").trim();
22654 }
22655 function formatDate(iso) {
22656 if (!iso) {
22657 return "";
22658 }
22659 try {
22660 return new Date(iso).toLocaleString();
22661 } catch {
22662 return iso;
22663 }
22664 }
22665 function renderPreviewEmpty() {
22666 const wrap = document.createElement("div");
22667 wrap.className = "desktop-mode-my-wordpress__preview-empty";
22668 wrap.textContent = __(
22669 "Select an item to preview it here.",
22670 "desktop-mode"
22671 );
22672 return wrap;
22673 }
22674 const ID_PREFIX = "desktop-mode-embed-";
22675 const DEFAULT_W = 800;
22676 const DEFAULT_H = 600;
22677 const MIN_W = 360;
22678 const MIN_H = 240;
22679 const PADDING = 16;
22680 const lastPersisted = /* @__PURE__ */ new Map();
22681 function openEmbedWindow(file, ctx) {
22682 const url = file.ref();
22683 if (!url) {
22684 return;
22685 }
22686 const wm = window.wp?.desktop?.windowManager;
22687 if (!wm) {
22688 return;
22689 }
22690 const placement = ctx?.placement;
22691 const meta = placement?.meta ?? null;
22692 const windowId = placement ? `${ID_PREFIX}${placement.id}` : `${ID_PREFIX}anon-${hash(url)}`;
22693 const customName = meta?.name?.trim() ?? "";
22694 const title = customName !== "" ? customName : file.title();
22695 const cfg = {
22696 id: windowId,
22697 baseId: windowId,
22698 url,
22699 title,
22700 icon: file.icon(),
22701 minWidth: MIN_W,
22702 minHeight: MIN_H
22703 };
22704 const saved = meta?.window;
22705 const area = document.getElementById("desktop-mode-area");
22706 const aw = area?.clientWidth ?? window.innerWidth;
22707 const ah = area?.clientHeight ?? window.innerHeight;
22708 if (saved && Number.isFinite(saved.width) && Number.isFinite(saved.height)) {
22709 const { x, y, width, height } = clampGeometry(saved, aw, ah);
22710 cfg.x = x;
22711 cfg.y = y;
22712 cfg.width = width;
22713 cfg.height = height;
22714 } else {
22715 cfg.width = Math.min(DEFAULT_W, Math.max(MIN_W, aw - PADDING * 2));
22716 cfg.height = Math.min(DEFAULT_H, Math.max(MIN_H, ah - PADDING * 2));
22717 }
22718 if (placement) {
22719 if (saved) {
22720 lastPersisted.set(windowId, { ...saved });
22721 }
22722 }
22723 wm.open(cfg);
22724 }
22725 let installed = false;
22726 function installEmbedPersistence() {
22727 if (installed) {
22728 return;
22729 }
22730 installed = true;
22731 const onChange = (payload) => {
22732 const p = payload;
22733 const id = p?.windowId;
22734 if (!id || !id.startsWith(ID_PREFIX)) {
22735 return;
22736 }
22737 const placementIdStr = id.slice(ID_PREFIX.length);
22738 const placementId = parseInt(placementIdStr, 10);
22739 if (!placementId) {
22740 return;
22741 }
22742 const wm = window.wp?.desktop?.windowManager;
22743 const win = wm?.getById?.(id);
22744 const el = win?.element;
22745 if (!el) {
22746 return;
22747 }
22748 const next = {
22749 x: el.offsetLeft,
22750 y: el.offsetTop,
22751 width: el.offsetWidth,
22752 height: el.offsetHeight
22753 };
22754 const prev = lastPersisted.get(id);
22755 if (prev && prev.x === next.x && prev.y === next.y && prev.width === next.width && prev.height === next.height) {
22756 return;
22757 }
22758 lastPersisted.set(id, next);
22759 void persist(placementId, next);
22760 };
22761 addAction(HOOKS.WINDOW_DRAG_END, "desktop-mode-embed-persist", onChange);
22762 addAction(HOOKS.WINDOW_RESIZE_END, "desktop-mode-embed-persist", onChange);
22763 }
22764 async function persist(placementId, geo) {
22765 try {
22766 const list2 = await listPlacements(0);
22767 const row = list2.placements.find((p) => p.id === placementId);
22768 const prevMeta = row?.meta ?? {};
22769 const nextMeta = {
22770 ...prevMeta,
22771 window: geo
22772 };
22773 await updatePlacement(placementId, { meta: nextMeta });
22774 } catch (err) {
22775 console.warn("[desktop-mode] embed window persist failed:", err);
22776 }
22777 }
22778 function clampGeometry(g, areaW, areaH) {
22779 const width = Math.max(MIN_W, Math.min(g.width, areaW - PADDING));
22780 const height = Math.max(MIN_H, Math.min(g.height, areaH - PADDING));
22781 const x = Math.max(0, Math.min(g.x, Math.max(0, areaW - width)));
22782 const y = Math.max(0, Math.min(g.y, Math.max(0, areaH - height)));
22783 return { x, y, width, height };
22784 }
22785 function hash(s) {
22786 let h = 0;
22787 for (let i = 0; i < s.length; i++) {
22788 h = (Math.imul(h, 31) + s.charCodeAt(i)) % 2147483647;
22789 }
22790 return Math.abs(h).toString(36);
22791 }
22792 function adminBase() {
22793 const cfg = window.wp?.desktop?.config;
22794 const url = cfg?.adminUrl ?? "/wp-admin/";
22795 return url.endsWith("/") ? url : `${url}/`;
22796 }
22797 function registerBuiltInFileOpeners() {
22798 registerOpener({
22799 id: "wp-post-editor",
22800 label: "Block Editor",
22801 types: ["post"],
22802 isDefault: true,
22803 sort: 10,
22804 handler: {
22805 kind: "url",
22806 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
22807 }
22808 });
22809 registerOpener({
22810 id: "wp-media-editor",
22811 label: "Media editor",
22812 types: ["attachment"],
22813 isDefault: true,
22814 sort: 10,
22815 handler: {
22816 kind: "url",
22817 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
22818 }
22819 });
22820 registerOpener({
22821 id: "wp-user-profile",
22822 label: "User profile",
22823 types: ["user"],
22824 isDefault: true,
22825 sort: 10,
22826 handler: {
22827 kind: "url",
22828 url: (file) => `${adminBase()}user-edit.php?user_id=${encodeURIComponent(file.ref())}`
22829 }
22830 });
22831 registerOpener({
22832 id: "wp-term-editor",
22833 label: "Term editor",
22834 types: ["term"],
22835 isDefault: true,
22836 sort: 10,
22837 handler: {
22838 kind: "url",
22839 url: (file) => {
22840 const [taxonomy, termId] = file.ref().split(":");
22841 return `${adminBase()}term.php?taxonomy=${encodeURIComponent(taxonomy ?? "")}&tag_ID=${encodeURIComponent(termId ?? "")}`;
22842 }
22843 }
22844 });
22845 registerOpener({
22846 id: "wp-comment-editor",
22847 label: "Comment editor",
22848 types: ["comment"],
22849 isDefault: true,
22850 sort: 10,
22851 handler: {
22852 kind: "url",
22853 url: (file) => `${adminBase()}comment.php?action=editcomment&c=${encodeURIComponent(file.ref())}`
22854 }
22855 });
22856 registerOpener({
22857 id: "desktop-mode-folder-window",
22858 label: "Open folder",
22859 types: ["folder"],
22860 isDefault: true,
22861 sort: 10,
22862 handler: {
22863 kind: "js",
22864 open: (file) => {
22865 const folderId = parseInt(file.ref(), 10);
22866 if (!folderId) {
22867 return;
22868 }
22869 const wm = window.wp?.desktop?.windowManager;
22870 if (!wm) {
22871 return;
22872 }
22873 const id = `desktop-mode-folder-${folderId}`;
22874 const folderRow = store.getState().folders.get(folderId);
22875 const viewerId2 = Number(window.desktopModeConfig?.currentUserId ?? 0);
22876 const isRecipient = !!folderRow && folderRow.ownerId > 0 && folderRow.ownerId !== viewerId2;
22877 const baseTitle = file.title();
22878 const titleWithCue = isRecipient ? `${baseTitle} · Shared` : baseTitle;
22879 wm.open({
22880 id,
22881 baseId: id,
22882 url: `#folder-${folderId}`,
22883 title: titleWithCue,
22884 icon: file.icon(),
22885 native: true,
22886 render: (body) => {
22887 body.replaceChildren();
22888 body.classList.add("desktop-mode-folder-window");
22889 const routes = [
22890 { folderId, title: file.title() }
22891 ];
22892 let currentDispose = null;
22893 const breadcrumbsHost = document.createElement("header");
22894 body.appendChild(breadcrumbsHost);
22895 const bodyHost = document.createElement("div");
22896 bodyHost.style.cssText = "flex:1 1 auto;min-height:0;display:flex;flex-direction:column;";
22897 body.appendChild(bodyHost);
22898 const paintBreadcrumbs = () => {
22899 const segments = routes.map(
22900 (route, idx) => {
22901 const isCurrent = idx === routes.length - 1;
22902 if (isCurrent) {
22903 return { label: route.title };
22904 }
22905 return {
22906 label: route.title,
22907 onClick: () => {
22908 routes.length = idx + 1;
22909 mountCurrent();
22910 }
22911 };
22912 }
22913 );
22914 renderBreadcrumbs(breadcrumbsHost, segments, {
22915 onBack: () => {
22916 if (routes.length <= 1) {
22917 return;
22918 }
22919 routes.pop();
22920 mountCurrent();
22921 },
22922 backDisabled: routes.length <= 1
22923 });
22924 };
22925 const mountCurrent = () => {
22926 currentDispose?.();
22927 currentDispose = null;
22928 bodyHost.replaceChildren();
22929 const split = document.createElement("div");
22930 split.className = "desktop-mode-folder-window__split";
22931 bodyHost.appendChild(split);
22932 const layerHost = document.createElement("div");
22933 layerHost.className = "desktop-mode-folder-window__layer";
22934 split.appendChild(layerHost);
22935 const previewPane = document.createElement("div");
22936 previewPane.className = "desktop-mode-folder-window__preview";
22937 previewPane.appendChild(renderPreviewEmpty());
22938 split.appendChild(previewPane);
22939 const route = routes[routes.length - 1];
22940 const layer = mountFilesLayer(
22941 layerHost,
22942 route.folderId
22943 );
22944 const offSelection = layer.onSelectionChange(
22945 (placement) => {
22946 if (!placement) {
22947 previewPane.replaceChildren(
22948 renderPreviewEmpty()
22949 );
22950 return;
22951 }
22952 renderPlacementPreview(
22953 placement,
22954 previewPane
22955 );
22956 }
22957 );
22958 const dblClickHandler = (e) => {
22959 if (!(e.target instanceof Element)) {
22960 return;
22961 }
22962 const tile2 = e.target.closest(
22963 ".desktop-mode-file-tile"
22964 );
22965 if (!tile2) {
22966 return;
22967 }
22968 if (tile2.dataset.fileType !== "folder") {
22969 return;
22970 }
22971 const subId = parseInt(
22972 tile2.dataset.fileRef ?? "",
22973 10
22974 );
22975 if (!subId) {
22976 return;
22977 }
22978 e.preventDefault();
22979 e.stopPropagation();
22980 const subTitle = tile2.querySelector(
22981 ".desktop-mode-file-tile__label"
22982 )?.textContent ?? `#${subId}`;
22983 routes.push({
22984 folderId: subId,
22985 title: subTitle
22986 });
22987 mountCurrent();
22988 };
22989 layerHost.addEventListener(
22990 "dblclick",
22991 dblClickHandler,
22992 true
22993 );
22994 const menu = attachIconCanvasMenu(layerHost, {
22995 scope: `desktop-mode-folder:${route.folderId}`,
22996 onSort: (mode) => layer.sort(mode),
22997 extraItems: [
22998 {
22999 id: "new-folder",
23000 label: "New folder",
23001 icon: "dashicons-portfolio",
23002 sort: 5,
23003 onClick: () => {
23004 openCreateFolderDialog({
23005 onSubmit: async (name) => {
23006 const folder = await createFolder({
23007 name
23008 });
23009 const peers = store.getState().placementsByFolder.get(
23010 route.folderId
23011 ) ?? [];
23012 const occupied = buildOccupiedSet(peers);
23013 const cell = snapToEmptyCell(
23014 GRID_PADDING,
23015 GRID_PADDING,
23016 occupied,
23017 layerHost
23018 );
23019 const placement = await createPlacement({
23020 type: "folder",
23021 ref: String(folder.id),
23022 parentId: route.folderId,
23023 x: cell.x,
23024 y: cell.y
23025 });
23026 store.upsertFolder(folder);
23027 store.upsertPlacement(
23028 placement
23029 );
23030 }
23031 });
23032 }
23033 }
23034 ]
23035 });
23036 const status = mountFolderStatusBar(
23037 bodyHost,
23038 route.folderId
23039 );
23040 currentDispose = () => {
23041 offSelection();
23042 menu.dispose();
23043 status.dispose();
23044 layerHost.removeEventListener(
23045 "dblclick",
23046 dblClickHandler,
23047 true
23048 );
23049 layer.dispose();
23050 };
23051 paintBreadcrumbs();
23052 };
23053 mountCurrent();
23054 },
23055 width: 720,
23056 height: 480,
23057 minWidth: 360,
23058 minHeight: 240
23059 });
23060 }
23061 }
23062 });
23063 registerOpener({
23064 id: "desktop-mode-shortcut-opener",
23065 label: "Open shortcut",
23066 types: ["shortcut"],
23067 isDefault: true,
23068 sort: 10,
23069 handler: {
23070 kind: "js",
23071 open: (file) => {
23072 const extras = file.shape;
23073 const wp = window.wp?.desktop;
23074 if (!wp) {
23075 return;
23076 }
23077 if (extras.shortcutWindow && wp.openWindow) {
23078 wp.openWindow(extras.shortcutWindow);
23079 return;
23080 }
23081 if (extras.shortcutUrl && wp.windowManager) {
23082 try {
23083 const u = new URL(extras.shortcutUrl, window.location.origin);
23084 if (u.origin !== window.location.origin) {
23085 window.open(u.toString(), "_blank", "noopener,noreferrer");
23086 return;
23087 }
23088 const adminUrl = wp.config?.adminUrl;
23089 const id = adminUrl ? deriveWindowId(u.toString(), adminUrl) : `desktop-icon-${file.ref()}`;
23090 wp.windowManager.open({
23091 id,
23092 baseId: id,
23093 url: u.toString(),
23094 title: file.title(),
23095 icon: file.icon()
23096 });
23097 } catch {
23098 }
23099 }
23100 }
23101 }
23102 });
23103 registerOpener({
23104 id: "browser-navigate",
23105 label: "Open in browser",
23106 types: ["bookmark"],
23107 isDefault: true,
23108 sort: 10,
23109 handler: {
23110 kind: "js",
23111 open: (file) => {
23112 const url = file.ref();
23113 if (!url) {
23114 return;
23115 }
23116 window.open(url, "_blank", "noopener,noreferrer");
23117 }
23118 }
23119 });
23120 registerOpener({
23121 id: "desktop-mode-link-opener",
23122 label: "Open in browser",
23123 types: ["link"],
23124 isDefault: true,
23125 sort: 10,
23126 handler: {
23127 kind: "js",
23128 open: (file) => {
23129 const url = file.ref();
23130 if (!url) {
23131 return;
23132 }
23133 window.open(url, "_blank", "noopener,noreferrer");
23134 }
23135 }
23136 });
23137 registerOpener({
23138 id: "desktop-mode-embed-opener",
23139 label: "Open as window",
23140 types: ["embed"],
23141 isDefault: true,
23142 sort: 10,
23143 handler: {
23144 kind: "js",
23145 open: (file, ctx) => {
23146 openEmbedWindow(file, ctx);
23147 }
23148 }
23149 });
23150 }
23151 const TAB_ID = "desktop-mode-file-associations";
23152 function registerFileAssociationsTab() {
23153 registerSettingsTab({
23154 id: TAB_ID,
23155 label: "File Associations",
23156 order: 50,
23157 render(body) {
23158 renderTab(body);
23159 }
23160 });
23161 }
23162 function renderTab(body) {
23163 body.replaceChildren();
23164 const types = getTypes();
23165 if (types.length === 0) {
23166 const empty = document.createElement("p");
23167 empty.className = "desktop-mode-file-associations__empty";
23168 empty.textContent = "No file types are registered.";
23169 body.appendChild(empty);
23170 return;
23171 }
23172 const intro = document.createElement("p");
23173 intro.className = "desktop-mode-file-associations__intro";
23174 intro.textContent = "Pick which app opens each kind of file when you double-click it on the desktop.";
23175 body.appendChild(intro);
23176 const associations = getUserAssociations();
23177 const list2 = document.createElement("div");
23178 list2.className = "desktop-mode-file-associations__list";
23179 list2.setAttribute("role", "list");
23180 for (const type of types) {
23181 list2.appendChild(buildRow(type.type, type.label, associations));
23182 }
23183 body.appendChild(list2);
23184 }
23185 function buildRow(typeSlug, typeLabel, associations) {
23186 const row = document.createElement("div");
23187 row.className = "desktop-mode-file-associations__row";
23188 row.setAttribute("role", "listitem");
23189 row.dataset.fileType = typeSlug;
23190 const label = document.createElement("label");
23191 label.className = "desktop-mode-file-associations__label";
23192 label.textContent = typeLabel;
23193 row.appendChild(label);
23194 const candidates = getOpenersForType(typeSlug);
23195 if (candidates.length === 0) {
23196 const empty = document.createElement("span");
23197 empty.className = "desktop-mode-file-associations__none";
23198 empty.textContent = "No app available";
23199 row.appendChild(empty);
23200 return row;
23201 }
23202 const resolved = resolveOpener(typeSlug);
23203 const currentId = associations[typeSlug] ?? resolved?.id ?? "";
23204 const select = document.createElement("wpd-select");
23205 select.setAttribute("value", currentId);
23206 select.setAttribute("aria-label", `Default app for ${typeLabel}`);
23207 select.className = "desktop-mode-file-associations__select";
23208 label.htmlFor = `assoc-${typeSlug}`;
23209 select.id = `assoc-${typeSlug}`;
23210 for (const o of candidates) {
23211 const opt = document.createElement("wpd-option");
23212 opt.setAttribute("value", o.id);
23213 opt.textContent = o.isDefault ? `${o.label} (default)` : o.label;
23214 select.appendChild(opt);
23215 }
23216 select.addEventListener("wpd-pick", (e) => {
23217 const next = e.detail?.value;
23218 if (!next) {
23219 return;
23220 }
23221 const merged = { ...getUserAssociations(), [typeSlug]: next };
23222 setUserAssociations(merged);
23223 void saveAssociations(merged).catch((err) => {
23224 console.error("[desktop-mode] saveAssociations failed:", err);
23225 });
23226 });
23227 row.appendChild(select);
23228 return row;
23229 }
23230 let _store$1 = null;
23231 function sharesStore() {
23232 if (!_store$1) {
23233 _store$1 = createSharedStore("desktop-files/shares", () => ({
23234 byFolder: /* @__PURE__ */ new Map(),
23235 pending: [],
23236 sharesVersion: 0,
23237 deniedFolders: /* @__PURE__ */ new Set()
23238 }));
23239 }
23240 return _store$1;
23241 }
23242 function setSharesForFolder(folderId, shares) {
23243 const s = sharesStore();
23244 s.state.byFolder.set(folderId, shares);
23245 s.notify();
23246 }
23247 function upsertShare(share) {
23248 if (!share || typeof share.folderId !== "number") {
23249 return;
23250 }
23251 const s = sharesStore();
23252 const existing = s.state.byFolder.get(share.folderId) ?? [];
23253 const next = existing.filter((r) => r.id !== share.id);
23254 next.push(share);
23255 s.state.byFolder.set(share.folderId, next);
23256 s.notify();
23257 }
23258 function removeShare(folderId, shareId) {
23259 const s = sharesStore();
23260 const existing = s.state.byFolder.get(folderId) ?? [];
23261 s.state.byFolder.set(
23262 folderId,
23263 existing.filter((r) => r.id !== shareId)
23264 );
23265 s.notify();
23266 }
23267 function inviteEquals(a, b) {
23268 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;
23269 }
23270 function ingestPendingInvites(invites) {
23271 const s = sharesStore();
23272 const existingById = new Map(s.state.pending.map((p) => [p.id, p]));
23273 let mutated = false;
23274 for (const inv of invites) {
23275 if (s.state.deniedFolders.has(inv.folderId)) {
23276 continue;
23277 }
23278 const existing = existingById.get(inv.id);
23279 if (existing) {
23280 if (inviteEquals(existing, inv)) {
23281 continue;
23282 }
23283 s.state.pending = s.state.pending.map((p) => p.id === inv.id ? inv : p);
23284 } else {
23285 s.state.pending.push(inv);
23286 }
23287 if (inv.invitedAtMs > s.state.sharesVersion) {
23288 s.state.sharesVersion = inv.invitedAtMs;
23289 }
23290 mutated = true;
23291 }
23292 if (mutated) {
23293 s.notify();
23294 }
23295 }
23296 function dropPending(shareId, opts = {}) {
23297 const s = sharesStore();
23298 s.state.pending = s.state.pending.filter((p) => p.id !== shareId);
23299 if (opts.denied && typeof opts.folderId === "number") {
23300 s.state.deniedFolders.add(opts.folderId);
23301 }
23302 s.notify();
23303 }
23304 const modalStyles = css`:host{display:none;position:fixed;inset:0;align-items:center;justify-content:center;background:rgba( 0,0,0,0.45 );backdrop-filter:blur( 2px );z-index:10000}:host( [ open ] ){display:flex}.dialog{max-width:92vw;max-height:90vh;background:var( --wpd-modal-bg,var( --desktop-mode-bg,#1d2327 ) );color:var( --wpd-modal-fg,var( --desktop-mode-fg,#fff ) );border:1px solid rgba( 255,255,255,0.08 );border-radius:10px;box-shadow:0 20px 50px rgba( 0,0,0,0.6 );display:flex;flex-direction:column;overflow:hidden}:host( [ size='sm' ] ) .dialog{width:min( 360px,92vw )}:host(:not( [ size ] ) ) .dialog,:host( [ size='md' ] ) .dialog{width:min( 540px,92vw )}:host( [ size='lg' ] ) .dialog{width:min( 760px,94vw )}.header{display:flex;align-items:center;gap:10px;padding:16px 20px 12px;border-bottom:1px solid rgba( 255,255,255,0.06 )}.title{margin:0;flex:1;font-size:15px;font-weight:600}.header-actions{display:flex;gap:6px}.header-actions::slotted( * ){margin-inline-start:6px}.close{background:transparent;border:0;color:inherit;font-size:18px;line-height:1;padding:4px 8px;border-radius:4px;cursor:pointer;opacity:0.7}.close:hover{opacity:1;background:rgba( 255,255,255,0.08 )}.body{padding:16px 20px;overflow:auto;flex:1 1 auto;font-size:13px;line-height:1.5}.footer{padding:12px 20px 16px;border-top:1px solid rgba( 255,255,255,0.06 )}.footer slot{display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap}:host( [ mandatory ] ) .close{display:none}`;
23305 const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
23306 const _WpdModal = class _WpdModal extends Component {
23307 constructor() {
23308 super(...arguments);
23309 this._prevFocus = null;
23310 this._onKey = (e) => {
23311 if (e.key === "Escape" && !this.hasAttribute("mandatory")) {
23312 e.preventDefault();
23313 this._cancel();
23314 return;
23315 }
23316 if (e.key === "Tab") {
23317 const f = this._focusables();
23318 if (f.length === 0) {
23319 return;
23320 }
23321 const first = f[0];
23322 const last = f[f.length - 1];
23323 const doc = this.ownerDocument;
23324 const fallback = doc ? doc.activeElement : null;
23325 const active2 = e.composedPath()[0] || fallback;
23326 if (e.shiftKey && active2 === first) {
23327 e.preventDefault();
23328 last.focus();
23329 } else if (!e.shiftKey && active2 === last) {
23330 e.preventDefault();
23331 first.focus();
23332 }
23333 }
23334 };
23335 this._onBackdrop = (e) => {
23336 if (this.hasAttribute("mandatory")) {
23337 return;
23338 }
23339 const path = e.composedPath();
23340 const original = path.length > 0 ? path[0] : e.target;
23341 if (original === this) {
23342 this._cancel();
23343 }
23344 };
23345 }
23346 connectedCallback() {
23347 super.connectedCallback();
23348 this.setAttribute("role", "dialog");
23349 this.setAttribute("aria-modal", "true");
23350 this.addEventListener("keydown", this._onKey);
23351 this.addEventListener("click", this._onBackdrop);
23352 }
23353 disconnectedCallback() {
23354 this.removeEventListener("keydown", this._onKey);
23355 this.removeEventListener("click", this._onBackdrop);
23356 }
23357 attributeChangedCallback(name, oldValue, newValue) {
23358 super.attributeChangedCallback?.(name, oldValue, newValue);
23359 if (name === "open") {
23360 if (newValue !== null) {
23361 const doc = this.ownerDocument;
23362 this._prevFocus = doc ? doc.activeElement : null;
23363 queueMicrotask(() => this._focusFirst());
23364 } else if (this._prevFocus) {
23365 try {
23366 this._prevFocus.focus();
23367 } catch (e) {
23368 }
23369 this._prevFocus = null;
23370 }
23371 }
23372 }
23373 showModal() {
23374 this.setAttribute("open", "");
23375 }
23376 hideModal() {
23377 this.removeAttribute("open");
23378 }
23379 _focusables() {
23380 const root = this.shadowRoot;
23381 if (!root) {
23382 return [];
23383 }
23384 const slotted = Array.from(this.querySelectorAll(FOCUSABLE));
23385 const inShadow = Array.from(root.querySelectorAll(FOCUSABLE));
23386 return [...slotted, ...inShadow].filter((el) => el.offsetParent !== null || el.tagName === "BUTTON");
23387 }
23388 _focusFirst() {
23389 const f = this._focusables();
23390 if (f.length > 0) {
23391 f[0].focus();
23392 } else {
23393 const inner = this.shadowRoot?.querySelector(".dialog");
23394 inner?.focus?.();
23395 }
23396 }
23397 _cancel() {
23398 const ev = new CustomEvent("wpd-modal-cancel", {
23399 bubbles: true,
23400 cancelable: true,
23401 composed: true
23402 });
23403 const allowed = this.dispatchEvent(ev);
23404 if (allowed) {
23405 this.hideModal();
23406 }
23407 }
23408 render() {
23409 const title = this.getAttribute("title") ?? "";
23410 const mandatory = this.hasAttribute("mandatory");
23411 return html`
23412 <div class="dialog" tabindex="-1">
23413 ${title ? html`
23414 <div class="header">
23415 <h2 class="title">${title}</h2>
23416 <div class="header-actions">
23417 <slot name="header-actions"></slot>
23418 ${mandatory ? html`` : html`<button
23419 type="button"
23420 class="close"
23421 aria-label="Close"
23422 @click=${() => this._cancel()}
23423 >×</button>`}
23424 </div>
23425 </div>
23426 ` : html``}
23427 <div class="body">
23428 <slot></slot>
23429 </div>
23430 <div class="footer">
23431 <slot name="footer"></slot>
23432 </div>
23433 </div>
23434 `;
23435 }
23436 };
23437 _WpdModal.props = ["open", "title", "size", "mandatory"];
23438 _WpdModal.styles = [modalStyles];
23439 _WpdModal.help = {
23440 title: "Modal overlay",
23441 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.",
23442 status: "experimental",
23443 since: "0.18.0",
23444 props: [
23445 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
23446 { name: "title", type: "string", description: "Heading shown at the top of the dialog." },
23447 { name: "size", type: "'sm' | 'md' | 'lg'", default: "md", description: "Width preset." },
23448 {
23449 name: "mandatory",
23450 type: "boolean attribute",
23451 description: "Disables ESC, click-outside and the close button."
23452 }
23453 ],
23454 slots: [
23455 { name: "(default)", description: "Body content." },
23456 { name: "footer", description: "Footer button row, right-aligned." },
23457 { name: "header-actions", description: "Extra actions next to the close button." }
23458 ],
23459 events: [
23460 {
23461 name: "wpd-modal-cancel",
23462 description: "Fires when the user dismisses the modal (ESC, click-outside, close button). Cancelable; calling `preventDefault()` keeps the modal open."
23463 }
23464 ]
23465 };
23466 let WpdModal = _WpdModal;
23467 defineComponent("wpd-modal", WpdModal);
23468 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}`;
23469 const _WpdUserSearch = class _WpdUserSearch extends Component {
23470 constructor() {
23471 super(...arguments);
23472 this._timer = null;
23473 this._abort = null;
23474 this._results = [];
23475 this._query = "";
23476 this._open = false;
23477 this._phase = "idle";
23478 this._error = "";
23479 this._dropdownStyle = "";
23480 this._onScrollOrResize = () => void 0;
23481 this._onInput = (e) => {
23482 const value = e.target.value;
23483 this._query = value;
23484 this._scheduleSearch(value);
23485 };
23486 this._onFocus = () => {
23487 if (this._results.length === 0 && this._phase === "idle") {
23488 this._scheduleSearch(this._query);
23489 return;
23490 }
23491 this._open = true;
23492 this._positionDropdown();
23493 this.requestUpdate();
23494 };
23495 this._onBlur = () => {
23496 setTimeout(() => {
23497 this._open = false;
23498 this.requestUpdate();
23499 }, 150);
23500 };
23501 this._pick = (user) => {
23502 this.emit("wpd-user-pick", { user });
23503 this._results = [];
23504 this._open = false;
23505 this._phase = "idle";
23506 this._query = "";
23507 const input = this.shadowRoot?.querySelector(".input");
23508 if (input) {
23509 input.value = "";
23510 }
23511 this.requestUpdate();
23512 };
23513 }
23514 connectedCallback() {
23515 super.connectedCallback();
23516 this._onScrollOrResize = () => {
23517 if (this._open) {
23518 this._positionDropdown();
23519 this.requestUpdate();
23520 }
23521 };
23522 window.addEventListener("resize", this._onScrollOrResize);
23523 window.addEventListener("scroll", this._onScrollOrResize, true);
23524 }
23525 disconnectedCallback() {
23526 if (this._timer) {
23527 clearTimeout(this._timer);
23528 }
23529 if (this._abort) {
23530 this._abort.abort();
23531 }
23532 window.removeEventListener("resize", this._onScrollOrResize);
23533 window.removeEventListener("scroll", this._onScrollOrResize, true);
23534 }
23535 _endpoint() {
23536 const attr = this.getAttribute("endpoint");
23537 if (attr) {
23538 return attr;
23539 }
23540 return window.desktopModeConfig?.filesUsersSearchUrl || "";
23541 }
23542 _scheduleSearch(q) {
23543 if (this._timer) {
23544 clearTimeout(this._timer);
23545 }
23546 this._phase = "loading";
23547 this._open = true;
23548 this._positionDropdown();
23549 this.requestUpdate();
23550 this._timer = setTimeout(() => this._runSearch(q), 200);
23551 }
23552 async _runSearch(q) {
23553 const url = this._endpoint();
23554 if (!url) {
23555 this._phase = "error";
23556 this._error = "Search endpoint is not configured.";
23557 this._results = [];
23558 this._open = true;
23559 this.requestUpdate();
23560 return;
23561 }
23562 if (this._abort) {
23563 this._abort.abort();
23564 }
23565 const ctrl = new AbortController();
23566 this._abort = ctrl;
23567 const exclude = this.getAttribute("exclude") || "";
23568 const full = url + "?q=" + encodeURIComponent(q) + "&exclude=" + encodeURIComponent(exclude);
23569 try {
23570 const init2 = {
23571 signal: ctrl.signal,
23572 credentials: "same-origin"
23573 };
23574 const res = await trackedFetch$1(full, init2, {
23575 source: "desktop-mode/files-user-search",
23576 silent: true
23577 });
23578 if (!res.ok) {
23579 throw new Error(`HTTP ${res.status}`);
23580 }
23581 const json = await res.json();
23582 this._results = json && Array.isArray(json.users) ? json.users : [];
23583 this._phase = "ready";
23584 this._error = "";
23585 this._open = true;
23586 } catch (e) {
23587 if (e.name === "AbortError") {
23588 return;
23589 }
23590 this._results = [];
23591 this._phase = "error";
23592 this._error = e.message || "Search failed.";
23593 this._open = true;
23594 }
23595 this._positionDropdown();
23596 this.requestUpdate();
23597 }
23598 _positionDropdown() {
23599 const input = this.shadowRoot?.querySelector(".input");
23600 if (!input) {
23601 return;
23602 }
23603 const rect = input.getBoundingClientRect();
23604 const top = rect.bottom + 4;
23605 const left = rect.left;
23606 const width = rect.width;
23607 const viewportH = window.innerHeight;
23608 const spaceBelow = viewportH - rect.bottom;
23609 const spaceAbove = rect.top;
23610 const maxHeight = Math.max(120, Math.min(280, Math.max(spaceBelow, spaceAbove) - 16));
23611 if (spaceBelow < 200 && spaceAbove > spaceBelow) {
23612 this._dropdownStyle = [
23613 "position:fixed",
23614 `left:${left}px`,
23615 `top:${rect.top - 4 - maxHeight}px`,
23616 `width:${width}px`,
23617 `max-height:${maxHeight}px`
23618 ].join(";");
23619 } else {
23620 this._dropdownStyle = [
23621 "position:fixed",
23622 `left:${left}px`,
23623 `top:${top}px`,
23624 `width:${width}px`,
23625 `max-height:${maxHeight}px`
23626 ].join(";");
23627 }
23628 }
23629 _dropdownContent() {
23630 if (this._phase === "loading") {
23631 return html`<div class="empty">Searching…</div>`;
23632 }
23633 if (this._phase === "error") {
23634 return html`<div class="empty error">${this._error}</div>`;
23635 }
23636 if (this._results.length === 0) {
23637 const message = this._query ? "No matches." : "No users available.";
23638 return html`<div class="empty">${message}</div>`;
23639 }
23640 return this._results.map(
23641 (u) => html`
23642 <button
23643 type="button"
23644 class="item"
23645 role="option"
23646 @mousedown=${(e) => e.preventDefault()}
23647 @click=${() => this._pick(u)}
23648 >
23649 <img class="avatar" src=${u.avatarUrl} alt="" />
23650 <div>
23651 <div class="name">${u.name}</div>
23652 <div class="slug">${u.slug}</div>
23653 </div>
23654 </button>
23655 `
23656 );
23657 }
23658 render() {
23659 const placeholder = this.getAttribute("placeholder") || "Search users…";
23660 return html`
23661 <input
23662 class="input"
23663 type="search"
23664 placeholder=${placeholder}
23665 autocomplete="off"
23666 @input=${this._onInput}
23667 @focus=${this._onFocus}
23668 @blur=${this._onBlur}
23669 .value=${this._query}
23670 />
23671 ${this._open ? html`
23672 <div class="dropdown" role="listbox" style=${this._dropdownStyle}>
23673 ${this._dropdownContent()}
23674 </div>
23675 ` : html``}
23676 `;
23677 }
23678 };
23679 _WpdUserSearch.props = ["placeholder", "exclude", "endpoint"];
23680 _WpdUserSearch.styles = [userSearchStyles];
23681 _WpdUserSearch.help = {
23682 title: "User autocomplete",
23683 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.",
23684 status: "experimental",
23685 since: "0.18.0",
23686 props: [
23687 { name: "placeholder", type: "string", description: "Input placeholder text." },
23688 {
23689 name: "exclude",
23690 type: "csv user ids",
23691 description: "Already-picked user ids to suppress in results."
23692 },
23693 {
23694 name: "endpoint",
23695 type: "URL",
23696 description: "Override the search URL (defaults to desktopModeConfig.filesUsersSearchUrl)."
23697 }
23698 ],
23699 events: [
23700 { name: "wpd-user-pick", description: "Emitted on pick. Detail: `{ user: SearchUser }`." }
23701 ]
23702 };
23703 let WpdUserSearch = _WpdUserSearch;
23704 defineComponent("wpd-user-search", WpdUserSearch);
23705 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}`;
23706 const _WpdRolePicker = class _WpdRolePicker extends Component {
23707 constructor() {
23708 super(...arguments);
23709 this._onToggle = (slug) => {
23710 const selected = !this._selectedSet().has(slug);
23711 this.emit("wpd-role-toggle", { slug, selected });
23712 };
23713 }
23714 _selectedSet() {
23715 const raw = this.getAttribute("selected") || "";
23716 return new Set(
23717 raw.split(",").map((s) => s.trim()).filter((s) => s !== "")
23718 );
23719 }
23720 _roles() {
23721 const attr = this.getAttribute("roles");
23722 if (attr) {
23723 try {
23724 const parsed = JSON.parse(attr);
23725 if (Array.isArray(parsed)) {
23726 return parsed;
23727 }
23728 } catch (e) {
23729 }
23730 }
23731 return window.desktopModeConfig?.shareEligibleRoles || [];
23732 }
23733 render() {
23734 const roles = this._roles();
23735 if (roles.length === 0) {
23736 return html`<span class="empty">No eligible roles.</span>`;
23737 }
23738 const set = this._selectedSet();
23739 return html`
23740 ${roles.map((r) => {
23741 const isSelected = set.has(r.slug);
23742 return html`
23743 <button
23744 type="button"
23745 class="chip"
23746 aria-pressed=${isSelected ? "true" : "false"}
23747 @click=${() => this._onToggle(r.slug)}
23748 >${r.name}</button>
23749 `;
23750 })}
23751 `;
23752 }
23753 };
23754 _WpdRolePicker.props = ["selected", "roles"];
23755 _WpdRolePicker.styles = [rolePickerStyles];
23756 _WpdRolePicker.help = {
23757 title: "Role picker",
23758 summary: "Chip multi-select for WordPress roles. Reads eligible roles from desktopModeConfig.shareEligibleRoles; emits wpd-role-toggle { slug, selected } on every change.",
23759 status: "experimental",
23760 since: "0.18.0",
23761 props: [
23762 {
23763 name: "selected",
23764 type: "csv role slugs",
23765 description: "Comma-separated role slugs that are currently selected."
23766 },
23767 {
23768 name: "roles",
23769 type: "JSON",
23770 description: "Override the source of eligible roles (defaults to the global config)."
23771 }
23772 ],
23773 events: [
23774 {
23775 name: "wpd-role-toggle",
23776 description: "Emitted on every click. Detail: `{ slug, selected }`."
23777 }
23778 ]
23779 };
23780 let WpdRolePicker = _WpdRolePicker;
23781 defineComponent("wpd-role-picker", WpdRolePicker);
23782 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}`;
23783 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}`;
23784 const _WpdSegment = class _WpdSegment extends Component {
23785 render() {
23786 this.setAttribute("role", "radio");
23787 return html`
23788 <button type="button" @click=${() => this._onPick()}>
23789 <slot></slot>
23790 </button>
23791 `;
23792 }
23793 _onPick() {
23794 this.emit("wpd-segment-pick", {
23795 value: this.value
23796 });
23797 }
23798 };
23799 _WpdSegment.props = ["value"];
23800 _WpdSegment.styles = [segmentStyles];
23801 _WpdSegment.help = {
23802 title: "Segment",
23803 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
23804 status: "stable",
23805 since: "0.9.0",
23806 props: [
23807 {
23808 name: "value",
23809 type: "string",
23810 description: "Identifier this segment contributes to the parent group selection."
23811 }
23812 ],
23813 slots: [
23814 { name: "(default)", description: "Visible segment label." }
23815 ],
23816 events: [
23817 {
23818 name: "wpd-segment-pick",
23819 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
23820 detail: "{ value: string }"
23821 }
23822 ]
23823 };
23824 let WpdSegment = _WpdSegment;
23825 defineComponent("wpd-segment", WpdSegment);
23826 const _WpdSegmented = class _WpdSegmented extends Component {
23827 connectedCallback() {
23828 super.connectedCallback();
23829 this.addEventListener("wpd-segment-pick", (e) => {
23830 const detail = e.detail;
23831 e.stopPropagation();
23832 this.value = detail.value;
23833 this.emit("wpd-pick", { value: detail.value });
23834 });
23835 }
23836 /**
23837 * Declarative item-list setter. Replaces the existing
23838 * `<wpd-segment>` children with a fresh set built from a
23839 * `{ value, label }` array; preserves the current selection
23840 * when the value still matches an entry, otherwise falls back
23841 * to the first item.
23842 *
23843 * Collapses the pre-0.11 imperative dance (clear children,
23844 * `createElement`, set `textContent`, `appendChild`, then
23845 * `setAttribute('value', …)` on the group — order matters) to
23846 * a single assignment:
23847 *
23848 * ```js
23849 * segmented.items = [
23850 * { value: 'm', label: 'm' },
23851 * { value: 'km', label: 'km' },
23852 * ];
23853 * ```
23854 *
23855 * @since 0.11.0
23856 */
23857 set items(list2) {
23858 const existing = this.querySelectorAll(":scope > wpd-segment");
23859 for (const el of Array.from(existing)) {
23860 el.remove();
23861 }
23862 for (const item of list2) {
23863 const seg = document.createElement("wpd-segment");
23864 seg.setAttribute("value", item.value);
23865 seg.textContent = item.label;
23866 this.appendChild(seg);
23867 }
23868 const current = this.value;
23869 const stillValid = current !== null && list2.some((i) => i.value === current);
23870 if (!stillValid && list2.length > 0) {
23871 this.value = list2[0].value;
23872 } else {
23873 this.requestUpdate();
23874 }
23875 }
23876 render() {
23877 const label = this.label || "";
23878 if (label) {
23879 this.setAttribute("aria-label", label);
23880 }
23881 this.setAttribute("role", "radiogroup");
23882 const current = this.value;
23883 queueMicrotask(() => {
23884 const segs = this.querySelectorAll("wpd-segment");
23885 for (const seg of Array.from(segs)) {
23886 const v = seg.getAttribute("value");
23887 seg.setAttribute(
23888 "aria-checked",
23889 v === current ? "true" : "false"
23890 );
23891 }
23892 });
23893 return html`<slot></slot>`;
23894 }
23895 };
23896 _WpdSegmented.props = ["value", "label"];
23897 _WpdSegmented.styles = [segmentedStyles];
23898 _WpdSegmented.help = {
23899 title: "Segmented",
23900 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
23901 status: "stable",
23902 since: "0.9.0",
23903 props: [
23904 {
23905 name: "value",
23906 type: "string",
23907 description: "Currently selected segment value. Mirrored onto child aria-checked."
23908 },
23909 {
23910 name: "label",
23911 type: "string",
23912 description: "aria-label for the radiogroup."
23913 }
23914 ],
23915 slots: [
23916 { name: "(default)", description: '<wpd-segment value="…"> children.' }
23917 ],
23918 events: [
23919 {
23920 name: "wpd-pick",
23921 description: "Fires when the selected segment changes.",
23922 detail: "{ value: string }"
23923 }
23924 ],
23925 cssProps: [
23926 { name: "--desktop-mode-window-bg", description: "Pill background." },
23927 { name: "--desktop-mode-text", description: "Active label colour." },
23928 { name: "--desktop-mode-muted", description: "Inactive label colour." }
23929 ],
23930 example: html`
23931 <wpd-segmented value="md" label="Dock size">
23932 <wpd-segment value="sm">Small</wpd-segment>
23933 <wpd-segment value="md">Medium</wpd-segment>
23934 <wpd-segment value="lg">Large</wpd-segment>
23935 </wpd-segmented>
23936 `
23937 };
23938 let WpdSegmented = _WpdSegmented;
23939 defineComponent("wpd-segmented", WpdSegmented);
23940 const styles$3 = css`:host{display:inline-flex}:host( [ fill-cell ] ){display:flex;width:100%}button{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:var( --wpd-button-padding,6px 12px );border-radius:var( --wpd-button-border-radius,6px );font:inherit;font-weight:500;cursor:pointer;transition:background-color 0.12s ease,color 0.12s ease,border-color 0.12s ease;background:var( --wpd-button-bg,transparent );color:var( --wpd-button-fg,var( --desktop-mode-text,#1d2327 ) );border:var( --wpd-button-border,1px solid var( --desktop-mode-border,#c3c4c7 ) )}:host( [ fill-cell ] ) button{width:100%;min-height:var( --wpd-button-min-height,44px )}button:disabled{opacity:0.5;cursor:not-allowed}button:hover:not(:disabled ){background:rgba( 0,0,0,0.04 )}:host( [ variant='primary' ] ) button{background:var( --wpd-button-bg,var( --wp-admin-theme-color,#2271b1 ) );color:var( --wpd-button-fg,#fff );border:var( --wpd-button-border,1px solid transparent )}:host( [ variant='primary' ] ) button:hover:not(:disabled ){filter:brightness( 1.06 );background:var( --wpd-button-bg,var( --wp-admin-theme-color,#2271b1 ) )}:host( [ variant='secondary' ] ) button{background:var( --wpd-button-bg,rgba( 0,0,0,0.06 ) );color:var( --wpd-button-fg,var( --desktop-mode-text,#1d2327 ) );border:var( --wpd-button-border,1px solid transparent )}:host( [ variant='secondary' ] ) button:hover:not(:disabled ){background:var( --wpd-button-bg-hover,rgba( 0,0,0,0.1 ) )}:host( [ variant='danger' ] ) button{background:var( --wpd-button-bg,transparent );color:var( --wpd-button-fg,#d63638 );border:var( --wpd-button-border,1px solid currentColor )}:host( [ variant='danger' ] ) button:hover:not(:disabled ){background:#d63638;color:#fff}:host( [ variant='link' ] ) button{background:transparent;color:var( --wpd-button-fg,var( --wp-admin-theme-color,#2271b1 ) );border:0;padding:0;text-decoration:underline}:host( [ busy ] ) button{pointer-events:none;opacity:0.75}`;
23941 const _WpdButton = class _WpdButton extends Component {
23942 render() {
23943 const disabled = this.disabled !== null;
23944 const type = this.type || "button";
23945 return html`
23946 <button part="button" type=${type} ?disabled=${disabled}>
23947 <slot></slot>
23948 </button>
23949 `;
23950 }
23951 };
23952 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
23953 _WpdButton.styles = [styles$3];
23954 _WpdButton.help = {
23955 title: "Button",
23956 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
23957 status: "stable",
23958 since: "0.9.0",
23959 props: [
23960 {
23961 name: "variant",
23962 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
23963 default: "ghost",
23964 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
23965 },
23966 {
23967 name: "disabled",
23968 type: "boolean attribute",
23969 description: "Disable pointer + keyboard interaction and dim the chrome."
23970 },
23971 {
23972 name: "type",
23973 type: "'button' | 'submit' | 'reset'",
23974 default: "button",
23975 description: "Forwarded to the underlying native <button>."
23976 },
23977 {
23978 name: "busy",
23979 type: "boolean attribute",
23980 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
23981 },
23982 {
23983 name: "fill-cell",
23984 type: "boolean attribute",
23985 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
23986 }
23987 ],
23988 slots: [{ name: "(default)", description: "Button label." }],
23989 parts: [{ name: "button", description: "Underlying <button> element." }],
23990 cssProps: [
23991 { name: "--wpd-button-bg", description: "Background color." },
23992 { name: "--wpd-button-fg", description: "Text color." },
23993 { name: "--wpd-button-border", description: "Border shorthand." },
23994 { name: "--wpd-button-border-radius", default: "6px" },
23995 { name: "--wpd-button-padding", default: "6px 12px" },
23996 {
23997 name: "--wpd-button-min-height",
23998 description: "Minimum height when fill-cell is set."
23999 }
24000 ],
24001 example: html`
24002 <wpd-cluster gap="8">
24003 <wpd-button variant="primary">Primary</wpd-button>
24004 <wpd-button variant="secondary">Secondary</wpd-button>
24005 <wpd-button variant="ghost">Ghost</wpd-button>
24006 <wpd-button variant="danger">Danger</wpd-button>
24007 <wpd-button variant="link">Link</wpd-button>
24008 </wpd-cluster>
24009 `
24010 };
24011 let WpdButton = _WpdButton;
24012 defineComponent("wpd-button", WpdButton);
24013 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}`;
24014 const toastStyles = css`:host{display:flex;align-items:center;gap:12px;min-width:280px;max-width:420px;padding:10px 14px;background:#1d2327;color:#fff;border-radius:8px;box-shadow:0 8px 24px rgba( 0,0,0,0.2 ),0 2px 6px rgba( 0,0,0,0.1 );font-size:13px;line-height:1.4;opacity:0;transform:translateY( -8px );transition:opacity 0.18s ease,transform 0.18s ease;pointer-events:auto}:host( [ state='in' ] ){opacity:1;transform:translateY( 0 )}:host( [ state='out' ] ){opacity:0;transform:translateY( -8px )}.wpd-toast__label{flex:1}button{flex-shrink:0;padding:4px 10px;border:none;border-radius:4px;background:rgba( 255,255,255,0.12 );color:#fff;font:inherit;font-size:12px;font-weight:500;cursor:pointer;transition:background-color 0.12s ease}button:hover{background:rgba( 255,255,255,0.22 )}button:focus-visible{outline:2px solid rgba( 255,255,255,0.6 );outline-offset:2px}@media ( prefers-reduced-motion:reduce ){:host{transition-duration:0.01ms}}`;
24015 const _WpdToastContainer = class _WpdToastContainer extends Component {
24016 connectedCallback() {
24017 super.connectedCallback();
24018 this.setAttribute("aria-live", "polite");
24019 }
24020 render() {
24021 return html`<slot></slot>`;
24022 }
24023 };
24024 _WpdToastContainer.styles = [containerStyles];
24025 _WpdToastContainer.help = {
24026 title: "Toast container",
24027 summary: "Singleton stack beneath <body> that hosts transient <wpd-toast> notifications in the top-right. Created lazily by showToast(); authors rarely place one themselves.",
24028 status: "stable",
24029 since: "0.9.0",
24030 slots: [
24031 { name: "(default)", description: "<wpd-toast> children, stacked vertically." }
24032 ],
24033 cssProps: [
24034 { name: "--desktop-mode-z-fullscreen", description: "z-index base — toasts sit above fullscreen windows." }
24035 ],
24036 example: html`
24037 <wpd-toast-container>
24038 <wpd-toast state="in">Settings saved.</wpd-toast>
24039 <wpd-toast state="in" action="Undo">Theme changed.</wpd-toast>
24040 </wpd-toast-container>
24041 `
24042 };
24043 let WpdToastContainer = _WpdToastContainer;
24044 defineComponent("wpd-toast-container", WpdToastContainer);
24045 const _WpdToast = class _WpdToast extends Component {
24046 connectedCallback() {
24047 super.connectedCallback();
24048 if (!this.hasAttribute("role")) {
24049 this.setAttribute("role", "status");
24050 }
24051 }
24052 render() {
24053 const action = this.action || "";
24054 return html`
24055 <span class="wpd-toast__label"><slot></slot></span>
24056 <button
24057 type="button"
24058 ?hidden=${!action}
24059 @click=${(e) => this._onAction(e)}
24060 >
24061 ${action}
24062 </button>
24063 `;
24064 }
24065 _onAction(e) {
24066 e.preventDefault();
24067 e.stopPropagation();
24068 this.emit("wpd-toast-action", {});
24069 }
24070 };
24071 _WpdToast.props = ["action", "state"];
24072 _WpdToast.styles = [toastStyles];
24073 _WpdToast.help = {
24074 title: "Toast",
24075 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.',
24076 status: "stable",
24077 since: "0.9.0",
24078 props: [
24079 {
24080 name: "action",
24081 type: "string",
24082 description: "Optional action button label. When set, a button renders on the right and emits wpd-toast-action on click."
24083 },
24084 {
24085 name: "state",
24086 type: "'in' | 'out'",
24087 description: 'Drives the CSS fade transition. Set to "in" when rendered, flip to "out" before removal.'
24088 }
24089 ],
24090 slots: [
24091 { name: "(default)", description: "Message text." }
24092 ],
24093 events: [
24094 {
24095 name: "wpd-toast-action",
24096 description: "Fires when the action button is clicked.",
24097 detail: "{}"
24098 }
24099 ],
24100 example: html`
24101 <wpd-toast state="in" action="Undo">Post moved to trash.</wpd-toast>
24102 `
24103 };
24104 let WpdToast = _WpdToast;
24105 defineComponent("wpd-toast", WpdToast);
24106 function buildCapSegmented(initial, onChange) {
24107 const segmented = document.createElement("wpd-segmented");
24108 segmented.setAttribute("value", initial);
24109 segmented.setAttribute("label", "Capability");
24110 segmented.style.setProperty("--wpd-segmented-bg", "rgba(255,255,255,0.06)");
24111 segmented.style.setProperty(
24112 "--desktop-mode-window-bg",
24113 "var(--wp-admin-theme-color, #2271b1)"
24114 );
24115 segmented.style.setProperty("--desktop-mode-text", "#fff");
24116 segmented.style.setProperty("--desktop-mode-muted", "rgba(255,255,255,0.65)");
24117 const segRead = document.createElement("wpd-segment");
24118 segRead.setAttribute("value", "read");
24119 segRead.textContent = "Read";
24120 segmented.appendChild(segRead);
24121 const segWrite = document.createElement("wpd-segment");
24122 segWrite.setAttribute("value", "write");
24123 segWrite.textContent = "Read + Write";
24124 segmented.appendChild(segWrite);
24125 segmented.addEventListener("wpd-pick", (e) => {
24126 const detail = e.detail;
24127 onChange(detail.value);
24128 });
24129 return segmented;
24130 }
24131 function buildIconButton(label, onClick, opts = {}) {
24132 const btn = document.createElement("wpd-button");
24133 btn.setAttribute("variant", "ghost");
24134 btn.setAttribute("aria-label", opts.danger ? "Remove" : "Dismiss");
24135 btn.textContent = label;
24136 const fg = opts.danger ? "#ff8080" : "rgba(255,255,255,0.75)";
24137 const border = opts.danger ? "1px solid rgba(255,128,128,0.45)" : "1px solid rgba(255,255,255,0.18)";
24138 btn.style.setProperty("--wpd-button-fg", fg);
24139 btn.style.setProperty("--wpd-button-border", border);
24140 btn.style.setProperty("--wpd-button-padding", "6px 12px");
24141 btn.style.setProperty("--wpd-button-border-radius", "7px");
24142 btn.style.setProperty("--wpd-button-min-height", "34px");
24143 btn.style.minWidth = "34px";
24144 btn.style.fontSize = "18px";
24145 btn.style.lineHeight = "1";
24146 btn.addEventListener("click", onClick);
24147 return btn;
24148 }
24149 async function openShareSettingsModal(opts) {
24150 const modal = document.createElement("wpd-modal");
24151 modal.setAttribute("open", "");
24152 modal.setAttribute("size", "lg");
24153 modal.setAttribute("title", `Share "${opts.folderName}"`);
24154 document.body.appendChild(modal);
24155 let shares = [];
24156 let pendingPicks = [];
24157 const renderBody = () => {
24158 modal.innerHTML = "";
24159 const owner = document.createElement("div");
24160 owner.style.cssText = "opacity:0.7;margin-bottom:14px;font-size:12px;";
24161 owner.textContent = opts.ownerName ? `Owner: ${opts.ownerName} — cannot be changed` : "Owner cannot be changed";
24162 modal.appendChild(owner);
24163 const addPeople = document.createElement("div");
24164 addPeople.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
24165 const addPeopleLabel = document.createElement("div");
24166 addPeopleLabel.textContent = "Add people";
24167 addPeopleLabel.style.cssText = "font-weight:600;";
24168 addPeople.appendChild(addPeopleLabel);
24169 const userSearch = document.createElement("wpd-user-search");
24170 const excludedUserIds = shares.filter((s) => s.principalType === "user").map((s) => s.principalRef).concat(pendingPicks.filter((p) => p.kind === "user").map((p) => p.ref));
24171 userSearch.setAttribute("exclude", excludedUserIds.join(","));
24172 userSearch.setAttribute("placeholder", "Search users…");
24173 userSearch.addEventListener("wpd-user-pick", (e) => {
24174 const detail = e.detail;
24175 pendingPicks.push({
24176 kind: "user",
24177 ref: String(detail.user.id),
24178 label: detail.user.name,
24179 cap: "read"
24180 });
24181 renderBody();
24182 });
24183 addPeople.appendChild(userSearch);
24184 modal.appendChild(addPeople);
24185 const addRoles = document.createElement("div");
24186 addRoles.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
24187 const addRolesLabel = document.createElement("div");
24188 addRolesLabel.textContent = "Add roles";
24189 addRolesLabel.style.cssText = "font-weight:600;";
24190 addRoles.appendChild(addRolesLabel);
24191 const rolePicker = document.createElement("wpd-role-picker");
24192 const grantedRoles = shares.filter((s) => s.principalType === "role").map((s) => s.principalRef);
24193 const pickedRoles = pendingPicks.filter((p) => p.kind === "role").map((p) => p.ref);
24194 rolePicker.setAttribute("selected", [...grantedRoles, ...pickedRoles].join(","));
24195 rolePicker.addEventListener("wpd-role-toggle", (e) => {
24196 const detail = e.detail;
24197 const existing = shares.find(
24198 (s) => s.principalType === "role" && s.principalRef === detail.slug
24199 );
24200 if (existing) {
24201 if (!detail.selected) {
24202 void revoke(existing);
24203 }
24204 return;
24205 }
24206 if (detail.selected) {
24207 const eligible = (window.desktopModeConfig?.shareEligibleRoles ?? []).find(
24208 (r) => r.slug === detail.slug
24209 );
24210 pendingPicks.push({
24211 kind: "role",
24212 ref: detail.slug,
24213 label: eligible ? eligible.name : detail.slug,
24214 cap: "read"
24215 });
24216 } else {
24217 pendingPicks = pendingPicks.filter(
24218 (p) => !(p.kind === "role" && p.ref === detail.slug)
24219 );
24220 }
24221 renderBody();
24222 });
24223 addRoles.appendChild(rolePicker);
24224 modal.appendChild(addRoles);
24225 if (pendingPicks.length > 0) {
24226 const pendingBlock = document.createElement("div");
24227 pendingBlock.style.cssText = "border:1px dashed rgba(255,255,255,0.18);border-radius:8px;padding:10px;margin-bottom:14px;";
24228 const pendingTitle = document.createElement("div");
24229 pendingTitle.textContent = "New invites (not sent yet)";
24230 pendingTitle.style.cssText = "font-weight:600;margin-bottom:6px;font-size:12px;";
24231 pendingBlock.appendChild(pendingTitle);
24232 for (const pick of pendingPicks) {
24233 const row = document.createElement("div");
24234 row.style.cssText = "display:flex;align-items:center;gap:8px;padding:4px 0;font-size:13px;";
24235 const tag = document.createElement("span");
24236 tag.textContent = pick.kind === "role" ? `Role: ${pick.label}` : pick.label;
24237 tag.style.flex = "1";
24238 row.appendChild(tag);
24239 const capSeg = buildCapSegmented(pick.cap, (next) => {
24240 pick.cap = next;
24241 });
24242 row.appendChild(capSeg);
24243 const removeBtn = buildIconButton("×", () => {
24244 pendingPicks = pendingPicks.filter(
24245 (p) => !(p.kind === pick.kind && p.ref === pick.ref)
24246 );
24247 renderBody();
24248 });
24249 row.appendChild(removeBtn);
24250 pendingBlock.appendChild(row);
24251 }
24252 const sendBtn = document.createElement("wpd-button");
24253 sendBtn.setAttribute("variant", "primary");
24254 sendBtn.textContent = `Send ${pendingPicks.length} invite${pendingPicks.length === 1 ? "" : "s"}`;
24255 sendBtn.style.marginTop = "8px";
24256 sendBtn.addEventListener("click", async () => {
24257 if (pendingPicks.length === 0) {
24258 return;
24259 }
24260 sendBtn.setAttribute("busy", "");
24261 sendBtn.setAttribute("disabled", "");
24262 const snapshot = pendingPicks.slice();
24263 let succeeded = 0;
24264 let firstError = null;
24265 for (const pick of snapshot) {
24266 try {
24267 await inviteShare(opts.folderId, {
24268 principalType: pick.kind,
24269 principalRef: pick.ref,
24270 capability: pick.cap
24271 });
24272 succeeded++;
24273 } catch (err) {
24274 firstError = err;
24275 break;
24276 }
24277 }
24278 if (succeeded > 0) {
24279 pendingPicks = pendingPicks.slice(succeeded);
24280 }
24281 try {
24282 await refresh();
24283 } catch (_e) {
24284 }
24285 if (firstError) {
24286 showToast({
24287 message: `Could not send invites: ${firstError.message}`
24288 });
24289 } else {
24290 showToast({
24291 message: 1 === succeeded ? "Invite sent." : `${succeeded} invites sent.`
24292 });
24293 }
24294 sendBtn.removeAttribute("busy");
24295 sendBtn.removeAttribute("disabled");
24296 renderBody();
24297 });
24298 pendingBlock.appendChild(sendBtn);
24299 modal.appendChild(pendingBlock);
24300 }
24301 const listTitle = document.createElement("div");
24302 listTitle.textContent = "Who has access";
24303 listTitle.style.cssText = "font-weight:600;margin:8px 0 6px;";
24304 modal.appendChild(listTitle);
24305 if (shares.length === 0) {
24306 const empty = document.createElement("div");
24307 empty.textContent = "Only you can see this folder.";
24308 empty.style.cssText = "opacity:0.6;font-size:12px;";
24309 modal.appendChild(empty);
24310 } else {
24311 for (const s of shares) {
24312 const row = document.createElement("div");
24313 row.style.cssText = "display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.04);";
24314 const label = document.createElement("div");
24315 label.style.flex = "1";
24316 label.textContent = s.principalType === "role" ? `Role: ${s.displayName}` : s.displayName;
24317 if (s.state === "pending") {
24318 const tag = document.createElement("span");
24319 tag.textContent = " · pending";
24320 tag.style.cssText = "opacity:0.6;font-size:12px;";
24321 label.appendChild(tag);
24322 } else if (s.state === "denied") {
24323 const tag = document.createElement("span");
24324 tag.textContent = " · denied";
24325 tag.style.cssText = "color:#d63638;font-size:12px;";
24326 label.appendChild(tag);
24327 }
24328 row.appendChild(label);
24329 const cap = s.capability === "write" ? "write" : "read";
24330 const capSeg = buildCapSegmented(cap, (next) => {
24331 void changeCap(s, next);
24332 });
24333 row.appendChild(capSeg);
24334 const removeBtn = buildIconButton(
24335 "×",
24336 () => {
24337 void revoke(s);
24338 },
24339 { danger: true }
24340 );
24341 row.appendChild(removeBtn);
24342 modal.appendChild(row);
24343 }
24344 }
24345 const footer = document.createElement("div");
24346 footer.setAttribute("slot", "footer");
24347 footer.style.display = "flex";
24348 footer.style.justifyContent = "flex-end";
24349 footer.style.gap = "10px";
24350 footer.style.flexWrap = "wrap";
24351 const doneBtn = document.createElement("wpd-button");
24352 doneBtn.setAttribute("variant", "secondary");
24353 doneBtn.textContent = "Done";
24354 doneBtn.addEventListener("click", () => modal.remove());
24355 footer.appendChild(doneBtn);
24356 modal.appendChild(footer);
24357 };
24358 const refresh = async () => {
24359 try {
24360 const res = await listShares(opts.folderId);
24361 shares = res.shares;
24362 setSharesForFolder(opts.folderId, shares);
24363 } catch (err) {
24364 showToast({
24365 message: `Could not load shares: ${err.message}`
24366 });
24367 }
24368 renderBody();
24369 };
24370 const revoke = async (s) => {
24371 try {
24372 await revokeShare(opts.folderId, s.id);
24373 removeShare(opts.folderId, s.id);
24374 await refresh();
24375 showToast({ message: "Access revoked." });
24376 } catch (err) {
24377 showToast({
24378 message: `Could not revoke: ${err.message}`
24379 });
24380 }
24381 };
24382 const changeCap = async (s, cap) => {
24383 try {
24384 const next = await updateShareCapability(opts.folderId, s.id, cap);
24385 upsertShare(next);
24386 await refresh();
24387 } catch (err) {
24388 showToast({
24389 message: `Could not update capability: ${err.message}`
24390 });
24391 }
24392 };
24393 modal.addEventListener("wpd-modal-cancel", () => modal.remove());
24394 renderBody();
24395 await refresh();
24396 }
24397 function openPendingInviteModal(invite) {
24398 return new Promise((resolve2) => {
24399 const modal = document.createElement("wpd-modal");
24400 modal.setAttribute("open", "");
24401 modal.setAttribute("title", invite.folderName ? `${invite.ownerName ?? "Someone"} shared "${invite.folderName}" with you` : "Folder shared with you");
24402 const body = document.createElement("div");
24403 const capLabel = invite.capability === "write" ? "Read + Write" : "Read";
24404 body.innerHTML = `
24405 <p style="margin: 0 0 12px;">Accept the invite to add this folder to your desktop.</p>
24406 <p style="margin: 0; opacity: 0.75;">Access level: <strong>${capLabel}</strong></p>
24407 `;
24408 modal.appendChild(body);
24409 const footer = document.createElement("div");
24410 footer.setAttribute("slot", "footer");
24411 footer.style.display = "flex";
24412 footer.style.justifyContent = "flex-end";
24413 footer.style.gap = "10px";
24414 footer.style.flexWrap = "wrap";
24415 const laterBtn = document.createElement("wpd-button");
24416 laterBtn.setAttribute("variant", "secondary");
24417 laterBtn.textContent = "Decide later";
24418 laterBtn.addEventListener("click", () => {
24419 modal.remove();
24420 resolve2("dismissed");
24421 });
24422 const denyBtn = document.createElement("wpd-button");
24423 denyBtn.setAttribute("variant", "danger");
24424 denyBtn.textContent = "Deny";
24425 denyBtn.addEventListener("click", async () => {
24426 denyBtn.setAttribute("busy", "");
24427 denyBtn.setAttribute("disabled", "");
24428 try {
24429 await denyShare(invite.folderId, invite.id);
24430 sharesStore().state.deniedFolders.add(invite.folderId);
24431 sharesStore().notify();
24432 modal.remove();
24433 resolve2("denied");
24434 } catch (err) {
24435 showToast({
24436 message: `Could not deny: ${err.message}`
24437 });
24438 denyBtn.removeAttribute("busy");
24439 denyBtn.removeAttribute("disabled");
24440 }
24441 });
24442 const acceptBtn = document.createElement("wpd-button");
24443 acceptBtn.setAttribute("variant", "primary");
24444 acceptBtn.textContent = "Accept";
24445 acceptBtn.addEventListener("click", async () => {
24446 acceptBtn.setAttribute("busy", "");
24447 acceptBtn.setAttribute("disabled", "");
24448 try {
24449 await acceptShare(invite.folderId, invite.id);
24450 try {
24451 const res = await listPlacements(0);
24452 setFolderPlacements(0, res.placements);
24453 } catch (_e) {
24454 }
24455 modal.remove();
24456 resolve2("accepted");
24457 } catch (err) {
24458 showToast({
24459 message: `Could not accept: ${err.message}`
24460 });
24461 acceptBtn.removeAttribute("busy");
24462 acceptBtn.removeAttribute("disabled");
24463 }
24464 });
24465 footer.appendChild(laterBtn);
24466 footer.appendChild(denyBtn);
24467 footer.appendChild(acceptBtn);
24468 modal.appendChild(footer);
24469 modal.addEventListener("wpd-modal-cancel", () => {
24470 modal.remove();
24471 resolve2("dismissed");
24472 });
24473 document.body.appendChild(modal);
24474 });
24475 }
24476 function viewerId() {
24477 return Number(window.desktopModeConfig?.currentUserId ?? 0);
24478 }
24479 function sharingEnabled$1() {
24480 const settings = window.wp?.desktop?.getOsSettings?.();
24481 if (!settings) {
24482 return true;
24483 }
24484 return settings.foldersSharingEnabled !== false;
24485 }
24486 function folderOwnerId(folderId) {
24487 const folder = getFilesState().folders.get(folderId);
24488 return folder ? Number(folder.ownerId) : 0;
24489 }
24490 function folderIdFromBaseId(baseId) {
24491 if (typeof baseId !== "string") {
24492 return null;
24493 }
24494 const m = /^desktop-mode-folder-(\d+)$/.exec(baseId);
24495 return m ? Number(m[1]) : null;
24496 }
24497 function placementFolderId(placement) {
24498 if (placement.file.type !== "folder") {
24499 return null;
24500 }
24501 const ref = Number(placement.file.ref);
24502 if (!Number.isFinite(ref) || ref <= 0) {
24503 return null;
24504 }
24505 return ref;
24506 }
24507 function placementOwnerId(placement) {
24508 return Number(placement.file.ownerId ?? 0);
24509 }
24510 function installShareMenuItems() {
24511 addFilter(
24512 "desktop-mode.files.tile-menu",
24513 "desktop-mode/folder-share",
24514 (items, placement) => {
24515 if (!sharingEnabled$1()) {
24516 return items;
24517 }
24518 const folderId = placementFolderId(placement);
24519 if (folderId === null) {
24520 return items;
24521 }
24522 const ownerId = folderOwnerId(folderId) || placementOwnerId(placement);
24523 const viewer = viewerId();
24524 if (ownerId === viewer) {
24525 const shared = !!placement.file.shareSummary?.shared;
24526 const label = shared ? "Manage sharing…" : "Share folder…";
24527 items.push({
24528 id: "desktop-mode/folder-share",
24529 label,
24530 icon: "dashicons-share",
24531 sort: 30,
24532 onClick: () => {
24533 void openShareSettingsModal({
24534 folderId,
24535 folderName: placement.file.title || `Folder ${folderId}`
24536 });
24537 }
24538 });
24539 } else if (ownerId > 0) {
24540 items.push({
24541 id: "desktop-mode/folder-leave",
24542 label: "Leave shared folder",
24543 icon: "dashicons-exit",
24544 sort: 80,
24545 danger: true,
24546 onClick: async () => {
24547 const ok = await wpdConfirm$1({
24548 title: "Leave this folder?",
24549 message: "The folder will be removed from your desktop. The original and its contents are not deleted; the owner keeps them.",
24550 confirmLabel: "Leave",
24551 danger: true
24552 });
24553 if (!ok) {
24554 return;
24555 }
24556 try {
24557 await leaveShare(folderId);
24558 removePlacement(placement.id);
24559 try {
24560 const res = await listPlacements(0);
24561 setFolderPlacements(0, res.placements);
24562 } catch (_e) {
24563 }
24564 const winId = `desktop-mode-folder-${folderId}`;
24565 const mgr = window.desktopMode?.windowManager;
24566 mgr?.close?.(winId);
24567 showToast({ message: "You left the shared folder." });
24568 } catch (err) {
24569 showToast({
24570 message: `Could not leave: ${err.message}`
24571 });
24572 }
24573 }
24574 });
24575 }
24576 return items;
24577 }
24578 );
24579 registerTitleBarButton({
24580 id: "desktop-mode/folder-share",
24581 label: "Share folder",
24582 icon: "dashicons-share",
24583 placement: "right",
24584 order: 50,
24585 match: (w) => {
24586 if (!sharingEnabled$1()) {
24587 return false;
24588 }
24589 const base = w.config.baseId ?? w.id;
24590 const folderId = folderIdFromBaseId(base);
24591 if (folderId === null) {
24592 return false;
24593 }
24594 return folderOwnerId(folderId) === viewerId();
24595 },
24596 onClick: (w) => {
24597 const base = w.config.baseId ?? w.id;
24598 const folderId = folderIdFromBaseId(base);
24599 if (folderId === null) {
24600 return;
24601 }
24602 void openShareSettingsModal({
24603 folderId,
24604 folderName: w.config.title || `Folder ${folderId}`
24605 });
24606 }
24607 });
24608 addAction(
24609 "desktop-mode.files.tile-rendered",
24610 "desktop-mode/folder-share",
24611 (payload) => {
24612 const { tile: tile2, placement } = payload;
24613 if (placement.file.type !== "folder") {
24614 return;
24615 }
24616 const summary = placement.file.shareSummary;
24617 if (!summary?.shared) {
24618 return;
24619 }
24620 if (tile2.querySelector(".desktop-mode-file-tile__share-badge")) {
24621 return;
24622 }
24623 const badge = document.createElement("span");
24624 badge.className = "desktop-mode-file-tile__share-badge dashicons dashicons-share";
24625 badge.setAttribute("aria-label", "Shared folder");
24626 badge.title = "Shared folder";
24627 badge.style.cssText = [
24628 "position:absolute",
24629 "top:6px",
24630 "inset-inline-end:6px",
24631 "background:rgba(0,0,0,0.55)",
24632 "color:#fff",
24633 "border-radius:50%",
24634 "width:18px",
24635 "height:18px",
24636 "font-size:12px",
24637 "line-height:18px",
24638 "text-align:center",
24639 "pointer-events:none"
24640 ].join(";");
24641 tile2.appendChild(badge);
24642 }
24643 );
24644 }
24645 const prompted = /* @__PURE__ */ new Set();
24646 function sharingEnabled() {
24647 const settings = window.wp?.desktop?.getOsSettings?.();
24648 if (!settings) {
24649 return true;
24650 }
24651 return settings.foldersSharingEnabled !== false;
24652 }
24653 function installShareInviteBanner() {
24654 const store2 = sharesStore();
24655 const handle = (state2) => {
24656 if (!sharingEnabled()) {
24657 return;
24658 }
24659 for (const invite of state2.pending) {
24660 if (prompted.has(invite.id)) {
24661 continue;
24662 }
24663 prompted.add(invite.id);
24664 void openPendingInviteModal({
24665 id: invite.id,
24666 folderId: invite.folderId,
24667 folderName: invite.folderName,
24668 ownerName: invite.ownerName,
24669 capability: invite.capability
24670 }).then((decision) => {
24671 if (decision === "accepted") {
24672 dropPending(invite.id);
24673 } else if (decision === "denied") {
24674 dropPending(invite.id, { denied: true, folderId: invite.folderId });
24675 }
24676 });
24677 }
24678 };
24679 store2.subscribe(handle);
24680 handle(store2.state);
24681 }
24682 registerBuiltInFileTypes();
24683 registerBuiltInFileOpeners();
24684 installEmbedPersistence();
24685 registerFileAssociationsTab();
24686 installShareMenuItems();
24687 const seededPending = window.desktopModeConfig?.serverPendingShares;
24688 if (Array.isArray(seededPending) && seededPending.length > 0) {
24689 ingestPendingInvites(seededPending);
24690 }
24691 installShareInviteBanner();
24692 const filesApi = {
24693 DesktopFile,
24694 registerType,
24695 unregisterType,
24696 getType,
24697 getTypes,
24698 resolve,
24699 subscribe,
24700 registerOpener,
24701 unregisterOpener,
24702 getOpener,
24703 getOpeners,
24704 getOpenersForType,
24705 resolveOpener,
24706 subscribeOpeners,
24707 getUserAssociations,
24708 open: openFile,
24709 rest: filesRest,
24710 store: {
24711 get: getFilesStore,
24712 getState: getFilesState,
24713 subscribe: subscribeFilesStore,
24714 setFolderPlacements,
24715 upsertPlacement,
24716 removePlacement,
24717 setFolders,
24718 upsertFolder,
24719 removeFolder
24720 }
24721 };
24722 const SYNTH_META_KEY = "__synthFromDockItem";
24723 function hashToNegativeId(s) {
24724 let h = 0;
24725 for (let i = 0; i < s.length; i++) {
24726 h = (h * 31 + s.charCodeAt(i)) % 2147483647;
24727 }
24728 return -(h + 1);
24729 }
24730 function buildSyntheticPlacement(item, persistedPositions) {
24731 const saved = persistedPositions[item.id];
24732 return {
24733 id: hashToNegativeId(item.id),
24734 parentId: 0,
24735 x: saved ? saved.x : 0,
24736 y: saved ? saved.y : 0,
24737 sortOrder: 9999,
24738 updatedAtMs: Date.now(),
24739 meta: { [SYNTH_META_KEY]: item.id },
24740 file: {
24741 type: "shortcut",
24742 ref: `dock-promoted:${item.id}`,
24743 title: item.title,
24744 icon: item.icon,
24745 previewUrl: "",
24746 exists: true,
24747 // The shortcut opener (built-in-openers.ts) reads these
24748 // off the file shape — `shortcutUrl` is what a dock-item
24749 // promotion naturally has.
24750 shortcutUrl: item.url
24751 }
24752 };
24753 }
24754 function readDockItems() {
24755 const api = window.wp?.desktop;
24756 if (api?.getMenuItems) {
24757 const items = api.getMenuItems();
24758 return items.map((i) => ({
24759 id: i.id,
24760 title: i.title,
24761 icon: i.icon,
24762 url: i.url,
24763 badge: i.badge ?? 0,
24764 submenu: i.submenu ?? []
24765 }));
24766 }
24767 const cfg = window.desktopModeConfig;
24768 return cfg?.dockItems ?? [];
24769 }
24770 function readServerIcons() {
24771 const cfg = window.desktopModeConfig;
24772 return cfg?.desktopIcons ?? [];
24773 }
24774 let reentrant = false;
24775 const removedServerPlacementsByRef = /* @__PURE__ */ new Map();
24776 function prunePromotedPositions(ids) {
24777 const api = window.wp?.desktop;
24778 if (!api?.getOsSettings || !api?.updateOsSettings) {
24779 return;
24780 }
24781 const current = api.getOsSettings().dockPromotedPositions ?? {};
24782 const next = { ...current };
24783 let changed = false;
24784 for (const id of ids) {
24785 if (id in next) {
24786 delete next[id];
24787 changed = true;
24788 }
24789 }
24790 if (changed) {
24791 api.updateOsSettings({ dockPromotedPositions: next });
24792 }
24793 }
24794 function syncShortcutsWithVisibility(visibility, positions = {}) {
24795 if (reentrant) {
24796 return;
24797 }
24798 reentrant = true;
24799 try {
24800 const dockItems = readDockItems();
24801 const serverIcons = readServerIcons();
24802 const state2 = filesApi.store.getState();
24803 const root = state2.placementsByFolder.get(0) ?? [];
24804 const currentSynth = /* @__PURE__ */ new Map();
24805 for (const p of root) {
24806 const sourceId = (p.meta ?? null) && typeof p.meta === "object" ? p.meta[SYNTH_META_KEY] : null;
24807 if (typeof sourceId === "string") {
24808 currentSynth.set(sourceId, p);
24809 }
24810 }
24811 const realByRef = /* @__PURE__ */ new Map();
24812 const registeredIconIds = new Set(
24813 serverIcons.map((i) => i.id)
24814 );
24815 for (const p of root) {
24816 const ref = p?.file?.ref;
24817 if (typeof ref === "string" && registeredIconIds.has(ref)) {
24818 realByRef.set(ref, p);
24819 }
24820 }
24821 const desiredSynth = /* @__PURE__ */ new Set();
24822 for (const item of dockItems) {
24823 const placement = visibility[item.id];
24824 if (placement === "desktop" || placement === "both") {
24825 desiredSynth.add(item.id);
24826 if (!currentSynth.has(item.id)) {
24827 filesApi.store.upsertPlacement(
24828 buildSyntheticPlacement(item, positions)
24829 );
24830 }
24831 }
24832 }
24833 const positionsToPrune = [];
24834 for (const [sourceId, p] of currentSynth) {
24835 if (!desiredSynth.has(sourceId)) {
24836 filesApi.store.removePlacement(p.id);
24837 if (positions[sourceId]) {
24838 positionsToPrune.push(sourceId);
24839 }
24840 }
24841 }
24842 if (positionsToPrune.length > 0) {
24843 prunePromotedPositions(positionsToPrune);
24844 }
24845 for (const icon of serverIcons) {
24846 const placement = visibility[icon.id];
24847 const inStore = realByRef.get(icon.id);
24848 if (placement === "dock" || placement === "hidden") {
24849 if (inStore) {
24850 removedServerPlacementsByRef.set(icon.id, inStore);
24851 filesApi.store.removePlacement(inStore.id);
24852 }
24853 continue;
24854 }
24855 if (!inStore) {
24856 const cached = removedServerPlacementsByRef.get(icon.id);
24857 if (cached) {
24858 filesApi.store.upsertPlacement(cached);
24859 removedServerPlacementsByRef.delete(icon.id);
24860 }
24861 }
24862 }
24863 } finally {
24864 reentrant = false;
24865 }
24866 }
24867 function installShortcutsSync(getVisibility, getPositions = () => ({})) {
24868 queueMicrotask(
24869 () => syncShortcutsWithVisibility(getVisibility(), getPositions())
24870 );
24871 const off = filesApi.store.subscribe(() => {
24872 syncShortcutsWithVisibility(getVisibility(), getPositions());
24873 });
24874 return off;
24875 }
24876 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%}`;
24877 const DEFAULT_EVENT = "desktop-mode-os-settings-save-lifecycle";
24878 const DEFAULT_AUTO_CLEAR_SAVED_MS = 2200;
24879 const DEFAULT_AUTO_CLEAR_FAILED_MS = 6e3;
24880 const _WpdSaveStatus = class _WpdSaveStatus extends Component {
24881 constructor() {
24882 super(...arguments);
24883 this._autoTimer = null;
24884 this._docListener = null;
24885 }
24886 connectedCallback() {
24887 super.connectedCallback();
24888 if (this.auto !== null) {
24889 this._installAutoListener();
24890 }
24891 }
24892 disconnectedCallback() {
24893 this._removeAutoListener();
24894 if (this._autoTimer !== null) {
24895 window.clearTimeout(this._autoTimer);
24896 this._autoTimer = null;
24897 }
24898 }
24899 attributeChangedCallback(name, oldValue, newValue) {
24900 super.attributeChangedCallback(name, oldValue, newValue);
24901 if (name === "auto" || name === "event") {
24902 this._removeAutoListener();
24903 if (this.auto !== null) {
24904 this._installAutoListener();
24905 }
24906 }
24907 if (name === "phase") {
24908 this._scheduleAutoClear();
24909 const detail = {
24910 phase: this.phase ?? "idle",
24911 error: this.error ?? void 0
24912 };
24913 this.emit("wpd-save-status-change", detail);
24914 }
24915 }
24916 render() {
24917 const phase = this.phase ?? "idle";
24918 const mode = this.mode ?? "dot";
24919 const error = this.error ?? "";
24920 const title = error || this._labelForPhase(phase);
24921 if (title) {
24922 this.setAttribute("title", title);
24923 } else {
24924 this.removeAttribute("title");
24925 }
24926 this.setAttribute("aria-live", phase === "failed" ? "assertive" : "polite");
24927 this.setAttribute("role", phase === "failed" ? "alert" : "status");
24928 return html`
24929 <span class="wpd-save-status">
24930 <span class="wpd-save-status__indicator" aria-hidden="true">
24931 <span class="wpd-save-status__glyph">${this._renderGlyph(phase)}</span>
24932 </span>
24933 ${mode === "pill" ? html`<span class="wpd-save-status__label"
24934 >${this._labelForPhase(phase)}</span
24935 >` : html``}
24936 </span>
24937 `;
24938 }
24939 _renderGlyph(phase) {
24940 if (phase === "saved") {
24941 return _iconCheck();
24942 }
24943 if (phase === "failed") {
24944 return _iconBang();
24945 }
24946 return "";
24947 }
24948 _labelForPhase(phase) {
24949 switch (phase) {
24950 case "pending":
24951 case "saving":
24952 return this["saving-label"] ?? "Saving…";
24953 case "saved":
24954 return this["saved-label"] ?? "Saved";
24955 case "failed": {
24956 const err = this.error ?? "";
24957 return err || "Couldn’t save";
24958 }
24959 default:
24960 return this["idle-label"] ?? "";
24961 }
24962 }
24963 _installAutoListener() {
24964 const eventName = this.event || DEFAULT_EVENT;
24965 this._docListener = (e) => {
24966 const detail = e.detail;
24967 if (!detail || typeof detail.phase !== "string") {
24968 return;
24969 }
24970 this.phase = detail.phase;
24971 if (detail.error) {
24972 this.error = detail.error;
24973 } else if (detail.phase !== "failed" && this.error) {
24974 this.removeAttribute("error");
24975 }
24976 };
24977 document.addEventListener(eventName, this._docListener);
24978 }
24979 _removeAutoListener() {
24980 if (!this._docListener) {
24981 return;
24982 }
24983 const eventName = this.event || DEFAULT_EVENT;
24984 document.removeEventListener(eventName, this._docListener);
24985 this._docListener = null;
24986 }
24987 _scheduleAutoClear() {
24988 if (this._autoTimer !== null) {
24989 window.clearTimeout(this._autoTimer);
24990 this._autoTimer = null;
24991 }
24992 const phase = this.phase ?? "idle";
24993 const ms = this._autoClearMsFor(phase);
24994 if (ms <= 0) {
24995 return;
24996 }
24997 this._autoTimer = window.setTimeout(() => {
24998 this._autoTimer = null;
24999 this.phase = "idle";
25000 }, ms);
25001 }
25002 _autoClearMsFor(phase) {
25003 if (phase === "saved") {
25004 const raw = this["auto-clear-saved-ms"];
25005 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_SAVED_MS;
25006 }
25007 if (phase === "failed") {
25008 const raw = this["auto-clear-failed-ms"];
25009 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_FAILED_MS;
25010 }
25011 return 0;
25012 }
25013 };
25014 _WpdSaveStatus.props = [
25015 "phase",
25016 "mode",
25017 "animation",
25018 "auto",
25019 "event",
25020 "error",
25021 "saving-label",
25022 "saved-label",
25023 "idle-label",
25024 "auto-clear-saved-ms",
25025 "auto-clear-failed-ms"
25026 ];
25027 _WpdSaveStatus.styles = [styles$2];
25028 _WpdSaveStatus.help = {
25029 title: "Save status",
25030 summary: 'Tiny status indicator for "is this change saved yet?" affordances. Three layouts (dot / icon / pill), four phases, optional auto-listen to a save-lifecycle CustomEvent so every input in the panel inherits feedback for free.',
25031 status: "experimental",
25032 since: "0.8.0",
25033 props: [
25034 {
25035 name: "phase",
25036 type: "'idle' | 'pending' | 'saving' | 'saved' | 'failed'",
25037 default: "idle",
25038 description: "Current lifecycle phase. Set manually for one-off integrations, or rely on `auto` to populate it from a CustomEvent."
25039 },
25040 {
25041 name: "mode",
25042 type: "'dot' | 'icon' | 'pill'",
25043 default: "dot",
25044 description: "Layout. `dot` is the smallest (10×10 colored dot); `icon` adds a glyph inside on saved/failed; `pill` adds an inline label."
25045 },
25046 {
25047 name: "animation",
25048 type: "'pulse' | 'modem'",
25049 default: "pulse",
25050 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."
25051 },
25052 {
25053 name: "auto",
25054 type: "boolean attribute",
25055 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="…"`.'
25056 },
25057 {
25058 name: "event",
25059 type: "string",
25060 default: "desktop-mode-os-settings-save-lifecycle",
25061 description: "CustomEvent name to listen on when `auto` is set."
25062 },
25063 {
25064 name: "error",
25065 type: "string",
25066 description: "Error message shown in `pill` mode and exposed as the host title attribute (so dot/icon modes still surface the message via tooltip)."
25067 },
25068 {
25069 name: "saving-label",
25070 type: "string",
25071 default: "Saving…",
25072 description: "Pill-mode label shown during `pending` / `saving`."
25073 },
25074 {
25075 name: "saved-label",
25076 type: "string",
25077 default: "Saved",
25078 description: "Pill-mode label shown during `saved`."
25079 },
25080 {
25081 name: "idle-label",
25082 type: "string",
25083 description: 'Optional pill-mode label shown during `idle` (e.g. "All changes saved"). When unset, the pill collapses to invisible while idle.'
25084 },
25085 {
25086 name: "auto-clear-saved-ms",
25087 type: "integer",
25088 default: "2200",
25089 description: "How long the `saved` phase stays visible before auto-fading back to `idle`."
25090 },
25091 {
25092 name: "auto-clear-failed-ms",
25093 type: "integer",
25094 default: "6000",
25095 description: "How long the `failed` phase stays visible before auto-fading back to `idle`."
25096 }
25097 ],
25098 events: [
25099 {
25100 name: "wpd-save-status-change",
25101 description: "Fires when the phase changes (manually or via auto-listen).",
25102 detail: "{ phase, error }"
25103 }
25104 ],
25105 cssProps: [
25106 {
25107 name: "--wpd-save-status-bg",
25108 description: "Indicator background color (saving/pending phase)."
25109 },
25110 {
25111 name: "--wpd-save-status-saved-bg",
25112 description: "Indicator background on saved."
25113 },
25114 {
25115 name: "--wpd-save-status-failed-bg",
25116 description: "Indicator background on failed."
25117 },
25118 {
25119 name: "--wpd-save-status-pill-bg",
25120 description: "Pill background (mode=pill)."
25121 },
25122 {
25123 name: "--wpd-save-status-pill-fg",
25124 description: "Pill foreground (mode=pill)."
25125 }
25126 ],
25127 example: html`
25128 <wpd-cluster gap="12">
25129 <wpd-save-status phase="pending"></wpd-save-status>
25130 <wpd-save-status phase="saving"></wpd-save-status>
25131 <wpd-save-status phase="saved"></wpd-save-status>
25132 <wpd-save-status phase="failed"></wpd-save-status>
25133 <wpd-save-status mode="pill" phase="saving"></wpd-save-status>
25134 <wpd-save-status mode="pill" phase="saved"></wpd-save-status>
25135 <wpd-save-status mode="pill" phase="failed" error="Network error."></wpd-save-status>
25136 </wpd-cluster>
25137 `
25138 };
25139 let WpdSaveStatus = _WpdSaveStatus;
25140 defineComponent("wpd-save-status", WpdSaveStatus);
25141 function _iconCheck() {
25142 return html`
25143 <svg
25144 viewBox="0 0 12 12"
25145 aria-hidden="true"
25146 focusable="false"
25147 fill="none"
25148 stroke="currentColor"
25149 stroke-width="2"
25150 stroke-linecap="round"
25151 stroke-linejoin="round"
25152 >
25153 <path d="M2.5 6 L5 8.5 L9.5 4" />
25154 </svg>
25155 `;
25156 }
25157 function _iconBang() {
25158 return html`
25159 <svg
25160 viewBox="0 0 12 12"
25161 aria-hidden="true"
25162 focusable="false"
25163 fill="currentColor"
25164 >
25165 <path
25166 d="M5 2 H7 V7 H5 z M5 8.5 H7 V10.5 H5 z"
25167 />
25168 </svg>
25169 `;
25170 }
25171 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}`;
25172 const _WpdTextarea = class _WpdTextarea extends Component {
25173 constructor() {
25174 super(...arguments);
25175 this._textareaEl = null;
25176 }
25177 connectedCallback() {
25178 super.connectedCallback();
25179 ensureAutoId(this);
25180 }
25181 render() {
25182 const label = this._attr("label") || "";
25183 const value = this._attr("value") ?? "";
25184 const placeholder = this._attr("placeholder") || "";
25185 const disabled = this._boolAttr("disabled");
25186 const readonly = this._boolAttr("readonly");
25187 const ariaLabel = this._attr("aria-label") || label;
25188 const name = this._attr("name") || "";
25189 const rows = Number(this._attr("rows")) || 3;
25190 const maxLength = this._attr("maxlength");
25191 const minLength = this._attr("minlength");
25192 const invalid = this._boolAttr("invalid");
25193 const hostId = this.id || "wpd-unnamed";
25194 const fieldId = `${hostId}__field`;
25195 return html`
25196 ${label ? html`<label class="wpd-textarea__label" for=${fieldId}>${label}</label>` : html``}
25197 <textarea
25198 id=${fieldId}
25199 part="textarea"
25200 .value=${value}
25201 placeholder=${placeholder}
25202 ?disabled=${disabled}
25203 ?readonly=${readonly}
25204 rows=${rows}
25205 maxlength=${maxLength ?? ""}
25206 minlength=${minLength ?? ""}
25207 name=${name}
25208 aria-invalid=${invalid ? "true" : "false"}
25209 aria-label=${ariaLabel || ""}
25210 @input=${(e) => this._onInput(e)}
25211 @change=${(e) => this._onChange(e)}
25212 @keydown=${(e) => this._onKeyDown(e)}
25213 ></textarea>
25214 `;
25215 }
25216 _attr(name) {
25217 return this.getAttribute(name);
25218 }
25219 _boolAttr(name) {
25220 return this.getAttribute(name) !== null;
25221 }
25222 _onInput(e) {
25223 const ta = e.target;
25224 this._textareaEl = ta;
25225 this.setAttribute("value", ta.value);
25226 this.emit("wpd-input-change", { value: ta.value });
25227 if (this._boolAttr("auto-grow")) {
25228 this._autosize(ta);
25229 }
25230 }
25231 _onChange(e) {
25232 const ta = e.target;
25233 this.emit("wpd-input-commit", { value: ta.value });
25234 }
25235 _onKeyDown(e) {
25236 if (!this._boolAttr("submit-on-enter")) {
25237 return;
25238 }
25239 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
25240 e.preventDefault();
25241 const ta = e.target;
25242 this.emit("wpd-submit", { value: ta.value });
25243 }
25244 }
25245 /**
25246 * Grow the textarea height to fit content, capped at `max-rows`.
25247 * Resets to scroll-height each input then clamps; cheap because
25248 * the browser caches layout.
25249 */
25250 _autosize(ta) {
25251 const maxRows = Number(this._attr("max-rows")) || 8;
25252 const cs = window.getComputedStyle(ta);
25253 const fontSize = parseFloat(cs.fontSize) || 13;
25254 const lineHeightRaw = cs.lineHeight;
25255 const lineHeight = lineHeightRaw === "normal" ? fontSize * 1.45 : parseFloat(lineHeightRaw) || fontSize * 1.45;
25256 const paddingTop = parseFloat(cs.paddingTop) || 0;
25257 const paddingBottom = parseFloat(cs.paddingBottom) || 0;
25258 const max = lineHeight * maxRows + paddingTop + paddingBottom;
25259 ta.style.height = "auto";
25260 const next = Math.min(ta.scrollHeight, max);
25261 ta.style.height = `${next}px`;
25262 }
25263 /** Public helper for callers that programmatically set `.value` and want autosize to re-run. */
25264 refreshAutosize() {
25265 if (this._textareaEl && this._boolAttr("auto-grow")) {
25266 this._autosize(this._textareaEl);
25267 }
25268 }
25269 /** Imperatively focus the underlying textarea. */
25270 focusInput() {
25271 const root = this.shadowRoot ?? this;
25272 const ta = root.querySelector("textarea");
25273 ta?.focus();
25274 }
25275 /** Imperatively clear the value. */
25276 clear() {
25277 this.setAttribute("value", "");
25278 const root = this.shadowRoot ?? this;
25279 const ta = root.querySelector("textarea");
25280 if (ta) {
25281 ta.value = "";
25282 if (this._boolAttr("auto-grow")) {
25283 this._autosize(ta);
25284 }
25285 }
25286 }
25287 };
25288 _WpdTextarea.props = [
25289 "label",
25290 "value",
25291 "placeholder",
25292 "disabled",
25293 "readonly",
25294 "ariaLabel",
25295 "name",
25296 "rows",
25297 "maxlength",
25298 "minlength",
25299 "invalid",
25300 "autoGrow",
25301 "maxRows",
25302 "submitOnEnter"
25303 ];
25304 _WpdTextarea.styles = [textareaStyles];
25305 _WpdTextarea.help = {
25306 title: "Textarea",
25307 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).",
25308 status: "stable",
25309 since: "0.22.0",
25310 props: [
25311 { name: "label", type: "string", description: "Visible label above the textarea." },
25312 { name: "value", type: "string", description: "Current value; reflected two-way." },
25313 { name: "placeholder", type: "string", description: "Native placeholder." },
25314 { name: "disabled", type: "boolean attribute" },
25315 { name: "readonly", type: "boolean attribute" },
25316 { name: "aria-label", type: "string", description: "Accessible label when no visible label is rendered." },
25317 { name: "name", type: "string", description: "Forwarded to native textarea for form submission." },
25318 { name: "rows", type: "integer (string)", default: "3", description: "Initial visible row count." },
25319 { name: "maxlength", type: "integer (string)" },
25320 { name: "minlength", type: "integer (string)" },
25321 { name: "invalid", type: "boolean attribute", description: "Sets aria-invalid + error styling." },
25322 { name: "auto-grow", type: "boolean attribute", description: "Grows up to max-rows as the user types." },
25323 { name: "max-rows", type: "integer (string)", default: "8" },
25324 {
25325 name: "submit-on-enter",
25326 type: "boolean attribute",
25327 description: "Enter fires wpd-submit; Shift+Enter inserts a newline."
25328 }
25329 ],
25330 events: [
25331 { name: "wpd-input-change", description: "Fires on every keystroke.", detail: "{ value: string }" },
25332 { name: "wpd-input-commit", description: "Fires on blur / native change.", detail: "{ value: string }" },
25333 {
25334 name: "wpd-submit",
25335 description: "Fires on Enter (without Shift) when submit-on-enter is set.",
25336 detail: "{ value: string }"
25337 }
25338 ],
25339 example: html`
25340 <wpd-textarea label="Message" rows="3" auto-grow max-rows="8" submit-on-enter></wpd-textarea>
25341 `
25342 };
25343 let WpdTextarea = _WpdTextarea;
25344 defineComponent("wpd-textarea", WpdTextarea);
25345 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}`;
25346 const ICONS = {
25347 minimize: '<path d="M3 6h6" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>',
25348 maximize: '<rect x="3" y="3" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.25" fill="none"/>',
25349 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"/>',
25350 "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"/>',
25351 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"/>',
25352 reload: (
25353 // Filled icon scaled from a 512×512 source into the 12×12 viewBox
25354 // shared with the other title-bar glyphs. The wrapping `<g>` does
25355 // the math; the inner path is dropped in unmodified so its
25356 // authoring tool can be re-edited and copy-pasted again.
25357 // `scale(0.021)` ≈ 90% of full fit, with `translate(0.6)` to
25358 // keep the result centered inside the 12×12 viewBox so the
25359 // glyph reads slightly smaller than min/max/close — closer to
25360 // the visual weight of the other title-bar buttons.
25361 '<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>'
25362 ),
25363 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"/>',
25364 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"/>'
25365 };
25366 const _WpdWindowButton = class _WpdWindowButton extends Component {
25367 constructor() {
25368 super(...arguments);
25369 this._activateWired = false;
25370 }
25371 render() {
25372 const iconKey = this.icon || "";
25373 const svgInner = ICONS[iconKey] || "";
25374 return html`
25375 <button type="button">
25376 <svg
25377 width="14"
25378 height="14"
25379 viewBox="0 0 12 12"
25380 aria-hidden="true"
25381 focusable="false"
25382 ></svg>
25383 <slot></slot>
25384 </button>
25385 <span data-svg-buffer style="display:none">${svgInner}</span>
25386 `;
25387 }
25388 /**
25389 * After each render, copy the raw SVG markup into the actual
25390 * `<svg>` element. The templater only writes text into slots,
25391 * so we stash the intended markup in a hidden buffer and
25392 * `innerHTML = ` the svg once here — a one-shot post-render
25393 * hook that keeps the declarative template honest.
25394 *
25395 * Also wires up the `wpd-button-activate` CustomEvent that
25396 * fires exactly once per gesture — the canonical contract
25397 * for plugin-registered title-bar buttons. Plugin authors who
25398 * use `addEventListener( 'click', cb )` directly still get
25399 * what they expect (the title bar's drag-handler now excludes
25400 * chrome buttons by class so static clicks land normally),
25401 * but `wpd-button-activate` is the documented surface that
25402 * documents the once-per-gesture contract explicitly. See
25403 * the class-level docblock for rationale.
25404 */
25405 connectedCallback() {
25406 super.connectedCallback();
25407 queueMicrotask(() => this._paintSvg());
25408 queueMicrotask(() => this._wireActivateEvent());
25409 }
25410 attributeChangedCallback(name, oldValue, newValue) {
25411 super.attributeChangedCallback(name, oldValue, newValue);
25412 queueMicrotask(() => this._paintSvg());
25413 }
25414 _paintSvg() {
25415 const root = this.shadowRoot;
25416 if (!root) {
25417 return;
25418 }
25419 const svg = root.querySelector("svg");
25420 const buffer = root.querySelector("[data-svg-buffer]");
25421 if (svg && buffer) {
25422 const markup = buffer.textContent || "";
25423 if (svg.innerHTML !== markup) {
25424 svg.innerHTML = markup;
25425 }
25426 }
25427 }
25428 _wireActivateEvent() {
25429 if (this._activateWired) {
25430 return;
25431 }
25432 const root = this.shadowRoot;
25433 if (!root) {
25434 return;
25435 }
25436 const button = root.querySelector("button");
25437 if (!button) {
25438 return;
25439 }
25440 this._activateWired = true;
25441 button.addEventListener("click", () => {
25442 this.dispatchEvent(
25443 new CustomEvent("wpd-button-activate", {
25444 bubbles: true,
25445 composed: true,
25446 cancelable: true
25447 })
25448 );
25449 });
25450 }
25451 };
25452 _WpdWindowButton.props = ["icon", "active", "danger"];
25453 _WpdWindowButton.styles = [styles$1];
25454 _WpdWindowButton.help = {
25455 title: "Window button",
25456 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.",
25457 status: "stable",
25458 since: "0.9.0",
25459 props: [
25460 {
25461 name: "icon",
25462 type: "'minimize' | 'maximize' | 'fullscreen' | 'fullscreen-exit' | 'detach' | 'reload' | 'close' | 'menu'",
25463 description: "Which built-in inline SVG to paint. Omit to supply your own via the slot."
25464 },
25465 {
25466 name: "active",
25467 type: "boolean attribute",
25468 description: "Applies the pressed-down look (used e.g. while a menu it triggers is open)."
25469 },
25470 {
25471 name: "danger",
25472 type: "boolean attribute",
25473 description: "Swaps the hover wash to red — used by the close button."
25474 }
25475 ],
25476 slots: [
25477 { name: "(default)", description: "Optional custom icon markup (inline SVG) when `icon` is omitted." }
25478 ],
25479 cssProps: [
25480 { name: "--wpd-btn-color", description: "Resting foreground." },
25481 { name: "--wpd-btn-color-hover", description: "Hover foreground." },
25482 { name: "--wpd-btn-bg-hover", description: "Hover background wash." },
25483 { name: "--wpd-btn-bg-active", description: "Pressed background." },
25484 { name: "--wpd-btn-danger-hover", description: "Hover background for danger variant." },
25485 { name: "--wpd-btn-outline", description: "Focus outline colour." }
25486 ],
25487 example: html`
25488 <wpd-cluster gap="2">
25489 <wpd-window-button icon="minimize"></wpd-window-button>
25490 <wpd-window-button icon="maximize"></wpd-window-button>
25491 <wpd-window-button icon="menu"></wpd-window-button>
25492 <wpd-window-button icon="close" danger></wpd-window-button>
25493 </wpd-cluster>
25494 `
25495 };
25496 let WpdWindowButton = _WpdWindowButton;
25497 defineComponent("wpd-window-button", WpdWindowButton);
25498 const DEFAULT_STICKY_TITLE = "Sticky Note";
25499 const LEGACY_METADATA_PREFIX = "<!-- wpworkspace-sticky:";
25500 const LEGACY_METADATA_SUFFIX = "-->";
25501 const TITLE_MAX = 64;
25502 const GENERATED_TITLE_MAX = 48;
25503 const EXCERPT_MAX = 180;
25504 function noteFromGuideline(guideline) {
25505 const title = titleField(guideline.title);
25506 const content = removeLegacyMetadataComment(
25507 textFieldValue(guideline.content, { stripHtmlForRendered: true })
25508 );
25509 const modifiedMs = modifiedTimeMs(guideline);
25510 return {
25511 localId: `guideline:${guideline.id}`,
25512 guidelineId: guideline.id,
25513 title,
25514 body: editorBody(title, content),
25515 modified: guideline.modified,
25516 ...modifiedMs > 0 ? { modifiedMs } : {},
25517 link: guideline.link,
25518 termIds: Array.isArray(guideline.wp_guideline_type) ? guideline.wp_guideline_type.filter(isFiniteNumber) : []
25519 };
25520 }
25521 function titleField(field) {
25522 const candidates = [];
25523 if (typeof field === "string") {
25524 candidates.push(field);
25525 } else if (field && typeof field === "object") {
25526 if (typeof field.raw === "string") {
25527 candidates.push(field.raw);
25528 }
25529 if (typeof field.rendered === "string") {
25530 candidates.push(stripHtml(field.rendered));
25531 }
25532 }
25533 for (const candidate of candidates) {
25534 const trimmed = stripHtml(candidate).trim();
25535 if (trimmed) {
25536 return trimmed;
25537 }
25538 }
25539 return DEFAULT_STICKY_TITLE;
25540 }
25541 function textFieldValue(field, options = {}) {
25542 if (typeof field === "string") {
25543 return field;
25544 }
25545 if (!field || typeof field !== "object") {
25546 return "";
25547 }
25548 if (typeof field.raw === "string" && field.raw.length > 0) {
25549 return field.raw;
25550 }
25551 if (typeof field.rendered === "string") {
25552 return options.stripHtmlForRendered ? stripHtml(field.rendered) : field.rendered;
25553 }
25554 return "";
25555 }
25556 function titleForBody(body) {
25557 const line = body.split(/\r?\n/).find((item) => item.trim().length > 0)?.trim();
25558 const title = line && line.length > 0 ? line : DEFAULT_STICKY_TITLE;
25559 return truncate(title, TITLE_MAX);
25560 }
25561 function generatedTitle(body) {
25562 const collapsed = body.replace(/\s+/g, " ").trim();
25563 const title = collapsed || DEFAULT_STICKY_TITLE;
25564 return truncate(title, GENERATED_TITLE_MAX);
25565 }
25566 function editorBody(title, content) {
25567 const trimmedTitle = title.trim();
25568 if (!trimmedTitle) {
25569 return content;
25570 }
25571 const firstLine = content.split(/\r?\n/)[0]?.trim();
25572 if (firstLine === trimmedTitle) {
25573 return content;
25574 }
25575 if (!content) {
25576 return trimmedTitle;
25577 }
25578 return `${trimmedTitle}
25579 ${content}`;
25580 }
25581 function noteComponentsForBody(editorValue, fallbackTitle = DEFAULT_STICKY_TITLE) {
25582 const fallback = fallbackTitle.trim() || DEFAULT_STICKY_TITLE;
25583 const title = titleForBody(editorValue);
25584 const firstNewline = editorValue.search(/\r?\n/);
25585 if (firstNewline === -1) {
25586 const resolvedTitle = title === DEFAULT_STICKY_TITLE ? fallback : title;
25587 return {
25588 title: resolvedTitle,
25589 content: "",
25590 excerpt: excerptFor(resolvedTitle)
25591 };
25592 }
25593 let content = editorValue.slice(firstNewline);
25594 content = content.replace(/^\r?\n/, "");
25595 if (content.startsWith("\n")) {
25596 content = content.slice(1);
25597 }
25598 return {
25599 title,
25600 content,
25601 excerpt: excerptFor(content.trim() ? content : title)
25602 };
25603 }
25604 function excerptFor(body) {
25605 const collapsed = body.replace(/[\n\t]+/g, " ").trim();
25606 return truncate(collapsed, EXCERPT_MAX);
25607 }
25608 function removeLegacyMetadataComment(content) {
25609 if (!content.startsWith(LEGACY_METADATA_PREFIX) || !content.includes(LEGACY_METADATA_SUFFIX)) {
25610 return content;
25611 }
25612 const end = content.indexOf(LEGACY_METADATA_SUFFIX);
25613 let body = content.slice(end + LEGACY_METADATA_SUFFIX.length);
25614 if (body.startsWith("\r\n")) {
25615 body = body.slice(2);
25616 } else if (body.startsWith("\n")) {
25617 body = body.slice(1);
25618 }
25619 return body;
25620 }
25621 function stripHtml(value) {
25622 if (typeof document !== "undefined") {
25623 const template = document.createElement("template");
25624 template.innerHTML = value;
25625 return (template.content.textContent ?? "").trim();
25626 }
25627 return value.replace(/<[^>]*>/g, "").trim();
25628 }
25629 function truncate(value, max) {
25630 return value.length > max ? `${value.slice(0, max)}...` : value;
25631 }
25632 function modifiedTimeMs(guideline) {
25633 if (typeof guideline.desktop_mode_modified_ms === "number" && Number.isFinite(guideline.desktop_mode_modified_ms)) {
25634 return guideline.desktop_mode_modified_ms;
25635 }
25636 if (!guideline.modified) {
25637 return 0;
25638 }
25639 const parsed = Date.parse(guideline.modified);
25640 return Number.isFinite(parsed) ? parsed : 0;
25641 }
25642 function isFiniteNumber(value) {
25643 return typeof value === "number" && Number.isFinite(value);
25644 }
25645 class StickyNotesRestError extends Error {
25646 constructor(message, status) {
25647 super(message);
25648 this.name = "StickyNotesRestError";
25649 this.status = status;
25650 }
25651 }
25652 async function resolveStickyTerms(config) {
25653 const terms = await fetchStickyTermCandidates(config);
25654 const picked = pickStickyTerms(
25655 [...terms.artifactTerms, ...terms.artifactsTerms],
25656 terms.noteTerms,
25657 terms.stickyTerms
25658 );
25659 if (picked) {
25660 return picked;
25661 }
25662 const artifact = await ensureTerm(config, {
25663 slug: "artifact",
25664 name: "Artifact",
25665 parent: 0
25666 });
25667 const note = await ensureTerm(config, {
25668 slug: "note",
25669 name: "Note",
25670 parent: artifact.id
25671 });
25672 const sticky = await ensureTerm(config, {
25673 slug: "sticky",
25674 name: "Sticky",
25675 parent: artifact.id
25676 });
25677 return {
25678 stickyTermId: sticky.id,
25679 termIds: uniqueNumbers([artifact.id, note.id, sticky.id])
25680 };
25681 }
25682 async function fetchStickyTermCandidates(config) {
25683 const [artifactTerms, artifactsTerms, noteTerms, stickyTerms] = await Promise.all([
25684 fetchTermsBySlug(config, "artifact"),
25685 fetchTermsBySlug(config, "artifacts"),
25686 fetchTermsBySlug(config, "note"),
25687 fetchTermsBySlug(config, "sticky")
25688 ]);
25689 return {
25690 artifactTerms,
25691 artifactsTerms,
25692 noteTerms,
25693 stickyTerms
25694 };
25695 }
25696 function pickStickyTerms(artifactTerms, noteTerms, stickyTerms) {
25697 if (stickyTerms.length === 0) {
25698 return null;
25699 }
25700 const artifact = artifactTerms.find(
25701 (term) => ["artifact", "artifacts"].includes(term.slug)
25702 ) ?? artifactTerms[0] ?? null;
25703 const sticky = artifact ? stickyTerms.find((term) => Number(term.parent) === artifact.id) ?? stickyTerms[0] : stickyTerms[0];
25704 if (!sticky) {
25705 return null;
25706 }
25707 const note = artifact ? noteTerms.find((term) => Number(term.parent) === artifact.id) ?? null : null;
25708 return {
25709 stickyTermId: sticky.id,
25710 termIds: uniqueNumbers([
25711 artifact?.id,
25712 note?.id,
25713 sticky.id
25714 ])
25715 };
25716 }
25717 async function fetchStickyNotes(config, stickyTermId) {
25718 const guidelines = await requestJson(
25719 config,
25720 pathWithQuery("wp/v2/guidelines", {
25721 context: "edit",
25722 status: "private",
25723 per_page: "100",
25724 orderby: "modified",
25725 order: "desc",
25726 wp_guideline_type: String(stickyTermId)
25727 }),
25728 void 0,
25729 true
25730 );
25731 return guidelines.filter(
25732 (guideline) => Array.isArray(guideline.wp_guideline_type) ? guideline.wp_guideline_type.includes(stickyTermId) : true
25733 ).map(noteFromGuideline);
25734 }
25735 async function saveStickyNote(config, note, terms) {
25736 const components = noteComponentsForBody(note.body, note.title);
25737 const payload = {
25738 status: "private",
25739 title: components.title,
25740 content: components.content,
25741 excerpt: components.excerpt
25742 };
25743 if (note.guidelineId === null) {
25744 payload.wp_guideline_type = terms.termIds;
25745 }
25746 const path = note.guidelineId === null ? "wp/v2/guidelines" : `wp/v2/guidelines/${note.guidelineId}`;
25747 const guideline = await requestJson(
25748 config,
25749 path,
25750 {
25751 method: "POST",
25752 headers: {
25753 "Content-Type": "application/json"
25754 },
25755 body: JSON.stringify(payload)
25756 },
25757 false
25758 );
25759 return noteFromGuideline(guideline);
25760 }
25761 function buildGuidelineEditUrl(adminUrl, guidelineId) {
25762 const url = new URL("post.php", adminUrl);
25763 url.searchParams.set("post", String(guidelineId));
25764 url.searchParams.set("action", "edit");
25765 return url.toString();
25766 }
25767 async function fetchTermsBySlug(config, slug) {
25768 try {
25769 return await requestJson(
25770 config,
25771 pathWithQuery("wp/v2/wp_guideline_type", {
25772 context: "edit",
25773 slug,
25774 per_page: "100"
25775 }),
25776 void 0,
25777 true
25778 );
25779 } catch (error) {
25780 if (error instanceof StickyNotesRestError && (error.status === 404 || error.status === 400)) {
25781 return [];
25782 }
25783 throw error;
25784 }
25785 }
25786 async function ensureTerm(config, term) {
25787 const existing = await fetchTermsBySlug(config, term.slug);
25788 const byParent = existing.find(
25789 (item) => Number(item.parent ?? 0) === term.parent
25790 );
25791 if (byParent) {
25792 return byParent;
25793 }
25794 if (existing[0]) {
25795 return existing[0];
25796 }
25797 try {
25798 return await requestJson(
25799 config,
25800 "wp/v2/wp_guideline_type",
25801 {
25802 method: "POST",
25803 headers: {
25804 "Content-Type": "application/json"
25805 },
25806 body: JSON.stringify(term)
25807 },
25808 true
25809 );
25810 } catch (error) {
25811 const fallback = await fetchTermsBySlug(config, term.slug);
25812 if (fallback[0]) {
25813 return fallback[0];
25814 }
25815 throw error;
25816 }
25817 }
25818 async function requestJson(config, path, init2, silent = true) {
25819 const response = await trackedFetch$1(
25820 joinRestUrl(restRoot(config), path),
25821 init2,
25822 {
25823 source: "desktop-mode/sticky-notes",
25824 silent
25825 }
25826 );
25827 if (!response.ok) {
25828 throw new StickyNotesRestError(
25829 response.statusText || `${DEFAULT_STICKY_TITLE} request failed`,
25830 response.status
25831 );
25832 }
25833 return await response.json();
25834 }
25835 function restRoot(config) {
25836 if (config.restUrl) {
25837 return config.restUrl;
25838 }
25839 return `${window.location.origin}/wp-json/`;
25840 }
25841 function pathWithQuery(path, query) {
25842 const params = new URLSearchParams();
25843 Object.entries(query).forEach(([key, value]) => {
25844 params.set(key, value);
25845 });
25846 return `${path}?${params.toString()}`;
25847 }
25848 function uniqueNumbers(values) {
25849 const out = [];
25850 values.forEach((value) => {
25851 if (typeof value === "number" && Number.isFinite(value) && !out.includes(value)) {
25852 out.push(value);
25853 }
25854 });
25855 return out;
25856 }
25857 const SUBSCRIBE_FIELD = "desktop_mode_sticky_notes_subscribe";
25858 const RESPONSE_FIELD = "desktop_mode_sticky_notes";
25859 let started$3 = false;
25860 let target = null;
25861 function startStickyNotesHeartbeat(nextTarget) {
25862 target = nextTarget;
25863 if (started$3) {
25864 return;
25865 }
25866 started$3 = true;
25867 heartbeat.contribute(
25868 SUBSCRIBE_FIELD,
25869 () => target?.getHeartbeatSubscription()
25870 );
25871 heartbeat.subscribe(
25872 RESPONSE_FIELD,
25873 (payload) => {
25874 target?.applyHeartbeatPayload(payload);
25875 }
25876 );
25877 }
25878 const GEOMETRY_KEY = "desktop-mode-sticky-notes-geometry";
25879 const DEFAULT_WIDTH = 264;
25880 const DEFAULT_HEIGHT = 176;
25881 const MIN_WIDTH = 180;
25882 const MIN_HEIGHT = 128;
25883 const EDGE_PADDING = 16;
25884 const SAVE_DEBOUNCE_MS = 1e3;
25885 class StickyNotesLayer {
25886 constructor(options) {
25887 this.root = null;
25888 this.terms = null;
25889 this.controllers = /* @__PURE__ */ new Map();
25890 this.contextMenuInstalled = false;
25891 this.desktopHooksInstalled = false;
25892 this.highWaterMs = 0;
25893 this.zIndexCounter = 0;
25894 this.host = options.host;
25895 this.config = options.config;
25896 this.available = options.available ?? true;
25897 this.openArtifact = options.openArtifact;
25898 this.getActiveDesktopId = options.getActiveDesktopId ?? (() => "desktop-1");
25899 this.onError = options.onError;
25900 }
25901 async boot() {
25902 if (!this.available) {
25903 return;
25904 }
25905 try {
25906 this.terms = await resolveStickyTerms(this.config);
25907 if (!this.terms) {
25908 return;
25909 }
25910 this.installContextMenu();
25911 this.installDesktopHooks();
25912 const notes = await fetchStickyNotes(
25913 this.config,
25914 this.terms.stickyTermId
25915 );
25916 this.bumpHighWaterFromNotes(notes);
25917 startStickyNotesHeartbeat(this);
25918 if (notes.length === 0) {
25919 return;
25920 }
25921 this.ensureRoot();
25922 sortNotesByModified(notes).forEach(
25923 (note, index2) => this.upsert(note, index2)
25924 );
25925 } catch (error) {
25926 if (error instanceof Error) {
25927 console.debug("[desktop-mode] Sticky notes unavailable:", error.message);
25928 }
25929 }
25930 }
25931 createNote(body = "") {
25932 if (!this.terms) {
25933 return;
25934 }
25935 const note = {
25936 localId: `local:${Date.now()}:${Math.random().toString(36).slice(2)}`,
25937 guidelineId: null,
25938 title: body.trim() ? generatedTitle(body) : DEFAULT_STICKY_TITLE,
25939 body,
25940 termIds: this.terms.termIds
25941 };
25942 const controller = this.upsert(note, this.controllers.size, {
25943 activate: true
25944 });
25945 controller.focus();
25946 }
25947 upsert(note, index2, options = {}) {
25948 this.ensureRoot();
25949 const key = noteKey(note);
25950 const existing = this.controllers.get(key);
25951 if (existing) {
25952 existing.replace(note);
25953 if (options.activate) {
25954 this.bringToFront(existing);
25955 }
25956 return existing;
25957 }
25958 const controller = new StickyNoteController({
25959 layer: this,
25960 note,
25961 index: index2
25962 });
25963 this.controllers.set(key, controller);
25964 this.root?.appendChild(controller.element);
25965 this.assignZIndex(controller);
25966 this.applyDesktopVisibility(controller);
25967 if (options.activate) {
25968 this.bringToFront(controller);
25969 }
25970 return controller;
25971 }
25972 ensureRoot() {
25973 if (this.root) {
25974 return this.root;
25975 }
25976 const root = document.createElement("section");
25977 root.className = "desktop-mode-sticky-notes";
25978 root.setAttribute("aria-label", __("Sticky notes"));
25979 this.host.appendChild(root);
25980 this.root = root;
25981 return root;
25982 }
25983 installContextMenu() {
25984 if (this.contextMenuInstalled) {
25985 return;
25986 }
25987 this.contextMenuInstalled = true;
25988 addFilter(
25989 "desktop-mode.wallpaper-context-menu",
25990 "desktop-mode/sticky-notes",
25991 (items) => {
25992 if (!Array.isArray(items) || !this.terms) {
25993 return items;
25994 }
25995 if (items.some(
25996 (item) => item.id === "new-sticky-note"
25997 )) {
25998 return items;
25999 }
26000 return [
26001 ...items,
26002 {
26003 id: "new-sticky-note",
26004 label: __("New sticky note"),
26005 icon: "dashicons-edit-page",
26006 sort: 14,
26007 onClick: () => this.createNote()
26008 }
26009 ];
26010 }
26011 );
26012 }
26013 installDesktopHooks() {
26014 if (this.desktopHooksInstalled) {
26015 return;
26016 }
26017 this.desktopHooksInstalled = true;
26018 addAction(
26019 HOOKS.DESKTOP_SWITCHED,
26020 "desktop-mode/sticky-notes",
26021 () => this.refreshDesktopVisibility()
26022 );
26023 addAction(
26024 HOOKS.DESKTOP_CLOSED,
26025 "desktop-mode/sticky-notes",
26026 (detail) => {
26027 this.migrateDesktopAssignments(detail?.desktopId, detail?.migratedTo);
26028 this.refreshDesktopVisibility();
26029 }
26030 );
26031 }
26032 save(note) {
26033 if (!this.terms) {
26034 return Promise.reject(new Error(__("Sticky term is unavailable.")));
26035 }
26036 return saveStickyNote(this.config, note, this.terms);
26037 }
26038 getHeartbeatSubscription() {
26039 if (!this.terms) {
26040 return void 0;
26041 }
26042 return {
26043 stickyTermId: this.terms.stickyTermId,
26044 knownIds: this.knownGuidelineIds(),
26045 version: this.highWaterMs
26046 };
26047 }
26048 applyHeartbeatPayload(payload) {
26049 for (const guideline of payload.notes ?? []) {
26050 const note = noteFromGuideline(guideline);
26051 this.upsertRemote(note);
26052 }
26053 for (const id of payload.removed ?? []) {
26054 this.forgetGuidelineId(id);
26055 }
26056 if (typeof payload.serverTimeMs === "number" && Number.isFinite(payload.serverTimeMs) && payload.serverTimeMs > this.highWaterMs) {
26057 this.highWaterMs = payload.serverTimeMs;
26058 }
26059 if (payload.truncated) {
26060 void this.reloadFromServer();
26061 }
26062 }
26063 openNoteArtifact(note) {
26064 if (note.guidelineId === null) {
26065 return;
26066 }
26067 this.openArtifact(
26068 buildGuidelineEditUrl(this.config.adminUrl, note.guidelineId),
26069 note.title,
26070 note.guidelineId
26071 );
26072 }
26073 notifyError(message) {
26074 this.onError?.(message);
26075 }
26076 hostSize() {
26077 return {
26078 width: Math.max(1, this.host.clientWidth),
26079 height: Math.max(1, this.host.clientHeight)
26080 };
26081 }
26082 defaultGeometry(index2) {
26083 const { width: hostWidth, height: hostHeight } = this.hostSize();
26084 const width = Math.min(
26085 DEFAULT_WIDTH,
26086 Math.max(MIN_WIDTH, hostWidth - EDGE_PADDING * 2)
26087 );
26088 const height = Math.min(
26089 DEFAULT_HEIGHT,
26090 Math.max(MIN_HEIGHT, hostHeight - EDGE_PADDING * 2)
26091 );
26092 const offset = index2 % 8 * 28;
26093 const left = clamp(
26094 hostWidth - width - 32 - offset,
26095 EDGE_PADDING,
26096 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
26097 );
26098 const top = clamp(
26099 32 + offset,
26100 EDGE_PADDING,
26101 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
26102 );
26103 return {
26104 x: left / hostWidth,
26105 y: top / hostHeight,
26106 width,
26107 height
26108 };
26109 }
26110 forget(controller) {
26111 this.controllers.delete(noteKey(controller.note));
26112 controller.dispose();
26113 controller.element.remove();
26114 if (this.controllers.size === 0) {
26115 this.root?.remove();
26116 this.root = null;
26117 }
26118 }
26119 replaceControllerKey(oldKey, controller) {
26120 const newKey = noteKey(controller.note);
26121 this.controllers.delete(oldKey);
26122 this.controllers.set(newKey, controller);
26123 moveStoredGeometry(oldKey, newKey);
26124 this.applyDesktopVisibility(controller);
26125 }
26126 bumpHighWaterFromNote(note) {
26127 const modifiedMs = noteModifiedMs(note);
26128 if (modifiedMs > this.highWaterMs) {
26129 this.highWaterMs = modifiedMs;
26130 }
26131 }
26132 bringToFront(controller) {
26133 controller.setZIndex(this.nextZIndex());
26134 }
26135 geometryForNote(note, index2) {
26136 const key = noteKey(note);
26137 const loaded = loadGeometry(key);
26138 const desktopId = this.normalizeDesktopId(loaded?.desktopId);
26139 const geometry = loaded ? { ...loaded, desktopId } : { ...this.defaultGeometry(index2), desktopId };
26140 if (!loaded || loaded.desktopId !== geometry.desktopId) {
26141 saveGeometry(key, geometry);
26142 }
26143 return geometry;
26144 }
26145 upsertRemote(note) {
26146 const key = noteKey(note);
26147 const existing = this.controllers.get(key);
26148 if (existing) {
26149 if (!existing.shouldReplaceFromRemote(note)) {
26150 this.bumpHighWaterFromNote(note);
26151 return existing;
26152 }
26153 existing.replace(note);
26154 this.bumpHighWaterFromNote(note);
26155 return existing;
26156 }
26157 const controller = this.upsert(note, this.controllers.size);
26158 this.bumpHighWaterFromNote(note);
26159 return controller;
26160 }
26161 forgetGuidelineId(guidelineId) {
26162 for (const controller of this.controllers.values()) {
26163 if (controller.note.guidelineId === guidelineId) {
26164 this.forget(controller);
26165 return;
26166 }
26167 }
26168 }
26169 knownGuidelineIds() {
26170 const ids = [];
26171 for (const controller of this.controllers.values()) {
26172 if (controller.note.guidelineId !== null) {
26173 ids.push(controller.note.guidelineId);
26174 }
26175 }
26176 return ids;
26177 }
26178 bumpHighWaterFromNotes(notes) {
26179 notes.forEach((note) => this.bumpHighWaterFromNote(note));
26180 }
26181 assignZIndex(controller) {
26182 controller.setZIndex(this.nextZIndex());
26183 }
26184 nextZIndex() {
26185 this.zIndexCounter += 1;
26186 return this.zIndexCounter;
26187 }
26188 applyDesktopVisibility(controller) {
26189 controller.setVisible(this.isNoteOnActiveDesktop(controller.note));
26190 }
26191 refreshDesktopVisibility() {
26192 for (const controller of this.controllers.values()) {
26193 this.applyDesktopVisibility(controller);
26194 }
26195 }
26196 isNoteOnActiveDesktop(note) {
26197 const key = noteKey(note);
26198 const geometry = loadGeometry(key);
26199 const desktopId = this.normalizeDesktopId(geometry?.desktopId);
26200 if (geometry && geometry.desktopId !== desktopId) {
26201 saveGeometry(key, { ...geometry, desktopId });
26202 }
26203 return desktopId === this.activeDesktopId();
26204 }
26205 migrateDesktopAssignments(desktopId, migratedTo) {
26206 if (!desktopId || !migratedTo || desktopId === migratedTo) {
26207 return;
26208 }
26209 const map = readGeometryMap();
26210 let changed = false;
26211 Object.entries(map).forEach(([key, geometry]) => {
26212 if (geometry.desktopId === desktopId) {
26213 map[key] = {
26214 ...geometry,
26215 desktopId: this.normalizeDesktopId(migratedTo)
26216 };
26217 changed = true;
26218 }
26219 });
26220 if (changed) {
26221 writeGeometryMap(map);
26222 }
26223 }
26224 activeDesktopId() {
26225 try {
26226 const id = this.getActiveDesktopId();
26227 return typeof id === "string" && id ? id : "desktop-1";
26228 } catch {
26229 return "desktop-1";
26230 }
26231 }
26232 normalizeDesktopId(desktopId) {
26233 if (!desktopId) {
26234 return this.activeDesktopId();
26235 }
26236 return desktopId;
26237 }
26238 async reloadFromServer() {
26239 if (!this.terms) {
26240 return;
26241 }
26242 try {
26243 const notes = await fetchStickyNotes(
26244 this.config,
26245 this.terms.stickyTermId
26246 );
26247 const ids = /* @__PURE__ */ new Set();
26248 sortNotesByModified(notes).forEach((note) => {
26249 if (note.guidelineId !== null) {
26250 ids.add(note.guidelineId);
26251 }
26252 this.upsertRemote(note);
26253 });
26254 this.knownGuidelineIds().forEach((id) => {
26255 if (!ids.has(id)) {
26256 this.forgetGuidelineId(id);
26257 }
26258 });
26259 } catch {
26260 }
26261 }
26262 }
26263 class StickyNoteController {
26264 constructor(options) {
26265 this.saveTimer = null;
26266 this.geometryTimer = null;
26267 this.saving = false;
26268 this.saveAgain = false;
26269 this.resizeObserver = null;
26270 this.disposed = false;
26271 this.layer = options.layer;
26272 this.note = options.note;
26273 this.index = options.index;
26274 this.element = document.createElement("article");
26275 this.element.className = "desktop-mode-sticky-note";
26276 this.element.dataset.stickyNoteId = noteKey(this.note);
26277 this.titleEl = document.createElement("span");
26278 this.editor = document.createElement("wpd-textarea");
26279 this.statusEl = document.createElement("wpd-save-status");
26280 this.openButton = document.createElement("wpd-window-button");
26281 this.paint();
26282 this.applyGeometry(this.layer.geometryForNote(this.note, this.index));
26283 this.element.addEventListener(
26284 "pointerdown",
26285 () => this.layer.bringToFront(this),
26286 { capture: true }
26287 );
26288 this.element.addEventListener("focusin", () => this.layer.bringToFront(this));
26289 this.watchResize();
26290 }
26291 focus() {
26292 window.setTimeout(() => this.editor.focusInput?.(), 0);
26293 }
26294 replace(note) {
26295 this.note = note;
26296 this.element.dataset.stickyNoteId = noteKey(this.note);
26297 this.titleEl.textContent = this.note.title;
26298 this.editor.setAttribute("value", this.note.body);
26299 this.refreshOpenButton();
26300 }
26301 shouldReplaceFromRemote(note) {
26302 if (this.hasLocalChanges()) {
26303 return false;
26304 }
26305 const currentMs = noteModifiedMs(this.note);
26306 const incomingMs = noteModifiedMs(note);
26307 if (currentMs > 0 && incomingMs > 0 && incomingMs <= currentMs && this.note.title === note.title && this.note.body === note.body) {
26308 return false;
26309 }
26310 return true;
26311 }
26312 setZIndex(zIndex) {
26313 this.element.style.zIndex = String(zIndex);
26314 }
26315 setVisible(visible) {
26316 this.element.style.display = visible ? "" : "none";
26317 }
26318 dispose() {
26319 this.disposed = true;
26320 if (this.saveTimer !== null) {
26321 window.clearTimeout(this.saveTimer);
26322 this.saveTimer = null;
26323 }
26324 if (this.geometryTimer !== null) {
26325 window.clearTimeout(this.geometryTimer);
26326 this.geometryTimer = null;
26327 }
26328 this.resizeObserver?.disconnect();
26329 this.resizeObserver = null;
26330 }
26331 paint() {
26332 this.element.innerHTML = "";
26333 this.element.style.minWidth = `${MIN_WIDTH}px`;
26334 this.element.style.minHeight = `${MIN_HEIGHT}px`;
26335 const header = document.createElement("div");
26336 header.className = "desktop-mode-sticky-note__header";
26337 const grip = document.createElement("span");
26338 grip.className = "desktop-mode-sticky-note__grip";
26339 grip.setAttribute("aria-hidden", "true");
26340 this.titleEl.className = "desktop-mode-sticky-note__title";
26341 this.titleEl.textContent = this.note.title;
26342 this.statusEl.setAttribute("mode", "icon");
26343 this.statusEl.setAttribute("phase", "idle");
26344 this.statusEl.className = "desktop-mode-sticky-note__status";
26345 this.openButton.setAttribute("icon", "detach");
26346 this.openButton.setAttribute("title", __("Open artifact"));
26347 this.openButton.className = "desktop-mode-sticky-note__open";
26348 this.openButton.addEventListener("wpd-button-activate", () => {
26349 this.layer.openNoteArtifact(this.note);
26350 });
26351 const close = document.createElement("wpd-window-button");
26352 close.setAttribute("icon", "close");
26353 close.setAttribute("danger", "");
26354 close.setAttribute("title", __("Hide sticky note"));
26355 close.className = "desktop-mode-sticky-note__close";
26356 close.addEventListener("wpd-button-activate", () => this.close());
26357 header.append(grip, this.titleEl, this.statusEl, this.openButton, close);
26358 header.addEventListener("pointerdown", (event) => this.startDrag(event));
26359 this.editor.className = "desktop-mode-sticky-note__editor";
26360 this.editor.setAttribute("aria-label", __("Sticky note text"));
26361 this.editor.setAttribute("rows", "8");
26362 this.editor.setAttribute("value", this.note.body);
26363 this.installEditorKeyboardGuard();
26364 this.editor.addEventListener("wpd-input-change", (event) => {
26365 const detail = event.detail;
26366 this.note.body = detail.value;
26367 this.note.title = titleForBody(detail.value);
26368 this.titleEl.textContent = this.note.title;
26369 this.setPhase("pending");
26370 this.scheduleSave();
26371 });
26372 this.editor.addEventListener("wpd-input-commit", () => this.flushSave());
26373 this.element.append(header, this.editor);
26374 this.refreshOpenButton();
26375 }
26376 installEditorKeyboardGuard() {
26377 ["keydown", "keypress", "keyup"].forEach((eventName) => {
26378 this.editor.addEventListener(eventName, (event) => {
26379 event.stopPropagation();
26380 });
26381 });
26382 }
26383 refreshOpenButton() {
26384 const disabled = this.note.guidelineId === null;
26385 this.openButton.classList.toggle("is-disabled", disabled);
26386 this.openButton.setAttribute("aria-disabled", disabled ? "true" : "false");
26387 }
26388 close() {
26389 if (this.note.guidelineId === null && this.note.body.trim().length === 0) {
26390 this.layer.forget(this);
26391 return;
26392 }
26393 this.flushSave();
26394 this.layer.forget(this);
26395 }
26396 scheduleSave() {
26397 if (this.note.guidelineId === null && this.note.body.trim().length === 0) {
26398 this.setPhase("idle");
26399 return;
26400 }
26401 if (this.saveTimer !== null) {
26402 window.clearTimeout(this.saveTimer);
26403 }
26404 this.saveTimer = window.setTimeout(() => {
26405 this.saveTimer = null;
26406 void this.save();
26407 }, SAVE_DEBOUNCE_MS);
26408 }
26409 flushSave() {
26410 if (this.saveTimer !== null) {
26411 window.clearTimeout(this.saveTimer);
26412 this.saveTimer = null;
26413 }
26414 if (this.note.guidelineId !== null || this.note.body.trim().length > 0) {
26415 void this.save();
26416 }
26417 }
26418 async save() {
26419 if (this.saving) {
26420 this.saveAgain = true;
26421 this.setPhase("pending");
26422 return;
26423 }
26424 this.saving = true;
26425 this.setPhase("saving");
26426 const bodyAtSave = this.note.body;
26427 try {
26428 const saved = await this.layer.save({
26429 ...this.note,
26430 body: bodyAtSave
26431 });
26432 if (this.disposed) {
26433 return;
26434 }
26435 const oldKey = noteKey(this.note);
26436 this.note.guidelineId = saved.guidelineId;
26437 this.note.modified = saved.modified;
26438 this.note.link = saved.link;
26439 this.note.termIds = saved.termIds.length > 0 ? saved.termIds : this.note.termIds;
26440 if (this.note.body === bodyAtSave) {
26441 this.note.title = saved.title;
26442 this.titleEl.textContent = saved.title;
26443 }
26444 if (oldKey !== noteKey(this.note)) {
26445 this.element.dataset.stickyNoteId = noteKey(this.note);
26446 this.layer.replaceControllerKey(oldKey, this);
26447 }
26448 this.layer.bumpHighWaterFromNote(this.note);
26449 this.refreshOpenButton();
26450 this.setPhase("saved");
26451 } catch (error) {
26452 if (this.disposed) {
26453 return;
26454 }
26455 const message = error instanceof Error ? error.message : __("Could not save sticky note.");
26456 this.setPhase("failed", message);
26457 this.layer.notifyError(message);
26458 } finally {
26459 this.saving = false;
26460 if (!this.disposed && this.saveAgain) {
26461 this.saveAgain = false;
26462 this.scheduleSave();
26463 }
26464 }
26465 }
26466 setPhase(phase, error) {
26467 this.statusEl.setAttribute("phase", phase);
26468 if (error) {
26469 this.statusEl.setAttribute("error", error);
26470 this.statusEl.setAttribute("title", error);
26471 } else {
26472 this.statusEl.removeAttribute("error");
26473 this.statusEl.removeAttribute("title");
26474 }
26475 }
26476 hasLocalChanges() {
26477 const phase = this.statusEl.getAttribute("phase");
26478 return this.saveTimer !== null || this.saving || this.saveAgain || phase === "pending" || phase === "failed";
26479 }
26480 startDrag(event) {
26481 if (event.button !== 0) {
26482 return;
26483 }
26484 const target2 = event.target;
26485 if (target2?.closest("wpd-window-button, wpd-save-status")) {
26486 return;
26487 }
26488 event.preventDefault();
26489 const startRect = this.element.getBoundingClientRect();
26490 const hostRect = this.layerHostRect();
26491 const startLeft = startRect.left - hostRect.left;
26492 const startTop = startRect.top - hostRect.top;
26493 const startX = event.clientX;
26494 const startY = event.clientY;
26495 this.element.classList.add("desktop-mode-sticky-note--dragging");
26496 this.element.setPointerCapture?.(event.pointerId);
26497 const move = (moveEvent) => {
26498 const width = this.element.offsetWidth;
26499 const height = this.element.offsetHeight;
26500 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26501 const left = clamp(
26502 startLeft + moveEvent.clientX - startX,
26503 EDGE_PADDING,
26504 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
26505 );
26506 const top = clamp(
26507 startTop + moveEvent.clientY - startY,
26508 EDGE_PADDING,
26509 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
26510 );
26511 this.element.style.left = `${left}px`;
26512 this.element.style.top = `${top}px`;
26513 };
26514 const up = (upEvent) => {
26515 this.element.classList.remove("desktop-mode-sticky-note--dragging");
26516 this.element.releasePointerCapture?.(upEvent.pointerId);
26517 document.removeEventListener("pointermove", move);
26518 document.removeEventListener("pointerup", up);
26519 this.persistGeometry();
26520 };
26521 document.addEventListener("pointermove", move);
26522 document.addEventListener("pointerup", up);
26523 }
26524 applyGeometry(geometry) {
26525 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26526 const width = clamp(geometry.width, MIN_WIDTH, hostWidth - EDGE_PADDING * 2);
26527 const height = clamp(geometry.height, MIN_HEIGHT, hostHeight - EDGE_PADDING * 2);
26528 const left = clamp(
26529 geometry.x * hostWidth,
26530 EDGE_PADDING,
26531 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
26532 );
26533 const top = clamp(
26534 geometry.y * hostHeight,
26535 EDGE_PADDING,
26536 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
26537 );
26538 this.element.style.left = `${left}px`;
26539 this.element.style.top = `${top}px`;
26540 this.element.style.width = `${width}px`;
26541 this.element.style.height = `${height}px`;
26542 }
26543 watchResize() {
26544 if (typeof ResizeObserver === "undefined") {
26545 return;
26546 }
26547 this.resizeObserver = new ResizeObserver(() => {
26548 if (this.geometryTimer !== null) {
26549 window.clearTimeout(this.geometryTimer);
26550 }
26551 this.geometryTimer = window.setTimeout(() => {
26552 this.geometryTimer = null;
26553 this.persistGeometry();
26554 }, 150);
26555 });
26556 this.resizeObserver.observe(this.element);
26557 }
26558 persistGeometry() {
26559 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26560 const left = parseFloat(this.element.style.left) || 0;
26561 const top = parseFloat(this.element.style.top) || 0;
26562 const existing = loadGeometry(noteKey(this.note));
26563 saveGeometry(noteKey(this.note), {
26564 ...existing ?? {},
26565 x: clamp(left / hostWidth, 0, 1),
26566 y: clamp(top / hostHeight, 0, 1),
26567 width: this.element.offsetWidth,
26568 height: this.element.offsetHeight
26569 });
26570 }
26571 layerHostRect() {
26572 const parent = this.element.parentElement?.parentElement;
26573 return (parent ?? document.body).getBoundingClientRect();
26574 }
26575 }
26576 function bootStickyNotes(options) {
26577 const layer = new StickyNotesLayer(options);
26578 void layer.boot();
26579 return layer;
26580 }
26581 function noteKey(note) {
26582 return note.guidelineId === null ? note.localId : `guideline:${note.guidelineId}`;
26583 }
26584 function noteModifiedMs(note) {
26585 if (typeof note.modifiedMs === "number" && Number.isFinite(note.modifiedMs)) {
26586 return note.modifiedMs;
26587 }
26588 if (!note.modified) {
26589 return 0;
26590 }
26591 const parsed = Date.parse(note.modified);
26592 return Number.isFinite(parsed) ? parsed : 0;
26593 }
26594 function sortNotesByModified(notes) {
26595 return [...notes].sort((a, b) => noteModifiedMs(a) - noteModifiedMs(b));
26596 }
26597 function loadGeometry(key) {
26598 const map = readGeometryMap();
26599 const value = map[key];
26600 if (!value || !Number.isFinite(value.x) || !Number.isFinite(value.y) || !Number.isFinite(value.width) || !Number.isFinite(value.height)) {
26601 return null;
26602 }
26603 return value;
26604 }
26605 function saveGeometry(key, geometry) {
26606 const map = readGeometryMap();
26607 map[key] = geometry;
26608 writeGeometryMap(map);
26609 }
26610 function moveStoredGeometry(oldKey, newKey) {
26611 if (oldKey === newKey) {
26612 return;
26613 }
26614 const map = readGeometryMap();
26615 if (map[oldKey]) {
26616 map[newKey] = map[oldKey];
26617 delete map[oldKey];
26618 writeGeometryMap(map);
26619 }
26620 }
26621 function readGeometryMap() {
26622 try {
26623 const raw = window.localStorage.getItem(GEOMETRY_KEY);
26624 return raw ? JSON.parse(raw) : {};
26625 } catch {
26626 return {};
26627 }
26628 }
26629 function writeGeometryMap(map) {
26630 try {
26631 window.localStorage.setItem(GEOMETRY_KEY, JSON.stringify(map));
26632 } catch {
26633 }
26634 }
26635 function clamp(value, min, max) {
26636 if (max < min) {
26637 return min;
26638 }
26639 return Math.min(max, Math.max(min, value));
26640 }
26641 const clock = {
26642 id: "clock",
26643 // Labels/descriptions on built-in defs stay string-literal at
26644 // module-eval time so the extract-pot pass picks them up. The
26645 // values are wrapped in `__()` so they translate at runtime.
26646 get label() {
26647 return __("Clock");
26648 },
26649 get description() {
26650 return __("Local time and date, refreshed every second.");
26651 },
26652 icon: "dashicons-clock",
26653 mount: (container) => {
26654 container.classList.add("desktop-mode-widget-clock");
26655 const time = document.createElement("div");
26656 time.className = "desktop-mode-widget-clock__time";
26657 container.appendChild(time);
26658 const date = document.createElement("div");
26659 date.className = "desktop-mode-widget-clock__date";
26660 container.appendChild(date);
26661 const render2 = () => {
26662 const now = /* @__PURE__ */ new Date();
26663 time.textContent = now.toLocaleTimeString(void 0, {
26664 hour: "2-digit",
26665 minute: "2-digit"
26666 });
26667 date.textContent = now.toLocaleDateString(void 0, {
26668 weekday: "long",
26669 month: "short",
26670 day: "numeric"
26671 });
26672 };
26673 render2();
26674 const msUntilNextSecond = 1e3 - Date.now() % 1e3;
26675 let interval = null;
26676 const kickoff = window.setTimeout(() => {
26677 render2();
26678 interval = window.setInterval(render2, 1e3);
26679 }, msUntilNextSecond);
26680 return () => {
26681 window.clearTimeout(kickoff);
26682 if (interval !== null) {
26683 window.clearInterval(interval);
26684 }
26685 };
26686 }
26687 };
26688 function registerBuiltInWidgets() {
26689 register(clock);
26690 }
26691 function createWidgetRegistrySync(deps2) {
26692 const { layer } = deps2;
26693 const registered = /* @__PURE__ */ new Set();
26694 const loadedScripts = /* @__PURE__ */ new Set();
26695 const ensureScript = async (entry) => {
26696 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
26697 return;
26698 }
26699 try {
26700 await loadVendorScript(entry.scriptUrl, {
26701 translations: entry.scriptTranslations,
26702 l10n: entry.scriptL10n,
26703 before: entry.scriptBefore,
26704 after: entry.scriptAfter
26705 });
26706 } catch (err) {
26707 doAction(HOOKS.SHELL_ERROR, {
26708 scope: "widget-script-load",
26709 id: entry.id,
26710 error: err
26711 });
26712 }
26713 loadedScripts.add(entry.scriptUrl);
26714 };
26715 const buildDefFromEntry = (entry) => {
26716 const globals = window.desktopModeWidgets || {};
26717 const mount = globals[entry.id];
26718 if (!mount) {
26719 doAction(HOOKS.SHELL_ERROR, {
26720 scope: "widget-missing-mount",
26721 id: entry.id,
26722 error: new Error(
26723 `[desktop-mode] No mount callback on window.desktopModeWidgets["${entry.id}"]. Plugin script loaded but didn't register. Check the plugin's enqueue + global assignment.`
26724 )
26725 });
26726 return null;
26727 }
26728 return {
26729 id: entry.id,
26730 label: entry.label,
26731 description: entry.description,
26732 icon: entry.icon,
26733 movable: entry.movable,
26734 resizable: entry.resizable,
26735 minWidth: entry.minWidth || void 0,
26736 minHeight: entry.minHeight || void 0,
26737 maxWidth: entry.maxWidth || void 0,
26738 maxHeight: entry.maxHeight || void 0,
26739 defaultWidth: entry.defaultWidth || void 0,
26740 defaultHeight: entry.defaultHeight || void 0,
26741 mount
26742 };
26743 };
26744 const registerEntry = async (entry) => {
26745 if (registered.has(entry.id)) {
26746 return;
26747 }
26748 await ensureScript(entry);
26749 const def = buildDefFromEntry(entry);
26750 if (!def) {
26751 return;
26752 }
26753 try {
26754 register(def);
26755 } catch (err) {
26756 doAction(HOOKS.SHELL_ERROR, {
26757 scope: "widget-register",
26758 id: entry.id,
26759 error: err
26760 });
26761 return;
26762 }
26763 registered.add(entry.id);
26764 refreshWidgetPicker();
26765 if (layer) {
26766 layer.mountIfEnabled(entry.id);
26767 }
26768 };
26769 const unregisterEntry = (id) => {
26770 if (!registered.has(id)) {
26771 return;
26772 }
26773 layer?.unmount(id);
26774 unregister(id);
26775 registered.delete(id);
26776 refreshWidgetPicker();
26777 };
26778 return async (list2) => {
26779 const incoming = /* @__PURE__ */ new Set();
26780 for (const entry of list2) {
26781 incoming.add(entry.id);
26782 }
26783 for (const id of Array.from(registered)) {
26784 if (!incoming.has(id)) {
26785 unregisterEntry(id);
26786 }
26787 }
26788 for (const entry of list2) {
26789 if (!registered.has(entry.id)) {
26790 await registerEntry(entry);
26791 }
26792 }
26793 };
26794 }
26795 const WPD_COMPONENT_TAGS = [
26796 "wpd-section",
26797 "wpd-button",
26798 "wpd-swatch",
26799 "wpd-swatch-grid",
26800 "wpd-segmented",
26801 "wpd-segment",
26802 "wpd-select",
26803 "wpd-option",
26804 "wpd-multiselect",
26805 "wpd-color-field",
26806 "wpd-range-field",
26807 "wpd-text-field",
26808 "wpd-number-field",
26809 "wpd-checkbox",
26810 "wpd-checkbox-label",
26811 "wpd-toast",
26812 "wpd-toast-container",
26813 "wpd-tabs",
26814 "wpd-tab",
26815 "wpd-tabpanel",
26816 "wpd-window-button",
26817 "wpd-menu",
26818 "wpd-menu-item",
26819 "wpd-context-menu",
26820 "wpd-context-menu-option",
26821 "wpd-confirm-dialog",
26822 "wpd-modal",
26823 "wpd-user-search",
26824 "wpd-role-picker",
26825 "wpd-flyout",
26826 "wpd-tab-chip",
26827 "wpd-stack",
26828 "wpd-cluster",
26829 "wpd-icon",
26830 "wpd-body",
26831 "wpd-panel",
26832 "wpd-row",
26833 "wpd-grid",
26834 "wpd-display",
26835 "wpd-empty-state",
26836 "wpd-key",
26837 "wpd-code",
26838 "wpd-badge",
26839 "wpd-log",
26840 "wpd-steps",
26841 "wpd-step",
26842 "wpd-table",
26843 "wpd-spinner",
26844 "wpd-relative-time",
26845 "wpd-avatar",
26846 "wpd-textarea",
26847 "wpd-chip",
26848 "wpd-tag-input",
26849 "wpd-form",
26850 "wpd-save-status",
26851 "wpd-category-picker",
26852 "wpd-crumb-chain",
26853 "wpd-card",
26854 "wpd-notice"
26855 ];
26856 const KNOWN = new Set(WPD_COMPONENT_TAGS);
26857 const WARN_GRACE_MS = 2e3;
26858 const warnedTags = /* @__PURE__ */ new Set();
26859 const observedRoots = /* @__PURE__ */ new WeakSet();
26860 let started$2 = false;
26861 function distance(a, b) {
26862 const m = a.length;
26863 const n = b.length;
26864 if (m === 0) {
26865 return n;
26866 }
26867 if (n === 0) {
26868 return m;
26869 }
26870 const dp = new Array(n + 1);
26871 for (let j = 0; j <= n; j++) {
26872 dp[j] = j;
26873 }
26874 for (let i = 1; i <= m; i++) {
26875 let prev = dp[0];
26876 dp[0] = i;
26877 for (let j = 1; j <= n; j++) {
26878 const tmp = dp[j];
26879 dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
26880 prev = tmp;
26881 }
26882 }
26883 return dp[n];
26884 }
26885 function suggest(tag) {
26886 let best = null;
26887 let bestD = Infinity;
26888 for (const known of KNOWN) {
26889 const d = distance(tag, known);
26890 if (d < bestD) {
26891 bestD = d;
26892 best = known;
26893 }
26894 }
26895 return bestD > 0 && bestD <= 3 ? best : null;
26896 }
26897 function folderFor(tag) {
26898 return tag.startsWith("wpd-") ? tag.slice(4) : tag;
26899 }
26900 function warnFor(tag, sample) {
26901 if (warnedTags.has(tag)) {
26902 return;
26903 }
26904 warnedTags.add(tag);
26905 const isKnown = KNOWN.has(tag);
26906 if (isKnown) {
26907 const folder = folderFor(tag);
26908 console.error(
26909 `[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.
26910
26911 Fix — side-effect-import the component module from wherever you render it:
26912
26913 import '<rel>/ui/components/${folder}/${folder}';
26914
26915 Or pull every wpd-* component in one go (heavier — only do this from an entry bundle):
26916
26917 import '<rel>/ui/components';
26918
26919 See docs/components-reference.md for the full list.`,
26920 "\nFirst offending element:",
26921 sample
26922 );
26923 return;
26924 }
26925 const guess = suggest(tag);
26926 if (guess) {
26927 console.error(
26928 `[wp.desktop] <${tag}> is not a registered wpd-* component. Did you mean <${guess}>?
26929
26930 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'.`,
26931 "\nFirst offending element:",
26932 sample
26933 );
26934 return;
26935 }
26936 console.error(
26937 `[wp.desktop] <${tag}> looks like a wpd-* tag but no component by that name exists.
26938
26939 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.`,
26940 "\nFirst offending element:",
26941 sample
26942 );
26943 }
26944 function checkElement(el) {
26945 const tag = el.tagName.toLowerCase();
26946 if (!tag.startsWith("wpd-")) {
26947 return;
26948 }
26949 if (warnedTags.has(tag)) {
26950 return;
26951 }
26952 if (customElements.get(tag)) {
26953 return;
26954 }
26955 let settled = false;
26956 customElements.whenDefined(tag).then(() => {
26957 settled = true;
26958 });
26959 setTimeout(() => {
26960 if (settled) {
26961 return;
26962 }
26963 if (customElements.get(tag)) {
26964 return;
26965 }
26966 warnFor(tag, el);
26967 }, WARN_GRACE_MS);
26968 }
26969 function walk(root) {
26970 if (root instanceof Element) {
26971 checkElement(root);
26972 if (root.shadowRoot) {
26973 observeRoot(root.shadowRoot);
26974 }
26975 }
26976 const all2 = root.querySelectorAll("*");
26977 for (let i = 0; i < all2.length; i++) {
26978 const el = all2[i];
26979 checkElement(el);
26980 if (el.shadowRoot) {
26981 observeRoot(el.shadowRoot);
26982 }
26983 }
26984 }
26985 function observeRoot(root) {
26986 if (observedRoots.has(root)) {
26987 return;
26988 }
26989 observedRoots.add(root);
26990 walk(root);
26991 const mo = new MutationObserver((records) => {
26992 for (let i = 0; i < records.length; i++) {
26993 const added = records[i].addedNodes;
26994 for (let j = 0; j < added.length; j++) {
26995 const node = added[j];
26996 if (node.nodeType === 1) {
26997 walk(node);
26998 }
26999 }
27000 }
27001 });
27002 mo.observe(root, { childList: true, subtree: true });
27003 }
27004 function patchAttachShadow() {
27005 const proto = Element.prototype;
27006 const original = proto.attachShadow;
27007 if (original.__wpdPatched) {
27008 return;
27009 }
27010 const patched = function(init2) {
27011 const root = original.call(this, init2);
27012 if (root.mode === "open") {
27013 observeRoot(root);
27014 }
27015 return root;
27016 };
27017 patched.__wpdPatched = true;
27018 proto.attachShadow = patched;
27019 }
27020 function startMissingImportWarner() {
27021 if (started$2) {
27022 return;
27023 }
27024 if (typeof document === "undefined") {
27025 return;
27026 }
27027 started$2 = true;
27028 patchAttachShadow();
27029 observeRoot(document);
27030 }
27031 const TRASHABLE_SHORTCUT_KINDS = /* @__PURE__ */ new Set(["post"]);
27032 function getMyWordpressTrashApi() {
27033 const api = window.wp?.desktop?.myWordpress;
27034 return api && typeof api.trashEntity === "function" ? api : null;
27035 }
27036 const TRASH_DROP_ACTIVE_ATTR = "data-desktop-mode-trash-drop-active";
27037 const RECYCLE_BIN_WINDOW_ID = "desktop-mode-recycle-bin";
27038 const BIN_TILE_SELECTORS = [
27039 `.desktop-mode-file-tile[data-file-ref="${RECYCLE_BIN_WINDOW_ID}"]`,
27040 `[data-icon-id="${RECYCLE_BIN_WINDOW_ID}"]`,
27041 `[data-system-id="${RECYCLE_BIN_WINDOW_ID}"]`
27042 ];
27043 function findBinTile() {
27044 for (const sel of BIN_TILE_SELECTORS) {
27045 const el = document.querySelector(sel);
27046 if (el instanceof HTMLElement) {
27047 return el;
27048 }
27049 }
27050 return null;
27051 }
27052 let _installed = false;
27053 let _dockDeregister = null;
27054 let _windowDeregister = null;
27055 let _binMutationObserver = null;
27056 function isDesktopFilePayload(session) {
27057 return session.payload.type === "desktop-file";
27058 }
27059 function isShortcutPayload(session) {
27060 return session.payload.type === "shortcut";
27061 }
27062 function isTrashableShortcut(data) {
27063 if (!data.kind || !data.ref || !data.entityId) {
27064 return false;
27065 }
27066 if (!TRASHABLE_SHORTCUT_KINDS.has(data.kind)) {
27067 return false;
27068 }
27069 const numericRef = Number.parseInt(data.ref, 10);
27070 if (!Number.isFinite(numericRef) || numericRef <= 0) {
27071 return false;
27072 }
27073 return getMyWordpressTrashApi() !== null;
27074 }
27075 function registerOn(dragManager, id, el) {
27076 return dragManager.registerDropTarget({
27077 id,
27078 element: el,
27079 // Override the ghost-chip label: while the cursor is over
27080 // the bin the user is trashing, not creating a shortcut /
27081 // moving the placement. The DragManager swaps this in for
27082 // the payload-default "Drop here to create shortcut" /
27083 // "Drop here to move" chip text whenever this target is the
27084 // current accept-mode target.
27085 acceptLabel: __("Move to Trash", "desktop-mode"),
27086 // Reject the drop UP FRONT when the viewer can't trash the
27087 // payload's placement (e.g. an item inside a read-only
27088 // shared folder, or someone else's tile in a shared
27089 // namespace). `accept` flipping to `false` means the
27090 // drop-active highlight never lights up + onDrop never
27091 // fires + the drag manager surfaces a `rejected` outcome.
27092 // The user sees the icon snap back instead of attempting a
27093 // REST call that would 403 and only log to the console.
27094 accept: (payload) => {
27095 if (payload.type === "desktop-file") {
27096 const data = payload.data;
27097 const placement = data?.placement;
27098 if (!placement) {
27099 return false;
27100 }
27101 if (placement.file?.ref === RECYCLE_BIN_WINDOW_ID) {
27102 return false;
27103 }
27104 return placement.canTrash !== false;
27105 }
27106 if (payload.type === "shortcut") {
27107 const data = payload.data;
27108 return isTrashableShortcut(data);
27109 }
27110 return false;
27111 },
27112 onEnter: () => {
27113 el.setAttribute(TRASH_DROP_ACTIVE_ATTR, "");
27114 },
27115 onLeave: () => {
27116 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
27117 },
27118 onDrop: (session) => {
27119 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
27120 if (isDesktopFilePayload(session)) {
27121 const placement = session.payload.data.placement;
27122 void trashByFileType(placement);
27123 return;
27124 }
27125 if (isShortcutPayload(session)) {
27126 const data = session.payload.data;
27127 const api = getMyWordpressTrashApi();
27128 if (!api?.trashEntity || !data.entityId) {
27129 return;
27130 }
27131 const numericRef = Number.parseInt(data.ref, 10);
27132 if (!Number.isFinite(numericRef) || numericRef <= 0) {
27133 return;
27134 }
27135 void api.trashEntity(data.entityId, numericRef).catch(
27136 (err) => {
27137 console.error(
27138 "[desktop-mode] recycle-bin: shortcut trash failed:",
27139 err
27140 );
27141 }
27142 );
27143 }
27144 }
27145 });
27146 }
27147 function installRecycleBinDropTargets(dragManager) {
27148 if (_installed) {
27149 return;
27150 }
27151 _installed = true;
27152 const reprobeTile = () => {
27153 const el = findBinTile();
27154 if (!el) {
27155 _dockDeregister?.();
27156 _dockDeregister = null;
27157 return;
27158 }
27159 if (_dockDeregister && getRegisteredElementId(dragManager) === el) {
27160 return;
27161 }
27162 _dockDeregister?.();
27163 _dockDeregister = registerOn(dragManager, "recycle-bin-dock", el);
27164 };
27165 reprobeTile();
27166 document.addEventListener("desktop-mode-files-changed", reprobeTile);
27167 document.addEventListener("desktop-mode-desktop-icons-rendered", reprobeTile);
27168 addAction(
27169 HOOKS.DOCK_AFTER_RENDER,
27170 "desktop-mode/files/recycle-bin-dock-target",
27171 reprobeTile
27172 );
27173 if (typeof MutationObserver !== "undefined") {
27174 _binMutationObserver = new MutationObserver(() => {
27175 reprobeTile();
27176 });
27177 const desktopArea = document.getElementById("desktop-mode-area") ?? document.body;
27178 _binMutationObserver.observe(desktopArea, {
27179 childList: true,
27180 subtree: true
27181 });
27182 }
27183 addAction(
27184 HOOKS.WINDOW_OPENED,
27185 "desktop-mode/files/recycle-bin-window-target",
27186 (detail) => {
27187 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
27188 return;
27189 }
27190 _windowDeregister?.();
27191 _windowDeregister = null;
27192 const el = document.querySelector(
27193 "[data-desktop-mode-recycle-bin-root]"
27194 );
27195 if (el instanceof HTMLElement) {
27196 _windowDeregister = registerOn(
27197 dragManager,
27198 "recycle-bin-window",
27199 el
27200 );
27201 }
27202 }
27203 );
27204 addAction(
27205 HOOKS.WINDOW_CLOSED,
27206 "desktop-mode/files/recycle-bin-window-cleanup",
27207 (detail) => {
27208 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
27209 return;
27210 }
27211 _windowDeregister?.();
27212 _windowDeregister = null;
27213 }
27214 );
27215 }
27216 function getRegisteredElementId(dragManager) {
27217 const t = dragManager.debug().listTargets().find((target2) => target2.id === "recycle-bin-dock");
27218 return t ? t.element : null;
27219 }
27220 let started$1 = false;
27221 let highWaterMs = 0;
27222 function startFilesHeartbeat() {
27223 if (started$1) {
27224 return;
27225 }
27226 started$1 = true;
27227 heartbeat.contribute("desktop_mode_files_subscribe", () => {
27228 const state2 = getFilesState();
27229 const folderVersions = {};
27230 for (const [id, folder] of state2.folders) {
27231 folderVersions[String(id)] = folder.updatedAtMs;
27232 }
27233 return {
27234 folderVersions,
27235 placementsVersion: highWaterMs,
27236 sharesVersion: sharesStore().state.sharesVersion
27237 };
27238 });
27239 heartbeat.subscribe("desktop_mode_files", (payload) => {
27240 applyDelta(payload);
27241 });
27242 }
27243 function applyDelta(payload) {
27244 const folders = payload.folders ?? [];
27245 for (const folder of folders) {
27246 upsertFolder(folder, "remote");
27247 if (folder.updatedAtMs > highWaterMs) {
27248 highWaterMs = folder.updatedAtMs;
27249 }
27250 }
27251 const placements = payload.placements ?? [];
27252 for (const placement of placements) {
27253 upsertPlacement(placement, "remote");
27254 if (placement.updatedAtMs > highWaterMs) {
27255 highWaterMs = placement.updatedAtMs;
27256 }
27257 }
27258 const removed = payload.removed ?? {};
27259 for (const id of removed.folders ?? []) {
27260 removeFolder(id, "remote");
27261 }
27262 for (const id of removed.placements ?? []) {
27263 removePlacement(id, "remote");
27264 }
27265 if (typeof payload.serverTimeMs === "number" && payload.serverTimeMs > highWaterMs) {
27266 highWaterMs = payload.serverTimeMs;
27267 }
27268 const pending2 = payload.shares?.pending;
27269 if (Array.isArray(pending2) && pending2.length > 0) {
27270 ingestPendingInvites(pending2);
27271 }
27272 if (payload.truncated) {
27273 const hydrated = Array.from(getFilesState().hydratedFolders);
27274 for (const folderId of hydrated) {
27275 void listPlacements(folderId).then((res) => {
27276 setFolderPlacements(folderId, res.placements);
27277 }).catch(() => {
27278 });
27279 }
27280 }
27281 }
27282 let started = false;
27283 const unsubscribers = [];
27284 function startFilesRestoreSync() {
27285 if (started) {
27286 return;
27287 }
27288 started = true;
27289 const onChange = (payload) => {
27290 const detail = payload;
27291 if (!detail || detail.action !== "untrashed") {
27292 return;
27293 }
27294 resyncFromServer();
27295 };
27296 unsubscribers.push(
27297 subscribe$2("desktop-mode.placement.changed", onChange),
27298 subscribe$2("desktop-mode.shortcut.changed", onChange),
27299 subscribe$2("desktop-mode.folder.changed", onChange)
27300 );
27301 }
27302 function resyncFromServer() {
27303 void listFolders().then((res) => {
27304 setFolders(res.folders);
27305 }).catch((err) => {
27306 console.error(
27307 "[desktop-mode] files restore-sync: listFolders failed",
27308 err
27309 );
27310 });
27311 const hydrated = Array.from(getFilesState().hydratedFolders);
27312 for (const folderId of hydrated) {
27313 void listPlacements(folderId).then((res) => {
27314 setFolderPlacements(folderId, res.placements);
27315 }).catch((err) => {
27316 console.error(
27317 "[desktop-mode] files restore-sync: listPlacements failed for",
27318 folderId,
27319 err
27320 );
27321 });
27322 }
27323 }
27324 const MENU_CLASS = "desktop-mode-wallpaper-menu";
27325 let activeMenu = null;
27326 function isWallpaperMenuOpen() {
27327 return activeMenu !== null;
27328 }
27329 let openGeneration = 0;
27330 function openWallpaperMenu(host, pos, items, options = {}) {
27331 closeWallpaperMenu();
27332 const myGen = ++openGeneration;
27333 openWithShellOverlays(
27334 () => myGen === openGeneration,
27335 () => openWallpaperMenuImmediate(host, pos, items, options)
27336 );
27337 }
27338 function openWallpaperMenuImmediate(host, pos, items, options = {}) {
27339 if (items.length === 0) {
27340 return;
27341 }
27342 items = items.slice().sort((a, b) => {
27343 const sa = typeof a.sort === "number" ? a.sort : 100;
27344 const sb = typeof b.sort === "number" ? b.sort : 100;
27345 if (sa !== sb) {
27346 return sa - sb;
27347 }
27348 return a.label.localeCompare(b.label);
27349 });
27350 const menu = document.createElement("wpd-context-menu");
27351 menu.setAttribute("open", "");
27352 menu.classList.add(MENU_CLASS);
27353 menu.style.left = `${pos.x}px`;
27354 menu.style.top = `${pos.y}px`;
27355 const itemById = /* @__PURE__ */ new Map();
27356 let activeFlyout2 = null;
27357 let activeFlyoutParent = null;
27358 const closeActiveFlyout = () => {
27359 if (activeFlyout2) {
27360 activeFlyout2.remove();
27361 activeFlyout2 = null;
27362 activeFlyoutParent = null;
27363 }
27364 };
27365 for (const item of items) {
27366 itemById.set(item.id, item);
27367 const opt = document.createElement("wpd-context-menu-option");
27368 opt.dataset.menuItemId = item.id;
27369 opt.setAttribute("value", item.id);
27370 if (item.heading) {
27371 opt.setAttribute("heading", "");
27372 }
27373 if (item.disabled) {
27374 opt.setAttribute("disabled", "");
27375 }
27376 if (item.icon) {
27377 opt.setAttribute("icon", sanitizeClass(item.icon));
27378 }
27379 const hasChildren2 = Array.isArray(item.children) && item.children.length > 0;
27380 if (hasChildren2) {
27381 opt.setAttribute("has-children", "");
27382 }
27383 opt.textContent = item.label;
27384 opt.addEventListener("mouseenter", () => {
27385 if (hasChildren2) {
27386 openFlyout2(item, opt);
27387 return;
27388 }
27389 closeActiveFlyout();
27390 });
27391 menu.appendChild(opt);
27392 }
27393 menu.addEventListener("wpd-context-menu-pick", (e) => {
27394 const detail = e.detail;
27395 const item = itemById.get(detail.id) ?? null;
27396 if (!item) {
27397 return;
27398 }
27399 if (Array.isArray(item.children) && item.children.length > 0) {
27400 e.stopPropagation();
27401 if (activeFlyoutParent && activeFlyoutParent.id === item.id) {
27402 closeActiveFlyout();
27403 return;
27404 }
27405 const anchor = menu.querySelector(
27406 `[data-menu-item-id="${item.id}"]`
27407 );
27408 if (anchor) {
27409 openFlyout2(item, anchor);
27410 }
27411 return;
27412 }
27413 closeWallpaperMenu();
27414 void item.onClick(new MouseEvent("click"));
27415 });
27416 function openFlyout2(parent, anchor) {
27417 closeActiveFlyout();
27418 const fly = document.createElement("wpd-context-menu");
27419 fly.setAttribute("open", "");
27420 fly.classList.add(MENU_CLASS, `${MENU_CLASS}--flyout`);
27421 fly.dataset.parentId = parent.id;
27422 const sortedKids = (parent.children ?? []).slice().sort((a, b) => {
27423 const sa = typeof a.sort === "number" ? a.sort : 100;
27424 const sb = typeof b.sort === "number" ? b.sort : 100;
27425 if (sa !== sb) {
27426 return sa - sb;
27427 }
27428 return a.label.localeCompare(b.label);
27429 });
27430 for (const child of sortedKids) {
27431 const kopt = document.createElement("wpd-context-menu-option");
27432 kopt.dataset.menuItemId = child.id;
27433 kopt.setAttribute("value", child.id);
27434 if (child.icon) {
27435 kopt.setAttribute("icon", sanitizeClass(child.icon));
27436 }
27437 if (child.disabled) {
27438 kopt.setAttribute("disabled", "");
27439 }
27440 if (child.checked) {
27441 kopt.setAttribute("checked", "");
27442 }
27443 kopt.textContent = child.label;
27444 kopt.addEventListener("wpd-context-menu-pick", (e) => {
27445 e.stopPropagation();
27446 closeWallpaperMenu();
27447 void child.onClick(new MouseEvent("click"));
27448 });
27449 fly.appendChild(kopt);
27450 }
27451 document.body.appendChild(fly);
27452 activeFlyout2 = fly;
27453 activeFlyoutParent = parent;
27454 positionFlyout2(fly, anchor);
27455 }
27456 function positionFlyout2(fly, anchor) {
27457 const ar = anchor.getBoundingClientRect();
27458 fly.style.position = "fixed";
27459 fly.style.left = `${ar.right}px`;
27460 fly.style.top = `${ar.top}px`;
27461 const fr = fly.getBoundingClientRect();
27462 if (fr.right > window.innerWidth) {
27463 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
27464 }
27465 if (fr.bottom > window.innerHeight) {
27466 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
27467 }
27468 }
27469 host.appendChild(menu);
27470 activeMenu = menu;
27471 const rect = menu.getBoundingClientRect();
27472 if (rect.right > window.innerWidth) {
27473 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
27474 }
27475 if (rect.bottom > window.innerHeight) {
27476 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
27477 }
27478 const detach = attachDismissable(menu, {
27479 close: () => closeWallpaperMenu(),
27480 siblingSelectors: [`.${MENU_CLASS}--flyout`],
27481 excludeOutsideTarget: options.excludeOutsideTarget
27482 });
27483 menu.addEventListener("wallpaper-menu-closed", detach);
27484 doAction("desktop-mode.wallpaper-menu.opened", { items: items.map((i) => i.id) });
27485 }
27486 function closeWallpaperMenu() {
27487 if (!activeMenu) {
27488 return;
27489 }
27490 document.querySelectorAll(`.${MENU_CLASS}--flyout`).forEach((el) => el.remove());
27491 activeMenu.dispatchEvent(new CustomEvent("wallpaper-menu-closed"));
27492 activeMenu.remove();
27493 activeMenu = null;
27494 doAction("desktop-mode.wallpaper-menu.closed", {});
27495 }
27496 function buildMenuItems(deps2) {
27497 const builtIn = [
27498 {
27499 id: "create-folder",
27500 label: deps2.labels.createFolder,
27501 icon: "dashicons-portfolio",
27502 sort: 10,
27503 onClick: () => deps2.createFolder()
27504 },
27505 {
27506 id: "new-url",
27507 label: deps2.labels.newUrl,
27508 icon: "dashicons-admin-links",
27509 sort: 12,
27510 onClick: () => deps2.createUrl()
27511 },
27512 {
27513 id: "sort-by",
27514 label: deps2.labels.sortHeading,
27515 icon: "dashicons-sort",
27516 sort: 16,
27517 onClick: () => void 0,
27518 children: [
27519 {
27520 id: "sort-name-asc",
27521 label: deps2.labels.sortNameAsc,
27522 sort: 10,
27523 checked: deps2.currentSortMode === "name-asc",
27524 onClick: () => deps2.sortIcons("name-asc")
27525 },
27526 {
27527 id: "sort-name-desc",
27528 label: deps2.labels.sortNameDesc,
27529 sort: 20,
27530 checked: deps2.currentSortMode === "name-desc",
27531 onClick: () => deps2.sortIcons("name-desc")
27532 },
27533 {
27534 id: "sort-date-desc",
27535 label: deps2.labels.sortDateDesc,
27536 sort: 30,
27537 checked: deps2.currentSortMode === "date-desc",
27538 onClick: () => deps2.sortIcons("date-desc")
27539 },
27540 {
27541 id: "sort-date-asc",
27542 label: deps2.labels.sortDateAsc,
27543 sort: 40,
27544 checked: deps2.currentSortMode === "date-asc",
27545 onClick: () => deps2.sortIcons("date-asc")
27546 }
27547 ]
27548 },
27549 ...deps2.includeShowDesktop === false ? [] : [
27550 {
27551 id: "show-desktop",
27552 label: deps2.labels.showDesktop,
27553 icon: "dashicons-desktop",
27554 sort: 20,
27555 onClick: () => deps2.toggleShowDesktop()
27556 }
27557 ],
27558 {
27559 id: "os-settings",
27560 label: deps2.labels.osSettings,
27561 icon: "dashicons-admin-generic",
27562 sort: 30,
27563 onClick: () => deps2.openOsSettings()
27564 }
27565 ];
27566 const serverItems = (deps2.serverItems ?? []).map(
27567 (s) => serverItemToMenuItem(s, deps2)
27568 );
27569 const merged = [...builtIn, ...serverItems];
27570 const filtered = applyFilters(
27571 "desktop-mode.wallpaper-context-menu",
27572 merged
27573 );
27574 return Array.isArray(filtered) ? filtered : merged;
27575 }
27576 function serverItemToMenuItem(server, deps2) {
27577 return {
27578 id: server.id,
27579 label: server.label,
27580 icon: server.icon,
27581 sort: server.sort,
27582 disabled: server.disabled,
27583 onClick: () => {
27584 if (server.callbackId) {
27585 const cb = deps2.serverCallbacks?.[server.callbackId];
27586 if (typeof cb === "function") {
27587 return cb();
27588 }
27589 }
27590 doAction("desktop-mode.wallpaper-context-menu.activated", {
27591 id: server.id,
27592 callbackId: server.callbackId ?? ""
27593 });
27594 }
27595 };
27596 }
27597 function sanitizeClass(raw) {
27598 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
27599 }
27600 const ROOT_CLASS = "desktop-mode-url-dialog";
27601 let active = null;
27602 function closeUrlDialog() {
27603 if (!active) {
27604 return;
27605 }
27606 active.dispatchEvent(new CustomEvent("url-dialog-closed"));
27607 active.remove();
27608 active = null;
27609 doAction("desktop-mode.files.url-dialog.closed", {});
27610 }
27611 function openUrlDialog(options) {
27612 closeUrlDialog();
27613 const decision = applyFilters(
27614 "desktop-mode.files.url-dialog",
27615 null,
27616 options
27617 );
27618 if (decision === false) {
27619 return;
27620 }
27621 const overlay = document.createElement("div");
27622 overlay.className = `${ROOT_CLASS}__overlay desktop-mode-create-folder-dialog__overlay`;
27623 overlay.setAttribute("role", "presentation");
27624 const dialog2 = document.createElement("div");
27625 dialog2.className = `${ROOT_CLASS} desktop-mode-create-folder-dialog`;
27626 dialog2.setAttribute("role", "dialog");
27627 dialog2.setAttribute("aria-modal", "true");
27628 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS}-title`);
27629 const title = document.createElement("h2");
27630 title.id = `${ROOT_CLASS}-title`;
27631 title.className = "desktop-mode-create-folder-dialog__title";
27632 title.textContent = options.title;
27633 dialog2.appendChild(title);
27634 if (options.description) {
27635 const desc = document.createElement("p");
27636 desc.className = `${ROOT_CLASS}__description`;
27637 desc.textContent = options.description;
27638 dialog2.appendChild(desc);
27639 }
27640 const nameField = document.createElement("wpd-text-field");
27641 nameField.setAttribute("label", options.nameLabel ?? "Name");
27642 nameField.setAttribute("value", options.initialName ?? "");
27643 nameField.setAttribute("placeholder", "My web app");
27644 nameField.setAttribute("autocomplete", "off");
27645 dialog2.appendChild(nameField);
27646 const urlField = document.createElement("wpd-text-field");
27647 urlField.setAttribute("label", options.urlLabel ?? "URL");
27648 urlField.setAttribute("value", options.initialUrl ?? "https://");
27649 urlField.setAttribute("placeholder", "https://example.com");
27650 urlField.setAttribute("type", "url");
27651 urlField.setAttribute("autocomplete", "off");
27652 dialog2.appendChild(urlField);
27653 const error = document.createElement("p");
27654 error.className = "desktop-mode-create-folder-dialog__error";
27655 error.hidden = true;
27656 error.setAttribute("role", "alert");
27657 dialog2.appendChild(error);
27658 const actions = document.createElement("div");
27659 actions.className = "desktop-mode-create-folder-dialog__actions";
27660 const cancel = document.createElement("button");
27661 cancel.type = "button";
27662 cancel.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--secondary";
27663 cancel.textContent = "Cancel";
27664 const submit = document.createElement("button");
27665 submit.type = "button";
27666 submit.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--primary";
27667 submit.textContent = options.submitLabel ?? "Create";
27668 actions.appendChild(cancel);
27669 actions.appendChild(submit);
27670 dialog2.appendChild(actions);
27671 overlay.appendChild(dialog2);
27672 document.body.appendChild(overlay);
27673 active = overlay;
27674 queueMicrotask(() => {
27675 const input = nameField.shadowRoot?.querySelector("input");
27676 input?.focus();
27677 input?.select();
27678 });
27679 doAction("desktop-mode.files.url-dialog.opened", {});
27680 const readValue = (field) => {
27681 const v = field.value;
27682 if (typeof v === "string") {
27683 return v;
27684 }
27685 return field.shadowRoot?.querySelector("input")?.value ?? "";
27686 };
27687 const setBusy = (busy) => {
27688 nameField.disabled = busy;
27689 urlField.disabled = busy;
27690 cancel.disabled = busy;
27691 submit.disabled = busy;
27692 dialog2.classList.toggle("desktop-mode-create-folder-dialog--busy", busy);
27693 };
27694 const showError = (msg) => {
27695 error.textContent = msg;
27696 error.hidden = false;
27697 };
27698 const doCancel = () => {
27699 closeUrlDialog();
27700 options.onCancel?.();
27701 };
27702 const doSubmit = async () => {
27703 const url = readValue(urlField).trim();
27704 if (!url) {
27705 showError("Please enter a URL.");
27706 return;
27707 }
27708 const finalUrl = /^[a-z][a-z0-9+\-.]*:/i.test(url) ? url : `https://${url}`;
27709 try {
27710 new URL(finalUrl);
27711 } catch {
27712 showError("That doesn't look like a valid URL.");
27713 return;
27714 }
27715 const name = readValue(nameField).trim();
27716 error.hidden = true;
27717 setBusy(true);
27718 try {
27719 await options.onSubmit({ name, url: finalUrl });
27720 closeUrlDialog();
27721 } catch (err) {
27722 setBusy(false);
27723 showError(err instanceof Error ? err.message : "Could not save.");
27724 }
27725 };
27726 cancel.addEventListener("click", () => doCancel());
27727 submit.addEventListener("click", () => void doSubmit());
27728 overlay.addEventListener("click", (e) => {
27729 if (e.target === overlay) {
27730 doCancel();
27731 }
27732 });
27733 const onKey = (e) => {
27734 if (e.key === "Escape") {
27735 e.preventDefault();
27736 doCancel();
27737 } else if (e.key === "Enter" && !e.isComposing) {
27738 e.preventDefault();
27739 void doSubmit();
27740 }
27741 };
27742 dialog2.addEventListener("keydown", onKey);
27743 overlay.addEventListener("url-dialog-closed", () => {
27744 dialog2.removeEventListener("keydown", onKey);
27745 });
27746 }
27747 const _earlyReadyQueue = [];
27748 let _earlyReady = false;
27749 (function installEarlyDesktopShim() {
27750 const w = window;
27751 if (!w.wp) {
27752 w.wp = {};
27753 }
27754 if (w.wp.desktop) {
27755 return;
27756 }
27757 const shim = {
27758 whenReady(cb) {
27759 if (typeof cb !== "function") {
27760 return;
27761 }
27762 if (_earlyReady) {
27763 Promise.resolve().then(cb);
27764 return;
27765 }
27766 _earlyReadyQueue.push(cb);
27767 },
27768 ready(cb) {
27769 shim.whenReady(cb);
27770 },
27771 isReady() {
27772 return _earlyReady;
27773 }
27774 };
27775 w.wp.desktop = shim;
27776 })();
27777 const OS_SETTINGS_WINDOW_ID = "desktop-mode-os-settings";
27778 let _idleBootQueue = [];
27779 let _idleBootTimeout = Number.POSITIVE_INFINITY;
27780 let _idleBootScheduled = false;
27781 function scheduleIdleBoot(cb, timeout = 1500) {
27782 _idleBootQueue.push(cb);
27783 if (timeout < _idleBootTimeout) {
27784 _idleBootTimeout = timeout;
27785 }
27786 if (_idleBootScheduled) {
27787 return;
27788 }
27789 _idleBootScheduled = true;
27790 const drain = () => {
27791 const callbacks = _idleBootQueue;
27792 _idleBootQueue = [];
27793 _idleBootTimeout = Number.POSITIVE_INFINITY;
27794 _idleBootScheduled = false;
27795 for (const fn of callbacks) {
27796 try {
27797 fn();
27798 } catch (err) {
27799 if (typeof console !== "undefined") {
27800 console.error(
27801 "[desktop-mode] scheduleIdleBoot callback threw:",
27802 err
27803 );
27804 }
27805 }
27806 }
27807 };
27808 if (typeof window.requestIdleCallback === "function") {
27809 window.requestIdleCallback(drain, { timeout: _idleBootTimeout });
27810 } else {
27811 window.setTimeout(drain, 0);
27812 }
27813 }
27814 function init() {
27815 const config = window.desktopModeConfig;
27816 if (!config) {
27817 return;
27818 }
27819 const desktopArea = document.getElementById("desktop-mode-area");
27820 if (!desktopArea) {
27821 return;
27822 }
27823 const manager = new WindowManager(desktopArea);
27824 const wallpaperEl = document.getElementById("desktop-mode-wallpaper");
27825 const pluginUrl = config.pluginUrl || "";
27826 let wallpaperLayer = null;
27827 if (wallpaperEl) {
27828 wallpaperLayer = new WallpaperLayer(wallpaperEl, pluginUrl);
27829 }
27830 const widgetsEl = document.getElementById("desktop-mode-widgets");
27831 let widgetLayer = null;
27832 registerBuiltInWidgets();
27833 installDefaultDockRailRenderer();
27834 if (widgetsEl) {
27835 widgetLayer = new WidgetLayer(widgetsEl, pluginUrl);
27836 }
27837 registerModule({
27838 id: "pixijs",
27839 url: `${pluginUrl}/assets/vendor/pixi.min.js`,
27840 isReady: () => typeof window.PIXI !== "undefined"
27841 });
27842 const osSettings = new OsSettings(
27843 {
27844 mediaUrl: config.mediaUrl,
27845 restNonce: config.restNonce,
27846 canUpload: !!config.canUpload,
27847 isAdmin: !!config.currentUserIsAdmin,
27848 aiPlatformSettings: config.aiPlatformSettings ?? null,
27849 aiPlatformSettingsUrl: config.aiPlatformSettingsUrl ?? "",
27850 extendedOptions: config.extendedOptions ?? null,
27851 extendedOptionsUrl: config.extendedOptionsUrl ?? "",
27852 osSettingsPanelBundleUrl: config.osSettingsPanelBundleUrl ?? ""
27853 },
27854 wallpaperLayer ?? new WallpaperLayer(document.createElement("div"), pluginUrl)
27855 );
27856 osSettings.apply();
27857 const aiAssistant = new AiAssistantStub(
27858 {
27859 aiSearchUrl: config.aiSearchUrl ?? "",
27860 aiSearchStreamUrl: config.aiSearchStreamUrl ?? "",
27861 restNonce: config.restNonce,
27862 // Transport picker lives in OS Settings → AI Settings. Read
27863 // live (not captured at construction) so a change applies on
27864 // the next search without a page reload.
27865 getTransport: () => osSettings.getOsSettingsSnapshot().ai.transport
27866 },
27867 config.aiAssistantBundleUrl ?? ""
27868 );
27869 aiAssistant.attachAsk(
27870 createAsk({
27871 config: () => config,
27872 fallbackContext: () => ({
27873 close: () => aiAssistant.close(),
27874 openInWindow: (url, title, icon) => {
27875 manager.open({
27876 url,
27877 title,
27878 icon: icon ?? "dashicons-admin-generic"
27879 });
27880 },
27881 confirm: (msg) => wpdConfirm({ message: msg })
27882 })
27883 })
27884 );
27885 const dragBridge = new DragBridge();
27886 const dragManager = new DragManager();
27887 document.addEventListener(DRAG_EVENTS.START, (e) => {
27888 const detail = e.detail;
27889 const payload = detail?.payload;
27890 if (!payload) {
27891 return;
27892 }
27893 if (payload.type !== "shortcut" && payload.type !== "desktop-file") {
27894 return;
27895 }
27896 const bridgePayload = payload.data?.bridgePayload;
27897 if (bridgePayload) {
27898 dragBridge.start(bridgePayload);
27899 }
27900 });
27901 document.addEventListener(DRAG_EVENTS.END, () => {
27902 dragBridge.end();
27903 });
27904 scheduleIdleBoot(() => installIframeDropTargets(dragManager));
27905 window.addEventListener("message", (e) => {
27906 if (e.origin !== window.location.origin) {
27907 return;
27908 }
27909 const data = e.data;
27910 if (!data || data.type !== "desktop-mode-drop-failed") {
27911 return;
27912 }
27913 showToast({
27914 message: "Could not insert into the editor."
27915 });
27916 });
27917 registerPalette({
27918 id: "desktop-mode-ai-assistant",
27919 label: "AI Assistant",
27920 open: () => aiAssistant.open(),
27921 close: () => aiAssistant.close(),
27922 isOpen: () => aiAssistant.isOpen
27923 });
27924 installPaletteShortcut();
27925 installWindowSwitcherShortcut(manager);
27926 installDesktopArrowShortcuts(manager);
27927 scheduleIdleBoot(() => {
27928 new IframeCommandBridge({
27929 manager,
27930 adminUrl: config.adminUrl
27931 }).install();
27932 new ShellCommandHarvester({
27933 manager,
27934 adminUrl: config.adminUrl
27935 }).install();
27936 });
27937 document.addEventListener("desktop-mode-open-ai", () => {
27938 openPaletteOnly("desktop-mode-ai-assistant");
27939 });
27940 const bottomDockEl = document.getElementById("desktop-mode-dock");
27941 const shellEl = document.getElementById("desktop-mode-shell");
27942 const shellBody = shellEl?.querySelector(
27943 ".desktop-mode-shell__body"
27944 );
27945 let layoutDispatcher = null;
27946 const nativeWindows = createNativeWindowSync({
27947 manager,
27948 appendSystemTile: (item) => layoutDispatcher?.appendSystemTile(item),
27949 removeSystemTile: (id) => layoutDispatcher?.removeSystemTile(id)
27950 });
27951 const syncNativeWindows = nativeWindows.sync;
27952 bindNativeUrlRemap({
27953 getSnapshot: () => osSettings.getOsSettingsSnapshot(),
27954 openById: (id) => nativeWindows.openById(id),
27955 adminUrl: config.adminUrl
27956 });
27957 const findDockEntryForUrl2 = (url) => {
27958 const targetSlug = deriveWindowId(url, config.adminUrl);
27959 const items = layoutDispatcher ? layoutDispatcher.getMenuItems() : config.dockItems ?? [];
27960 for (const item of items) {
27961 if (deriveWindowId(item.url, config.adminUrl) === targetSlug) {
27962 return {
27963 title: item.title,
27964 icon: item.icon,
27965 url: item.url,
27966 submenu: item.submenu,
27967 multi: item.multi
27968 };
27969 }
27970 for (const sub of item.submenu ?? []) {
27971 if (deriveWindowId(sub.url, config.adminUrl) === targetSlug) {
27972 return {
27973 title: sub.title,
27974 // Sub-menu entries inherit the parent tile's
27975 // icon — that's the dock's own convention and
27976 // avoids painting a generic glyph on a window
27977 // the user knows by its parent's identity.
27978 icon: item.icon,
27979 // `url` holds the PARENT tile's landing page, so
27980 // the new window's synthetic "back to parent"
27981 // tab links to the dock URL (themes.php) rather
27982 // than to the sub-page itself.
27983 url: item.url,
27984 multi: item.multi
27985 };
27986 }
27987 }
27988 }
27989 return null;
27990 };
27991 bindAdminLinkDispatch({
27992 adminUrl: config.adminUrl,
27993 deriveSlug: (url) => deriveWindowId(url, config.adminUrl),
27994 openWindow: (windowConfig) => {
27995 void manager.open(windowConfig);
27996 },
27997 findDockEntry: findDockEntryForUrl2
27998 });
27999 registerNativeUrlRemap({
28000 id: "desktop-mode-posts",
28001 nativeWindowId: "desktop-mode-posts",
28002 matches: (_url, parsed) => {
28003 if (!parsed.pathname.endsWith("/edit.php")) {
28004 return false;
28005 }
28006 const postType = parsed.searchParams.get("post_type");
28007 return !postType || postType === "post";
28008 },
28009 enabled: (snapshot) => snapshot.nativePostsEnabled === true
28010 });
28011 registerNativeUrlRemap({
28012 id: "desktop-mode-pages",
28013 nativeWindowId: "desktop-mode-pages",
28014 matches: (_url, parsed) => {
28015 if (!parsed.pathname.endsWith("/edit.php")) {
28016 return false;
28017 }
28018 return parsed.searchParams.get("post_type") === "page";
28019 },
28020 enabled: (snapshot) => snapshot.nativePagesEnabled === true
28021 });
28022 registerNativeUrlRemap({
28023 id: "desktop-mode-users",
28024 nativeWindowId: "desktop-mode-users",
28025 matches: (_url, parsed) => parsed.pathname.endsWith("/users.php"),
28026 enabled: (snapshot) => snapshot.nativeUsersEnabled === true
28027 });
28028 registerNativeUrlRemap({
28029 id: "desktop-mode-user-edit",
28030 nativeWindowId: "desktop-mode-user-edit",
28031 matches: (_url, parsed) => {
28032 const path = parsed.pathname;
28033 if (path.endsWith("/profile.php")) {
28034 return true;
28035 }
28036 if (path.endsWith("/user-edit.php")) {
28037 return parsed.searchParams.has("user_id");
28038 }
28039 return false;
28040 },
28041 enabled: (snapshot) => snapshot.nativeUsersEnabled === true,
28042 onMatch: (_url, parsed) => {
28043 const userId = parseInt(
28044 parsed.searchParams.get("user_id") ?? "0",
28045 10
28046 );
28047 if (userId > 0) {
28048 setUserEditTarget(userId);
28049 }
28050 }
28051 });
28052 registerNativeUrlRemap({
28053 id: "desktop-mode-comments",
28054 nativeWindowId: "desktop-mode-comments",
28055 matches: (_url, parsed) => parsed.pathname.endsWith("/edit-comments.php"),
28056 enabled: (snapshot) => snapshot.nativeCommentsEnabled === true
28057 });
28058 registerNativeUrlRemap({
28059 id: "desktop-mode-plugins",
28060 nativeWindowId: "desktop-mode-plugins",
28061 matches: (_url, parsed) => {
28062 const path = parsed.pathname;
28063 return path.endsWith("/plugins.php") || path.endsWith("/plugin-install.php");
28064 },
28065 enabled: (snapshot) => snapshot.nativePluginsEnabled === true,
28066 onMatch: (_url, parsed) => {
28067 const tab = parsed.pathname.endsWith("/plugin-install.php") ? "browse" : "installed";
28068 void Promise.resolve().then(() => tabTarget).then((m) => {
28069 m.setPluginsWindowTab(tab);
28070 });
28071 }
28072 });
28073 if (bottomDockEl && shellEl && shellBody && config.dockItems) {
28074 desktopArea.classList.add("desktop-mode-area--with-dock");
28075 const initialLayout = osSettings.getOsSettingsSnapshot().desktopLayout;
28076 const renderIcons2 = (icons) => {
28077 renderDesktopIcons(desktopArea, icons, {
28078 openWindow: nativeWindows.openById,
28079 manager,
28080 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
28081 });
28082 };
28083 layoutDispatcher = createLayoutDispatcher(
28084 {
28085 shellRoot: shellEl,
28086 shellBody,
28087 bottomDockEl,
28088 desktopArea,
28089 windowManager: manager,
28090 adminUrl: config.adminUrl,
28091 renderIcons: renderIcons2,
28092 getSettings: () => {
28093 const snap = osSettings.getOsSettingsSnapshot();
28094 return {
28095 itemVisibility: snap.itemVisibility,
28096 dockOrder: snap.dockOrder
28097 };
28098 }
28099 },
28100 initialLayout,
28101 config.dockItems,
28102 config.desktopIcons
28103 );
28104 layoutDispatcher.appendSystemTile(
28105 {
28106 id: OS_SETTINGS_WINDOW_ID,
28107 title: "OS Settings",
28108 icon: "dashicons-desktop",
28109 // "Open" for the dock dot means "open on the currently
28110 // active desktop." OS Settings on another desktop
28111 // shouldn't paint the dot on the active view.
28112 isOpen: () => {
28113 const win = manager.getById(OS_SETTINGS_WINDOW_ID);
28114 if (!win) {
28115 return false;
28116 }
28117 return (win.config.desktopId || manager.getActiveDesktopId()) === manager.getActiveDesktopId();
28118 },
28119 onOpen: openOsSettings
28120 },
28121 "core"
28122 );
28123 if (!isStandaloneDisplay()) {
28124 layoutDispatcher.appendSystemTile(
28125 getInstallTileDef(
28126 config.pwa?.appName || "WordPress",
28127 showToast
28128 ),
28129 "core"
28130 );
28131 }
28132 window.matchMedia("(display-mode: standalone)").addEventListener("change", (e) => {
28133 if (e.matches) {
28134 layoutDispatcher?.removeSystemTile(
28135 "desktop-mode-pwa-install"
28136 );
28137 }
28138 });
28139 void isLikelyInstalled().then((installed2) => {
28140 if (installed2) {
28141 layoutDispatcher?.removeSystemTile(
28142 "desktop-mode-pwa-install"
28143 );
28144 }
28145 });
28146 }
28147 function openOsSettings(opts = {}) {
28148 if (opts.tabId) {
28149 osSettings.activeTabId = opts.tabId;
28150 }
28151 void manager.open({
28152 id: OS_SETTINGS_WINDOW_ID,
28153 baseId: OS_SETTINGS_WINDOW_ID,
28154 url: "#os-settings",
28155 title: "OS Settings",
28156 icon: "dashicons-desktop",
28157 native: true,
28158 render: (body) => osSettings.renderPanel(body),
28159 width: 820,
28160 height: 720,
28161 minWidth: 560,
28162 minHeight: 480
28163 });
28164 if (opts.tabId) {
28165 osSettings.focusTab(opts.tabId);
28166 }
28167 }
28168 function openBugReport() {
28169 void manager.open({
28170 id: BUG_REPORT_WINDOW_ID,
28171 baseId: BUG_REPORT_WINDOW_ID,
28172 url: `#${BUG_REPORT_WINDOW_ID}`,
28173 title: "Report a bug",
28174 icon: "dashicons-buddicons-replies",
28175 native: true,
28176 render: (body) => renderBugReport(body),
28177 width: 560,
28178 height: 620,
28179 minWidth: 420,
28180 minHeight: 480
28181 });
28182 }
28183 document.addEventListener("desktop-mode-open-bug-report", () => {
28184 openBugReport();
28185 });
28186 if (layoutDispatcher) {
28187 layoutDispatcher.appendSystemTile(
28188 {
28189 id: BUG_REPORT_WINDOW_ID,
28190 title: "Report a bug",
28191 icon: "dashicons-buddicons-replies",
28192 isOpen: () => {
28193 const win = manager.getById(BUG_REPORT_WINDOW_ID);
28194 if (!win) {
28195 return false;
28196 }
28197 return (win.config.desktopId || manager.getActiveDesktopId()) === manager.getActiveDesktopId();
28198 },
28199 onOpen: openBugReport
28200 },
28201 "core"
28202 );
28203 layoutDispatcher.appendSystemTile(
28204 getExitDesktopModeTileDef(),
28205 "core"
28206 );
28207 }
28208 const dock = layoutDispatcher?.getPrimary() ?? null;
28209 void syncNativeWindows(
28210 Array.isArray(config.nativeWindows) ? config.nativeWindows : []
28211 );
28212 const hasSession = hasRestorableSession(config.session);
28213 const sessionRestore = hasSession ? restoreSession(manager, config, desktopArea).catch((err) => {
28214 if (typeof console !== "undefined") {
28215 console.error("[desktop-mode] session restore failed:", err);
28216 }
28217 }) : Promise.resolve();
28218 const defaultEnabled = config.defaultWindow?.enabled !== false;
28219 const defaultUrlEarly = config.defaultWindow?.url ?? "";
28220 const isNativeDefault = typeof defaultUrlEarly === "string" && defaultUrlEarly.startsWith("native:");
28221 if (shouldAutoOpenCurrentPage({
28222 fromPortal: config.fromPortal,
28223 fromPortalIntent: config.fromPortalIntent,
28224 hasSession,
28225 defaultEnabled,
28226 isNativeDefault
28227 })) {
28228 void sessionRestore.then(
28229 () => openCurrentPage(manager, config).catch((err) => {
28230 if (typeof console !== "undefined") {
28231 console.error("[desktop-mode] openCurrentPage failed:", err);
28232 }
28233 })
28234 );
28235 }
28236 const saveSession = createSessionSaver(manager, config);
28237 wireSessionEvents(saveSession);
28238 const setDefaultWindow = async (url) => {
28239 try {
28240 const response = await trackedFetch(
28241 manager,
28242 config.defaultWindowUrl,
28243 {
28244 method: "POST",
28245 credentials: "same-origin",
28246 headers: {
28247 "Content-Type": "application/json",
28248 "X-WP-Nonce": config.restNonce
28249 },
28250 body: JSON.stringify({ url })
28251 },
28252 { source: "desktop-mode/default-window" }
28253 );
28254 if (!response.ok) {
28255 throw new Error(`HTTP ${response.status}`);
28256 }
28257 const data = await response.json();
28258 config.defaultWindow = data;
28259 document.dispatchEvent(
28260 new CustomEvent("desktop-mode-default-window-changed", {
28261 detail: data
28262 })
28263 );
28264 } catch (err) {
28265 doAction(HOOKS.SHELL_ERROR, { scope: "default-window-save", error: err });
28266 if (typeof console !== "undefined") {
28267 console.error(
28268 "[desktop-mode] Failed to save default window:",
28269 err
28270 );
28271 }
28272 }
28273 };
28274 manager.onToggleStartupRequested = (win) => {
28275 const currentPref = config.defaultWindow;
28276 const isNative = !!win.config.native;
28277 const winValue = isNative ? `native:${win.id}` : win.getCurrentUrl();
28278 const matchesCurrent = isNative ? currentPref?.url === winValue : urlMatchKey(currentPref?.url ?? "") === urlMatchKey(winValue);
28279 const alreadyDefault = !!currentPref?.enabled && matchesCurrent;
28280 void setDefaultWindow(alreadyDefault ? null : winValue);
28281 };
28282 if (config.defaultWindow?.enabled && config.fromPortal && !config.fromPortalIntent && !hasSession && isNativeDefault) {
28283 const nativeId = defaultUrlEarly.slice("native:".length);
28284 queueMicrotask(() => {
28285 if (nativeId === OS_SETTINGS_WINDOW_ID) {
28286 openOsSettings();
28287 return;
28288 }
28289 void nativeWindows.openById(nativeId);
28290 });
28291 }
28292 const placeSystemTile = (item) => {
28293 layoutDispatcher?.appendSystemTile(item);
28294 };
28295 const syncServerWidgets = createWidgetRegistrySync({
28296 layer: widgetLayer
28297 });
28298 void syncServerWidgets(
28299 Array.isArray(config.serverWidgets) ? config.serverWidgets : []
28300 );
28301 const syncServerWallpapers = createWallpaperRegistrySync({
28302 osSettings
28303 });
28304 void syncServerWallpapers(
28305 Array.isArray(config.serverWallpapers) ? config.serverWallpapers : []
28306 );
28307 const syncServerCommands = createCommandRegistrySync();
28308 void syncServerCommands(
28309 Array.isArray(config.serverCommandScripts) ? config.serverCommandScripts : [],
28310 Array.isArray(config.serverCommands) ? config.serverCommands : []
28311 );
28312 const syncServerSettingsTabs = createSettingsTabRegistrySync();
28313 void syncServerSettingsTabs(
28314 Array.isArray(config.serverSettingsTabScripts) ? config.serverSettingsTabScripts : [],
28315 Array.isArray(config.serverSettingsTabs) ? config.serverSettingsTabs : []
28316 );
28317 const syncServerTitleBarButtons = createTitleBarButtonRegistrySync();
28318 void syncServerTitleBarButtons(
28319 Array.isArray(config.serverTitleBarButtonScripts) ? config.serverTitleBarButtonScripts : []
28320 );
28321 const syncServerUnfocusEffects = createUnfocusEffectRegistrySync();
28322 void syncServerUnfocusEffects(
28323 Array.isArray(config.serverUnfocusEffectScripts) ? config.serverUnfocusEffectScripts : []
28324 );
28325 startUnfocusEngine({ manager, osSettings });
28326 const syncServerDockRailRenderers = createDockRailRendererSync();
28327 void syncServerDockRailRenderers(
28328 Array.isArray(config.serverDockRailRendererScripts) ? config.serverDockRailRendererScripts : []
28329 );
28330 const syncServerWindowThemes = createWindowThemeRegistrySync();
28331 void syncServerWindowThemes(
28332 Array.isArray(config.serverWindowThemeScripts) ? config.serverWindowThemeScripts : [],
28333 Array.isArray(config.serverWindowThemes) ? config.serverWindowThemes : []
28334 );
28335 registerBuiltInControls();
28336 const syncServerWindowControls = createWindowControlRegistrySync();
28337 void syncServerWindowControls(
28338 Array.isArray(config.serverWindowControlScripts) ? config.serverWindowControlScripts : [],
28339 Array.isArray(config.serverWindowControls) ? config.serverWindowControls : []
28340 );
28341 const syncServerWindowSlots = createWindowSlotRegistrySync();
28342 void syncServerWindowSlots(
28343 Array.isArray(config.serverWindowSlotScripts) ? config.serverWindowSlotScripts : [],
28344 Array.isArray(config.serverWindowSlots) ? config.serverWindowSlots : []
28345 );
28346 applyServerWindowNotices(
28347 Array.isArray(config.serverWindowNotices) ? config.serverWindowNotices : []
28348 );
28349 const syncServerWindowChromes = createWindowChromeRegistrySync();
28350 void syncServerWindowChromes(
28351 Array.isArray(config.serverWindowChromeScripts) ? config.serverWindowChromeScripts : [],
28352 Array.isArray(config.serverWindowChromes) ? config.serverWindowChromes : []
28353 );
28354 const connectionBridge = createConnectionBridge(manager);
28355 attachBroadcastBus(manager);
28356 scheduleIdleBoot(() => installBroadcastReceiver());
28357 installWindowLoadingTransitions();
28358 addAction(
28359 "desktop-mode.shell.toast",
28360 "desktop-mode/shell-toast",
28361 (payload) => {
28362 if (!payload || typeof payload.message !== "string") {
28363 return;
28364 }
28365 showToast({
28366 message: payload.message,
28367 action: payload.action,
28368 duration: payload.duration
28369 });
28370 }
28371 );
28372 const cfgWithBin = config;
28373 const cfgCountRaw = cfgWithBin.recycleBinCount;
28374 startRecycleBinBadge(
28375 Number(cfgCountRaw) || 0,
28376 typeof cfgWithBin.recycleBinCountUrl === "string" ? cfgWithBin.recycleBinCountUrl : ""
28377 );
28378 registerBuiltInPeekRenderers({
28379 getRecycleBinCount: _currentRecycleBinBadge
28380 });
28381 window.__desktopModeConnectionBridge = connectionBridge;
28382 addAction(HOOKS.WINDOW_CLOSED, "desktop-mode/connection-cleanup", (e) => {
28383 if (e?.windowId) {
28384 connectionBridge.onWindowClosed(e.windowId);
28385 }
28386 });
28387 addAction(HOOKS.IFRAME_READY, "desktop-mode/connection-rearm", (e) => {
28388 if (e?.windowId) {
28389 connectionBridge.onIframeReady(e.windowId);
28390 }
28391 });
28392 const registerWindow = createRegisterWindow(manager);
28393 const renderIcons = (icons) => {
28394 if (layoutDispatcher) {
28395 layoutDispatcher.applyDesktopIcons(icons);
28396 return;
28397 }
28398 renderDesktopIcons(desktopArea, icons, {
28399 openWindow: nativeWindows.openById,
28400 manager,
28401 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
28402 });
28403 };
28404 const refreshMenu = bindMenuRefresh({
28405 layoutDispatcher,
28406 desktopArea,
28407 config,
28408 syncNativeWindows,
28409 syncServerWidgets,
28410 syncServerWallpapers,
28411 syncServerCommands,
28412 syncServerSettingsTabs,
28413 syncServerTitleBarButtons,
28414 syncServerUnfocusEffects,
28415 syncServerDockRailRenderers,
28416 renderIcons
28417 });
28418 osSettings.subscribeOsSettings((snapshot) => {
28419 if (!layoutDispatcher) {
28420 return;
28421 }
28422 const prevLayout = layoutDispatcher.getLayout();
28423 layoutDispatcher.setLayout(snapshot.desktopLayout);
28424 desktopApi.dock = layoutDispatcher.getPrimary();
28425 desktopApi.sideDock = layoutDispatcher.getSide();
28426 desktopApi.desktopLayout = snapshot.desktopLayout;
28427 if (prevLayout === snapshot.desktopLayout) {
28428 layoutDispatcher.refresh();
28429 }
28430 syncShortcutsWithVisibility(
28431 snapshot.itemVisibility,
28432 snapshot.dockPromotedPositions
28433 );
28434 setCurrentLayout(snapshot.desktopLayout);
28435 });
28436 installShortcutsSync(
28437 () => osSettings.getOsSettingsSnapshot().itemVisibility,
28438 () => osSettings.getOsSettingsSnapshot().dockPromotedPositions
28439 );
28440 setCurrentLayout(osSettings.getOsSettingsSnapshot().desktopLayout);
28441 const desktopApi = buildPublicApi({
28442 manager,
28443 dock,
28444 layoutDispatcher,
28445 osSettings,
28446 iconsApi,
28447 filesApi,
28448 saveSession,
28449 widgetLayer,
28450 registerWindow,
28451 openWindowById: nativeWindows.openById,
28452 openNewWindowById: nativeWindows.openNewById,
28453 placeSystemTile,
28454 setDefaultWindow,
28455 refreshMenu,
28456 openOsSettings,
28457 aiAssistant,
28458 dragBridge,
28459 dragManager,
28460 connect: connectionBridge.connect,
28461 getConnection: connectionBridge.getConnection,
28462 config
28463 });
28464 installPublicApi(desktopApi);
28465 scheduleIdleBoot(() => installRecycleBinDropTargets(dragManager));
28466 bootHeartbeatBus();
28467 scheduleIdleBoot(() => bootNonceRefresh());
28468 bootStickyNotes({
28469 host: desktopArea,
28470 config,
28471 // Only boot when the Gutenberg Guidelines experiment is live
28472 // server-side; otherwise the layer's REST probes would 404. The
28473 // flag is `undefined` on shells older than the one that added it
28474 // → the layer treats that as available (boot and swallow).
28475 available: config.stickyNotes?.available,
28476 getActiveDesktopId: () => manager.getActiveDesktopId(),
28477 openArtifact: (url, title) => {
28478 const id = deriveWindowId(url, config.adminUrl);
28479 void manager.open({
28480 id,
28481 baseId: id,
28482 url,
28483 title,
28484 icon: "dashicons-edit-page"
28485 });
28486 },
28487 onError: (message) => {
28488 showToast({ message });
28489 }
28490 });
28491 installOpenDeps({
28492 openUrl: ({ id, url, title, icon }) => {
28493 if (tryNativeUrlRemap(url)) {
28494 return true;
28495 }
28496 void manager.open({ id, baseId: id, url, title, icon });
28497 return true;
28498 },
28499 openNativeWindow: (id) => nativeWindows.openById(id),
28500 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
28501 });
28502 setUserAssociations(
28503 config.userFileAssociations ?? {}
28504 );
28505 if (typeof config.filesUrl === "string" && config.filesUrl) {
28506 installRestDeps({
28507 baseUrl: config.filesUrl,
28508 nonce: config.restNonce
28509 });
28510 const rootHost = document.getElementById("desktop-mode-area");
28511 if (rootHost) {
28512 const layerHandle = mountFilesLayer(rootHost, 0);
28513 const reveal = () => {
28514 if (!desktopArea.classList.contains("desktop-mode-area--booting")) {
28515 return;
28516 }
28517 requestAnimationFrame(() => {
28518 desktopArea.classList.remove("desktop-mode-area--booting");
28519 });
28520 };
28521 const safetyTimer = setTimeout(reveal, 2e3);
28522 void layerHandle.hydrated.then(() => {
28523 clearTimeout(safetyTimer);
28524 reveal();
28525 });
28526 }
28527 }
28528 scheduleIdleBoot(() => startFilesHeartbeat());
28529 scheduleIdleBoot(() => startFilesRestoreSync());
28530 scheduleIdleBoot(() => bootPresenceProbe());
28531 doAction(HOOKS.COMPONENTS_REGISTERED, { tags: [...WPD_COMPONENT_TAGS] });
28532 registerBuiltInCommands();
28533 bootstrapPwa(config, showToast);
28534 const overlayPreload = () => {
28535 preloadShellOverlays(config.shellOverlaysBundleUrl ?? "");
28536 preloadWindowSystem(config.windowSystemBundleUrl ?? "");
28537 };
28538 if (typeof window.requestIdleCallback === "function") {
28539 window.requestIdleCallback(overlayPreload, { timeout: 1500 });
28540 } else {
28541 window.setTimeout(overlayPreload, 0);
28542 }
28543 doAction(HOOKS.INIT, { config });
28544 _earlyReady = true;
28545 const queued = _earlyReadyQueue.splice(0);
28546 for (const cb of queued) {
28547 try {
28548 cb();
28549 } catch (err) {
28550 doAction(HOOKS.SHELL_ERROR, {
28551 scope: "when-ready-cb",
28552 error: err
28553 });
28554 if (typeof console !== "undefined") {
28555 console.error("[desktop-mode] whenReady cb threw:", err);
28556 }
28557 }
28558 }
28559 osSettings.apply();
28560 widgetLayer?.hydrate();
28561 window.addEventListener("pagehide", () => {
28562 wallpaperLayer?.teardownActive();
28563 widgetLayer?.disposeAll();
28564 });
28565 bindShellLifecycle();
28566 bindTopWindowLinkInterceptor(manager, config);
28567 const relayoutRoot = (transform, persist2 = true) => {
28568 const root = filesApi.store.getState().placementsByFolder.get(0) ?? [];
28569 const ordered = transform(root);
28570 const rowsPerCol = Math.max(
28571 1,
28572 Math.floor((desktopArea.clientHeight - 16) / 110)
28573 );
28574 const occupied = /* @__PURE__ */ new Set();
28575 let i = 0;
28576 for (const p of ordered) {
28577 const cell = snapToEmptyCell(
28578 16 + Math.floor(i / rowsPerCol) * 96,
28579 16 + i % rowsPerCol * 110,
28580 occupied,
28581 desktopArea
28582 );
28583 occupied.add(`${cell.col},${cell.row}`);
28584 i++;
28585 if (p.x === cell.x && p.y === cell.y) {
28586 continue;
28587 }
28588 filesApi.store.upsertPlacement({
28589 ...p,
28590 x: cell.x,
28591 y: cell.y,
28592 sortOrder: i
28593 });
28594 if (!persist2) {
28595 continue;
28596 }
28597 void updatePlacement(p.id, {
28598 x: cell.x,
28599 y: cell.y,
28600 sortOrder: i
28601 }).catch((err) => {
28602 console.error("[desktop-mode] relayout persist failed", err);
28603 });
28604 }
28605 };
28606 const rootSortTransform = (mode) => (arr) => {
28607 const sorted = arr.slice();
28608 switch (mode) {
28609 case "name-asc":
28610 sorted.sort(
28611 (a, b) => a.file.title.localeCompare(b.file.title)
28612 );
28613 break;
28614 case "name-desc":
28615 sorted.sort(
28616 (a, b) => b.file.title.localeCompare(a.file.title)
28617 );
28618 break;
28619 case "date-asc":
28620 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
28621 break;
28622 case "date-desc":
28623 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
28624 break;
28625 }
28626 return sorted;
28627 };
28628 const ROOT_SORT_MODE_KEY = "desktop-mode:root-sort-mode";
28629 const isRootSortMode = (v) => v === "name-asc" || v === "name-desc" || v === "date-asc" || v === "date-desc";
28630 let rootSortMode = (() => {
28631 try {
28632 const raw = window.localStorage.getItem(ROOT_SORT_MODE_KEY);
28633 return isRootSortMode(raw) ? raw : null;
28634 } catch {
28635 return null;
28636 }
28637 })();
28638 const setRootSortMode = (mode) => {
28639 rootSortMode = mode;
28640 try {
28641 if (mode) {
28642 window.localStorage.setItem(ROOT_SORT_MODE_KEY, mode);
28643 } else {
28644 window.localStorage.removeItem(ROOT_SORT_MODE_KEY);
28645 }
28646 } catch {
28647 }
28648 };
28649 addAction(
28650 "desktop-mode.files.tile-manually-placed",
28651 "desktop-mode/root-sort-clear",
28652 (payload) => {
28653 const folderId = payload?.folderId;
28654 if (folderId === 0) {
28655 setRootSortMode(null);
28656 }
28657 }
28658 );
28659 if (typeof ResizeObserver !== "undefined") {
28660 let lastW = desktopArea.clientWidth;
28661 let lastH = desktopArea.clientHeight;
28662 const ro = new ResizeObserver(() => {
28663 if (!rootSortMode) {
28664 return;
28665 }
28666 const w = desktopArea.clientWidth;
28667 const h = desktopArea.clientHeight;
28668 if (w === lastW && h === lastH) {
28669 return;
28670 }
28671 lastW = w;
28672 lastH = h;
28673 relayoutRoot(rootSortTransform(rootSortMode), false);
28674 });
28675 ro.observe(desktopArea);
28676 }
28677 let pointerdownOnWallpaper = false;
28678 desktopArea.addEventListener("pointerdown", (e) => {
28679 if (!e.isPrimary) {
28680 return;
28681 }
28682 pointerdownOnWallpaper = e.target === desktopArea;
28683 });
28684 desktopArea.addEventListener("click", (e) => {
28685 if (!osSettings.state.showDesktopOnWallpaperClick) {
28686 return;
28687 }
28688 if (e.target !== desktopArea) {
28689 return;
28690 }
28691 if (!pointerdownOnWallpaper) {
28692 return;
28693 }
28694 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
28695 return;
28696 }
28697 if (isWallpaperMenuOpen()) {
28698 return;
28699 }
28700 if (dragManager.recentlyEndedDrag()) {
28701 return;
28702 }
28703 manager.toggleShowDesktop();
28704 });
28705 desktopArea.addEventListener("contextmenu", (e) => {
28706 if (e.target !== desktopArea) {
28707 return;
28708 }
28709 e.preventDefault();
28710 const clientX = e.clientX;
28711 const clientY = e.clientY;
28712 (() => {
28713 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
28714 return;
28715 }
28716 if (isWallpaperMenuOpen()) {
28717 closeWallpaperMenu();
28718 return;
28719 }
28720 const dropClient = { x: clientX, y: clientY };
28721 const cellAtClick = () => {
28722 const rect = desktopArea.getBoundingClientRect();
28723 const rawX = Math.max(0, dropClient.x - rect.left);
28724 const rawY = Math.max(0, dropClient.y - rect.top);
28725 const occupied = buildOccupiedSet(
28726 filesApi.store.getState().placementsByFolder.get(0) ?? []
28727 );
28728 return snapToEmptyCell(rawX, rawY, occupied, desktopArea);
28729 };
28730 const createUrlPlacement = (dialogTitle, description) => {
28731 openUrlDialog({
28732 title: dialogTitle,
28733 description,
28734 nameLabel: "Name",
28735 urlLabel: "URL",
28736 submitLabel: "Create",
28737 onSubmit: async ({ name, url }) => {
28738 const cell = cellAtClick();
28739 const placement = await createPlacement({
28740 type: "link",
28741 ref: url,
28742 parentId: 0,
28743 x: cell.x,
28744 y: cell.y,
28745 meta: name ? { name } : void 0
28746 });
28747 filesApi.store.upsertPlacement(placement);
28748 }
28749 });
28750 };
28751 const items = buildMenuItems({
28752 createFolder: () => {
28753 openCreateFolderDialog({
28754 onSubmit: async (name) => {
28755 const folder = await createFolder({ name });
28756 const cell = cellAtClick();
28757 const placement = await createPlacement({
28758 type: "folder",
28759 ref: String(folder.id),
28760 parentId: 0,
28761 x: cell.x,
28762 y: cell.y
28763 });
28764 filesApi.store.upsertFolder(folder);
28765 filesApi.store.upsertPlacement(placement);
28766 }
28767 });
28768 },
28769 createUrl: () => createUrlPlacement(
28770 "New URL",
28771 "Opens the URL in a new browser tab."
28772 ),
28773 toggleShowDesktop: () => manager.toggleShowDesktop(),
28774 openOsSettings: () => openOsSettings(),
28775 sortIcons: (mode) => {
28776 setRootSortMode(mode);
28777 relayoutRoot(rootSortTransform(mode));
28778 },
28779 currentSortMode: rootSortMode,
28780 includeShowDesktop: !osSettings.state.showDesktopOnWallpaperClick,
28781 labels: {
28782 createFolder: "New folder",
28783 showDesktop: "Show desktop",
28784 osSettings: "OS Settings",
28785 sortHeading: "Sort by",
28786 sortNameAsc: "Name (A → Z)",
28787 sortNameDesc: "Name (Z → A)",
28788 sortDateAsc: "Date (oldest first)",
28789 sortDateDesc: "Date (newest first)",
28790 newUrl: "New URL"
28791 },
28792 serverItems: config.serverWallpaperMenuItems ?? []
28793 });
28794 openWallpaperMenu(
28795 document.body,
28796 { x: clientX, y: clientY },
28797 items
28798 );
28799 })();
28800 });
28801 void Promise.resolve().then(() => index).then((mod) => {
28802 mod.bootOsFileDrop({
28803 config: config.dropConfig,
28804 mediaUrl: config.mediaUrl,
28805 restNonce: config.restNonce
28806 });
28807 });
28808 document.dispatchEvent(
28809 new CustomEvent("desktop-mode-init", {
28810 detail: { config, restored: hasSession }
28811 })
28812 );
28813 }
28814 startMissingImportWarner();
28815 if (document.readyState === "loading") {
28816 document.addEventListener("DOMContentLoaded", init);
28817 } else {
28818 init();
28819 }
28820 const _initial = {
28821 tab: null,
28822 requestedAt: 0
28823 };
28824 let _store = null;
28825 function getStore() {
28826 if (_store) {
28827 return _store;
28828 }
28829 const w = window;
28830 const factory = w.wp?.desktop?.createSharedStore;
28831 if (typeof factory !== "function") {
28832 return null;
28833 }
28834 _store = factory(
28835 "desktop-mode/plugins-window/tab-target",
28836 () => ({ ..._initial })
28837 );
28838 return _store;
28839 }
28840 function setPluginsWindowTab(tab) {
28841 const store2 = getStore();
28842 if (store2) {
28843 store2.state.tab = tab;
28844 store2.state.requestedAt = Date.now();
28845 store2.notify();
28846 return;
28847 }
28848 const w = window;
28849 w._wpdPluginsWindowTab = { tab, requestedAt: Date.now() };
28850 }
28851 function consumePluginsWindowTab() {
28852 const store2 = getStore();
28853 if (store2) {
28854 const tab = store2.state.tab;
28855 if (tab !== null) {
28856 store2.state.tab = null;
28857 store2.state.requestedAt = 0;
28858 store2.notify();
28859 }
28860 return tab;
28861 }
28862 const w = window;
28863 const prev = w._wpdPluginsWindowTab;
28864 if (prev) {
28865 w._wpdPluginsWindowTab = { tab: null, requestedAt: 0 };
28866 return prev.tab;
28867 }
28868 return null;
28869 }
28870 function subscribePluginsWindowTab(cb) {
28871 const store2 = getStore();
28872 if (!store2) {
28873 return () => {
28874 };
28875 }
28876 return store2.subscribe((state2) => cb({ ...state2 }));
28877 }
28878 const tabTarget = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
28879 __proto__: null,
28880 consumePluginsWindowTab,
28881 setPluginsWindowTab,
28882 subscribePluginsWindowTab
28883 }, Symbol.toStringTag, { value: "Module" }));
28884 const FILE_DROP_HOOKS = {
28885 /**
28886 * Filter — fires once per drop, after the manager has parsed
28887 * the OS `DataTransfer` into `File[]` and BEFORE the mime /
28888 * size filter runs.
28889 *
28890 * Signature: `(files: File[], ctx: DropContext) => File[]`.
28891 * Return an empty array to abort the drop silently.
28892 */
28893 FILES_DETECTED: "desktop-mode.drop.files-detected",
28894 /**
28895 * Action — fires after the mime / size filter has rejected
28896 * one or more files. Payload: `{ rejections: DropRejection[],
28897 * context: DropContext }`. The shell toasts a default message;
28898 * subscribers can surface a custom UX (a side panel with the
28899 * list, an analytics call).
28900 */
28901 FILES_REJECTED: "desktop-mode.drop.files-rejected",
28902 /**
28903 * Filter — fires per file before the upload dialog renders.
28904 * Receives `DropFileEntry` (the underlying file + the
28905 * manager's default `fields`). Mutate `fields` (or return a
28906 * new object) to change what the user sees in the form.
28907 *
28908 * Signature: `(entry: DropFileEntry, ctx: DropContext)
28909 * => DropFileEntry`.
28910 */
28911 DIALOG_FIELDS: "desktop-mode.drop.dialog-fields",
28912 /**
28913 * Filter — last call before the manager `POST`s to
28914 * `wp/v2/media`. Receives `{ file: File, fields:
28915 * DropDialogFields, mime: string }`. Return `null` to cancel
28916 * the upload entirely (e.g. a plugin handled it via a
28917 * different endpoint).
28918 *
28919 * Signature: `(payload, ctx: DropContext) => payload | null`.
28920 */
28921 BEFORE_UPLOAD: "desktop-mode.drop.before-upload",
28922 /**
28923 * Action — fires once `BEFORE_UPLOAD` has cleared and the XHR
28924 * is `open()`ed, immediately before `send()`. Payload:
28925 * `{ file: File, fields: DropDialogFields, context: DropContext,
28926 * abort: () => void }`. The `abort` handle aborts the in-flight
28927 * request; the manager rejects with `UploadAbortedError` and
28928 * fires `UPLOAD_FAILED` with that error.
28929 *
28930 * Pair with `UPLOAD_PROGRESS` to drive a progress UI; pair with
28931 * `AFTER_UPLOAD` / `UPLOAD_FAILED` to know when the upload ends.
28932 *
28933 * @since 0.31.0
28934 */
28935 UPLOAD_STARTED: "desktop-mode.drop.upload-started",
28936 /**
28937 * Action — fires for every `XMLHttpRequestUpload.progress` event.
28938 * Payload: `{ file: File, fields: DropDialogFields, context:
28939 * DropContext, loaded: number, total: number, indeterminate:
28940 * boolean }`. `total` is `0` and `indeterminate` is `true` when
28941 * the request body length isn't known (rare for multipart, but
28942 * possible on transcoding proxies); subscribers should treat
28943 * that as an indeterminate state.
28944 *
28945 * A synthetic 100%-loaded event is dispatched once the `upload`
28946 * stream emits `load` so a HUD can show a definite "wrapping up"
28947 * state while the server finishes the response.
28948 *
28949 * @since 0.31.0
28950 */
28951 UPLOAD_PROGRESS: "desktop-mode.drop.upload-progress",
28952 /**
28953 * Action — fires after a successful upload. Payload:
28954 * `{ file: File, result: DropUploadResult, fields:
28955 * DropDialogFields, context: DropContext }`.
28956 *
28957 * The `file` field carries the same `File` reference that
28958 * `UPLOAD_STARTED` / `UPLOAD_PROGRESS` exposed (i.e. the
28959 * payload returned by the `BEFORE_UPLOAD` filter, in case a
28960 * plugin swapped the file). Subscribers tracking per-file
28961 * state — progress HUDs, sequence counters — should match on
28962 * this identity rather than the filename: two drops of
28963 * `photo.jpg` from different folders would otherwise route
28964 * each other's success event to the wrong row.
28965 *
28966 * @since 0.31.0 the `file` field was added; pre-0.31.0 code
28967 * that destructured `{ result, fields, context }` keeps working.
28968 */
28969 AFTER_UPLOAD: "desktop-mode.drop.after-upload",
28970 /**
28971 * Action — fires after an upload fails. Payload:
28972 * `{ file: File, error: Error, context: DropContext }`.
28973 * `error` is an `UploadAbortedError` when the failure came
28974 * from the caller invoking the `abort()` handle on
28975 * `UPLOAD_STARTED`.
28976 *
28977 * `file` carries the same identity as `UPLOAD_STARTED` /
28978 * `UPLOAD_PROGRESS` / `AFTER_UPLOAD` — the post-`BEFORE_UPLOAD`
28979 * `File`, in case a plugin swapped it. Match by reference, not
28980 * filename: a HUD that keys its row map on the started-File
28981 * needs the same key here, otherwise the row stays stuck in
28982 * "running" after a failure when a `BEFORE_UPLOAD` filter
28983 * replaced the file.
28984 */
28985 UPLOAD_FAILED: "desktop-mode.drop.upload-failed"
28986 };
28987 const IFRAME_PASSTHROUGH_SELECTORS = [
28988 ".components-drop-zone",
28989 "[data-drop-zone]",
28990 ".uploader-window",
28991 ".media-frame-content"
28992 ];
28993 function dragHasFiles(ev) {
28994 const types = ev.dataTransfer?.types;
28995 if (!types) {
28996 return false;
28997 }
28998 const list2 = types;
28999 if (typeof list2.includes === "function") {
29000 return list2.includes("Files");
29001 }
29002 if (typeof list2.contains === "function") {
29003 return list2.contains("Files");
29004 }
29005 for (let i = 0; i < list2.length; i++) {
29006 if (list2[i] === "Files") {
29007 return true;
29008 }
29009 }
29010 return false;
29011 }
29012 function resolveWindowIdFromSource(source) {
29013 if (!source) {
29014 return void 0;
29015 }
29016 const iframes = document.querySelectorAll("iframe");
29017 for (const f of Array.from(iframes)) {
29018 if (f.contentWindow === source) {
29019 const host = f.closest("[data-window-id]");
29020 return host?.getAttribute("data-window-id") || void 0;
29021 }
29022 }
29023 return void 0;
29024 }
29025 function mountOsFileDropManager(opts) {
29026 const host = window;
29027 if (host.__desktopModeOsFileDropMounted) {
29028 return host.__desktopModeOsFileDropMounted;
29029 }
29030 if (!opts.config.enabled) {
29031 return mountNoOp();
29032 }
29033 const overlayEl = ensureDropOverlay();
29034 let dragDepth = 0;
29035 let dragWatchdog = null;
29036 const resetOverlay = () => {
29037 dragDepth = 0;
29038 overlayEl.classList.remove("is-active");
29039 if (dragWatchdog !== null) {
29040 clearTimeout(dragWatchdog);
29041 dragWatchdog = null;
29042 }
29043 };
29044 const bumpWatchdog = () => {
29045 if (dragWatchdog !== null) {
29046 clearTimeout(dragWatchdog);
29047 }
29048 dragWatchdog = setTimeout(resetOverlay, 250);
29049 };
29050 const onDragEnter = (ev) => {
29051 if (!dragHasFiles(ev)) {
29052 return;
29053 }
29054 ev.preventDefault();
29055 dragDepth++;
29056 overlayEl.classList.add("is-active");
29057 bumpWatchdog();
29058 };
29059 const onDragOver = (ev) => {
29060 if (!dragHasFiles(ev)) {
29061 return;
29062 }
29063 if (ev.defaultPrevented) {
29064 resetOverlay();
29065 return;
29066 }
29067 ev.preventDefault();
29068 if (ev.dataTransfer) {
29069 ev.dataTransfer.dropEffect = "copy";
29070 }
29071 bumpWatchdog();
29072 };
29073 const onDragLeave = () => {
29074 dragDepth = Math.max(0, dragDepth - 1);
29075 if (dragDepth === 0) {
29076 overlayEl.classList.remove("is-active");
29077 }
29078 };
29079 const onDrop = (ev) => {
29080 if (!dragHasFiles(ev)) {
29081 return;
29082 }
29083 if (ev.defaultPrevented) {
29084 resetOverlay();
29085 return;
29086 }
29087 ev.preventDefault();
29088 resetOverlay();
29089 const files = ev.dataTransfer?.files ? Array.from(ev.dataTransfer.files) : [];
29090 if (files.length === 0) {
29091 return;
29092 }
29093 const ctx = classifyDropTarget(ev);
29094 void handleFiles(files, ctx, opts);
29095 };
29096 const onDragEnd2 = () => resetOverlay();
29097 const onVisibilityChange = () => {
29098 if (document.visibilityState === "hidden") {
29099 resetOverlay();
29100 }
29101 };
29102 const onIframeMessage = (ev) => {
29103 if (ev.origin !== window.location.origin) {
29104 return;
29105 }
29106 const data = ev.data;
29107 if (!data || data.type !== "desktop-mode-os-file-drop") {
29108 return;
29109 }
29110 if (!Array.isArray(data.files) || data.files.length === 0) {
29111 return;
29112 }
29113 const files = data.files.filter((f) => f instanceof File);
29114 if (files.length === 0) {
29115 return;
29116 }
29117 const windowId = resolveWindowIdFromSource(ev.source);
29118 if (!windowId) {
29119 return;
29120 }
29121 const ctx = {
29122 surface: "iframe",
29123 windowId,
29124 x: typeof data.x === "number" ? data.x : 0,
29125 y: typeof data.y === "number" ? data.y : 0
29126 };
29127 dragDepth = 0;
29128 overlayEl.classList.remove("is-active");
29129 void handleFiles(files, ctx, opts);
29130 };
29131 window.addEventListener("dragenter", onDragEnter);
29132 window.addEventListener("dragover", onDragOver);
29133 window.addEventListener("dragleave", onDragLeave);
29134 window.addEventListener("drop", onDrop);
29135 window.addEventListener("dragend", onDragEnd2);
29136 document.addEventListener("visibilitychange", onVisibilityChange);
29137 window.addEventListener("blur", onDragEnd2);
29138 window.addEventListener("message", onIframeMessage);
29139 const manager = {
29140 dispose: () => {
29141 window.removeEventListener("dragenter", onDragEnter);
29142 window.removeEventListener("dragover", onDragOver);
29143 window.removeEventListener("dragleave", onDragLeave);
29144 window.removeEventListener("drop", onDrop);
29145 window.removeEventListener("dragend", onDragEnd2);
29146 document.removeEventListener(
29147 "visibilitychange",
29148 onVisibilityChange
29149 );
29150 window.removeEventListener("blur", onDragEnd2);
29151 window.removeEventListener("message", onIframeMessage);
29152 overlayEl.remove();
29153 delete window.__desktopModeOsFileDropMounted;
29154 }
29155 };
29156 host.__desktopModeOsFileDropMounted = manager;
29157 return manager;
29158 }
29159 function ensureDropOverlay() {
29160 const existing = document.querySelector(".desktop-mode-os-drop-overlay");
29161 if (existing) {
29162 return existing;
29163 }
29164 const el = document.createElement("div");
29165 el.className = "desktop-mode-os-drop-overlay";
29166 el.setAttribute("aria-hidden", "true");
29167 el.style.cssText = [
29168 "position:fixed",
29169 "inset:0",
29170 "pointer-events:none",
29171 "z-index:200",
29172 "opacity:0",
29173 "transition:opacity 120ms ease",
29174 "background:radial-gradient(circle at center, rgba(34,113,177,0.18) 0%, rgba(34,113,177,0.06) 60%, transparent 100%)",
29175 "box-shadow:inset 0 0 0 3px rgba(34,113,177,0.55)"
29176 ].join(";");
29177 const label = document.createElement("div");
29178 label.style.cssText = [
29179 "position:absolute",
29180 "top:50%",
29181 "left:50%",
29182 "transform:translate(-50%,-50%)",
29183 "padding:14px 22px",
29184 "border-radius:12px",
29185 "background:rgba(20,20,24,0.78)",
29186 "color:#fff",
29187 "font:600 14px/1.2 -apple-system,BlinkMacSystemFont,sans-serif",
29188 "letter-spacing:0.02em"
29189 ].join(";");
29190 label.textContent = "Drop to upload";
29191 el.appendChild(label);
29192 document.body.appendChild(el);
29193 const style = document.createElement("style");
29194 style.textContent = ".desktop-mode-os-drop-overlay.is-active{opacity:1!important;}";
29195 document.head.appendChild(style);
29196 return el;
29197 }
29198 function mountNoOp() {
29199 const cancel = (ev) => {
29200 if (!dragHasFiles(ev)) {
29201 return;
29202 }
29203 const target2 = ev.target;
29204 if (target2?.closest && IFRAME_PASSTHROUGH_SELECTORS.some((s) => target2.closest(s))) {
29205 return;
29206 }
29207 ev.preventDefault();
29208 };
29209 window.addEventListener("dragover", cancel);
29210 window.addEventListener("drop", cancel);
29211 const host = window;
29212 const manager = {
29213 dispose: () => {
29214 window.removeEventListener("dragover", cancel);
29215 window.removeEventListener("drop", cancel);
29216 delete host.__desktopModeOsFileDropMounted;
29217 }
29218 };
29219 host.__desktopModeOsFileDropMounted = manager;
29220 return manager;
29221 }
29222 function classifyDropTarget(ev) {
29223 const x = ev.clientX;
29224 const y = ev.clientY;
29225 let node = ev.target;
29226 while (node && node !== document.body) {
29227 if (node.tagName === "IFRAME") {
29228 const id = node.closest(
29229 "[data-window-id]"
29230 );
29231 return {
29232 surface: "iframe",
29233 windowId: id?.getAttribute("data-window-id") || void 0,
29234 x,
29235 y
29236 };
29237 }
29238 if (node.hasAttribute("data-window-id")) {
29239 return {
29240 surface: "window",
29241 windowId: node.getAttribute("data-window-id") || void 0,
29242 x,
29243 y
29244 };
29245 }
29246 if (node.classList.contains("desktop-mode-folder-grid")) {
29247 return { surface: "folder", x, y };
29248 }
29249 if (node.id === "desktop-mode-wallpaper" || node.classList.contains("desktop-mode-wallpaper") || node.classList.contains("desktop-mode-desktop")) {
29250 return { surface: "wallpaper", x, y };
29251 }
29252 node = node.parentElement;
29253 }
29254 return { surface: "unknown", x, y };
29255 }
29256 async function handleFiles(rawFiles, ctx, opts) {
29257 const detected = applyFilters(
29258 FILE_DROP_HOOKS.FILES_DETECTED,
29259 rawFiles,
29260 ctx
29261 );
29262 if (!Array.isArray(detected) || detected.length === 0) {
29263 return;
29264 }
29265 const { accepted, rejected } = partitionByPolicy(
29266 detected,
29267 opts.config
29268 );
29269 if (rejected.length > 0) {
29270 doAction(FILE_DROP_HOOKS.FILES_REJECTED, {
29271 rejections: rejected,
29272 context: ctx
29273 });
29274 showToast({
29275 message: rejected.length === 1 ? rejected[0].message : `${rejected.length} files couldn't be uploaded.`
29276 });
29277 }
29278 if (accepted.length === 0) {
29279 return;
29280 }
29281 const entries = accepted.map(({ file, mime }) => {
29282 const base = {
29283 file,
29284 mime,
29285 fields: defaultFields(file, mime)
29286 };
29287 const filtered = applyFilters(
29288 FILE_DROP_HOOKS.DIALOG_FIELDS,
29289 base,
29290 ctx
29291 );
29292 if (!filtered || typeof filtered !== "object" || !("fields" in filtered) || typeof filtered.fields !== "object") {
29293 return base;
29294 }
29295 return filtered;
29296 });
29297 await opts.openDialog(entries, ctx);
29298 }
29299 function partitionByPolicy(files, config) {
29300 const accepted = [];
29301 const rejected = [];
29302 for (const file of files) {
29303 if (file.size === 0) {
29304 rejected.push({
29305 file,
29306 reason: "empty",
29307 message: `“${file.name}” is empty.`
29308 });
29309 continue;
29310 }
29311 if (config.maxSize > 0 && file.size > config.maxSize) {
29312 rejected.push({
29313 file,
29314 reason: "size",
29315 message: `“${file.name}” exceeds the ${formatBytes$1(
29316 config.maxSize
29317 )} upload limit.`
29318 });
29319 continue;
29320 }
29321 const mime = resolveAllowedMime(
29322 file,
29323 config.allowedMimes,
29324 config.extToMime
29325 );
29326 if (!mime) {
29327 rejected.push({
29328 file,
29329 reason: "mime",
29330 message: `“${file.name}” is not an allowed file type.`
29331 });
29332 continue;
29333 }
29334 accepted.push({ file, mime });
29335 }
29336 return { accepted, rejected };
29337 }
29338 function resolveAllowedMime(file, allowedMimes, extToMime) {
29339 if (allowedMimes.length === 0) {
29340 return null;
29341 }
29342 const lower = file.type.toLowerCase();
29343 if (lower && allowedMimes.includes(lower)) {
29344 return lower;
29345 }
29346 const ext = extensionOf(file.name);
29347 if (!ext) {
29348 return null;
29349 }
29350 if (extToMime) {
29351 for (const [key, mime] of Object.entries(extToMime)) {
29352 if (key.split("|").includes(ext) && allowedMimes.includes(mime)) {
29353 return mime;
29354 }
29355 }
29356 return null;
29357 }
29358 const guess = EXTENSION_GUESSES[ext];
29359 if (guess && allowedMimes.includes(guess)) {
29360 return guess;
29361 }
29362 return null;
29363 }
29364 const EXTENSION_GUESSES = {
29365 jpg: "image/jpeg",
29366 jpeg: "image/jpeg",
29367 png: "image/png",
29368 gif: "image/gif",
29369 webp: "image/webp",
29370 avif: "image/avif",
29371 heic: "image/heic",
29372 heif: "image/heif",
29373 svg: "image/svg+xml",
29374 mp4: "video/mp4",
29375 mov: "video/quicktime",
29376 webm: "video/webm",
29377 mp3: "audio/mpeg",
29378 wav: "audio/wav",
29379 pdf: "application/pdf"
29380 };
29381 function extensionOf(name) {
29382 const dot = name.lastIndexOf(".");
29383 if (dot < 0) {
29384 return "";
29385 }
29386 return name.slice(dot + 1).toLowerCase();
29387 }
29388 function defaultFields(file, mime) {
29389 const safeName = sanitizeFilename(file.name);
29390 const ext = extensionOf(safeName);
29391 const stem = ext ? safeName.slice(0, safeName.length - ext.length - 1) : safeName;
29392 const title = humanize(stem);
29393 return {
29394 title,
29395 altText: mime.startsWith("image/") ? title : "",
29396 caption: "",
29397 description: "",
29398 filename: safeName
29399 };
29400 }
29401 function sanitizeFilename(name) {
29402 const cleaned = name.replace(/[\\/]/g, "-").replace(/[\x00-\x1f\x7f]/g, "").replace(/\s+/g, " ").replace(/ *- */g, "-").replace(/-+/g, "-").trim().replace(/^[-.]+|[-.]+$/g, "");
29403 return cleaned || "upload";
29404 }
29405 function humanize(stem) {
29406 const spaced = stem.replace(/[-_]+/g, " ").trim();
29407 if (!spaced) {
29408 return "Upload";
29409 }
29410 return spaced.charAt(0).toUpperCase() + spaced.slice(1);
29411 }
29412 function formatBytes$1(bytes) {
29413 if (bytes >= 1024 * 1024) {
29414 return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
29415 }
29416 if (bytes >= 1024) {
29417 return `${(bytes / 1024).toFixed(0)} KB`;
29418 }
29419 return `${bytes} B`;
29420 }
29421 function formatBytes(bytes) {
29422 if (!Number.isFinite(bytes) || bytes <= 0) {
29423 return "0 B";
29424 }
29425 const units = ["B", "KB", "MB", "GB", "TB"];
29426 let v = bytes;
29427 let i = 0;
29428 while (v >= 1024 && i < units.length - 1) {
29429 v /= 1024;
29430 i++;
29431 }
29432 const decimals = v >= 100 || i === 0 ? 0 : 1;
29433 return `${v.toFixed(decimals)} ${units[i]}`;
29434 }
29435 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}}`;
29436 const _WpdProgressBar = class _WpdProgressBar extends Component {
29437 constructor() {
29438 super(...arguments);
29439 this._ownedAriaLabel = null;
29440 }
29441 render() {
29442 return html`<div class="root" part="root">
29443 <div class="header" part="header" hidden>
29444 <span class="label" part="label"></span>
29445 <span class="percent" part="percent"></span>
29446 </div>
29447 <div class="track" part="track">
29448 <div class="fill" part="fill"></div>
29449 </div>
29450 </div>`;
29451 }
29452 requestUpdate() {
29453 super.requestUpdate();
29454 queueMicrotask(() => this._paint());
29455 }
29456 connectedCallback() {
29457 super.connectedCallback();
29458 queueMicrotask(() => this._paint());
29459 }
29460 _paint() {
29461 const root = this.shadowRoot;
29462 if (!root) {
29463 return;
29464 }
29465 const max = this._readMax();
29466 const indeterminate = this.hasAttribute("indeterminate") || max <= 0;
29467 const value = indeterminate ? 0 : this._readValue(max);
29468 const ratio = indeterminate ? 0 : value / max;
29469 const percent = Math.round(ratio * 100);
29470 const label = this.getAttribute("label") ?? "";
29471 const showPercent = this.hasAttribute("show-percent");
29472 const fill = root.querySelector(".fill");
29473 if (fill && !indeterminate) {
29474 fill.style.width = `${(ratio * 100).toFixed(2)}%`;
29475 } else if (fill && indeterminate) {
29476 fill.style.removeProperty("width");
29477 }
29478 const header = root.querySelector(".header");
29479 const labelEl = root.querySelector(".label");
29480 const percentEl = root.querySelector(".percent");
29481 if (header && labelEl && percentEl) {
29482 const visible = label || showPercent && !indeterminate;
29483 header.hidden = !visible;
29484 labelEl.textContent = label;
29485 percentEl.hidden = !(showPercent && !indeterminate);
29486 percentEl.textContent = `${percent}%`;
29487 }
29488 this._syncAria(max, value, indeterminate, label);
29489 const track = root.querySelector(".track");
29490 if (track) {
29491 track.setAttribute("role", "progressbar");
29492 track.setAttribute("aria-valuemin", "0");
29493 if (indeterminate) {
29494 track.removeAttribute("aria-valuenow");
29495 track.removeAttribute("aria-valuemax");
29496 } else {
29497 track.setAttribute("aria-valuemax", String(max));
29498 track.setAttribute("aria-valuenow", String(value));
29499 }
29500 if (label) {
29501 track.setAttribute("aria-label", label);
29502 } else {
29503 track.removeAttribute("aria-label");
29504 }
29505 }
29506 }
29507 _syncAria(max, value, indeterminate, label) {
29508 this.setAttribute("role", "progressbar");
29509 this.setAttribute("aria-valuemin", "0");
29510 if (indeterminate) {
29511 this.removeAttribute("aria-valuenow");
29512 this.removeAttribute("aria-valuemax");
29513 } else {
29514 this.setAttribute("aria-valuemax", String(max));
29515 this.setAttribute("aria-valuenow", String(value));
29516 }
29517 const existing = this.getAttribute("aria-label");
29518 if (label) {
29519 if (existing === null || existing === this._ownedAriaLabel) {
29520 this.setAttribute("aria-label", label);
29521 this._ownedAriaLabel = label;
29522 }
29523 } else if (existing !== null && existing === this._ownedAriaLabel) {
29524 this.removeAttribute("aria-label");
29525 this._ownedAriaLabel = null;
29526 }
29527 }
29528 _readMax() {
29529 const attr = this.getAttribute("max");
29530 if (attr === null) {
29531 return 100;
29532 }
29533 const raw = parseFloat(attr);
29534 return Number.isFinite(raw) ? raw : 100;
29535 }
29536 _readValue(max) {
29537 const raw = parseFloat(this.getAttribute("value") ?? "0");
29538 if (!Number.isFinite(raw)) {
29539 return 0;
29540 }
29541 if (raw < 0) {
29542 return 0;
29543 }
29544 if (raw > max) {
29545 return max;
29546 }
29547 return raw;
29548 }
29549 };
29550 _WpdProgressBar.props = [
29551 "value",
29552 "max",
29553 "indeterminate",
29554 "tone",
29555 "label",
29556 "showPercent"
29557 ];
29558 _WpdProgressBar.styles = [styles];
29559 _WpdProgressBar.help = {
29560 title: "Progress bar",
29561 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.",
29562 status: "experimental",
29563 since: "0.31.0",
29564 props: [
29565 {
29566 name: "value",
29567 type: "number",
29568 default: "0",
29569 description: "Current progress. Clamped to `[0, max]`."
29570 },
29571 {
29572 name: "max",
29573 type: "number",
29574 default: "100",
29575 description: "Maximum value. Setting `max <= 0` forces indeterminate."
29576 },
29577 {
29578 name: "indeterminate",
29579 type: "boolean",
29580 description: "Show the sweeping indeterminate animation instead of a value-driven fill. The `value` attribute is ignored while this is set."
29581 },
29582 {
29583 name: "tone",
29584 type: '"default" | "success" | "warning" | "danger"',
29585 default: "default",
29586 description: "Tints the fill from the shared status palette."
29587 },
29588 {
29589 name: "label",
29590 type: "string",
29591 description: "Optional inline label rendered above the track. Also wired into `aria-label` when set."
29592 },
29593 {
29594 name: "show-percent",
29595 type: "boolean",
29596 description: "Render a right-aligned percent readout next to the label. Only meaningful in determinate mode."
29597 }
29598 ],
29599 cssProps: [
29600 {
29601 name: "--wpd-progress-track-bg",
29602 default: "var(--desktop-mode-control-bg, rgba(0,0,0,0.08))"
29603 },
29604 {
29605 name: "--wpd-progress-fill",
29606 default: "var(--wp-admin-theme-color, #2271b1)"
29607 },
29608 { name: "--wpd-progress-height", default: "6px" },
29609 { name: "--wpd-progress-radius", default: "999px" },
29610 { name: "--wpd-progress-label-color", default: "inherit" },
29611 { name: "--wpd-progress-label-size", default: "12px" },
29612 { name: "--wpd-progress-label-gap", default: "4px" }
29613 ],
29614 example: html`<wpd-progress-bar
29615 value="42"
29616 label="Uploading hero.jpg"
29617 show-percent
29618 ></wpd-progress-bar>`
29619 };
29620 let WpdProgressBar = _WpdProgressBar;
29621 defineComponent("wpd-progress-bar", WpdProgressBar);
29622 const ROWS = /* @__PURE__ */ new Map();
29623 let panel = null;
29624 function mountUploadProgressHud() {
29625 if (document.body.hasAttribute("data-desktop-mode-suppress-upload-hud")) {
29626 return;
29627 }
29628 if (window.__wpdUploadHud) {
29629 return;
29630 }
29631 window.__wpdUploadHud = true;
29632 const ns = "desktop-mode/os-file-drop-hud";
29633 addAction(
29634 FILE_DROP_HOOKS.UPLOAD_STARTED,
29635 ns,
29636 (payload) => onStarted(payload.file, payload.fields, payload.abort)
29637 );
29638 addAction(
29639 FILE_DROP_HOOKS.UPLOAD_PROGRESS,
29640 ns,
29641 (payload) => onProgress(
29642 payload.file,
29643 payload.loaded,
29644 payload.total,
29645 payload.indeterminate
29646 )
29647 );
29648 addAction(
29649 FILE_DROP_HOOKS.AFTER_UPLOAD,
29650 ns,
29651 (payload) => onComplete(payload.file, payload.fields, payload.result)
29652 );
29653 addAction(
29654 FILE_DROP_HOOKS.UPLOAD_FAILED,
29655 ns,
29656 (payload) => onFailed(payload.file, payload.error)
29657 );
29658 }
29659 function onStarted(file, fields, abort) {
29660 const p = ensurePanel();
29661 const row = document.createElement("div");
29662 row.className = "desktop-mode-upload-hud__row";
29663 const meta = document.createElement("div");
29664 meta.className = "desktop-mode-upload-hud__meta";
29665 const name = document.createElement("div");
29666 name.className = "desktop-mode-upload-hud__name";
29667 name.textContent = fields.filename || file.name;
29668 name.title = fields.filename || file.name;
29669 const statusEl = document.createElement("div");
29670 statusEl.className = "desktop-mode-upload-hud__status";
29671 statusEl.textContent = "Uploading…";
29672 meta.append(name, statusEl);
29673 const bar = document.createElement("wpd-progress-bar");
29674 bar.setAttribute("indeterminate", "");
29675 bar.setAttribute("show-percent", "");
29676 const actions = document.createElement("div");
29677 actions.className = "desktop-mode-upload-hud__actions";
29678 const cancelBtn = document.createElement("wpd-button");
29679 cancelBtn.setAttribute("variant", "tertiary");
29680 cancelBtn.setAttribute("size", "small");
29681 cancelBtn.textContent = "Cancel";
29682 cancelBtn.addEventListener("click", () => {
29683 const r = ROWS.get(file);
29684 if (!r) {
29685 return;
29686 }
29687 if (r.state === "running") {
29688 r.statusEl.textContent = "Cancelling…";
29689 r.cancelBtn.disabled = true;
29690 r.abort();
29691 } else {
29692 dismissRow(r);
29693 }
29694 });
29695 actions.appendChild(cancelBtn);
29696 row.append(meta, bar, actions);
29697 p.querySelector(".desktop-mode-upload-hud__list").appendChild(row);
29698 ROWS.set(file, {
29699 file,
29700 abort,
29701 root: row,
29702 bar,
29703 statusEl,
29704 cancelBtn,
29705 state: "running",
29706 lingerTimer: null
29707 });
29708 updateHeader();
29709 }
29710 function onProgress(file, loaded, total, indeterminate) {
29711 const r = ROWS.get(file);
29712 if (!r || r.state !== "running") {
29713 return;
29714 }
29715 if (indeterminate || total <= 0) {
29716 r.bar.setAttribute("indeterminate", "");
29717 r.statusEl.textContent = `${formatBytes(loaded)} sent`;
29718 } else {
29719 r.bar.removeAttribute("indeterminate");
29720 r.bar.setAttribute("max", String(total));
29721 r.bar.setAttribute("value", String(loaded));
29722 r.statusEl.textContent = `${formatBytes(loaded)} / ${formatBytes(total)}`;
29723 }
29724 }
29725 function onComplete(file, fields, result) {
29726 const r = ROWS.get(file);
29727 if (!r) {
29728 return;
29729 }
29730 r.state = "success";
29731 r.bar.removeAttribute("indeterminate");
29732 r.bar.setAttribute("value", "100");
29733 r.bar.setAttribute("max", "100");
29734 r.bar.setAttribute("tone", "success");
29735 r.statusEl.textContent = "Uploaded";
29736 r.cancelBtn.textContent = "Dismiss";
29737 r.lingerTimer = setTimeout(() => dismissRow(r), 2500);
29738 updateHeader();
29739 activity.publish("desktop-mode/upload-hud-complete", {
29740 filename: fields.filename || result.filename,
29741 attachmentId: result.id
29742 });
29743 }
29744 function onFailed(file, error) {
29745 const r = ROWS.get(file);
29746 if (!r) {
29747 return;
29748 }
29749 r.bar.removeAttribute("indeterminate");
29750 r.bar.setAttribute("tone", "danger");
29751 r.cancelBtn.textContent = "Dismiss";
29752 r.cancelBtn.disabled = false;
29753 if (error.name === "UploadAbortedError") {
29754 r.state = "aborted";
29755 r.statusEl.textContent = "Cancelled";
29756 } else {
29757 r.state = "failed";
29758 r.statusEl.textContent = error.message || "Upload failed";
29759 }
29760 updateHeader();
29761 }
29762 function dismissRow(r) {
29763 if (r.lingerTimer) {
29764 clearTimeout(r.lingerTimer);
29765 }
29766 ROWS.delete(r.file);
29767 r.root.remove();
29768 updateHeader();
29769 if (ROWS.size === 0 && panel) {
29770 panel.hidden = true;
29771 }
29772 }
29773 function ensurePanel() {
29774 if (panel && panel.isConnected) {
29775 panel.hidden = false;
29776 return panel;
29777 }
29778 const p = document.createElement("div");
29779 p.className = "desktop-mode-upload-hud";
29780 p.setAttribute("role", "region");
29781 p.setAttribute("aria-label", "Uploads");
29782 const header = document.createElement("div");
29783 header.className = "desktop-mode-upload-hud__header";
29784 const title = document.createElement("div");
29785 title.className = "desktop-mode-upload-hud__title";
29786 title.textContent = "Uploads";
29787 const closeBtn = document.createElement("button");
29788 closeBtn.type = "button";
29789 closeBtn.className = "desktop-mode-upload-hud__close";
29790 closeBtn.setAttribute("aria-label", "Hide upload panel");
29791 closeBtn.textContent = "×";
29792 closeBtn.addEventListener("click", () => {
29793 for (const r of [...ROWS.values()]) {
29794 if (r.state !== "running") {
29795 dismissRow(r);
29796 }
29797 }
29798 if (ROWS.size === 0) {
29799 p.hidden = true;
29800 }
29801 });
29802 header.append(title, closeBtn);
29803 const list2 = document.createElement("div");
29804 list2.className = "desktop-mode-upload-hud__list";
29805 p.append(header, list2);
29806 document.body.appendChild(p);
29807 panel = p;
29808 return p;
29809 }
29810 function updateHeader() {
29811 if (!panel) {
29812 return;
29813 }
29814 const title = panel.querySelector(
29815 ".desktop-mode-upload-hud__title"
29816 );
29817 if (!title) {
29818 return;
29819 }
29820 const total = ROWS.size;
29821 const running = [...ROWS.values()].filter((r) => r.state === "running").length;
29822 if (running > 0) {
29823 title.textContent = running === total ? `Uploading ${running} file${running === 1 ? "" : "s"}…` : `${running} of ${total} uploading…`;
29824 } else if (total > 0) {
29825 title.textContent = `Uploads (${total})`;
29826 } else {
29827 title.textContent = "Uploads";
29828 }
29829 }
29830 function mountMediaLibraryRefresher() {
29831 if (document.body.hasAttribute(
29832 "data-desktop-mode-suppress-media-library-refresh"
29833 )) {
29834 return;
29835 }
29836 const sentinel = window;
29837 if (sentinel.__wpdMediaLibraryRefresher) {
29838 return;
29839 }
29840 sentinel.__wpdMediaLibraryRefresher = true;
29841 addAction(
29842 FILE_DROP_HOOKS.AFTER_UPLOAD,
29843 "desktop-mode/os-file-drop-library-refresh",
29844 () => refreshOpenLibraries()
29845 );
29846 }
29847 function refreshOpenLibraries() {
29848 const iframes = document.querySelectorAll("iframe");
29849 for (const frame of Array.from(iframes)) {
29850 if (!isMediaLibraryUrl(resolveIframeUrl(frame))) {
29851 continue;
29852 }
29853 try {
29854 frame.contentWindow?.location.reload();
29855 } catch {
29856 const reloadHref = resolveIframeUrl(frame);
29857 if (reloadHref) {
29858 frame.setAttribute("src", reloadHref);
29859 }
29860 }
29861 }
29862 }
29863 function resolveIframeUrl(frame) {
29864 try {
29865 return frame.contentWindow?.location.href ?? frame.src ?? "";
29866 } catch {
29867 return frame.src ?? "";
29868 }
29869 }
29870 function isMediaLibraryUrl(url) {
29871 if (!url) {
29872 return false;
29873 }
29874 return /\/wp-admin\/upload\.php(?:[?#]|$)/.test(url);
29875 }
29876 function bootOsFileDrop(args) {
29877 const config = args.config || {
29878 enabled: false,
29879 allowedMimes: [],
29880 maxSize: 0
29881 };
29882 mountUploadProgressHud();
29883 mountMediaLibraryRefresher();
29884 mountOsFileDropManager({
29885 config,
29886 mediaUrl: args.mediaUrl,
29887 restNonce: args.restNonce,
29888 openDialog: async (entries, ctx) => {
29889 const { openUploadDialog: openUploadDialog2 } = await Promise.resolve().then(() => dialog);
29890 await openUploadDialog2({
29891 entries,
29892 context: ctx,
29893 mediaUrl: args.mediaUrl,
29894 restNonce: args.restNonce
29895 });
29896 }
29897 });
29898 }
29899 const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
29900 __proto__: null,
29901 FILE_DROP_HOOKS,
29902 bootOsFileDrop
29903 }, Symbol.toStringTag, { value: "Module" }));
29904 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}`;
29905 const _WpdTextField = class _WpdTextField extends Component {
29906 constructor() {
29907 super(...arguments);
29908 this._revealed = false;
29909 }
29910 connectedCallback() {
29911 super.connectedCallback();
29912 ensureAutoId(this);
29913 }
29914 render() {
29915 const label = this.label || "";
29916 const value = this.value ?? "";
29917 const placeholder = this.placeholder || "";
29918 const disabled = this.disabled !== null;
29919 const readonly = this.readonly !== null;
29920 const declaredAutocomplete = this.autocomplete;
29921 const declaredType = this.type || "text";
29922 const isPassword = declaredType === "password";
29923 let autocomplete = declaredAutocomplete || "off";
29924 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
29925 autocomplete = "new-password";
29926 }
29927 const maxLength = this.maxlength;
29928 const minLength = this.minlength;
29929 const pattern = this.pattern || "";
29930 const name = this.name || "";
29931 const suffix = this.suffix || "";
29932 const invalid = this.invalid !== null;
29933 const reveal = this.reveal !== null;
29934 const isPasswordIntent = declaredType === "password";
29935 const isMasked = isPasswordIntent && !(reveal && this._revealed);
29936 let effectiveType;
29937 if (isPasswordIntent) {
29938 effectiveType = "text";
29939 } else if (reveal && this._revealed) {
29940 effectiveType = "text";
29941 } else {
29942 effectiveType = declaredType;
29943 }
29944 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
29945 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
29946 const hostId = this.id || "wpd-unnamed";
29947 const inputId = `${hostId}__input`;
29948 return html`
29949 ${label ? html`<label
29950 class="wpd-text-field__label"
29951 for=${inputId}
29952 >${label}</label>` : html``}
29953 <span class=${rowClass}>
29954 <input
29955 id=${inputId}
29956 class=${inputClass}
29957 type=${effectiveType}
29958 .value=${value}
29959 placeholder=${placeholder}
29960 ?disabled=${disabled}
29961 ?readonly=${readonly}
29962 autocomplete=${autocomplete}
29963 maxlength=${maxLength ?? ""}
29964 minlength=${minLength ?? ""}
29965 pattern=${pattern}
29966 name=${name}
29967 aria-invalid=${invalid ? "true" : "false"}
29968 aria-label=${label || ""}
29969 @input=${(e) => this._onInput(e)}
29970 @change=${(e) => this._onChange(e)}
29971 @keydown=${(e) => this._onKeyDown(e)}
29972 />
29973 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
29974 ${reveal ? this._renderRevealButton(disabled) : html``}
29975 </span>
29976 `;
29977 }
29978 _renderRevealButton(disabled) {
29979 const label = this._revealed ? "Hide" : "Show";
29980 return html`
29981 <button
29982 type="button"
29983 class="wpd-text-field__reveal"
29984 aria-label=${label}
29985 aria-pressed=${this._revealed ? "true" : "false"}
29986 ?disabled=${disabled}
29987 tabindex="0"
29988 @click=${() => this._onToggleReveal()}
29989 >
29990 ${this._revealed ? _iconEyeOff() : _iconEye()}
29991 </button>
29992 `;
29993 }
29994 _onToggleReveal() {
29995 this._revealed = !this._revealed;
29996 this.requestUpdate();
29997 }
29998 _onInput(e) {
29999 const input = e.target;
30000 this.value = input.value;
30001 this.emit("wpd-input-change", { value: input.value });
30002 }
30003 _onChange(e) {
30004 const input = e.target;
30005 this.emit("wpd-input-commit", { value: input.value });
30006 }
30007 _onKeyDown(e) {
30008 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
30009 const input = e.target;
30010 this.emit("wpd-submit", { value: input.value });
30011 }
30012 }
30013 };
30014 _WpdTextField.props = [
30015 "label",
30016 "value",
30017 "placeholder",
30018 "disabled",
30019 "readonly",
30020 "autocomplete",
30021 "type",
30022 "maxlength",
30023 "minlength",
30024 "pattern",
30025 "name",
30026 "suffix",
30027 "invalid",
30028 "reveal"
30029 ];
30030 _WpdTextField.styles = [textFieldStyles];
30031 _WpdTextField.help = {
30032 title: "Text field",
30033 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.",
30034 status: "stable",
30035 since: "0.11.0",
30036 props: [
30037 { name: "label", type: "string", description: "Visible label above the input." },
30038 { name: "value", type: "string", description: "Current input value; reflected two-way." },
30039 { name: "placeholder", type: "string", description: "Native placeholder string." },
30040 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
30041 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
30042 {
30043 name: "autocomplete",
30044 type: "string",
30045 default: "off",
30046 description: "Forwarded to the native input autocomplete attribute."
30047 },
30048 {
30049 name: "type",
30050 type: "string",
30051 default: "text",
30052 description: "Native input type (text, password, email, search, tel, url)."
30053 },
30054 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
30055 { name: "minlength", type: "integer (string)", description: "Native minlength." },
30056 { name: "pattern", type: "regex string", description: "Native validation pattern." },
30057 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
30058 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
30059 {
30060 name: "invalid",
30061 type: "boolean attribute",
30062 description: "Marks the field aria-invalid and applies the error style."
30063 },
30064 {
30065 name: "reveal",
30066 type: "boolean attribute",
30067 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
30068 }
30069 ],
30070 events: [
30071 {
30072 name: "wpd-input-change",
30073 description: "Fires on every input keystroke.",
30074 detail: "{ value: string }"
30075 },
30076 {
30077 name: "wpd-input-commit",
30078 description: "Fires on the native change event (blur / Enter).",
30079 detail: "{ value: string }"
30080 },
30081 {
30082 name: "wpd-submit",
30083 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
30084 detail: "{ value: string }"
30085 }
30086 ],
30087 cssProps: [
30088 { name: "--desktop-mode-text", description: "Text colour." },
30089 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
30090 { name: "--desktop-mode-border", description: "Input outline." },
30091 { name: "--desktop-mode-window-bg", description: "Input background." }
30092 ],
30093 example: html`
30094 <wpd-stack gap="8">
30095 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
30096 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
30097 </wpd-stack>
30098 `
30099 };
30100 let WpdTextField = _WpdTextField;
30101 defineComponent("wpd-text-field", WpdTextField);
30102 function _iconEye() {
30103 return html`
30104 <svg
30105 viewBox="0 0 16 16"
30106 width="14"
30107 height="14"
30108 fill="none"
30109 stroke="currentColor"
30110 stroke-width="1.5"
30111 stroke-linecap="round"
30112 stroke-linejoin="round"
30113 aria-hidden="true"
30114 focusable="false"
30115 >
30116 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
30117 <circle cx="8" cy="8" r="2" />
30118 </svg>
30119 `;
30120 }
30121 function _iconEyeOff() {
30122 return html`
30123 <svg
30124 viewBox="0 0 16 16"
30125 width="14"
30126 height="14"
30127 fill="none"
30128 stroke="currentColor"
30129 stroke-width="1.5"
30130 stroke-linecap="round"
30131 stroke-linejoin="round"
30132 aria-hidden="true"
30133 focusable="false"
30134 >
30135 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
30136 <circle cx="8" cy="8" r="2" />
30137 <line x1="2" y1="2" x2="14" y2="14" />
30138 </svg>
30139 `;
30140 }
30141 async function uploadFile(args) {
30142 const initial = {
30143 file: args.file,
30144 mime: args.mime,
30145 fields: args.fields
30146 };
30147 const filtered = applyFilters(
30148 FILE_DROP_HOOKS.BEFORE_UPLOAD,
30149 initial,
30150 args.context
30151 );
30152 if (!filtered) {
30153 throw new UploadCancelledError();
30154 }
30155 const body = new FormData();
30156 const renamed = filtered.fields.filename !== filtered.file.name ? new File([filtered.file], filtered.fields.filename, {
30157 type: filtered.mime || filtered.file.type
30158 }) : filtered.file;
30159 body.append("file", renamed);
30160 body.append("title", filtered.fields.title);
30161 body.append("alt_text", filtered.fields.altText);
30162 body.append("caption", filtered.fields.caption);
30163 body.append("description", filtered.fields.description);
30164 return new Promise((resolve2, reject) => {
30165 const xhr = new XMLHttpRequest();
30166 xhr.open("POST", args.mediaUrl, true);
30167 xhr.withCredentials = true;
30168 xhr.setRequestHeader("X-WP-Nonce", args.restNonce);
30169 xhr.responseType = "text";
30170 let aborted = false;
30171 let bodyFullySent = false;
30172 let cancelRequested = false;
30173 const abort = () => {
30174 cancelRequested = true;
30175 if (bodyFullySent) {
30176 return;
30177 }
30178 aborted = true;
30179 try {
30180 xhr.abort();
30181 } catch {
30182 }
30183 };
30184 doAction(FILE_DROP_HOOKS.UPLOAD_STARTED, {
30185 file: filtered.file,
30186 fields: filtered.fields,
30187 context: args.context,
30188 abort
30189 });
30190 xhr.upload.addEventListener("progress", (e) => {
30191 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
30192 file: filtered.file,
30193 fields: filtered.fields,
30194 context: args.context,
30195 loaded: e.loaded,
30196 total: e.lengthComputable ? e.total : 0,
30197 indeterminate: !e.lengthComputable
30198 });
30199 });
30200 xhr.upload.addEventListener("load", () => {
30201 bodyFullySent = true;
30202 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
30203 file: filtered.file,
30204 fields: filtered.fields,
30205 context: args.context,
30206 loaded: filtered.file.size,
30207 total: filtered.file.size,
30208 indeterminate: false
30209 });
30210 });
30211 xhr.addEventListener("error", () => {
30212 if (aborted) {
30213 return;
30214 }
30215 const error = new Error("Network error during upload.");
30216 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
30217 // `filtered.file` — same identity as UPLOAD_STARTED /
30218 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
30219 // that swapped the File would otherwise route this
30220 // failure to a row keyed by the original (pre-swap)
30221 // File, leaving the HUD row stuck in "running".
30222 file: filtered.file,
30223 error,
30224 context: args.context
30225 });
30226 reject(error);
30227 });
30228 xhr.addEventListener("abort", () => {
30229 const error = new UploadAbortedError();
30230 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
30231 // `filtered.file` — same identity as UPLOAD_STARTED /
30232 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
30233 // that swapped the File would otherwise route this
30234 // failure to a row keyed by the original (pre-swap)
30235 // File, leaving the HUD row stuck in "running".
30236 file: filtered.file,
30237 error,
30238 context: args.context
30239 });
30240 reject(error);
30241 });
30242 xhr.addEventListener("load", () => {
30243 if (aborted) {
30244 return;
30245 }
30246 if (xhr.status < 200 || xhr.status >= 300) {
30247 const message = extractXhrMessage(xhr);
30248 const error = new Error(message);
30249 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
30250 file: filtered.file,
30251 error,
30252 context: args.context
30253 });
30254 reject(error);
30255 return;
30256 }
30257 let data;
30258 try {
30259 data = JSON.parse(xhr.responseText);
30260 } catch (err) {
30261 const error = err instanceof Error ? err : new Error("Could not parse server response.");
30262 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
30263 file: filtered.file,
30264 error,
30265 context: args.context
30266 });
30267 reject(error);
30268 return;
30269 }
30270 if (cancelRequested && data.id) {
30271 void deleteAttachment(
30272 args.mediaUrl,
30273 args.restNonce,
30274 data.id
30275 );
30276 const error = new UploadAbortedError();
30277 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
30278 file: filtered.file,
30279 error,
30280 context: args.context
30281 });
30282 reject(error);
30283 return;
30284 }
30285 const result = {
30286 id: data.id,
30287 url: data.source_url,
30288 mime: data.mime_type || filtered.mime,
30289 title: data.title?.rendered || filtered.fields.title,
30290 filename: data.media_details?.file || filtered.fields.filename
30291 };
30292 doAction(FILE_DROP_HOOKS.AFTER_UPLOAD, {
30293 file: filtered.file,
30294 result,
30295 fields: filtered.fields,
30296 context: args.context
30297 });
30298 resolve2(result);
30299 });
30300 xhr.send(body);
30301 });
30302 }
30303 class UploadCancelledError extends Error {
30304 constructor() {
30305 super("Upload cancelled by desktop-mode.drop.before-upload filter.");
30306 this.name = "UploadCancelledError";
30307 }
30308 }
30309 class UploadAbortedError extends Error {
30310 constructor() {
30311 super("Upload aborted by the caller.");
30312 this.name = "UploadAbortedError";
30313 }
30314 }
30315 function deleteAttachment(mediaUrl, restNonce, id) {
30316 const url = `${mediaUrl.replace(/\/$/, "")}/${id}?force=true`;
30317 const cleanup = new XMLHttpRequest();
30318 cleanup.open("DELETE", url, true);
30319 cleanup.withCredentials = true;
30320 cleanup.setRequestHeader("X-WP-Nonce", restNonce);
30321 return new Promise((resolve2) => {
30322 cleanup.addEventListener("loadend", () => {
30323 if (cleanup.status < 200 || cleanup.status >= 300) {
30324 console.warn(
30325 `[os-file-drop] late-cancel cleanup failed for attachment ${id} (HTTP ${cleanup.status}). The attachment remains in the Media Library; delete it manually.`
30326 );
30327 }
30328 resolve2();
30329 });
30330 cleanup.addEventListener("error", () => {
30331 console.warn(
30332 `[os-file-drop] late-cancel cleanup network error for attachment ${id}. The attachment remains in the Media Library; delete it manually.`
30333 );
30334 resolve2();
30335 });
30336 try {
30337 cleanup.send();
30338 } catch (err) {
30339 console.warn(
30340 `[os-file-drop] late-cancel cleanup could not be dispatched for attachment ${id}:`,
30341 err
30342 );
30343 resolve2();
30344 }
30345 });
30346 }
30347 function extractXhrMessage(xhr) {
30348 const fallback = `Upload failed (HTTP ${xhr.status}).`;
30349 const text = xhr.responseText;
30350 if (!text) {
30351 return fallback;
30352 }
30353 try {
30354 const data = JSON.parse(text);
30355 if (data && typeof data.message === "string") {
30356 return data.message;
30357 }
30358 } catch {
30359 }
30360 return fallback;
30361 }
30362 async function openUploadDialog(args) {
30363 if (args.entries.length === 0) {
30364 return;
30365 }
30366 const modal = document.createElement("wpd-modal");
30367 modal.setAttribute("open", "");
30368 modal.setAttribute("size", "md");
30369 modal.setAttribute(
30370 "title",
30371 args.entries.length === 1 ? "Upload to Media Library" : `Upload ${args.entries.length} files to Media Library`
30372 );
30373 document.body.appendChild(modal);
30374 const draft = args.entries.map((entry) => ({
30375 ...entry.fields
30376 }));
30377 const renderBody = () => {
30378 modal.innerHTML = "";
30379 const list2 = document.createElement("div");
30380 list2.style.cssText = "display:flex;flex-direction:column;gap:18px;max-height:60vh;overflow:auto;padding-right:6px;";
30381 args.entries.forEach((entry, i) => {
30382 list2.appendChild(renderEntry(entry, draft[i], i + 1));
30383 });
30384 modal.appendChild(list2);
30385 const footer = document.createElement("div");
30386 footer.setAttribute("slot", "footer");
30387 footer.style.cssText = "display:flex;gap:8px;justify-content:flex-end;";
30388 const cancel = document.createElement("wpd-button");
30389 cancel.setAttribute("variant", "secondary");
30390 cancel.textContent = "Cancel";
30391 cancel.addEventListener("click", () => {
30392 modal.remove();
30393 });
30394 const upload = document.createElement("wpd-button");
30395 upload.setAttribute("variant", "primary");
30396 upload.textContent = args.entries.length === 1 ? "Upload" : `Upload ${args.entries.length} files`;
30397 upload.addEventListener("click", () => {
30398 void runUploads(upload, cancel);
30399 });
30400 footer.appendChild(cancel);
30401 footer.appendChild(upload);
30402 modal.appendChild(footer);
30403 };
30404 const renderEntry = (entry, fields, index2) => {
30405 const wrap = document.createElement("div");
30406 wrap.style.cssText = "display:flex;flex-direction:column;gap:8px;border:1px solid rgba(255,255,255,0.08);border-radius:10px;padding:14px;";
30407 const heading = document.createElement("div");
30408 heading.style.cssText = "display:flex;gap:10px;align-items:center;font-weight:600;";
30409 const tag = document.createElement("span");
30410 tag.textContent = args.entries.length === 1 ? "" : `#${index2} · `;
30411 tag.style.opacity = "0.6";
30412 const fname = document.createElement("span");
30413 fname.textContent = entry.file.name;
30414 fname.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
30415 const size = document.createElement("span");
30416 size.textContent = `${entry.mime || "unknown"} · ${formatBytes(
30417 entry.file.size
30418 )}`;
30419 size.style.cssText = "opacity:0.6;font-size:12px;";
30420 heading.appendChild(tag);
30421 heading.appendChild(fname);
30422 heading.appendChild(size);
30423 wrap.appendChild(heading);
30424 wrap.appendChild(textField("Title", fields.title, (v) => fields.title = v));
30425 wrap.appendChild(textField("Filename", fields.filename, (v) => fields.filename = v));
30426 if (entry.mime.startsWith("image/")) {
30427 wrap.appendChild(
30428 textField("Alt text", fields.altText, (v) => fields.altText = v)
30429 );
30430 }
30431 wrap.appendChild(textField("Caption", fields.caption, (v) => fields.caption = v));
30432 wrap.appendChild(
30433 textareaField("Description", fields.description, (v) => fields.description = v)
30434 );
30435 return wrap;
30436 };
30437 const runUploads = async (uploadBtn, cancelBtn) => {
30438 uploadBtn.disabled = true;
30439 cancelBtn.disabled = true;
30440 uploadBtn.textContent = "Uploading…";
30441 const total = args.entries.length;
30442 let successes = 0;
30443 let failures = 0;
30444 let cancelled = 0;
30445 const failureDetails = [];
30446 for (let i = 0; i < total; i++) {
30447 const entry = args.entries[i];
30448 try {
30449 await uploadFile({
30450 file: entry.file,
30451 mime: entry.mime,
30452 fields: draft[i],
30453 context: args.context,
30454 mediaUrl: args.mediaUrl,
30455 restNonce: args.restNonce
30456 });
30457 successes++;
30458 } catch (err) {
30459 if (err instanceof UploadCancelledError) {
30460 cancelled++;
30461 continue;
30462 }
30463 if (err instanceof UploadAbortedError) {
30464 cancelled++;
30465 continue;
30466 }
30467 failures++;
30468 const message = err instanceof Error ? err.message : "Upload failed.";
30469 failureDetails.push(`“${entry.file.name}” — ${message}`);
30470 }
30471 }
30472 modal.remove();
30473 showBatchSummaryToast({
30474 total,
30475 successes,
30476 failures,
30477 cancelled,
30478 failureDetails
30479 });
30480 };
30481 renderBody();
30482 await new Promise((resolve2) => {
30483 modal.addEventListener("wpd-modal-cancel", () => {
30484 modal.remove();
30485 resolve2();
30486 });
30487 const observer = new MutationObserver(() => {
30488 if (!modal.isConnected) {
30489 observer.disconnect();
30490 resolve2();
30491 }
30492 });
30493 observer.observe(document.body, { childList: true, subtree: true });
30494 });
30495 }
30496 function textField(label, value, onChange) {
30497 const el = document.createElement("wpd-text-field");
30498 el.setAttribute("label", label);
30499 el.setAttribute("value", value);
30500 el.addEventListener("input", () => {
30501 const v = el.value;
30502 if (typeof v === "string") {
30503 onChange(v);
30504 }
30505 });
30506 return el;
30507 }
30508 function textareaField(label, value, onChange) {
30509 const el = document.createElement("wpd-textarea");
30510 el.setAttribute("label", label);
30511 el.setAttribute("value", value);
30512 el.setAttribute("rows", "3");
30513 el.addEventListener("input", () => {
30514 const v = el.value;
30515 if (typeof v === "string") {
30516 onChange(v);
30517 }
30518 });
30519 return el;
30520 }
30521 function showBatchSummaryToast(args) {
30522 const { total, successes, failures, cancelled, failureDetails } = args;
30523 if (total === 0) {
30524 return;
30525 }
30526 if (total === 1) {
30527 if (successes === 1) {
30528 showToast({ message: "Uploaded to Media Library." });
30529 } else if (failures === 1 && failureDetails[0]) {
30530 showToast({ message: failureDetails[0] });
30531 } else if (cancelled === 1) {
30532 showToast({ message: "Upload cancelled." });
30533 }
30534 return;
30535 }
30536 if (successes === total) {
30537 showToast({
30538 message: `Uploaded ${successes} files to Media Library.`
30539 });
30540 return;
30541 }
30542 if (cancelled === total) {
30543 showToast({ message: "All uploads cancelled." });
30544 return;
30545 }
30546 if (failures === total) {
30547 showToast({
30548 message: failures === 1 && failureDetails[0] ? failureDetails[0] : `${failures} uploads failed.`
30549 });
30550 return;
30551 }
30552 const parts = [];
30553 if (successes > 0) {
30554 parts.push(
30555 `Uploaded ${successes} file${successes === 1 ? "" : "s"}.`
30556 );
30557 }
30558 if (cancelled > 0) {
30559 parts.push(`Cancelled ${cancelled}.`);
30560 }
30561 if (failures > 0) {
30562 parts.push(`Failed ${failures}.`);
30563 }
30564 showToast({ message: parts.join(" ") });
30565 }
30566 const dialog = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
30567 __proto__: null,
30568 openUploadDialog
30569 }, Symbol.toStringTag, { value: "Module" }));
30570 exports.clampGeometryToViewport = clampGeometryToViewport;
30571 Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
30572 return exports;
30573 }({});
30574