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

30,621 lines 999.5 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 once per iframe when the chromeless bridge
104 * script has finished wiring its message listeners. Payload:
105 * `{ windowId: string }`. Subscribers get a reliable "safe to
106 * talk to this iframe" signal — the browser's native `load`
107 * event fires before our bridge attaches, so messages sent on
108 * `load` can be dropped on the floor. Use this instead when
109 * timing matters (first-focus dispatch, auto-fill handshakes).
110 *
111 * @since 0.5.0
112 */
113 IFRAME_READY: "desktop-mode.iframe.ready",
114 /**
115 * Action, fires when a chromeless iframe's `error` or
116 * `unhandledrejection` handler catches an exception. Payload: `{
117 * windowId: string, kind: 'error' | 'unhandledrejection', message:
118 * string, filename: string | null, lineno: number | null, colno:
119 * number | null, stack: string | null }`. Origin-filtered at the
120 * parent shell; cross-origin iframe errors never reach here.
121 */
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.8.6
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.6.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 chrome layer has been mounted /
557 * remounted. Payload: `{ windowId, layer: 'chrome' | 'controls'
558 * | 'slots', chromeId? }` — `chromeId` is present only when
559 * `layer` is `'chrome'`. Subscribers can post-decorate the
560 * chrome (attach observers, anchor pickers).
561 *
562 * @since 0.6.0
563 */
564 WINDOW_CHROME_APPLIED: "desktop-mode.window.chrome.applied",
565 /**
566 * Action, fires after a window's theme tokens are applied to its
567 * outer element. Payload: `{ windowId, themeId, tokens }`. Lets
568 * plugins react to theme changes without diffing CSS variables.
569 *
570 * @since 0.6.0
571 */
572 WINDOW_CHROME_THEME_CHANGED: "desktop-mode.window.chrome.theme-changed",
573 /**
574 * Action, fires when a user clicks a desktop icon (a shortcut
575 * tile registered server-side via `desktop_mode_register_icon()`
576 * and rendered on the wallpaper). Payload: `{ id: string,
577 * target: 'window' | 'url' }`. Fires BEFORE the default open
578 * action — plugins cannot cancel the open from this hook, but
579 * can use it to track click-throughs or augment behaviour (e.g.
580 * play a sound, surface a confirmation toast).
581 *
582 * @since 0.5.0
583 */
584 DESKTOP_ICON_CLICKED: "desktop-mode.desktop-icon.clicked",
585 /**
586 * Action, fires after the wallpaper icon grid is rendered or
587 * re-rendered. Payload:
588 *
589 * {
590 * ids: string[]; // paint order
591 * container: HTMLElement; // <div class="desktop-mode-icons">
592 * tiles: ReadonlyMap<string, HTMLElement>; // id → tile <button>
593 * }
594 *
595 * Plugins that decorate icons with surfaces the framework doesn't
596 * natively expose (drag handles, status dots, cursor adornments)
597 * subscribe here so their decorations survive a live menu refresh
598 * that legitimately rebuilds the grid. The `container` and
599 * `tiles` map mirror the {@link DOCK_AFTER_RENDER}
600 * `tileElements` contract — reach into them directly instead of
601 * re-`querySelector`ing the rendered DOM.
602 *
603 * Notification badges have a first-class API since 0.6.0 —
604 * use `wp.desktop.icons.setBadge( id, count )` (and subscribe
605 * to {@link ICON_BADGE_CHANGED}) instead of decorating from
606 * here. The framework persists badge state across rebuilds, so
607 * a plugin that uses the API doesn't need to re-decorate on
608 * every render.
609 *
610 * Suppressed entirely when the rendered DOM is unchanged from
611 * the previous call (the fingerprint short-circuit upstream
612 * skips both the rebuild and this signal). When the icon list
613 * is empty the hook does not fire at all — the previous
614 * container is removed and no new one is appended.
615 *
616 * @since 0.6.0
617 * @since 0.8.6 — `container` + `tiles` added to the payload
618 * (`ids` retained for back-compat).
619 */
620 DESKTOP_ICONS_RENDERED: "desktop-mode.desktop-icons.rendered",
621 /**
622 * Action, fires whenever the badge count on a desktop icon
623 * changes. Payload: `{ iconId: string, count: number,
624 * previousCount: number }`. Symmetric to {@link DOCK_ITEM_APPENDED}
625 * and the dock/taskbar `wpd-dock-item-badge-changed` CustomEvent
626 * — the icon rail's lifecycle hook for badge transitions.
627 *
628 * Mirrors `desktop-mode/badge-changed` on the activity bus with
629 * `rail: 'icon'`. Subscribe to whichever surface fits — the
630 * activity channel composes across rails for global widgets,
631 * this hook fires only for icon-rail badges with the previous
632 * count carried alongside for delta-aware consumers.
633 *
634 * @since 0.6.0
635 */
636 ICON_BADGE_CHANGED: "desktop-mode.icon.badge-changed",
637 // ------------------------------------------------------------------
638 // Cross-plugin composition.
639 // ------------------------------------------------------------------
640 /**
641 * Action, fires ONCE after every shell-shipped `<wpd-*>` custom
642 * element has registered with `customElements`. Payload: `{
643 * tags: string[] }` — the list of registered tag names. Plugins
644 * that need to defer work until the component registry is
645 * complete (e.g. hydrate user content that uses these tags)
646 * subscribe here instead of polling `customElements.get()`.
647 */
648 COMPONENTS_REGISTERED: "desktop-mode.components.registered",
649 /**
650 * Action, fires after `wp.desktop.registerSystemTile()` inserts
651 * a tile into the unified dock. Payload: `{ id: string }`. Useful
652 * for plugins that want to decorate tiles they didn't register
653 * themselves — analytics, theming, per-tile badges.
654 */
655 DOCK_ITEM_APPENDED: "desktop-mode.dock.item-appended",
656 /**
657 * Action, fires after a system tile is removed from a rail
658 * via `Dock.removeSystemItem()` (typically the server-driven
659 * native-window-sync path on plugin deactivation). Payload:
660 * `{ id: string, placement: 'dock' | 'taskbar' }`. Symmetric
661 * to {@link DOCK_ITEM_APPENDED}; lets analytics / decorators /
662 * cleanup hooks see the full lifecycle without polling the DOM.
663 *
664 * @since 0.6.0
665 */
666 DOCK_ITEM_REMOVED: "desktop-mode.dock.item-removed",
667 // ------------------------------------------------------------------
668 // Dock decoration hooks — render-pipeline filters and actions the
669 // default `Dock` renderer fires while painting tiles. Plugins
670 // compose decoration (animations, classNames, wrappers, tooltips)
671 // without forking the renderer. Custom rail renderers SHOULD fire
672 // the same hooks for ecosystem compatibility — see
673 // `docs/examples/dock-decoration-hooks.md` for the contract.
674 //
675 // Every detail object carries `{ rail, orientation, dockId,
676 // container }` so a single subscriber can disambiguate when two
677 // rails coexist (Classic layout's left side bar + bottom dock).
678 // `dockId` matches the host element's `id` (e.g. `'desktop-mode-dock'`
679 // or `'desktop-mode-side-dock'`) and is the stable
680 // disambiguator — `rail` and `orientation` are convenience
681 // projections of where the renderer is painting.
682 // ------------------------------------------------------------------
683 /**
684 * Action, fires at the start of every dock paint pass — both the
685 * initial mount and every `replaceItems()` that follows on the
686 * live menu-refresh path. Payload `DockRenderContext`. Use this
687 * to invalidate cached per-render decoration state before the
688 * tiles repopulate.
689 *
690 * @since 0.5.2
691 */
692 DOCK_BEFORE_RENDER: "desktop-mode.dock.before-render",
693 /**
694 * Action, fires once every menu and system tile has landed in
695 * the DOM for a paint pass. Payload `DockRenderContext` plus a
696 * frozen `tileElements: ReadonlyMap<string, HTMLElement>` so a
697 * plugin can decorate every tile in one sweep. Symmetric to
698 * {@link DOCK_BEFORE_RENDER}.
699 *
700 * @since 0.5.2
701 */
702 DOCK_AFTER_RENDER: "desktop-mode.dock.after-render",
703 /**
704 * Filter, runs once per tile while the renderer is composing the
705 * className list. Plugins may add, remove, or reorder classes.
706 * Signature: `( classes: string[], detail: DockTileContext ) =>
707 * string[]`. Order is preserved.
708 *
709 * @since 0.5.2
710 */
711 DOCK_TILE_CLASS: "desktop-mode.dock.tile-class",
712 /**
713 * Filter, runs once per tile after the renderer finishes building
714 * the element but before it lands in the DOM. Return the same
715 * element with mutations, or replace with a wrapper — the shell
716 * inserts whatever you return. Signature:
717 * `( el: HTMLElement, detail: DockTileContext ) => HTMLElement`.
718 *
719 * Returning a different node still has to expose a stable
720 * `[data-menu-slug="<id>"]` (or `[data-system-id="<id>"]`)
721 * descendant for active-state / badge updates to find the tile;
722 * wrap, don't replace.
723 *
724 * @since 0.5.2
725 */
726 DOCK_TILE_ELEMENT: "desktop-mode.dock.tile-element",
727 /**
728 * Action, fires once per tile after it has been inserted into
729 * the DOM. Payload `DockTileContext` plus the resolved `el`. Use
730 * for post-insertion decoration where computed layout matters
731 * (measurements, IntersectionObserver bindings, etc.).
732 *
733 * @since 0.5.2
734 */
735 DOCK_TILE_RENDERED: "desktop-mode.dock.tile-rendered",
736 /**
737 * Filter, resolves the tooltip text for a tile. Runs once at
738 * bind time so the dock doesn't re-filter on every pointerenter.
739 * Signature: `( label: string, detail: DockTileContext ) =>
740 * string`. Return an empty string to suppress the tooltip.
741 *
742 * @since 0.5.2
743 */
744 DOCK_TILE_TOOLTIP: "desktop-mode.dock.tile-tooltip",
745 /**
746 * Filter, resolves the body content of a single hover-peek card.
747 * Runs once per card build (i.e., on every show of the peek for
748 * a multi-instance dock tile that has ≥1 open window). Lets a
749 * plugin render a custom thumbnail, status block, or any other
750 * markup inside the card in place of (or alongside) the default
751 * mini-window styling.
752 *
753 * Signature:
754 * ( body: HTMLElement, detail: DockPeekCardContext ) => HTMLElement
755 *
756 * Where `body` is the `<span class="desktop-mode-dock-peek__card-body">`
757 * element that the peek would otherwise populate with ghosted
758 * content lines. The filter may:
759 * - Mutate `body` in place (e.g., append a custom child) and
760 * return it.
761 * - Empty `body` and append plugin-owned children.
762 * - Return an entirely different element to replace `body`.
763 *
764 * `detail.window` is the live `Window` instance the card represents
765 * — plugins can read `window.config`, call `window.getCurrentUrl()`,
766 * subscribe to lifecycle events, etc. `detail.item` is the dock
767 * item descriptor (id / title / icon / url).
768 *
769 * The filter is invoked under the `applyFilters` namespace
770 * `desktop-mode.dock.peek-card-content`.
771 *
772 * @since 0.6.2
773 */
774 DOCK_PEEK_CARD_CONTENT: "desktop-mode.dock.peek-card-content",
775 /**
776 * Filter, runs once per peek card right before it's appended to
777 * the popover. Receives the fully-built default card (with its
778 * mini-window chrome already populated) and can return either
779 * the same node, a mutated version, or an entirely different
780 * element to replace the card outright. Use this when the
781 * `peek-card-content` body filter isn't enough — e.g., when a
782 * plugin wants to swap the whole card chrome (custom titlebar,
783 * different shape) or wrap the card in a third-party component.
784 *
785 * Signature:
786 * ( card: HTMLElement, detail: DockPeekCardContext ) => HTMLElement
787 *
788 * If a plugin returns a brand-new node, it is responsible for
789 * preserving anything the peek relies on:
790 * - The `desktop-mode-dock-peek__card` class (used by the
791 * fan-out animation timing + hover styles).
792 * - A `click` handler if the card should still focus the
793 * window. The default click handler lives on the original
794 * node — replacing the node loses it.
795 *
796 * @since 0.6.2
797 */
798 DOCK_PEEK_CARD_ELEMENT: "desktop-mode.dock.peek-card-element",
799 // ------------------------------------------------------------------
800 // Overview / Arrange lifecycle actions.
801 //
802 // The "Arrange" admin-bar menu drives two layout algorithms —
803 // Cascade (instantly reposition every window in a staggered
804 // stack) and Overview (zoom-out grid view with click-to-focus).
805 // These hooks surface the state transitions so plugins can
806 // instrument analytics, apply custom transitions, override
807 // thumbnail decorations, etc. All actions; a filter for
808 // mutating the overview layout may be added later if plugins
809 // want to reorder or group thumbnails.
810 // ------------------------------------------------------------------
811 /** Action, fires before the overview enter animation starts. */
812 OVERVIEW_ENTERING: "desktop-mode.overview.entering",
813 /** Action, fires once the overview enter animation has completed. */
814 OVERVIEW_ENTERED: "desktop-mode.overview.entered",
815 /**
816 * Action, fires at the start of the overview-exit animation.
817 * Payload: `{ windowId?: string, reason: 'select' | 'cancel' }` —
818 * `windowId` set when the user clicked a thumbnail (reason
819 * 'select'); omitted when the user pressed Escape or clicked
820 * the backdrop (reason 'cancel').
821 */
822 OVERVIEW_EXITING: "desktop-mode.overview.exiting",
823 /** Action, fires once the overview-exit animation has settled. */
824 OVERVIEW_EXITED: "desktop-mode.overview.exited",
825 /** Action, fires when the cursor enters a thumbnail. Payload `{ windowId }`. */
826 OVERVIEW_WINDOW_HOVER: "desktop-mode.overview.window-hover",
827 /** Action, fires when the cursor leaves a thumbnail. Payload `{ windowId }`. */
828 OVERVIEW_WINDOW_UNHOVER: "desktop-mode.overview.window-unhover",
829 /** Action, fires the instant a thumbnail click is registered (before exit + maximize kick in). Payload `{ windowId }`. */
830 OVERVIEW_WINDOW_CLICK: "desktop-mode.overview.window-click",
831 /** Action, fires before cascade computes + applies new positions. Payload `{ windowCount }`. */
832 ARRANGE_CASCADE_STARTING: "desktop-mode.arrange.cascade.starting",
833 /** Action, fires after cascade has positioned every window. Payload `{ windowCount }`. */
834 ARRANGE_CASCADE_APPLIED: "desktop-mode.arrange.cascade.applied",
835 /** Action, fires before tile computes + applies new positions. Payload `{ windowCount, cols, rows }`. */
836 ARRANGE_TILE_STARTING: "desktop-mode.arrange.tile.starting",
837 /** Action, fires after tile has positioned every window. Payload `{ windowCount, cols, rows }`. */
838 ARRANGE_TILE_APPLIED: "desktop-mode.arrange.tile.applied",
839 /**
840 * Filter on the tile-grid dimensions chosen by the built-in
841 * algorithm. Receives `{ cols, rows }` plus a context arg
842 * `{ windowCount, areaWidth, areaHeight }`. Plugins can return
843 * a different `{ cols, rows }` to enforce a custom layout
844 * (fixed-column newsroom, golden-ratio cells, etc.). Returned
845 * values are validated — non-positive integers, or a product
846 * smaller than `windowCount`, fall back to the original.
847 */
848 ARRANGE_TILE_DIMENSIONS: "desktop-mode.arrange.tile.dimensions",
849 /** Action, fires when snap-to-grid is toggled. Payload `{ enabled }`. */
850 ARRANGE_SNAP_CHANGED: "desktop-mode.arrange.snap.changed",
851 /**
852 * Filter on the snap-grid cell size. Receives
853 * `{ cellWidth, cellHeight }` plus a context arg
854 * `{ areaWidth, areaHeight }`. Plugins can return different
855 * dimensions to enforce a Tetris-style fixed grid, a musical
856 * staff aspect, etc. Non-positive returns fall back to the
857 * original.
858 */
859 ARRANGE_SNAP_CELL_SIZE: "desktop-mode.arrange.snap.cell-size",
860 /**
861 * Action, fires when the user clicks a plugin-registered entry in
862 * the Arrange admin-bar submenu (items added via the
863 * `desktop_mode_arrange_menu_items` PHP filter). Payload `{ id }`
864 * where `id` is the item's `id` field as registered. Plugins
865 * subscribe here to run their custom arrangement logic.
866 */
867 ARRANGE_CUSTOM_ACTION: "desktop-mode.arrange.custom-action",
868 // ------------------------------------------------------------------
869 // Snap-zones — Windows-style edge snapping with a split-overview
870 // picker to fill the opposite half after commit.
871 // ------------------------------------------------------------------
872 /**
873 * Action, fires when the drag cursor enters a snap zone and the
874 * shell shows the target-position preview. Payload
875 * `{ windowId, zone: 'left' | 'right' }`.
876 */
877 SNAP_ZONE_PENDING: "desktop-mode.snap.zone-pending",
878 /**
879 * Action, fires when the drag cursor leaves the snap zone without
880 * releasing — the preview disappears. Payload `{ windowId }`.
881 */
882 SNAP_ZONE_CANCELED: "desktop-mode.snap.zone-canceled",
883 /**
884 * Action, fires once the window has animated into its snapped
885 * bounds. Payload `{ windowId, zone: 'left' | 'right' }`.
886 */
887 SNAP_ZONE_COMMITTED: "desktop-mode.snap.zone-committed",
888 /**
889 * Action, fires when a user picks a thumbnail from the split
890 * overview to fill the opposite half. Payload
891 * `{ windowId, zone: 'left' | 'right' }`.
892 */
893 SNAP_SPLIT_FILLED: "desktop-mode.snap.split-filled",
894 // ------------------------------------------------------------------
895 // Widgets — the right-side column. Widgets paint above the
896 // wallpaper but beneath windows. Lifecycle mirrors canvas
897 // wallpapers: register via filter, mount/unmount actions bracket
898 // each paint, mount-failed fires on sync throws / async rejects.
899 // ------------------------------------------------------------------
900 /** Filter, receives the widget registry array. */
901 WIDGETS: "desktop-mode.widgets",
902 /** Action before a widget mounts. Payload `{ id, container, ctx }`. */
903 WIDGET_MOUNTING: "desktop-mode.widget.mounting",
904 /** Action after a widget mounts successfully. Payload `{ id, container, ctx }`. */
905 WIDGET_MOUNTED: "desktop-mode.widget.mounted",
906 /** Action before a widget tears down. Payload `{ id }`. */
907 WIDGET_UNMOUNTING: "desktop-mode.widget.unmounting",
908 /** Action when a widget's mount throws / rejects. Payload `{ id, error }`. */
909 WIDGET_MOUNT_FAILED: "desktop-mode.widget.mount-failed",
910 /** Action when the user adds a widget via the picker. Payload `{ id }`. */
911 WIDGET_ADDED: "desktop-mode.widget.added",
912 /** Action when the user removes a widget via the card's × button. Payload `{ id }`. */
913 WIDGET_REMOVED: "desktop-mode.widget.removed",
914 // ------------------------------------------------------------------
915 // Virtual-desktop ("Spaces") lifecycle actions.
916 //
917 // Spaces let users group windows into separate workspaces and flip
918 // between them from the overview top bar. These hooks expose every
919 // state change so plugins can persist per-space state, sync custom
920 // indicators, or react to the user's workspace context.
921 // ------------------------------------------------------------------
922 /** Action, fires when a new desktop is created. Payload `{ desktopId }`. */
923 DESKTOP_CREATED: "desktop-mode.desktop.created",
924 /** Action, fires when a desktop is closed. Payload `{ desktopId, migratedTo }`. */
925 DESKTOP_CLOSED: "desktop-mode.desktop.closed",
926 /** Action, fires when the active desktop changes. Payload `{ from, to }`. */
927 DESKTOP_SWITCHED: "desktop-mode.desktop.switched",
928 /**
929 * Filter. Returns the id of the "primary" desktop — the one the
930 * shell treats as canonical for batch operations. Receives the
931 * default (first desktop's id) and the full `Desktop[]` list.
932 * @since 0.5.0
933 */
934 PRIMARY_DESKTOP_ID: "desktop-mode.primary-desktop-id",
935 // ------------------------------------------------------------------
936 // Batch window operations.
937 // ------------------------------------------------------------------
938 /**
939 * Action, fires before {@link WindowManager.closeAll} starts
940 * iterating. Payload `{ candidates: Window[] }` — every window the
941 * shell is about to close (after `exceptIds` was applied).
942 * @since 0.5.0
943 */
944 WINDOWS_BEFORE_CLOSE_ALL: "desktop-mode.windows.before-close-all",
945 /**
946 * Filter, runs inside {@link WindowManager.closeAll}. Receives the
947 * candidate `Window[]` list and returns the (possibly trimmed) list
948 * that will actually be closed. Plugins use this to PROTECT specific
949 * windows from a bulk close — e.g. keep the active draft open.
950 * Returning an empty array cancels the close entirely.
951 * @since 0.5.0
952 */
953 WINDOWS_CLOSE_ALL: "desktop-mode.windows.close-all",
954 /**
955 * Action, fires after {@link WindowManager.closeAll} has finished.
956 * Payload `{ closed: number, skipped: Window[] }`.
957 * @since 0.5.0
958 */
959 WINDOWS_AFTER_CLOSE_ALL: "desktop-mode.windows.after-close-all",
960 // ------------------------------------------------------------------
961 // Slash-command lifecycle.
962 // ------------------------------------------------------------------
963 /**
964 * Filter. Runs immediately before a command's `run()` is invoked.
965 * Receives `{ proceed: true, slug, args, command }` and may return
966 * the same shape with `proceed: false` to cancel the run.
967 * @since 0.5.0
968 */
969 COMMAND_BEFORE_RUN: "desktop-mode.command.before-run",
970 /**
971 * Action, fires after a command's `run()` resolves successfully.
972 * Payload `{ slug, args, command, result }`.
973 * @since 0.5.0
974 */
975 COMMAND_AFTER_RUN: "desktop-mode.command.after-run",
976 /**
977 * Action, fires when a command's `run()` throws. Payload
978 * `{ slug, args, command, error }`.
979 * @since 0.5.0
980 */
981 COMMAND_ERROR: "desktop-mode.command.error",
982 // ------------------------------------------------------------------
983 // Shell-level lifecycle actions.
984 // ------------------------------------------------------------------
985 /**
986 * Action, fires (debounced) after the browser viewport stops
987 * resizing. Payload `{ width, height }` describes the shell's
988 * bounding rect — plugins that render canvas-driven UIs hook here
989 * to adjust their render surface.
990 */
991 SHELL_RESIZED: "desktop-mode.shell.resized",
992 /**
993 * Action mirroring `document.visibilitychange` for the shell as a
994 * whole. Payload `{ state: 'visible' | 'hidden' }`. Different from
995 * the wallpaper-specific visibility action in that it fires
996 * regardless of which wallpaper (if any) is active.
997 */
998 SHELL_VISIBILITY: "desktop-mode.shell.visibility",
999 /**
1000 * Action — fires when a `wp.desktop.connect()` connection
1001 * completes its iframe handshake. Payload:
1002 * `{ connectionId, targetWindowId, topics }`.
1003 *
1004 * @since 0.5.2
1005 */
1006 CONNECTION_OPENED: "desktop-mode.connection.opened",
1007 /**
1008 * Action — fires when a connection tears down. Payload:
1009 * `{ connectionId, reason: 'disconnect' | 'window-closed' | 'navigated' }`.
1010 *
1011 * @since 0.5.2
1012 */
1013 CONNECTION_CLOSED: "desktop-mode.connection.closed",
1014 /**
1015 * Action — fires for every message routed through a connection.
1016 * Payload: `{ connectionId, topic, direction: 'in' | 'out' }`.
1017 * Used for debug consoles + traffic auditing; high-volume topics
1018 * fire this many times per second, so subscribers should be
1019 * cheap.
1020 *
1021 * @since 0.5.2
1022 */
1023 CONNECTION_MESSAGE: "desktop-mode.connection.message",
1024 /**
1025 * Filter — fires when an iframe calls
1026 * `wp.desktop.iframe.requestConnection()`. Default value is
1027 * `true` (accept). Return `false` to reject, or an object
1028 * `{ topics: string[] }` to accept while narrowing the topic
1029 * list. `$context` carries `{ windowId, requestId, topics }`.
1030 *
1031 * @since 0.5.2
1032 */
1033 IFRAME_CONNECTION_REQUEST: "desktop-mode.iframe.connection-request",
1034 // ------------------------------------------------------------------
1035 // OS-file drop manager (since 0.30.0). Catches files dragged from
1036 // the user's host OS (Finder / Explorer / Nautilus) onto any
1037 // desktop-mode surface and routes them through a confirmation
1038 // dialog before uploading to the Media Library. Authoritative
1039 // constants live in `src/os-file-drop/hooks.ts`; mirrored here so
1040 // every hook the shell fires is reachable from a single `HOOKS`
1041 // import. See `docs/examples/os-file-drop.md`.
1042 // ------------------------------------------------------------------
1043 /** Filter — `(files: File[], ctx) => File[]`, before mime/size check. */
1044 FILE_DROP_FILES_DETECTED: "desktop-mode.drop.files-detected",
1045 /** Action — `{ rejections, context }` for files that failed policy. */
1046 FILE_DROP_FILES_REJECTED: "desktop-mode.drop.files-rejected",
1047 /** Filter — `(entry, ctx) => entry`, per-file dialog defaults. */
1048 FILE_DROP_DIALOG_FIELDS: "desktop-mode.drop.dialog-fields",
1049 /** Filter — `(payload, ctx) => payload | null`, last call before POST. */
1050 FILE_DROP_BEFORE_UPLOAD: "desktop-mode.drop.before-upload",
1051 /** Action — `{ file, fields, context, abort }` once XHR is open and about to send. @since 0.31.0 */
1052 FILE_DROP_UPLOAD_STARTED: "desktop-mode.drop.upload-started",
1053 /** Action — `{ file, fields, context, loaded, total, indeterminate }` per progress tick. @since 0.31.0 */
1054 FILE_DROP_UPLOAD_PROGRESS: "desktop-mode.drop.upload-progress",
1055 /** Action — `{ file, result, fields, context }` after successful upload. `file` since 0.31.0. */
1056 FILE_DROP_AFTER_UPLOAD: "desktop-mode.drop.after-upload",
1057 /** Action — `{ file, error, context }` on upload failure. */
1058 FILE_DROP_UPLOAD_FAILED: "desktop-mode.drop.upload-failed"
1059 };
1060 let _whenReadySeq = 0;
1061 function whenReady(cb) {
1062 if (didAction(HOOKS.INIT) > 0) {
1063 Promise.resolve().then(cb);
1064 return;
1065 }
1066 const ns = `desktop-mode/when-ready-${++_whenReadySeq}`;
1067 addAction(HOOKS.INIT, ns, cb);
1068 }
1069 function isReady() {
1070 return didAction(HOOKS.INIT) > 0;
1071 }
1072 let inflight$1 = null;
1073 function isLoaded$1() {
1074 return !!window.desktopModeWindowSystem;
1075 }
1076 function injectScript$1(scriptUrl) {
1077 return new Promise((resolve2, reject) => {
1078 const existing = document.querySelector(
1079 'script[data-desktop-mode-window-system="1"]'
1080 );
1081 const finish = () => {
1082 if (isLoaded$1()) {
1083 resolve2();
1084 return;
1085 }
1086 reject(
1087 new Error(
1088 "[desktop-mode] window-system bundle loaded but did not register `window.desktopModeWindowSystem`."
1089 )
1090 );
1091 };
1092 if (existing) {
1093 if (isLoaded$1()) {
1094 finish();
1095 } else {
1096 existing.addEventListener("load", finish);
1097 existing.addEventListener(
1098 "error",
1099 () => reject(new Error("failed to load window-system bundle"))
1100 );
1101 }
1102 return;
1103 }
1104 const s = document.createElement("script");
1105 s.src = scriptUrl;
1106 s.async = true;
1107 s.dataset.desktopModeWindowSystem = "1";
1108 s.addEventListener("load", finish);
1109 s.addEventListener(
1110 "error",
1111 () => reject(new Error("failed to load window-system bundle"))
1112 );
1113 document.head.appendChild(s);
1114 });
1115 }
1116 function windowSystemBundleUrl() {
1117 const cfg = window.desktopModeConfig;
1118 return cfg?.windowSystemBundleUrl ?? "";
1119 }
1120 function preloadWindowSystem(scriptUrl) {
1121 if (!scriptUrl || isLoaded$1() || inflight$1) {
1122 return;
1123 }
1124 inflight$1 = injectScript$1(scriptUrl).catch((err) => {
1125 inflight$1 = null;
1126 if (typeof console !== "undefined") {
1127 console.warn(
1128 "[desktop-mode] window-system preload failed; will retry on first open():",
1129 err
1130 );
1131 }
1132 });
1133 }
1134 async function ensureWindowSystemLoaded(scriptUrl) {
1135 if (isLoaded$1()) {
1136 return window.desktopModeWindowSystem;
1137 }
1138 if (!scriptUrl) {
1139 const fn = window.desktopModeWindowSystem;
1140 if (fn) {
1141 return fn;
1142 }
1143 throw new Error(
1144 "[desktop-mode] ensureWindowSystemLoaded(): no bundle URL configured and `window.desktopModeWindowSystem` is not pre-registered."
1145 );
1146 }
1147 if (!inflight$1) {
1148 inflight$1 = injectScript$1(scriptUrl);
1149 }
1150 await inflight$1;
1151 return window.desktopModeWindowSystem;
1152 }
1153 const CANARY_TAG = "wpd-confirm-dialog";
1154 let inflight = null;
1155 function isLoaded() {
1156 return typeof window.customElements !== "undefined" && !!window.customElements.get(CANARY_TAG);
1157 }
1158 function injectScript(scriptUrl) {
1159 return new Promise((resolve2, reject) => {
1160 const existing = document.querySelector(
1161 'script[data-desktop-mode-shell-overlays="1"]'
1162 );
1163 const finish = () => {
1164 if (isLoaded()) {
1165 resolve2();
1166 return;
1167 }
1168 reject(
1169 new Error(
1170 "[desktop-mode] shell-overlays bundle loaded but did not register the overlay components."
1171 )
1172 );
1173 };
1174 if (existing) {
1175 if (isLoaded()) {
1176 finish();
1177 } else {
1178 existing.addEventListener("load", finish);
1179 existing.addEventListener(
1180 "error",
1181 () => reject(new Error("failed to load shell-overlays bundle"))
1182 );
1183 }
1184 return;
1185 }
1186 const s = document.createElement("script");
1187 s.src = scriptUrl;
1188 s.async = true;
1189 s.dataset.desktopModeShellOverlays = "1";
1190 s.addEventListener("load", finish);
1191 s.addEventListener(
1192 "error",
1193 () => reject(new Error("failed to load shell-overlays bundle"))
1194 );
1195 document.head.appendChild(s);
1196 });
1197 }
1198 function preloadShellOverlays(scriptUrl) {
1199 if (!scriptUrl || isLoaded() || inflight) {
1200 return;
1201 }
1202 inflight = injectScript(scriptUrl).catch((err) => {
1203 inflight = null;
1204 if (typeof console !== "undefined") {
1205 console.warn(
1206 "[desktop-mode] shell-overlays preload failed; will retry on first overlay use:",
1207 err
1208 );
1209 }
1210 });
1211 }
1212 function ensureShellOverlaysLoaded(scriptUrl) {
1213 if (isLoaded()) {
1214 return Promise.resolve();
1215 }
1216 if (!scriptUrl) {
1217 return Promise.resolve();
1218 }
1219 if (!inflight) {
1220 inflight = injectScript(scriptUrl);
1221 }
1222 return inflight;
1223 }
1224 function shellOverlaysBundleUrl() {
1225 const cfg = window.desktopModeConfig;
1226 return cfg?.shellOverlaysBundleUrl ?? "";
1227 }
1228 function openWithShellOverlays(isStillCurrent, fn) {
1229 const url = shellOverlaysBundleUrl();
1230 if (isLoaded() || !url) {
1231 fn();
1232 return;
1233 }
1234 void ensureShellOverlaysLoaded(url).then(() => {
1235 if (!isStillCurrent()) {
1236 return;
1237 }
1238 fn();
1239 }).catch((err) => {
1240 if (typeof console !== "undefined") {
1241 console.warn(
1242 "[desktop-mode] shell-overlays failed to load; menu/dialog suppressed:",
1243 err
1244 );
1245 }
1246 });
1247 }
1248 const TEXT_DOMAIN = "desktop-mode";
1249 function i18n() {
1250 return window.wp?.i18n;
1251 }
1252 function __(text, domain = TEXT_DOMAIN) {
1253 return i18n()?.__(text, domain) ?? text;
1254 }
1255 function _n(single, plural, number, domain = TEXT_DOMAIN) {
1256 return i18n()?._n(single, plural, number, domain) ?? (number === 1 ? single : plural);
1257 }
1258 function sprintf(format, ...args) {
1259 const impl = i18n()?.sprintf;
1260 if (impl) {
1261 return impl(format, ...args);
1262 }
1263 let i = 0;
1264 return format.replace(/%[sd]/g, () => String(args[i++] ?? ""));
1265 }
1266 function isValidGrid(candidate, windowCount) {
1267 if (!candidate || typeof candidate !== "object") {
1268 return false;
1269 }
1270 const c = candidate.cols;
1271 const r = candidate.rows;
1272 if (typeof c !== "number" || typeof r !== "number") {
1273 return false;
1274 }
1275 if (!Number.isFinite(c) || !Number.isFinite(r)) {
1276 return false;
1277 }
1278 if (c < 1 || r < 1) {
1279 return false;
1280 }
1281 return Math.floor(c) * Math.floor(r) >= windowCount;
1282 }
1283 function isValidCellSize(candidate) {
1284 if (!candidate || typeof candidate !== "object") {
1285 return false;
1286 }
1287 const w = candidate.cellWidth;
1288 const h = candidate.cellHeight;
1289 if (typeof w !== "number" || typeof h !== "number") {
1290 return false;
1291 }
1292 if (!Number.isFinite(w) || !Number.isFinite(h)) {
1293 return false;
1294 }
1295 return w > 0 && h > 0;
1296 }
1297 function pickGridDimensions(n, width, height) {
1298 if (n <= 1) {
1299 return { cols: 1, rows: 1 };
1300 }
1301 const areaAspect = width / Math.max(1, height);
1302 const max = 6;
1303 let best = { cols: n, rows: 1, score: Infinity };
1304 for (let cols = 1; cols <= Math.min(max, n); cols++) {
1305 const rows = Math.min(max, Math.ceil(n / cols));
1306 if (cols * rows < n) {
1307 continue;
1308 }
1309 const cellAspect = width / cols / Math.max(1, height / rows);
1310 const aspectDelta = Math.abs(cellAspect - areaAspect);
1311 const emptyCells = cols * rows - n;
1312 const score = aspectDelta + emptyCells * 0.05;
1313 if (score < best.score) {
1314 best = { cols, rows, score };
1315 }
1316 }
1317 return { cols: best.cols, rows: best.rows };
1318 }
1319 function computeOverviewLayout(windows, rect, topInset = 0) {
1320 const n = windows.length;
1321 if (n === 0) {
1322 return [];
1323 }
1324 const cols = Math.ceil(Math.sqrt(n));
1325 const rows = Math.ceil(n / cols);
1326 const padding = 40;
1327 const gap = 24;
1328 const labelReserve = 34;
1329 const cellWidth = (rect.width - padding * 2 - gap * (cols - 1)) / cols;
1330 const cellHeight = (rect.height - padding * 2 - topInset - gap * (rows - 1)) / rows;
1331 const thumbCellHeight = Math.max(40, cellHeight - labelReserve);
1332 return windows.map((win, i) => {
1333 const col = i % cols;
1334 const row = Math.floor(i / cols);
1335 const cellX = rect.left + padding + col * (cellWidth + gap);
1336 const cellY = rect.top + topInset + padding + row * (cellHeight + gap) + labelReserve;
1337 const sourceW = win.element.offsetWidth;
1338 const sourceH = win.element.offsetHeight;
1339 const scale = Math.min(
1340 cellWidth / sourceW,
1341 thumbCellHeight / sourceH
1342 );
1343 const scaledW = sourceW * scale;
1344 const scaledH = sourceH * scale;
1345 return {
1346 win,
1347 x: cellX + (cellWidth - scaledW) / 2,
1348 y: cellY + (thumbCellHeight - scaledH) / 2,
1349 scale
1350 };
1351 });
1352 }
1353 const OVERVIEW_TOP_BAR_RESERVE = 120;
1354 function enterOverview(mgr) {
1355 if (mgr._overviewActive) {
1356 return;
1357 }
1358 const onActive = mgr._stack.filter(
1359 (w) => w.config.desktopId === mgr._activeDesktopId
1360 );
1361 if (onActive.length > 0 && onActive.every((w) => w.state === "minimized")) {
1362 for (const w of onActive) {
1363 try {
1364 w.restore();
1365 } catch (err) {
1366 if (typeof console !== "undefined") {
1367 console.error(
1368 "[desktop-mode] enterOverview: window.restore() threw for",
1369 w.id,
1370 err
1371 );
1372 }
1373 }
1374 }
1375 }
1376 const eligible = mgr._stack.filter(
1377 (w) => w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
1378 );
1379 mgr._overviewActive = true;
1380 doAction(HOOKS.OVERVIEW_ENTERING, {});
1381 mgr._overviewSnapshot.clear();
1382 for (const w of eligible) {
1383 mgr._overviewSnapshot.set(w.id, {
1384 transform: w.element.style.transform || "",
1385 transition: w.element.style.transition || ""
1386 });
1387 }
1388 for (const w of eligible) {
1389 if (w.state === "fullscreen") {
1390 w.toggleFullscreen();
1391 }
1392 }
1393 const currentRect = mgr._desktop.getBoundingClientRect();
1394 const docks = Array.from(
1395 document.querySelectorAll(".desktop-mode-dock")
1396 );
1397 let reclaimedWidth = 0;
1398 for (const d of docks) {
1399 const r = d.getBoundingClientRect();
1400 const verticallyOverlaps = r.bottom > currentRect.top && r.top < currentRect.bottom;
1401 const isHorizontalRail = r.height > r.width;
1402 if (verticallyOverlaps && isHorizontalRail) {
1403 reclaimedWidth += r.width;
1404 }
1405 }
1406 const targetRect = new DOMRect(
1407 0,
1408 0,
1409 currentRect.width + reclaimedWidth,
1410 currentRect.height
1411 );
1412 mgr._desktop.classList.add("desktop-mode-area--overview");
1413 const shell = document.getElementById("desktop-mode-shell");
1414 shell?.classList.add("desktop-mode-shell--overview");
1415 mgr._overviewTopBar = buildOverviewTopBar(mgr);
1416 mgr._desktop.appendChild(mgr._overviewTopBar);
1417 const layout = computeOverviewLayout(
1418 eligible,
1419 targetRect,
1420 OVERVIEW_TOP_BAR_RESERVE
1421 );
1422 mgr._overviewLabels.clear();
1423 for (const item of layout) {
1424 const el = item.win.element;
1425 el.classList.add("desktop-mode-window--overview");
1426 const dx = item.x - el.offsetLeft;
1427 const dy = item.y - el.offsetTop;
1428 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
1429 const label = createOverviewLabel(item);
1430 el.insertAdjacentElement("afterend", label);
1431 mgr._overviewLabels.set(item.win.id, label);
1432 }
1433 const pressTargetForEvent = (e) => {
1434 const target2 = e.target;
1435 const winEl = target2?.closest(
1436 ".desktop-mode-window--overview"
1437 );
1438 if (winEl) {
1439 return {
1440 id: winEl.id.replace(/^wp-window-/, ""),
1441 element: winEl
1442 };
1443 }
1444 if (target2 === mgr._desktop) {
1445 return { id: "backdrop", element: mgr._desktop };
1446 }
1447 return null;
1448 };
1449 mgr._overviewPointerDownHandler = (e) => {
1450 if (e.button !== 0) {
1451 mgr._overviewPressTarget = null;
1452 return;
1453 }
1454 mgr._overviewPressTarget = pressTargetForEvent(e);
1455 if (mgr._overviewPressTarget) {
1456 e.preventDefault();
1457 e.stopPropagation();
1458 }
1459 };
1460 mgr._overviewPointerUpHandler = (e) => {
1461 if (e.button !== 0) {
1462 return;
1463 }
1464 const pressed = mgr._overviewPressTarget;
1465 mgr._overviewPressTarget = null;
1466 if (!pressed) {
1467 return;
1468 }
1469 const rect = pressed.element.getBoundingClientRect();
1470 const inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
1471 if (!inside) {
1472 return;
1473 }
1474 e.preventDefault();
1475 e.stopPropagation();
1476 if (pressed.id === "backdrop") {
1477 exitOverview(mgr);
1478 return;
1479 }
1480 const selected = mgr.getById(pressed.id);
1481 doAction(HOOKS.OVERVIEW_WINDOW_CLICK, { windowId: pressed.id });
1482 exitOverview(mgr, selected, true);
1483 };
1484 mgr._overviewKeyHandler = (e) => {
1485 if (e.key === "Escape") {
1486 exitOverview(mgr);
1487 return;
1488 }
1489 if (e.key === "Enter") {
1490 e.preventDefault();
1491 if (mgr._overviewAddTileFocused) {
1492 commitAddTile(mgr);
1493 return;
1494 }
1495 exitOverview(mgr);
1496 }
1497 };
1498 mgr._desktop.addEventListener(
1499 "pointerdown",
1500 mgr._overviewPointerDownHandler,
1501 true
1502 );
1503 mgr._desktop.addEventListener(
1504 "pointerup",
1505 mgr._overviewPointerUpHandler,
1506 true
1507 );
1508 mgr._overviewClickBlocker = (e) => {
1509 const target2 = e.target;
1510 if (target2?.closest(".desktop-mode-overview-top-bar")) {
1511 return;
1512 }
1513 e.stopPropagation();
1514 e.preventDefault();
1515 };
1516 mgr._desktop.addEventListener(
1517 "click",
1518 mgr._overviewClickBlocker,
1519 true
1520 );
1521 document.addEventListener("keydown", mgr._overviewKeyHandler);
1522 mgr._lastOverviewHoverId = null;
1523 mgr._overviewMouseHandler = (e) => {
1524 const target2 = e.target;
1525 const winEl = target2?.closest(
1526 ".desktop-mode-window--overview"
1527 );
1528 const newId = winEl ? winEl.id.replace(/^wp-window-/, "") : null;
1529 if (newId === mgr._lastOverviewHoverId) {
1530 return;
1531 }
1532 if (mgr._lastOverviewHoverId) {
1533 doAction(HOOKS.OVERVIEW_WINDOW_UNHOVER, {
1534 windowId: mgr._lastOverviewHoverId
1535 });
1536 }
1537 if (newId) {
1538 doAction(HOOKS.OVERVIEW_WINDOW_HOVER, { windowId: newId });
1539 }
1540 mgr._lastOverviewHoverId = newId;
1541 };
1542 mgr._desktop.addEventListener("mouseover", mgr._overviewMouseHandler);
1543 window.setTimeout(() => {
1544 if (mgr._overviewActive) {
1545 doAction(HOOKS.OVERVIEW_ENTERED, {});
1546 }
1547 }, 300);
1548 }
1549 function buildOverviewTopBar(mgr) {
1550 const bar = document.createElement("div");
1551 bar.className = "desktop-mode-overview-top-bar";
1552 const list2 = document.createElement("div");
1553 list2.className = "desktop-mode-overview-top-bar__list";
1554 bar.appendChild(list2);
1555 for (const d of mgr._desktops) {
1556 list2.appendChild(buildDesktopTile(mgr, d));
1557 }
1558 const addTile = document.createElement("button");
1559 addTile.type = "button";
1560 addTile.className = "desktop-mode-overview-top-bar__tile desktop-mode-overview-top-bar__tile--add";
1561 if (mgr._overviewAddTileFocused) {
1562 addTile.classList.add(
1563 "desktop-mode-overview-top-bar__tile--cursor"
1564 );
1565 }
1566 addTile.setAttribute("aria-label", __("Add new desktop"));
1567 addTile.innerHTML = '<span class="desktop-mode-overview-top-bar__tile-plus" aria-hidden="true">+</span>';
1568 addTile.addEventListener("click", (e) => {
1569 e.preventDefault();
1570 e.stopPropagation();
1571 commitAddTile(mgr);
1572 });
1573 list2.appendChild(addTile);
1574 return bar;
1575 }
1576 function commitAddTile(mgr) {
1577 const created = createDesktop(mgr);
1578 mgr._overviewAddTileFocused = false;
1579 exitOverviewToDesktop(mgr, created.id);
1580 }
1581 function buildDesktopTile(mgr, d) {
1582 const tile2 = document.createElement("button");
1583 tile2.type = "button";
1584 tile2.className = "desktop-mode-overview-top-bar__tile";
1585 tile2.dataset.desktopId = d.id;
1586 if (d.id === mgr._activeDesktopId && !mgr._overviewAddTileFocused) {
1587 tile2.classList.add("desktop-mode-overview-top-bar__tile--active");
1588 }
1589 tile2.setAttribute("aria-label", sprintf(__("Switch to %s"), d.label));
1590 const preview = document.createElement("span");
1591 preview.className = "desktop-mode-overview-top-bar__tile-preview";
1592 const count = mgr._stack.filter(
1593 (w) => w.config.desktopId === d.id
1594 ).length;
1595 if (count > 0) {
1596 const badge = document.createElement("span");
1597 badge.className = "desktop-mode-overview-top-bar__tile-count";
1598 badge.textContent = String(count);
1599 preview.appendChild(badge);
1600 }
1601 tile2.appendChild(preview);
1602 const label = document.createElement("span");
1603 label.className = "desktop-mode-overview-top-bar__tile-label";
1604 label.textContent = d.label;
1605 tile2.appendChild(label);
1606 const closeBtn = document.createElement("span");
1607 closeBtn.className = "desktop-mode-overview-top-bar__tile-close";
1608 closeBtn.setAttribute("role", "button");
1609 closeBtn.setAttribute("tabindex", "0");
1610 closeBtn.setAttribute("aria-label", sprintf(__("Close %s"), d.label));
1611 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>';
1612 closeBtn.addEventListener("click", (e) => {
1613 e.preventDefault();
1614 e.stopPropagation();
1615 closeDesktop(mgr, d.id);
1616 refreshOverviewTopBar(mgr);
1617 });
1618 tile2.appendChild(closeBtn);
1619 tile2.addEventListener("click", (e) => {
1620 e.preventDefault();
1621 e.stopPropagation();
1622 exitOverviewToDesktop(mgr, d.id);
1623 });
1624 return tile2;
1625 }
1626 function refreshOverviewTopBar(mgr) {
1627 if (!mgr._overviewTopBar) {
1628 return;
1629 }
1630 const fresh = buildOverviewTopBar(mgr);
1631 mgr._overviewTopBar.replaceWith(fresh);
1632 mgr._overviewTopBar = fresh;
1633 }
1634 function exitOverviewToDesktop(mgr, desktopId) {
1635 switchDesktop(mgr, desktopId);
1636 exitOverview(mgr);
1637 }
1638 function createOverviewLabel(item) {
1639 const label = document.createElement("div");
1640 label.className = "desktop-mode-overview-label";
1641 label.dataset.windowId = item.win.id;
1642 const thumbW = item.win.element.offsetWidth * item.scale;
1643 label.style.left = `${item.x}px`;
1644 label.style.top = `${item.y - 34}px`;
1645 label.style.width = `${thumbW}px`;
1646 const iconClass = item.win.config.icon || "dashicons-admin-generic";
1647 const icon = document.createElement("span");
1648 icon.className = `desktop-mode-overview-label__icon dashicons ${iconClass}`;
1649 icon.setAttribute("aria-hidden", "true");
1650 label.appendChild(icon);
1651 const title = document.createElement("span");
1652 title.className = "desktop-mode-overview-label__title";
1653 title.textContent = item.win.config.title;
1654 label.appendChild(title);
1655 const tabCount = item.win.getExternalTabCount();
1656 if (tabCount > 0) {
1657 const meta = document.createElement("span");
1658 meta.className = "desktop-mode-overview-label__meta";
1659 meta.textContent = sprintf(
1660 // translators: %d is the number of external sub-tabs open on this window.
1661 _n("· %d open tab", "· %d open tabs", tabCount),
1662 tabCount
1663 );
1664 label.appendChild(meta);
1665 }
1666 return label;
1667 }
1668 function exitOverview(mgr, selected, maximize = false) {
1669 if (!mgr._overviewActive) {
1670 return;
1671 }
1672 mgr._overviewActive = false;
1673 mgr._overviewAddTileFocused = false;
1674 doAction(HOOKS.OVERVIEW_EXITING, {
1675 windowId: selected && maximize ? selected.id : void 0,
1676 reason: selected && maximize ? "select" : "cancel"
1677 });
1678 mgr._desktop.classList.remove("desktop-mode-area--overview");
1679 const shell = document.getElementById("desktop-mode-shell");
1680 shell?.classList.remove("desktop-mode-shell--overview");
1681 for (const [id, snap] of mgr._overviewSnapshot) {
1682 const w = mgr.getById(id);
1683 if (!w) {
1684 continue;
1685 }
1686 w.element.style.transform = snap.transform;
1687 }
1688 if (selected && maximize) {
1689 mgr.focus(selected);
1690 selected.maximize();
1691 }
1692 for (const label of mgr._overviewLabels.values()) {
1693 label.classList.add("desktop-mode-overview-label--out");
1694 }
1695 if (mgr._overviewTopBar) {
1696 mgr._overviewTopBar.classList.add(
1697 "desktop-mode-overview-top-bar--out"
1698 );
1699 }
1700 const ANIMATION_MS = 280;
1701 window.setTimeout(() => {
1702 for (const w of mgr._stack) {
1703 w.element.classList.remove("desktop-mode-window--overview");
1704 }
1705 for (const label of mgr._overviewLabels.values()) {
1706 label.remove();
1707 }
1708 mgr._overviewLabels.clear();
1709 mgr._overviewSnapshot.clear();
1710 if (mgr._overviewTopBar) {
1711 mgr._overviewTopBar.remove();
1712 mgr._overviewTopBar = null;
1713 }
1714 if (mgr._overviewClickBlocker) {
1715 mgr._desktop.removeEventListener(
1716 "click",
1717 mgr._overviewClickBlocker,
1718 true
1719 );
1720 mgr._overviewClickBlocker = null;
1721 }
1722 doAction(HOOKS.OVERVIEW_EXITED, {
1723 windowId: selected && maximize ? selected.id : void 0,
1724 reason: selected && maximize ? "select" : "cancel"
1725 });
1726 }, ANIMATION_MS);
1727 if (mgr._overviewPointerDownHandler) {
1728 mgr._desktop.removeEventListener(
1729 "pointerdown",
1730 mgr._overviewPointerDownHandler,
1731 true
1732 );
1733 mgr._overviewPointerDownHandler = null;
1734 }
1735 if (mgr._overviewPointerUpHandler) {
1736 mgr._desktop.removeEventListener(
1737 "pointerup",
1738 mgr._overviewPointerUpHandler,
1739 true
1740 );
1741 mgr._overviewPointerUpHandler = null;
1742 }
1743 mgr._overviewPressTarget = null;
1744 if (mgr._overviewKeyHandler) {
1745 document.removeEventListener("keydown", mgr._overviewKeyHandler);
1746 mgr._overviewKeyHandler = null;
1747 }
1748 if (mgr._overviewMouseHandler) {
1749 mgr._desktop.removeEventListener(
1750 "mouseover",
1751 mgr._overviewMouseHandler
1752 );
1753 mgr._overviewMouseHandler = null;
1754 }
1755 if (mgr._lastOverviewHoverId) {
1756 doAction(HOOKS.OVERVIEW_WINDOW_UNHOVER, {
1757 windowId: mgr._lastOverviewHoverId
1758 });
1759 mgr._lastOverviewHoverId = null;
1760 }
1761 }
1762 function getDesktops(mgr) {
1763 return [...mgr._desktops];
1764 }
1765 function getActiveDesktop(mgr) {
1766 const found = mgr._desktops.find((d) => d.id === mgr._activeDesktopId);
1767 return found ?? mgr._desktops[0];
1768 }
1769 function getActiveDesktopId(mgr) {
1770 return getActiveDesktop(mgr).id;
1771 }
1772 function applyDesktopVisibility(mgr, win) {
1773 const visible = win.config.desktopId === mgr._activeDesktopId;
1774 win.element.style.display = visible ? "" : "none";
1775 }
1776 function refreshDesktopVisibility(mgr) {
1777 for (const w of mgr._stack) {
1778 applyDesktopVisibility(mgr, w);
1779 }
1780 }
1781 function createDesktop(mgr) {
1782 mgr._desktopSeq++;
1783 const desktop = {
1784 id: `desktop-${mgr._desktopSeq}`,
1785 // translators: %d is the desktop number (e.g., "Desktop 2")
1786 label: sprintf(__("Desktop %d"), mgr._desktopSeq)
1787 };
1788 mgr._desktops.push(desktop);
1789 doAction(HOOKS.DESKTOP_CREATED, { desktopId: desktop.id });
1790 return desktop;
1791 }
1792 function switchDesktop(mgr, id, opts) {
1793 if (id === mgr._activeDesktopId) {
1794 return;
1795 }
1796 if (!mgr._desktops.some((d) => d.id === id)) {
1797 return;
1798 }
1799 const previousId = mgr._activeDesktopId;
1800 mgr._activeDesktopId = id;
1801 if (mgr._overviewActive) {
1802 relayoutOverviewForActiveDesktop(mgr);
1803 refreshOverviewTopBar(mgr);
1804 } else {
1805 refreshDesktopVisibility(mgr);
1806 if (opts?.direction) {
1807 animateDesktopSwitch(mgr, opts.direction);
1808 }
1809 const topOnNew = [...mgr._stack].reverse().find(
1810 (w) => w.config.desktopId === id && w.state !== "minimized"
1811 );
1812 if (topOnNew) {
1813 mgr.focus(topOnNew);
1814 }
1815 }
1816 doAction(HOOKS.DESKTOP_SWITCHED, {
1817 from: previousId,
1818 to: id
1819 });
1820 }
1821 function animateDesktopSwitch(mgr, direction) {
1822 const el = mgr._desktop;
1823 const cls = direction === "next" ? "desktop-mode-area--sliding-from-right" : "desktop-mode-area--sliding-from-left";
1824 el.classList.remove(
1825 "desktop-mode-area--sliding-from-right",
1826 "desktop-mode-area--sliding-from-left"
1827 );
1828 void el.offsetWidth;
1829 el.classList.add(cls);
1830 const onEnd = (e) => {
1831 if (!e.animationName.startsWith("desktop-mode-area-slide-from-")) {
1832 return;
1833 }
1834 el.classList.remove(cls);
1835 el.removeEventListener("animationend", onEnd);
1836 };
1837 el.addEventListener("animationend", onEnd);
1838 }
1839 function closeDesktop(mgr, id) {
1840 if (mgr._desktops.length <= 1) {
1841 return;
1842 }
1843 const idx = mgr._desktops.findIndex((d) => d.id === id);
1844 if (idx === -1) {
1845 return;
1846 }
1847 const survivorIdx = idx > 0 ? idx - 1 : 1;
1848 const survivor = mgr._desktops[survivorIdx];
1849 for (const w of mgr._stack) {
1850 if (w.config.desktopId === id) {
1851 w.config.desktopId = survivor.id;
1852 }
1853 }
1854 mgr._desktops.splice(idx, 1);
1855 const wasActive = mgr._activeDesktopId === id;
1856 if (wasActive) {
1857 mgr._activeDesktopId = survivor.id;
1858 }
1859 if (mgr._overviewActive) {
1860 relayoutOverviewForActiveDesktop(mgr);
1861 } else {
1862 refreshDesktopVisibility(mgr);
1863 }
1864 doAction(HOOKS.DESKTOP_CLOSED, {
1865 desktopId: id,
1866 migratedTo: survivor.id
1867 });
1868 }
1869 function relayoutOverviewForActiveDesktop(mgr) {
1870 for (const [winId, snap] of mgr._overviewSnapshot) {
1871 const w = mgr.getById(winId);
1872 if (w) {
1873 w.element.style.transform = snap.transform;
1874 w.element.style.transition = snap.transition;
1875 w.element.classList.remove("desktop-mode-window--overview");
1876 }
1877 }
1878 for (const label of mgr._overviewLabels.values()) {
1879 label.remove();
1880 }
1881 mgr._overviewLabels.clear();
1882 mgr._overviewSnapshot.clear();
1883 refreshDesktopVisibility(mgr);
1884 const eligible = mgr._stack.filter(
1885 (w) => w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
1886 );
1887 if (eligible.length === 0) {
1888 return;
1889 }
1890 for (const w of eligible) {
1891 mgr._overviewSnapshot.set(w.id, {
1892 transform: w.element.style.transform || "",
1893 transition: w.element.style.transition || ""
1894 });
1895 }
1896 const live = mgr._desktop.getBoundingClientRect();
1897 const targetRect = new DOMRect(0, 0, live.width, live.height);
1898 const layout = computeOverviewLayout(
1899 eligible,
1900 targetRect,
1901 OVERVIEW_TOP_BAR_RESERVE
1902 );
1903 for (const item of layout) {
1904 const el = item.win.element;
1905 el.classList.add("desktop-mode-window--overview");
1906 const dx = item.x - el.offsetLeft;
1907 const dy = item.y - el.offsetTop;
1908 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
1909 const label = createOverviewLabel(item);
1910 el.insertAdjacentElement("afterend", label);
1911 mgr._overviewLabels.set(item.win.id, label);
1912 }
1913 }
1914 function seedDesktops(mgr, desktops, activeDesktopId) {
1915 if (desktops.length === 0) {
1916 return;
1917 }
1918 mgr._desktops = desktops.map((d) => ({ ...d }));
1919 mgr._activeDesktopId = desktops.some((d) => d.id === activeDesktopId) ? activeDesktopId : desktops[0].id;
1920 let highest = 0;
1921 for (const d of desktops) {
1922 const match = d.id.match(/^desktop-(\d+)$/);
1923 if (match) {
1924 const n = parseInt(match[1], 10);
1925 if (Number.isFinite(n) && n > highest) {
1926 highest = n;
1927 }
1928 }
1929 }
1930 mgr._desktopSeq = Math.max(mgr._desktopSeq, highest);
1931 }
1932 function cascade(mgr) {
1933 const eligible = mgr._stack.filter(
1934 (w) => w.config.desktopId === mgr._activeDesktopId
1935 );
1936 if (eligible.length === 0) {
1937 return;
1938 }
1939 doAction(HOOKS.ARRANGE_CASCADE_STARTING, {
1940 windowCount: eligible.length
1941 });
1942 for (const w of eligible) {
1943 if (w.state === "minimized") {
1944 w.restore();
1945 }
1946 if (w.state === "fullscreen") {
1947 w.toggleFullscreen();
1948 }
1949 if (w.state === "maximized") {
1950 w.toggleMaximize();
1951 }
1952 }
1953 const rect = mgr._desktop.getBoundingClientRect();
1954 const padding = 30;
1955 const offset = 30;
1956 const targetWidth = Math.min(Math.round(rect.width * 0.7), 1100);
1957 const targetHeight = Math.min(Math.round(rect.height * 0.75), 750);
1958 const maxStepsX = Math.max(
1959 1,
1960 Math.floor((rect.width - targetWidth - padding) / offset)
1961 );
1962 const maxStepsY = Math.max(
1963 1,
1964 Math.floor((rect.height - targetHeight - padding) / offset)
1965 );
1966 const maxSteps = Math.min(maxStepsX, maxStepsY);
1967 eligible.forEach((w, i) => {
1968 const step = i % Math.max(1, maxSteps);
1969 w.element.style.left = `${padding + step * offset}px`;
1970 w.element.style.top = `${padding + step * offset}px`;
1971 w.element.style.width = `${targetWidth}px`;
1972 w.element.style.height = `${targetHeight}px`;
1973 });
1974 const focused = mgr.getFocused();
1975 if (focused) {
1976 mgr.focus(focused);
1977 }
1978 document.dispatchEvent(
1979 new CustomEvent("desktop-mode-window-changed", {
1980 detail: { reason: "cascade" }
1981 })
1982 );
1983 doAction(HOOKS.ARRANGE_CASCADE_APPLIED, {
1984 windowCount: eligible.length
1985 });
1986 }
1987 function tile(mgr) {
1988 const eligible = mgr._stack.filter(
1989 (w) => w.config.desktopId === mgr._activeDesktopId
1990 );
1991 if (eligible.length === 0) {
1992 return;
1993 }
1994 for (const w of eligible) {
1995 if (w.state === "minimized") {
1996 w.restore();
1997 }
1998 if (w.state === "fullscreen") {
1999 w.toggleFullscreen();
2000 }
2001 if (w.state === "maximized") {
2002 w.toggleMaximize();
2003 }
2004 }
2005 const rect = mgr._desktop.getBoundingClientRect();
2006 const auto = pickGridDimensions(
2007 eligible.length,
2008 rect.width,
2009 rect.height
2010 );
2011 const filtered = applyFilters(
2012 HOOKS.ARRANGE_TILE_DIMENSIONS,
2013 auto,
2014 {
2015 windowCount: eligible.length,
2016 areaWidth: rect.width,
2017 areaHeight: rect.height
2018 }
2019 );
2020 const { cols, rows } = isValidGrid(filtered, eligible.length) ? { cols: Math.floor(filtered.cols), rows: Math.floor(filtered.rows) } : auto;
2021 doAction(HOOKS.ARRANGE_TILE_STARTING, {
2022 windowCount: eligible.length,
2023 cols,
2024 rows
2025 });
2026 const padding = 16;
2027 const gap = 12;
2028 const cellWidth = Math.floor(
2029 (rect.width - padding * 2 - gap * (cols - 1)) / cols
2030 );
2031 const cellHeight = Math.floor(
2032 (rect.height - padding * 2 - gap * (rows - 1)) / rows
2033 );
2034 eligible.forEach((w, i) => {
2035 const col = i % cols;
2036 const row = Math.floor(i / cols);
2037 w.element.style.left = `${padding + col * (cellWidth + gap)}px`;
2038 w.element.style.top = `${padding + row * (cellHeight + gap)}px`;
2039 w.element.style.width = `${cellWidth}px`;
2040 w.element.style.height = `${cellHeight}px`;
2041 });
2042 const focused = mgr.getFocused();
2043 if (focused) {
2044 mgr.focus(focused);
2045 }
2046 document.dispatchEvent(
2047 new CustomEvent("desktop-mode-window-changed", {
2048 detail: { reason: "tile" }
2049 })
2050 );
2051 doAction(HOOKS.ARRANGE_TILE_APPLIED, {
2052 windowCount: eligible.length,
2053 cols,
2054 rows
2055 });
2056 }
2057 const SNAP_STORAGE_KEY = "desktop-mode-snap-to-grid";
2058 function loadSnapEnabled() {
2059 try {
2060 return window.localStorage.getItem(SNAP_STORAGE_KEY) === "1";
2061 } catch {
2062 return false;
2063 }
2064 }
2065 function setSnapEnabled(mgr, enabled) {
2066 if (mgr._snapEnabled === enabled) {
2067 return;
2068 }
2069 mgr._snapEnabled = enabled;
2070 try {
2071 window.localStorage.setItem(SNAP_STORAGE_KEY, enabled ? "1" : "0");
2072 } catch {
2073 }
2074 doAction(HOOKS.ARRANGE_SNAP_CHANGED, { enabled });
2075 }
2076 function getSnapConfig(mgr) {
2077 if (!mgr._snapEnabled) {
2078 return { enabled: false, cellWidth: 0, cellHeight: 0 };
2079 }
2080 const rect = mgr._desktop.getBoundingClientRect();
2081 const targetCols = rect.width >= rect.height ? 12 : 8;
2082 const auto = {
2083 cellWidth: Math.max(40, Math.round(rect.width / targetCols)),
2084 cellHeight: Math.max(
2085 40,
2086 Math.round(rect.height / Math.round(targetCols * 0.66))
2087 )
2088 };
2089 const filtered = applyFilters(
2090 HOOKS.ARRANGE_SNAP_CELL_SIZE,
2091 auto,
2092 { areaWidth: rect.width, areaHeight: rect.height }
2093 );
2094 const { cellWidth, cellHeight } = isValidCellSize(filtered) ? filtered : auto;
2095 return { enabled: true, cellWidth, cellHeight };
2096 }
2097 function enterSplitOverview(mgr, anchor, zone) {
2098 if (mgr._splitOverviewActive) {
2099 return;
2100 }
2101 mgr._splitOverviewActive = true;
2102 mgr._splitOverviewAnchor = anchor;
2103 mgr._splitOverviewZone = zone;
2104 const eligible = mgr._stack.filter(
2105 (w) => w !== anchor && w.state !== "minimized" && w.config.desktopId === mgr._activeDesktopId
2106 );
2107 if (eligible.length === 0) {
2108 cleanupSplitOverviewState(mgr);
2109 return;
2110 }
2111 mgr._splitOverviewSnapshot.clear();
2112 for (const w of eligible) {
2113 mgr._splitOverviewSnapshot.set(w.id, {
2114 transform: w.element.style.transform || "",
2115 transition: w.element.style.transition || ""
2116 });
2117 }
2118 mgr._desktop.classList.add("desktop-mode-area--split-overview");
2119 const rect = oppositeHalfRect(mgr, zone);
2120 const layout = computeOverviewLayout(eligible, rect, 0);
2121 mgr._splitOverviewLabels.clear();
2122 for (const item of layout) {
2123 const el = item.win.element;
2124 el.classList.add("desktop-mode-window--overview");
2125 const dx = item.x - el.offsetLeft;
2126 const dy = item.y - el.offsetTop;
2127 el.style.transform = `translate(${dx}px, ${dy}px) scale(${item.scale})`;
2128 const label = createOverviewLabel(item);
2129 el.insertAdjacentElement("afterend", label);
2130 mgr._splitOverviewLabels.set(item.win.id, label);
2131 }
2132 const pressTargetForEvent = (e) => {
2133 const target2 = e.target;
2134 const winEl = target2?.closest(
2135 ".desktop-mode-window--overview"
2136 );
2137 if (winEl) {
2138 return {
2139 id: winEl.id.replace(/^wp-window-/, ""),
2140 element: winEl
2141 };
2142 }
2143 if (target2) {
2144 return { id: "dismiss", element: mgr._desktop };
2145 }
2146 return null;
2147 };
2148 mgr._splitOverviewPointerDown = (e) => {
2149 if (e.button !== 0) {
2150 mgr._splitOverviewPressTarget = null;
2151 return;
2152 }
2153 mgr._splitOverviewPressTarget = pressTargetForEvent(e);
2154 if (mgr._splitOverviewPressTarget) {
2155 e.preventDefault();
2156 e.stopPropagation();
2157 }
2158 };
2159 mgr._splitOverviewPointerUp = (e) => {
2160 if (e.button !== 0) {
2161 return;
2162 }
2163 const pressed = mgr._splitOverviewPressTarget;
2164 mgr._splitOverviewPressTarget = null;
2165 if (!pressed) {
2166 return;
2167 }
2168 const r = pressed.element.getBoundingClientRect();
2169 const inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
2170 if (!inside) {
2171 return;
2172 }
2173 e.preventDefault();
2174 e.stopPropagation();
2175 if (pressed.id === "dismiss") {
2176 exitSplitOverview(mgr);
2177 return;
2178 }
2179 const selected = mgr.getById(pressed.id);
2180 if (!selected) {
2181 exitSplitOverview(mgr);
2182 return;
2183 }
2184 fillOppositeHalfAndExit(mgr, selected);
2185 };
2186 mgr._splitOverviewKey = (e) => {
2187 if (e.key === "Escape") {
2188 exitSplitOverview(mgr);
2189 }
2190 };
2191 mgr._splitOverviewClickBlocker = (e) => {
2192 e.stopPropagation();
2193 e.preventDefault();
2194 };
2195 mgr._desktop.addEventListener(
2196 "pointerdown",
2197 mgr._splitOverviewPointerDown,
2198 true
2199 );
2200 mgr._desktop.addEventListener(
2201 "pointerup",
2202 mgr._splitOverviewPointerUp,
2203 true
2204 );
2205 mgr._desktop.addEventListener(
2206 "click",
2207 mgr._splitOverviewClickBlocker,
2208 true
2209 );
2210 document.addEventListener("keydown", mgr._splitOverviewKey);
2211 }
2212 function fillOppositeHalfAndExit(mgr, selected) {
2213 const anchorZone = mgr._splitOverviewZone;
2214 if (!anchorZone) {
2215 exitSplitOverview(mgr);
2216 return;
2217 }
2218 const partnerZone = anchorZone === "left" ? "right" : "left";
2219 selected.element.style.transform = "";
2220 selected.element.classList.remove("desktop-mode-window--overview");
2221 selected.applySnap(partnerZone);
2222 mgr._splitOverviewSnapshot.delete(selected.id);
2223 mgr.focus(selected);
2224 doAction(HOOKS.SNAP_SPLIT_FILLED, {
2225 windowId: selected.id,
2226 zone: partnerZone
2227 });
2228 exitSplitOverview(mgr);
2229 }
2230 function exitSplitOverview(mgr) {
2231 if (!mgr._splitOverviewActive) {
2232 return;
2233 }
2234 mgr._splitOverviewActive = false;
2235 for (const [id, snap] of mgr._splitOverviewSnapshot) {
2236 const w = mgr.getById(id);
2237 if (!w) {
2238 continue;
2239 }
2240 w.element.style.transform = snap.transform;
2241 }
2242 for (const label of mgr._splitOverviewLabels.values()) {
2243 label.classList.add("desktop-mode-overview-label--out");
2244 }
2245 mgr._desktop.classList.remove("desktop-mode-area--split-overview");
2246 const ANIMATION_MS = 260;
2247 window.setTimeout(() => {
2248 for (const w of mgr._stack) {
2249 if (mgr._splitOverviewSnapshot.has(w.id)) {
2250 w.element.classList.remove("desktop-mode-window--overview");
2251 }
2252 }
2253 for (const label of mgr._splitOverviewLabels.values()) {
2254 label.remove();
2255 }
2256 cleanupSplitOverviewState(mgr);
2257 }, ANIMATION_MS);
2258 if (mgr._splitOverviewPointerDown) {
2259 mgr._desktop.removeEventListener(
2260 "pointerdown",
2261 mgr._splitOverviewPointerDown,
2262 true
2263 );
2264 mgr._splitOverviewPointerDown = null;
2265 }
2266 if (mgr._splitOverviewPointerUp) {
2267 mgr._desktop.removeEventListener(
2268 "pointerup",
2269 mgr._splitOverviewPointerUp,
2270 true
2271 );
2272 mgr._splitOverviewPointerUp = null;
2273 }
2274 if (mgr._splitOverviewClickBlocker) {
2275 mgr._desktop.removeEventListener(
2276 "click",
2277 mgr._splitOverviewClickBlocker,
2278 true
2279 );
2280 mgr._splitOverviewClickBlocker = null;
2281 }
2282 if (mgr._splitOverviewKey) {
2283 document.removeEventListener("keydown", mgr._splitOverviewKey);
2284 mgr._splitOverviewKey = null;
2285 }
2286 mgr._splitOverviewPressTarget = null;
2287 }
2288 function cleanupSplitOverviewState(mgr) {
2289 mgr._splitOverviewSnapshot.clear();
2290 mgr._splitOverviewLabels.clear();
2291 mgr._splitOverviewAnchor = null;
2292 mgr._splitOverviewZone = null;
2293 mgr._splitOverviewActive = false;
2294 }
2295 const SNAP_EDGE_THRESHOLD = 30;
2296 const SNAP_COMMIT_MS = 260;
2297 function detectSnapZone(clientX, desktopRect) {
2298 if (clientX <= desktopRect.left + SNAP_EDGE_THRESHOLD) {
2299 return "left";
2300 }
2301 if (clientX >= desktopRect.right - SNAP_EDGE_THRESHOLD) {
2302 return "right";
2303 }
2304 return null;
2305 }
2306 function snapZoneBounds(mgr, zone) {
2307 const rect = mgr._desktop.getBoundingClientRect();
2308 const halfW = Math.floor(rect.width / 2);
2309 const height = Math.floor(rect.height);
2310 return {
2311 x: zone === "left" ? 0 : rect.width - halfW,
2312 y: 0,
2313 width: halfW,
2314 height
2315 };
2316 }
2317 function oppositeHalfRect(mgr, zone) {
2318 const rect = mgr._desktop.getBoundingClientRect();
2319 const halfW = Math.floor(rect.width / 2);
2320 const height = Math.floor(rect.height);
2321 if (zone === "left") {
2322 return new DOMRect(halfW, 0, halfW, height);
2323 }
2324 return new DOMRect(0, 0, halfW, height);
2325 }
2326 function showSnapPreview(mgr, zone) {
2327 if (mgr._snapPendingZone === zone && mgr._snapPreviewEl) {
2328 return;
2329 }
2330 mgr._snapPendingZone = zone;
2331 if (!mgr._snapPreviewEl) {
2332 const el = document.createElement("div");
2333 el.className = "desktop-mode-snap-preview";
2334 el.setAttribute("aria-hidden", "true");
2335 mgr._desktop.appendChild(el);
2336 mgr._snapPreviewEl = el;
2337 Promise.resolve().then(() => {
2338 el.classList.add("desktop-mode-snap-preview--visible");
2339 });
2340 }
2341 const b = snapZoneBounds(mgr, zone);
2342 mgr._snapPreviewEl.style.left = `${b.x}px`;
2343 mgr._snapPreviewEl.style.top = `${b.y}px`;
2344 mgr._snapPreviewEl.style.width = `${b.width}px`;
2345 mgr._snapPreviewEl.style.height = `${b.height}px`;
2346 mgr._snapPreviewEl.dataset.zone = zone;
2347 }
2348 function hideSnapPreview(mgr) {
2349 if (!mgr._snapPreviewEl) {
2350 mgr._snapPendingZone = null;
2351 return;
2352 }
2353 const el = mgr._snapPreviewEl;
2354 mgr._snapPreviewEl = null;
2355 mgr._snapPendingZone = null;
2356 el.classList.remove("desktop-mode-snap-preview--visible");
2357 window.setTimeout(() => {
2358 el.remove();
2359 }, SNAP_COMMIT_MS);
2360 }
2361 function updateSnapZoneForDrag(mgr, win, clientX) {
2362 if (mgr._splitOverviewActive) {
2363 return;
2364 }
2365 const rect = mgr._desktop.getBoundingClientRect();
2366 const zone = detectSnapZone(clientX, rect);
2367 const previous = mgr._snapPendingZone;
2368 if (zone) {
2369 showSnapPreview(mgr, zone);
2370 if (previous !== zone) {
2371 doAction(HOOKS.SNAP_ZONE_PENDING, {
2372 windowId: win.id,
2373 zone
2374 });
2375 }
2376 } else if (previous) {
2377 hideSnapPreview(mgr);
2378 doAction(HOOKS.SNAP_ZONE_CANCELED, { windowId: win.id });
2379 }
2380 }
2381 function commitSnapIfPending(mgr, win) {
2382 const zone = mgr._snapPendingZone;
2383 if (!zone) {
2384 return false;
2385 }
2386 hideSnapPreview(mgr);
2387 if (win.state === "normal") {
2388 win._savedGeometry = {
2389 x: win.element.offsetLeft,
2390 y: win.element.offsetTop,
2391 width: win.element.offsetWidth,
2392 height: win.element.offsetHeight
2393 };
2394 }
2395 win.applySnap(zone);
2396 doAction(HOOKS.SNAP_ZONE_COMMITTED, {
2397 windowId: win.id,
2398 zone
2399 });
2400 window.requestAnimationFrame(() => {
2401 enterSplitOverview(mgr, win, zone);
2402 });
2403 return true;
2404 }
2405 function abortSnapIfPending(mgr) {
2406 if (mgr._snapPendingZone) {
2407 hideSnapPreview(mgr);
2408 }
2409 }
2410 const NATIVE_GEOMETRY_STORAGE_KEY = "desktop-mode-native-window-geometry";
2411 const MAX_ENTRIES = 64;
2412 const MAX_DIMENSION = 8192;
2413 function readMap$1() {
2414 try {
2415 const raw = window.localStorage.getItem(NATIVE_GEOMETRY_STORAGE_KEY);
2416 if (!raw) {
2417 return {};
2418 }
2419 const parsed = JSON.parse(raw);
2420 if (!parsed || typeof parsed !== "object") {
2421 return {};
2422 }
2423 return parsed;
2424 } catch {
2425 return {};
2426 }
2427 }
2428 function writeMap$1(map) {
2429 try {
2430 window.localStorage.setItem(
2431 NATIVE_GEOMETRY_STORAGE_KEY,
2432 JSON.stringify(map)
2433 );
2434 } catch {
2435 }
2436 }
2437 function loadNativeWindowGeometry(baseId) {
2438 if (!baseId) {
2439 return null;
2440 }
2441 const map = readMap$1();
2442 const entry = map[baseId];
2443 if (!entry) {
2444 return null;
2445 }
2446 const width = Number(entry.width);
2447 const height = Number(entry.height);
2448 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2449 return null;
2450 }
2451 const state2 = entry.state === "maximized" ? "maximized" : void 0;
2452 const x = Number(entry.x);
2453 const y = Number(entry.y);
2454 const hasPosition = Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0 && x <= MAX_DIMENSION && y <= MAX_DIMENSION;
2455 return {
2456 width: Math.round(width),
2457 height: Math.round(height),
2458 ...hasPosition ? { x: Math.round(x), y: Math.round(y) } : {},
2459 ...state2 ? { state: state2 } : {}
2460 };
2461 }
2462 function saveNativeWindowGeometry(baseId, geometry) {
2463 if (!baseId) {
2464 return;
2465 }
2466 const width = Math.round(Number(geometry.width));
2467 const height = Math.round(Number(geometry.height));
2468 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2469 return;
2470 }
2471 const map = readMap$1();
2472 const prev = map[baseId];
2473 const state2 = prev && prev.state === "maximized" ? "maximized" : void 0;
2474 const carriedX = typeof prev?.x === "number" ? prev.x : void 0;
2475 const carriedY = typeof prev?.y === "number" ? prev.y : void 0;
2476 if (prev && prev.width === width && prev.height === height && prev.state === state2 && prev.x === carriedX && prev.y === carriedY) {
2477 return;
2478 }
2479 upsertEntry(map, baseId, {
2480 width,
2481 height,
2482 ...typeof carriedX === "number" && typeof carriedY === "number" ? { x: carriedX, y: carriedY } : {},
2483 ...state2 ? { state: state2 } : {}
2484 });
2485 writeMapTrimmed(map);
2486 }
2487 function saveNativeWindowPosition(baseId, position) {
2488 if (!baseId) {
2489 return;
2490 }
2491 const x = Math.round(Number(position.x));
2492 const y = Math.round(Number(position.y));
2493 if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || y < 0 || x > MAX_DIMENSION || y > MAX_DIMENSION) {
2494 return;
2495 }
2496 const map = readMap$1();
2497 const prev = map[baseId];
2498 if (!prev) {
2499 return;
2500 }
2501 if (prev.x === x && prev.y === y) {
2502 return;
2503 }
2504 upsertEntry(map, baseId, {
2505 ...prev,
2506 x,
2507 y
2508 });
2509 writeMapTrimmed(map);
2510 }
2511 function setNativeWindowSavedState(baseId, state2, defaults) {
2512 if (!baseId) {
2513 return;
2514 }
2515 const map = readMap$1();
2516 const prev = map[baseId];
2517 if (!prev) {
2518 if (state2 === null || !defaults) {
2519 return;
2520 }
2521 const width = Math.round(Number(defaults.width));
2522 const height = Math.round(Number(defaults.height));
2523 if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
2524 return;
2525 }
2526 upsertEntry(map, baseId, { width, height, state: state2 });
2527 writeMapTrimmed(map);
2528 return;
2529 }
2530 if (state2 === null) {
2531 if (!prev.state) {
2532 return;
2533 }
2534 const { state: _state2, ...rest } = prev;
2535 upsertEntry(map, baseId, rest);
2536 writeMapTrimmed(map);
2537 return;
2538 }
2539 if (prev.state === state2) {
2540 return;
2541 }
2542 upsertEntry(map, baseId, {
2543 ...prev,
2544 state: state2
2545 });
2546 writeMapTrimmed(map);
2547 }
2548 function upsertEntry(map, baseId, entry) {
2549 delete map[baseId];
2550 map[baseId] = entry;
2551 }
2552 function writeMapTrimmed(map) {
2553 const keys = Object.keys(map);
2554 if (keys.length > MAX_ENTRIES) {
2555 const trimmed = {};
2556 for (const key of keys.slice(-MAX_ENTRIES)) {
2557 trimmed[key] = map[key];
2558 }
2559 writeMap$1(trimmed);
2560 return;
2561 }
2562 writeMap$1(map);
2563 }
2564 const BASE_Z_INDEX = 100;
2565 const CASCADE_OFFSET = 30;
2566 class WindowManager {
2567 constructor(desktop) {
2568 this._stack = [];
2569 this.cascadeIndex = 0;
2570 this._desktops = [
2571 // translators: default desktop name — "Desktop 1"
2572 { id: "desktop-1", label: "Desktop 1" }
2573 ];
2574 this._activeDesktopId = "desktop-1";
2575 this._desktopSeq = 1;
2576 this.onToggleStartupRequested = null;
2577 this.desktopResizeObserver = null;
2578 this._reflowRestoreTimer = null;
2579 this._snapEnabled = loadSnapEnabled();
2580 this._overviewActive = false;
2581 this._overviewSnapshot = /* @__PURE__ */ new Map();
2582 this._overviewLabels = /* @__PURE__ */ new Map();
2583 this._overviewPointerDownHandler = null;
2584 this._overviewPointerUpHandler = null;
2585 this._overviewKeyHandler = null;
2586 this._overviewPressTarget = null;
2587 this._overviewClickBlocker = null;
2588 this._overviewTopBar = null;
2589 this._overviewMouseHandler = null;
2590 this._lastOverviewHoverId = null;
2591 this._overviewAddTileFocused = false;
2592 this._snapPendingZone = null;
2593 this._snapPreviewEl = null;
2594 this._splitOverviewActive = false;
2595 this._splitOverviewAnchor = null;
2596 this._splitOverviewZone = null;
2597 this._splitOverviewSnapshot = /* @__PURE__ */ new Map();
2598 this._splitOverviewLabels = /* @__PURE__ */ new Map();
2599 this._splitOverviewPointerDown = null;
2600 this._splitOverviewPointerUp = null;
2601 this._splitOverviewPressTarget = null;
2602 this._splitOverviewClickBlocker = null;
2603 this._splitOverviewKey = null;
2604 this._desktop = desktop;
2605 if (typeof ResizeObserver !== "undefined") {
2606 this.desktopResizeObserver = new ResizeObserver(
2607 () => this.reflowStatefulWindows()
2608 );
2609 this.desktopResizeObserver.observe(desktop);
2610 }
2611 this.installIframeFocusBridge();
2612 }
2613 /**
2614 * Clicks inside an iframe don't cross the browsing-context
2615 * boundary — pointerdown / focusin in the iframe's document never
2616 * reach the parent. BUT the parent `window` does lose focus,
2617 * because focus moves to the iframe's content window.
2618 *
2619 * We use that signal: listen for `window.blur` on the parent,
2620 * check `document.activeElement` — if it's an iframe, walk up to
2621 * its owning `.desktop-mode-window`, find the matching Window in
2622 * our stack, and focus it. Covers clicks on the primary iframe
2623 * AND any external-tab sub-iframes mounted as descendants of the
2624 * window element.
2625 */
2626 installIframeFocusBridge() {
2627 window.addEventListener("blur", () => {
2628 window.setTimeout(() => {
2629 const active2 = this._desktop.ownerDocument?.activeElement ?? null;
2630 if (!active2 || active2.tagName !== "IFRAME") {
2631 return;
2632 }
2633 const winEl = active2.closest(
2634 ".desktop-mode-window"
2635 );
2636 if (!winEl) {
2637 return;
2638 }
2639 const id = winEl.id.replace(/^wp-window-/, "");
2640 const win = this.getById(id);
2641 if (!win) {
2642 return;
2643 }
2644 if (this._overviewActive) {
2645 return;
2646 }
2647 if (this.getFocused() === win) {
2648 return;
2649 }
2650 this.focus(win);
2651 }, 0);
2652 });
2653 }
2654 /**
2655 * Re-apply state-driven bounds to any window whose geometry is
2656 * derived from the desktop area's dimensions: maximized (full
2657 * area) and snapped-left / snapped-right (half area). Called from
2658 * the desktop-area ResizeObserver so shrinking the browser window
2659 * drags the stateful windows along with it.
2660 *
2661 * Inlines the geometry writes instead of calling `applySnap` —
2662 * that method emits `_emitChange('state')` which would spam the
2663 * session saver on every resize tick. Viewport resize is an
2664 * INCOMING shape change (the shell reshaped us), not an outgoing
2665 * user action worth persisting.
2666 *
2667 * Also toggles `desktop-mode-window--reflowing` so the base
2668 * left/top/width/height transition doesn't interpolate between
2669 * every ResizeObserver tick — without that, the windows would
2670 * always lag ~250 ms behind a browser edge-drag.
2671 *
2672 * Skipped while overview is active — windows are mid-transform
2673 * and touching their inline geometry would desync the live
2674 * transform math; overview exit re-applies state correctly via
2675 * its own path.
2676 */
2677 reflowStatefulWindows() {
2678 if (this._overviewActive) {
2679 return;
2680 }
2681 for (const w of this._stack) {
2682 const parent = w.element.parentElement;
2683 if (!parent) {
2684 continue;
2685 }
2686 if (w.state === "maximized") {
2687 w.element.classList.add("desktop-mode-window--reflowing");
2688 w.element.style.width = `${parent.clientWidth}px`;
2689 w.element.style.height = `${parent.clientHeight}px`;
2690 } else if (w.state === "snapped-left" || w.state === "snapped-right") {
2691 w.element.classList.add("desktop-mode-window--reflowing");
2692 const halfW = Math.floor(parent.clientWidth / 2);
2693 const height = parent.clientHeight;
2694 const left = w.state === "snapped-left" ? 0 : halfW;
2695 w.element.style.left = `${left}px`;
2696 w.element.style.top = "0px";
2697 w.element.style.width = `${halfW}px`;
2698 w.element.style.height = `${height}px`;
2699 }
2700 }
2701 if (this._reflowRestoreTimer !== null) {
2702 window.clearTimeout(this._reflowRestoreTimer);
2703 }
2704 this._reflowRestoreTimer = window.setTimeout(() => {
2705 this._reflowRestoreTimer = null;
2706 for (const w of this._stack) {
2707 w.element.classList.remove("desktop-mode-window--reflowing");
2708 }
2709 }, 140);
2710 }
2711 /**
2712 * Open a new window — or focus an existing one — for the given
2713 * page.
2714 *
2715 * Matches any existing window sharing the same `baseId`
2716 * (defaulting to the config's `id`). For singleton pages
2717 * (Settings, Dashboard, …) `baseId === id`, so this behaves
2718 * exactly like strict id matching. For multi pages, clicking the
2719 * dock icon while a window is already open focuses the
2720 * most-recent instance rather than creating a twin.
2721 *
2722 * To force a brand-new instance alongside an existing one, use
2723 * {@link openNew}.
2724 */
2725 async open(config) {
2726 if (!config || typeof config !== "object") {
2727 throw new TypeError(
2728 "windowManager.open() requires a config object with at least { id, url, title }; received " + (config === null ? "null" : typeof config)
2729 );
2730 }
2731 if (typeof config.id !== "string" || config.id === "") {
2732 throw new TypeError(
2733 "windowManager.open(): config.id must be a non-empty string."
2734 );
2735 }
2736 if (typeof config.url !== "string" || config.url === "") {
2737 throw new TypeError(
2738 '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.'
2739 );
2740 }
2741 if (typeof config.title !== "string") {
2742 throw new TypeError(
2743 "windowManager.open(): config.title must be a string."
2744 );
2745 }
2746 const baseId = config.baseId || config.id;
2747 const existing = this.getByBaseIdOnActiveDesktop(baseId);
2748 if (existing) {
2749 const wasMinimized = existing.state === "minimized";
2750 this.focus(existing);
2751 if (wasMinimized) {
2752 existing.restore();
2753 }
2754 const reopenedDetail = {
2755 windowId: existing.id,
2756 baseId,
2757 wasMinimized
2758 };
2759 document.dispatchEvent(
2760 new CustomEvent("desktop-mode-window-reopened", { detail: reopenedDetail })
2761 );
2762 doAction(HOOKS.WINDOW_REOPENED, reopenedDetail);
2763 return existing;
2764 }
2765 const id = this.getByBaseId(baseId) ? this.nextInstanceId(baseId) : config.id;
2766 return this.createWindow({ ...config, id, baseId });
2767 }
2768 /**
2769 * Open a brand-new window even if one is already open for this
2770 * page. Only makes sense for pages flagged `multi`.
2771 *
2772 * Duplicates always open in the floating ('normal') state and at
2773 * a fresh cascade slot — the per-baseId saved size / state /
2774 * position preferences apply to the primary instance only.
2775 * Spawning a maximized twin alongside the maximized primary
2776 * would hide the primary; landing a twin on top of the primary's
2777 * remembered position would hide it too. Callers can override
2778 * either default by passing `initialState` / `x` / `y` explicitly.
2779 */
2780 async openNew(config) {
2781 const baseId = config.baseId || config.id;
2782 const nextId2 = this.nextInstanceId(baseId);
2783 const cascadeX = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2784 const cascadeY = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2785 return this.createWindow({
2786 initialState: "normal",
2787 x: cascadeX,
2788 y: cascadeY,
2789 ...config,
2790 id: nextId2,
2791 baseId
2792 });
2793 }
2794 /**
2795 * Build and mount a window element. Common tail shared by
2796 * `open()` and `openNew()`.
2797 */
2798 async createWindow(config) {
2799 const desktopRect = this._desktop.getBoundingClientRect();
2800 const defaultWidth = Math.min(Math.round(desktopRect.width * 0.8), 1200);
2801 const defaultHeight = Math.min(Math.round(desktopRect.height * 0.8), 800);
2802 const cascadeX = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2803 const cascadeY = 40 + this.cascadeIndex % 8 * CASCADE_OFFSET;
2804 const resolvedBaseId = config.baseId || config.id;
2805 const minWidth = config.minWidth ?? 320;
2806 const minHeight = config.minHeight ?? 200;
2807 const hasExplicitWidth = typeof config.width === "number";
2808 const hasExplicitHeight = typeof config.height === "number";
2809 const hasExplicitX = typeof config.x === "number";
2810 const hasExplicitY = typeof config.y === "number";
2811 const hasExplicitState = typeof config.initialState === "string";
2812 const saved = !hasExplicitWidth || !hasExplicitHeight || !hasExplicitState || !hasExplicitX || !hasExplicitY ? loadNativeWindowGeometry(resolvedBaseId) : null;
2813 const resolvedWidth = config.width ?? (saved ? Math.max(saved.width, minWidth) : defaultWidth);
2814 const resolvedHeight = config.height ?? (saved ? Math.max(saved.height, minHeight) : defaultHeight);
2815 const resolvedState = config.initialState ?? (saved?.state === "maximized" ? "maximized" : void 0);
2816 let clampedSavedX;
2817 let clampedSavedY;
2818 if (saved && typeof saved.x === "number" && typeof saved.y === "number") {
2819 const margin = 12;
2820 const maxX = Math.max(
2821 0,
2822 desktopRect.width - resolvedWidth - margin
2823 );
2824 const maxY = Math.max(
2825 0,
2826 desktopRect.height - resolvedHeight - margin
2827 );
2828 clampedSavedX = Math.max(margin, Math.min(saved.x, maxX));
2829 clampedSavedY = Math.max(margin, Math.min(saved.y, maxY));
2830 }
2831 const resolvedX = config.x ?? clampedSavedX ?? cascadeX;
2832 const resolvedY = config.y ?? clampedSavedY ?? cascadeY;
2833 const callerPinned = hasExplicitWidth || hasExplicitHeight || hasExplicitX || hasExplicitY || hasExplicitState;
2834 const hasSavedGeometry = !!saved;
2835 const preFilterGeometry = {
2836 x: resolvedX,
2837 y: resolvedY,
2838 width: resolvedWidth,
2839 height: resolvedHeight,
2840 state: resolvedState
2841 };
2842 let filtered;
2843 try {
2844 filtered = applyFilters(
2845 HOOKS.WINDOW_GEOMETRY,
2846 preFilterGeometry,
2847 {
2848 windowId: config.id,
2849 baseId: resolvedBaseId,
2850 hasSavedGeometry,
2851 callerPinned,
2852 desktopRect: {
2853 width: desktopRect.width,
2854 height: desktopRect.height
2855 }
2856 }
2857 );
2858 } catch (err) {
2859 doAction(HOOKS.SHELL_ERROR, {
2860 scope: "window-geometry-filter",
2861 windowId: config.id,
2862 error: err
2863 });
2864 if (typeof console !== "undefined") {
2865 console.error(
2866 `[desktop-mode] WINDOW_GEOMETRY filter threw for "${config.id}":`,
2867 err
2868 );
2869 }
2870 filtered = preFilterGeometry;
2871 }
2872 const coalesce = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback;
2873 const safeFiltered = filtered && typeof filtered === "object" ? filtered : preFilterGeometry;
2874 const finalWidth = Math.max(
2875 coalesce(safeFiltered.width, resolvedWidth),
2876 minWidth
2877 );
2878 const finalHeight = Math.max(
2879 coalesce(safeFiltered.height, resolvedHeight),
2880 minHeight
2881 );
2882 const finalX = coalesce(safeFiltered.x, resolvedX);
2883 const finalY = coalesce(safeFiltered.y, resolvedY);
2884 const finalState = safeFiltered.state ?? resolvedState;
2885 const fullConfig = {
2886 icon: config.icon || "dashicons-admin-generic",
2887 ...config,
2888 // Spread `config` first so callers can pass through any
2889 // extras (render, ownerHandle, parentUrl, …), then pin the
2890 // dimensions + state we resolved above. The pin has to
2891 // follow the spread because an explicit `width: undefined`
2892 // from the caller would otherwise blow away the default.
2893 x: finalX,
2894 y: finalY,
2895 width: finalWidth,
2896 height: finalHeight,
2897 minWidth,
2898 minHeight,
2899 ...finalState ? { initialState: finalState } : {},
2900 baseId: resolvedBaseId,
2901 // New windows always join the active desktop. A caller can
2902 // pre-seed `desktopId` (e.g. session restore) by passing it
2903 // in `config`, which the spread above preserves.
2904 desktopId: config.desktopId || this._activeDesktopId
2905 };
2906 this.cascadeIndex++;
2907 const [system] = await Promise.all([
2908 ensureWindowSystemLoaded(windowSystemBundleUrl()),
2909 ensureShellOverlaysLoaded(shellOverlaysBundleUrl())
2910 ]);
2911 const win = system.createWindow(fullConfig);
2912 win.onFocusRequest = (w) => this.focus(w);
2913 win.onClose = (w) => this.remove(w);
2914 win.onMinimize = () => {
2915 const visible = this._stack.filter((w) => w.state !== "minimized");
2916 if (visible.length > 0) {
2917 this.focus(visible[visible.length - 1]);
2918 }
2919 };
2920 win.onOpenAnother = (w) => {
2921 const baseId = w.config.baseId || w.id;
2922 if (w.config.native) {
2923 const api = window.wp?.desktop;
2924 if (api?.openNewWindow?.(baseId, { source: "open-another" })) {
2925 return;
2926 }
2927 }
2928 void this.openNew({
2929 id: baseId,
2930 baseId,
2931 url: w.config.url || "",
2932 title: w.config.title,
2933 icon: w.config.icon,
2934 submenu: w.config.submenu,
2935 multi: true
2936 });
2937 };
2938 win.onOpenInNewWindow = (w) => {
2939 const baseId = w.config.baseId || w.id;
2940 if (w.config.native) {
2941 const api = window.wp?.desktop;
2942 if (api?.openNewWindow?.(baseId, { source: "open-in-new-window" })) {
2943 return;
2944 }
2945 }
2946 const currentUrl = w.getCurrentUrl();
2947 void this.openNew({
2948 id: baseId,
2949 baseId,
2950 url: currentUrl || w.config.url || "",
2951 title: w.config.title,
2952 icon: w.config.icon,
2953 submenu: w.config.submenu,
2954 multi: true
2955 });
2956 };
2957 win.onToggleStartup = (w) => {
2958 this.onToggleStartupRequested?.(w);
2959 };
2960 win.snapConfigProvider = () => this.getSnapConfig();
2961 win.onDragMove = (w, clientX) => {
2962 updateSnapZoneForDrag(this, w, clientX);
2963 };
2964 win.onDragEnd = (w) => {
2965 if (this._snapPendingZone) {
2966 return commitSnapIfPending(this, w);
2967 }
2968 abortSnapIfPending(this);
2969 return false;
2970 };
2971 this._stack.push(win);
2972 this._desktop.appendChild(win.element);
2973 applyDesktopVisibility(this, win);
2974 win.hydrateNative();
2975 this.focus(win);
2976 const openedDetail = {
2977 windowId: win.id,
2978 page: config.url,
2979 title: config.title,
2980 url: config.url
2981 };
2982 document.dispatchEvent(
2983 new CustomEvent("desktop-mode-window-opened", { detail: openedDetail })
2984 );
2985 doAction(HOOKS.WINDOW_OPENED, openedDetail);
2986 return win;
2987 }
2988 /**
2989 * Find the next unused suffixed id for a given baseId. Prefers
2990 * the bare baseId itself if free (user closed the original), then
2991 * walks `-2`, `-3`, … until it lands on one not currently in the
2992 * stack.
2993 */
2994 nextInstanceId(baseId) {
2995 const taken = new Set(this._stack.map((w) => w.id));
2996 if (!taken.has(baseId)) {
2997 return baseId;
2998 }
2999 let n = 2;
3000 while (taken.has(`${baseId}-${n}`)) {
3001 n++;
3002 }
3003 return `${baseId}-${n}`;
3004 }
3005 /** Focus a window: bring it to top of z-stack. */
3006 focus(win) {
3007 const previouslyFocused = this._stack.length > 0 ? this._stack[this._stack.length - 1] : null;
3008 const priorFullscreen = this._stack.find(
3009 (w) => w !== win && w.isFocused() && w.isFullscreen()
3010 );
3011 if (priorFullscreen) {
3012 const shouldExit = applyFilters(
3013 HOOKS.WINDOW_AUTO_EXIT_FULLSCREEN,
3014 true,
3015 { windowId: priorFullscreen.id, focusedTo: win.id }
3016 );
3017 if (shouldExit) {
3018 priorFullscreen.toggleFullscreen();
3019 }
3020 }
3021 const idx = this._stack.indexOf(win);
3022 if (idx > -1) {
3023 this._stack.splice(idx, 1);
3024 }
3025 this._stack.push(win);
3026 this._stack.forEach((w, i) => {
3027 w.setZIndex(BASE_Z_INDEX + i);
3028 w.setFocused(i === this._stack.length - 1);
3029 });
3030 if (previouslyFocused && previouslyFocused !== win && previouslyFocused.id !== win.id) {
3031 const blurredDetail = {
3032 windowId: previouslyFocused.id,
3033 focusedTo: win.id
3034 };
3035 document.dispatchEvent(
3036 new CustomEvent("desktop-mode-window-blurred", { detail: blurredDetail })
3037 );
3038 doAction(HOOKS.WINDOW_BLURRED, blurredDetail);
3039 }
3040 const focusedDetail = { windowId: win.id };
3041 document.dispatchEvent(
3042 new CustomEvent("desktop-mode-window-focused", { detail: focusedDetail })
3043 );
3044 doAction(HOOKS.WINDOW_FOCUSED, focusedDetail);
3045 }
3046 /** Remove a window from the stack and DOM. */
3047 remove(win) {
3048 const idx = this._stack.indexOf(win);
3049 if (idx > -1) {
3050 this._stack.splice(idx, 1);
3051 }
3052 for (let i = this._stack.length - 1; i >= 0; i--) {
3053 const candidate = this._stack[i];
3054 if (candidate.state === "minimized") {
3055 continue;
3056 }
3057 const candidateDesktop = candidate.config.desktopId || this._activeDesktopId;
3058 if (candidateDesktop !== this._activeDesktopId) {
3059 continue;
3060 }
3061 this.focus(candidate);
3062 break;
3063 }
3064 const closingDetail = { windowId: win.id, element: win.element };
3065 document.dispatchEvent(
3066 new CustomEvent("desktop-mode-window-closing", { detail: closingDetail })
3067 );
3068 doAction(HOOKS.WINDOW_CLOSING, closingDetail);
3069 const closedDetail = { windowId: win.id };
3070 document.dispatchEvent(
3071 new CustomEvent("desktop-mode-window-closed", { detail: closedDetail })
3072 );
3073 doAction(HOOKS.WINDOW_CLOSED, closedDetail);
3074 }
3075 /** Get a window by its ID. */
3076 getById(id) {
3077 return this._stack.find((w) => w.id === id);
3078 }
3079 /**
3080 * Get the most-recently-focused window for a given baseId.
3081 *
3082 * Multi-instance windows share a baseId; the stack is ordered
3083 * bottom to top by focus, so iterating from the end finds the
3084 * best candidate to bring forward when the user re-clicks the
3085 * dock icon.
3086 */
3087 getByBaseId(baseId) {
3088 for (let i = this._stack.length - 1; i >= 0; i--) {
3089 const w = this._stack[i];
3090 if ((w.config.baseId || w.id) === baseId) {
3091 return w;
3092 }
3093 }
3094 return void 0;
3095 }
3096 /**
3097 * Like {@link getByBaseId} but only considers windows on the
3098 * currently-active virtual desktop. The dock's "open or focus"
3099 * path uses this — a Plugins instance that lives on Desktop 2 is
3100 * invisible from Desktop 1's dock click, so clicking Plugins on
3101 * Desktop 1 should open a fresh instance there instead of trying
3102 * to focus the far-off sibling (which would silently do nothing
3103 * because the other desktop's windows are display: none here).
3104 */
3105 getByBaseIdOnActiveDesktop(baseId) {
3106 for (let i = this._stack.length - 1; i >= 0; i--) {
3107 const w = this._stack[i];
3108 if ((w.config.baseId || w.id) !== baseId) {
3109 continue;
3110 }
3111 const winDesktop = w.config.desktopId || this._activeDesktopId;
3112 if (winDesktop === this._activeDesktopId) {
3113 return w;
3114 }
3115 }
3116 return void 0;
3117 }
3118 /**
3119 * Get every open window sharing the given baseId, ordered by
3120 * instance slot (bare baseId first, then `-2`, `-3`, …) rather
3121 * than z-order — so the dock's instance rail keeps a stable
3122 * left-to-right order even as the user focuses between windows.
3123 */
3124 getAllByBaseId(baseId) {
3125 const instanceSlot = (id) => {
3126 if (id === baseId) {
3127 return 1;
3128 }
3129 const prefix = `${baseId}-`;
3130 if (id.startsWith(prefix)) {
3131 const n = parseInt(id.slice(prefix.length), 10);
3132 return Number.isFinite(n) ? n : 999;
3133 }
3134 return 999;
3135 };
3136 return this._stack.filter((w) => (w.config.baseId || w.id) === baseId).sort((a, b) => instanceSlot(a.id) - instanceSlot(b.id));
3137 }
3138 /** Get all open windows. */
3139 getAll() {
3140 return [...this._stack];
3141 }
3142 /**
3143 * Find the window whose iframe's contentWindow matches the given
3144 * message source. Used by cross-frame bridges to attribute inbound
3145 * `postMessage` events to the originating window without reaching
3146 * into `_stack`.
3147 */
3148 findByIframeSource(source) {
3149 if (!source) {
3150 return void 0;
3151 }
3152 return this._stack.find(
3153 (w) => w.iframe !== null && w.iframe.contentWindow === source
3154 );
3155 }
3156 /** Get the currently focused (topmost) window. */
3157 getFocused() {
3158 return this._stack.length > 0 ? this._stack[this._stack.length - 1] : void 0;
3159 }
3160 /**
3161 * "Is the window with this id currently in front of the user?"
3162 *
3163 * Returns true when the window exists in the manager AND it
3164 * isn't minimized AND it's the currently focused (topmost)
3165 * window. False otherwise — including for unknown ids, closed
3166 * windows, minimized windows, or windows that exist but aren't
3167 * on top.
3168 *
3169 * The canonical query for plugins implementing the "show
3170 * something *only when the user can't already see my
3171 * window*" pattern (badge counts, attention pulses, sounds,
3172 * toasts). Plugins that previously hand-rolled
3173 * `getById(id) && state !== 'minimized' && focused` can
3174 * collapse to this.
3175 *
3176 * @since 0.5.5
3177 *
3178 * @param id Window id to query.
3179 * @return True when the user is actively looking at this window.
3180 */
3181 isActive(id) {
3182 const win = this.getById(id);
3183 if (!win) {
3184 return false;
3185 }
3186 if (win.state === "minimized") {
3187 return false;
3188 }
3189 const focused = this.getFocused();
3190 return !!focused && focused.id === id;
3191 }
3192 // ---- Virtual desktop delegations ----
3193 getDesktops() {
3194 return getDesktops(this);
3195 }
3196 getActiveDesktop() {
3197 return getActiveDesktop(this);
3198 }
3199 getActiveDesktopId() {
3200 return getActiveDesktopId(this);
3201 }
3202 createDesktop() {
3203 return createDesktop(this);
3204 }
3205 switchDesktop(id, opts) {
3206 switchDesktop(this, id, opts);
3207 }
3208 closeDesktop(id) {
3209 closeDesktop(this, id);
3210 }
3211 /**
3212 * Returns the "primary" desktop id — the one new sessions land on
3213 * and that batch operations like {@link closeAll} treat as the
3214 * survivor when an `onlyOnPrimary` mode is requested.
3215 *
3216 * Default: the first desktop in `getDesktops()`. Filterable via
3217 * `desktop-mode.primary-desktop-id` so downstream code that wants a
3218 * different convention (e.g. a pinned "Inbox" desktop) can override
3219 * without having to fork the manager.
3220 *
3221 * @since 0.5.0
3222 */
3223 getPrimaryDesktopId() {
3224 const all2 = this.getDesktops();
3225 const fallback = all2.length > 0 ? all2[0].id : "desktop-1";
3226 const filtered = applyFilters(
3227 HOOKS.PRIMARY_DESKTOP_ID,
3228 fallback,
3229 all2
3230 );
3231 if (typeof filtered !== "string" || filtered === "") {
3232 return fallback;
3233 }
3234 const exists = all2.some((d) => d.id === filtered);
3235 return exists ? filtered : fallback;
3236 }
3237 /**
3238 * Close every open window in batch.
3239 *
3240 * Hook chain:
3241 *
3242 * 1. `desktop-mode.windows.before-close-all` — action. Subscribers
3243 * can prepare for the wipe (cancel pending saves, dismiss
3244 * menus, etc.). Detail: `{ candidates: Window[] }`.
3245 *
3246 * 2. `desktop-mode.windows.close-all` — filter. Receives the
3247 * candidate Window list and returns the (possibly smaller) list
3248 * that will actually be closed. Plugins use this to PROTECT
3249 * specific windows — e.g. keep a draft post window open during
3250 * a "Close all" operation. Returning an empty array cancels
3251 * the close entirely.
3252 *
3253 * 3. Each surviving window's `close()` is called.
3254 *
3255 * 4. `desktop-mode.windows.after-close-all` — action. Detail:
3256 * `{ closed: number, skipped: Window[] }`.
3257 *
3258 * @since 0.5.0
3259 *
3260 * @param options Close options.
3261 * @param options.exceptIds Window ids to skip even before the filter runs.
3262 * @return Number of windows actually closed.
3263 */
3264 closeAll(options) {
3265 const exceptSet = new Set(options?.exceptIds ?? []);
3266 const initialCandidates = this._stack.filter(
3267 (w) => !exceptSet.has(w.id)
3268 );
3269 doAction(HOOKS.WINDOWS_BEFORE_CLOSE_ALL, { candidates: initialCandidates });
3270 const filtered = applyFilters(
3271 HOOKS.WINDOWS_CLOSE_ALL,
3272 initialCandidates
3273 );
3274 const finalList = Array.isArray(filtered) ? filtered : initialCandidates;
3275 const skipped = initialCandidates.filter((w) => !finalList.includes(w));
3276 let closed = 0;
3277 for (const win of finalList.slice()) {
3278 try {
3279 win.close();
3280 closed++;
3281 } catch (err) {
3282 if (typeof console !== "undefined") {
3283 console.error(
3284 "[desktop-mode] closeAll: window.close() threw for",
3285 win.id,
3286 err
3287 );
3288 }
3289 }
3290 }
3291 doAction(HOOKS.WINDOWS_AFTER_CLOSE_ALL, { closed, skipped });
3292 return closed;
3293 }
3294 /**
3295 * Minimize every currently-non-minimized window. Returns the
3296 * exact set that was minimized — i.e., excludes windows already
3297 * in the `'minimized'` state — so callers can pair the call with
3298 * a later {@link restoreFrom} that touches only the windows
3299 * they minimized.
3300 *
3301 * The "Show Desktop" gesture (clicking the wallpaper) routes
3302 * through this method (and {@link restoreFrom} on the second
3303 * click); plugin authors building expand/collapse UIs that
3304 * mimic the gesture should use these primitives instead of
3305 * rolling the loop themselves.
3306 *
3307 * @public
3308 * @since 0.6.0
3309 */
3310 minimizeAll() {
3311 const minimized = [];
3312 for (const win of this._stack.slice()) {
3313 if (win.state === "minimized") {
3314 continue;
3315 }
3316 try {
3317 win.minimize();
3318 minimized.push(win);
3319 } catch (err) {
3320 if (typeof console !== "undefined") {
3321 console.error(
3322 "[desktop-mode] minimizeAll: window.minimize() threw for",
3323 win.id,
3324 err
3325 );
3326 }
3327 }
3328 }
3329 return minimized;
3330 }
3331 /**
3332 * Restore the given window list — the symmetric counterpart to
3333 * {@link minimizeAll}. Skips windows that have since been
3334 * closed and windows the user manually un-minimized between
3335 * the minimize and the restore.
3336 *
3337 * Pass the array {@link minimizeAll} returned to restore
3338 * exactly what you minimized; pass any subset to restore
3339 * selectively.
3340 *
3341 * @public
3342 * @since 0.6.0
3343 */
3344 restoreFrom(windows) {
3345 if (!Array.isArray(windows)) {
3346 return;
3347 }
3348 const live = new Set(this._stack);
3349 for (const win of windows) {
3350 if (!live.has(win)) {
3351 continue;
3352 }
3353 if (win.state !== "minimized") {
3354 continue;
3355 }
3356 try {
3357 win.restore();
3358 } catch (err) {
3359 if (typeof console !== "undefined") {
3360 console.error(
3361 "[desktop-mode] restoreFrom: window.restore() threw for",
3362 win.id,
3363 err
3364 );
3365 }
3366 }
3367 }
3368 }
3369 /**
3370 * Toggle the "Show Desktop" state — if every live window is
3371 * already minimized, restore them all; otherwise minimize the
3372 * non-minimized cohort. Returns `true` when the new state is
3373 * "showing the desktop" (everything minimized after the call),
3374 * `false` when windows have just been restored.
3375 *
3376 * Mirrors the wallpaper-click gesture exactly, in one call.
3377 *
3378 * @public
3379 * @since 0.6.0
3380 */
3381 toggleShowDesktop() {
3382 const all2 = this._stack.slice();
3383 if (all2.length === 0) {
3384 return false;
3385 }
3386 const allMinimized = all2.every((w) => w.state === "minimized");
3387 if (allMinimized) {
3388 for (const win of all2) {
3389 try {
3390 win.restore();
3391 } catch {
3392 }
3393 }
3394 return false;
3395 }
3396 this.minimizeAll();
3397 return true;
3398 }
3399 // ---- Arrange + snap delegations ----
3400 cascade() {
3401 cascade(this);
3402 }
3403 tile() {
3404 tile(this);
3405 }
3406 isSnapEnabled() {
3407 return this._snapEnabled;
3408 }
3409 setSnapEnabled(enabled) {
3410 setSnapEnabled(this, enabled);
3411 }
3412 getSnapConfig() {
3413 return getSnapConfig(this);
3414 }
3415 // ---- Overview delegations ----
3416 enterOverview() {
3417 enterOverview(this);
3418 }
3419 exitOverview(selected, maximize = false) {
3420 exitOverview(this, selected, maximize);
3421 }
3422 /**
3423 * Snapshot every open window's current geometry + state.
3424 *
3425 * Returns a plain array of `{ windowId, rect, state, element }`
3426 * entries — one per window in the stack, regardless of which
3427 * virtual desktop owns it. Rect coordinates are in desktop-area
3428 * space (the same coordinate space the windows themselves use
3429 * inline-style left/top); `state` is the live `WindowState`, and
3430 * `element` is the window's outer DOM node.
3431 *
3432 * Intended for wallpaper / overlay plugins that used to scrape
3433 * `document.querySelectorAll('.desktop-mode-window')` + read the
3434 * `--minimized` / `--maximized` modifier classes by name. The
3435 * accessor decouples plugin code from the shell's CSS class
3436 * naming, so a future refactor of modifier prefixes is not an
3437 * ecosystem break.
3438 *
3439 * The array contains every window in the stack — callers filter
3440 * on `state` if they want only "actually visible" (typically
3441 * `state !== 'minimized'`). Minimized windows are included so
3442 * plugins that care about the "will be restored to X geometry"
3443 * case still have the data; filtering them out would be a
3444 * subtraction the caller can do but the provider can't reverse.
3445 *
3446 * Order matches the internal z-stack: earliest-opened first,
3447 * focused window last.
3448 */
3449 getVisibleRects() {
3450 return this._stack.map((w) => {
3451 const snap = w.getSnapshot();
3452 return {
3453 windowId: w.id,
3454 rect: {
3455 x: snap.x,
3456 y: snap.y,
3457 width: snap.width,
3458 height: snap.height
3459 },
3460 state: snap.state,
3461 element: w.element
3462 };
3463 });
3464 }
3465 /**
3466 * Serialize the current window stack for session persistence.
3467 *
3468 * Order in the returned `windows` array mirrors z-order (earliest
3469 * opened / lowest-z first, focused last) so restoring preserves
3470 * the stacking the user left behind.
3471 */
3472 snapshot() {
3473 const focused = this.getFocused();
3474 const persistable = this._stack.filter((w) => !w.config.native);
3475 const windows = persistable.map((w) => {
3476 const snap = w.getSnapshot();
3477 const externalTabs = w.getExternalTabsSnapshot();
3478 return {
3479 id: w.id,
3480 baseId: w.config.baseId || w.id,
3481 desktopId: w.config.desktopId || this._activeDesktopId,
3482 url: w.getCurrentUrl(),
3483 title: w.config.title,
3484 icon: w.config.icon,
3485 state: snap.state,
3486 x: snap.x,
3487 y: snap.y,
3488 width: snap.width,
3489 height: snap.height,
3490 ...externalTabs.length > 0 ? { externalTabs } : {}
3491 };
3492 });
3493 const focusedId = focused && !focused.config.native ? focused.id : "";
3494 return {
3495 windows,
3496 desktops: this.getDesktops(),
3497 activeDesktop: this._activeDesktopId,
3498 focused: focusedId,
3499 updated: Math.floor(Date.now() / 1e3)
3500 };
3501 }
3502 seedDesktops(desktops, activeDesktopId) {
3503 seedDesktops(this, desktops, activeDesktopId);
3504 }
3505 }
3506 function cycleableWindows(mgr) {
3507 const activeDesktopId = mgr.getActiveDesktopId();
3508 const domOrder = Array.from(mgr._desktop.children);
3509 return mgr.getAll().filter((w) => {
3510 const winDesktop = w.config.desktopId || activeDesktopId;
3511 return winDesktop === activeDesktopId;
3512 }).sort(
3513 (a, b) => domOrder.indexOf(a.element) - domOrder.indexOf(b.element)
3514 );
3515 }
3516 function cycleFocus(mgr, direction) {
3517 if (mgr._overviewActive) {
3518 return;
3519 }
3520 const list2 = cycleableWindows(mgr);
3521 if (list2.length < 2) {
3522 return;
3523 }
3524 const focused = mgr.getFocused();
3525 const currentIdx = focused ? list2.indexOf(focused) : -1;
3526 const step = direction === "next" ? 1 : -1;
3527 const nextIdx = (currentIdx + step + list2.length) % list2.length;
3528 const target2 = list2[nextIdx];
3529 if (target2.state === "minimized") {
3530 target2.restore();
3531 } else {
3532 mgr.focus(target2);
3533 }
3534 }
3535 let installed$3 = false;
3536 function isTextEntryFocus(doc) {
3537 let el = doc.activeElement;
3538 while (el && el.shadowRoot && el.shadowRoot.activeElement) {
3539 el = el.shadowRoot.activeElement;
3540 }
3541 if (!el) {
3542 return false;
3543 }
3544 if (el instanceof HTMLIFrameElement) {
3545 return true;
3546 }
3547 if (el instanceof HTMLTextAreaElement) {
3548 return true;
3549 }
3550 if (el instanceof HTMLInputElement) {
3551 const textTypes = /* @__PURE__ */ new Set([
3552 "text",
3553 "search",
3554 "url",
3555 "email",
3556 "password",
3557 "tel",
3558 "number",
3559 "date",
3560 "datetime-local",
3561 "month",
3562 "week",
3563 "time"
3564 ]);
3565 return textTypes.has(el.type);
3566 }
3567 if (el instanceof HTMLElement && el.isContentEditable === true) {
3568 return true;
3569 }
3570 const ce = el.getAttribute("contenteditable");
3571 return ce !== null && ce !== "false";
3572 }
3573 function installWindowSwitcherShortcut(mgr) {
3574 if (installed$3) {
3575 return;
3576 }
3577 installed$3 = true;
3578 document.addEventListener(
3579 "keydown",
3580 (e) => {
3581 if (e.ctrlKey || e.metaKey || e.altKey) {
3582 return;
3583 }
3584 if (e.code !== "Backquote") {
3585 return;
3586 }
3587 if (isTextEntryFocus(document)) {
3588 return;
3589 }
3590 e.preventDefault();
3591 cycleFocus(mgr, e.shiftKey ? "prev" : "next");
3592 },
3593 true
3594 );
3595 const origin = window.location.origin;
3596 window.addEventListener("message", (e) => {
3597 if (e.origin !== origin) {
3598 return;
3599 }
3600 const data = e.data;
3601 if (!data || data.type !== "desktop-mode-window-switch") {
3602 return;
3603 }
3604 cycleFocus(mgr, data.direction === "prev" ? "prev" : "next");
3605 });
3606 }
3607 function switchToAdjacentDesktop(mgr, direction) {
3608 const desktops = mgr.getDesktops();
3609 if (desktops.length < 2) {
3610 return false;
3611 }
3612 const activeId = mgr.getActiveDesktopId();
3613 const idx = desktops.findIndex((d) => d.id === activeId);
3614 if (idx === -1) {
3615 return false;
3616 }
3617 const step = direction === "next" ? 1 : -1;
3618 const targetIdx = (idx + step + desktops.length) % desktops.length;
3619 if (targetIdx === idx) {
3620 return false;
3621 }
3622 mgr.switchDesktop(desktops[targetIdx].id, { direction });
3623 return true;
3624 }
3625 function cycleOverviewCursor(mgr, direction) {
3626 if (!mgr._overviewActive) {
3627 return false;
3628 }
3629 const desktops = mgr.getDesktops();
3630 const cycleLength = desktops.length + 1;
3631 const ADD_INDEX = desktops.length;
3632 const currentIdx = mgr._overviewAddTileFocused ? ADD_INDEX : desktops.findIndex((d) => d.id === mgr.getActiveDesktopId());
3633 if (currentIdx === -1) {
3634 return false;
3635 }
3636 const step = direction === "next" ? 1 : -1;
3637 const targetIdx = (currentIdx + step + cycleLength) % cycleLength;
3638 if (targetIdx === currentIdx) {
3639 return false;
3640 }
3641 if (targetIdx === ADD_INDEX) {
3642 mgr._overviewAddTileFocused = true;
3643 refreshOverviewTopBar(mgr);
3644 return true;
3645 }
3646 mgr._overviewAddTileFocused = false;
3647 mgr.switchDesktop(desktops[targetIdx].id, { direction });
3648 return true;
3649 }
3650 function toggleOverview(mgr) {
3651 if (mgr._overviewActive) {
3652 mgr.exitOverview();
3653 } else {
3654 mgr.enterOverview();
3655 }
3656 return true;
3657 }
3658 function toggleShowDesktop(mgr) {
3659 if (mgr._overviewActive) {
3660 return false;
3661 }
3662 if (mgr.getAll().length === 0) {
3663 return false;
3664 }
3665 mgr.toggleShowDesktop();
3666 return true;
3667 }
3668 function exitOverviewIfActive(mgr) {
3669 if (!mgr._overviewActive) {
3670 return false;
3671 }
3672 mgr.exitOverview();
3673 return true;
3674 }
3675 function isShowDesktopActive(mgr) {
3676 const all2 = mgr.getAll();
3677 if (all2.length === 0) {
3678 return false;
3679 }
3680 return all2.every((w) => w.state === "minimized");
3681 }
3682 function exitShowDesktopIfActive(mgr) {
3683 if (!isShowDesktopActive(mgr)) {
3684 return false;
3685 }
3686 mgr.toggleShowDesktop();
3687 return true;
3688 }
3689 let installed$2 = false;
3690 function installDesktopArrowShortcuts(mgr) {
3691 if (installed$2) {
3692 return;
3693 }
3694 installed$2 = true;
3695 document.addEventListener(
3696 "keydown",
3697 (e) => {
3698 if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) {
3699 return;
3700 }
3701 if (e.code !== "ArrowLeft" && e.code !== "ArrowRight" && e.code !== "ArrowUp" && e.code !== "ArrowDown") {
3702 return;
3703 }
3704 if (isTextEntryFocus(document)) {
3705 return;
3706 }
3707 let handled = false;
3708 switch (e.code) {
3709 case "ArrowLeft":
3710 handled = mgr._overviewActive ? cycleOverviewCursor(mgr, "prev") : switchToAdjacentDesktop(mgr, "prev");
3711 break;
3712 case "ArrowRight":
3713 handled = mgr._overviewActive ? cycleOverviewCursor(mgr, "next") : switchToAdjacentDesktop(mgr, "next");
3714 break;
3715 case "ArrowUp":
3716 handled = exitOverviewIfActive(mgr) || exitShowDesktopIfActive(mgr) || toggleOverview(mgr);
3717 break;
3718 case "ArrowDown":
3719 handled = exitOverviewIfActive(mgr) || toggleShowDesktop(mgr);
3720 break;
3721 }
3722 if (handled) {
3723 e.preventDefault();
3724 }
3725 },
3726 true
3727 );
3728 }
3729 const IDENTITY_PARAMS = [
3730 "post_type",
3731 "page",
3732 "taxonomy",
3733 // WooCommerce (and other React-app-style plugins) register
3734 // SEPARATE top-level admin menus that all share `?page=wc-admin`
3735 // and only differ by `path` (e.g. `path=/analytics/overview`,
3736 // `path=/marketing`). Without `path` in the identity set, every
3737 // such menu collapses to the same window id — opening any one of
3738 // them lights up the dock indicator for ALL of them. WC's
3739 // /admin/path query is the most prominent example today; future
3740 // plugins that route inside `admin.php?page=` via a custom param
3741 // can either piggyback on `path` or grow this list.
3742 "path",
3743 // The post ID on `post.php?post=X&action=edit`. Without this, every
3744 // individual post edit URL collapses to `post-php`, so clicking a
3745 // second row in the Posts window just refocuses the first post's
3746 // window instead of opening the new one.
3747 "post",
3748 // Site-editor entity path: `site-editor.php?p=/wp_template_part/
3749 // twentytwentyfive//footer-columns`. Each template / template
3750 // part / pattern / navigation entity is a distinct "page" from
3751 // the user's perspective — picking "Header" after "Footer column"
3752 // should open a new window, not refocus the existing footer one.
3753 // Without `p` in identity, every site-editor URL collapses to
3754 // `site-editor-php` and the second pick is a no-op.
3755 "p"
3756 ];
3757 function slugify$1(path) {
3758 return path.replace(/\.php/g, "-php").replace(/[?&=]/g, "-").replace(/[^a-zA-Z0-9_-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "") || "index";
3759 }
3760 function deriveWindowId(url, adminUrl) {
3761 let parsed = null;
3762 try {
3763 parsed = new URL(url, adminUrl);
3764 } catch (err) {
3765 parsed = null;
3766 }
3767 if (parsed) {
3768 const basePath = new URL(adminUrl).pathname;
3769 const filename = parsed.pathname.replace(basePath, "").replace(/^\/+/, "");
3770 const significant = new URLSearchParams();
3771 for (const key of IDENTITY_PARAMS) {
3772 const value = parsed.searchParams.get(key);
3773 if (value) {
3774 significant.set(key, value);
3775 }
3776 }
3777 const query = significant.toString();
3778 return slugify$1(query ? `${filename}?${query}` : filename);
3779 }
3780 let path = url.replace(adminUrl, "");
3781 if (path.startsWith("/")) {
3782 path = path.substring(1);
3783 }
3784 return slugify$1(path);
3785 }
3786 function sanitizeClassName(value) {
3787 return value.replace(/[^a-zA-Z0-9_-]/g, "");
3788 }
3789 function applyTileEntryStagger(tile2) {
3790 tile2.style.setProperty(
3791 "--desktop-mode-file-tile-enter-delay",
3792 `${(Math.random() * 0.25).toFixed(3)}s`
3793 );
3794 tile2.style.setProperty(
3795 "--desktop-mode-file-tile-enter-duration",
3796 `${(0.3 + Math.random() * 0.25).toFixed(3)}s`
3797 );
3798 }
3799 function urlMatchKey(url) {
3800 try {
3801 const parsed = new URL(url, window.location.origin);
3802 parsed.searchParams.delete("desktop_mode_chromeless");
3803 parsed.searchParams.delete("desktop_mode_portal");
3804 return parsed.pathname.replace(/\/+$/, "") + "?" + parsed.searchParams.toString();
3805 } catch {
3806 return url;
3807 }
3808 }
3809 function sanitizeIconSvg(svg) {
3810 if (typeof svg !== "string" || svg === "") {
3811 return "";
3812 }
3813 if (typeof DOMParser === "undefined") {
3814 return "";
3815 }
3816 let doc;
3817 try {
3818 doc = new DOMParser().parseFromString(svg, "image/svg+xml");
3819 } catch {
3820 return "";
3821 }
3822 const root = doc.documentElement;
3823 if (!root || root.nodeName.toLowerCase() !== "svg") {
3824 return "";
3825 }
3826 if (doc.getElementsByTagName("parsererror").length > 0) {
3827 return "";
3828 }
3829 const BANNED_TAGS = /* @__PURE__ */ new Set(["script", "style", "foreignobject", "iframe", "object", "embed"]);
3830 const walk2 = (el) => {
3831 const children = Array.from(el.children);
3832 for (const child of children) {
3833 if (BANNED_TAGS.has(child.nodeName.toLowerCase())) {
3834 child.remove();
3835 continue;
3836 }
3837 for (const attr of Array.from(child.attributes)) {
3838 const name = attr.name.toLowerCase();
3839 const value = attr.value.trim().toLowerCase();
3840 if (name.startsWith("on")) {
3841 child.removeAttribute(attr.name);
3842 continue;
3843 }
3844 if (value.startsWith("javascript:")) {
3845 child.removeAttribute(attr.name);
3846 }
3847 }
3848 walk2(child);
3849 }
3850 };
3851 walk2(root);
3852 for (const attr of Array.from(root.attributes)) {
3853 const name = attr.name.toLowerCase();
3854 const value = attr.value.trim().toLowerCase();
3855 if (name.startsWith("on") || value.startsWith("javascript:")) {
3856 root.removeAttribute(attr.name);
3857 }
3858 }
3859 return root.outerHTML;
3860 }
3861 const _parentSubs = /* @__PURE__ */ new Map();
3862 const _nativeSubs = /* @__PURE__ */ new Map();
3863 function bucket(root, windowId, channel, create) {
3864 let perWindow = root.get(windowId);
3865 if (!perWindow) {
3866 if (!create) {
3867 return void 0;
3868 }
3869 perWindow = /* @__PURE__ */ new Map();
3870 root.set(windowId, perWindow);
3871 }
3872 let bucketSet = perWindow.get(channel);
3873 if (!bucketSet) {
3874 if (!create) {
3875 return void 0;
3876 }
3877 bucketSet = /* @__PURE__ */ new Set();
3878 perWindow.set(channel, bucketSet);
3879 }
3880 return bucketSet;
3881 }
3882 function dispatch(root, windowId, channel, payload) {
3883 const meta = { channel, windowId };
3884 const exact = bucket(root, windowId, channel, false);
3885 if (exact) {
3886 for (const cb of Array.from(exact)) {
3887 try {
3888 cb(payload, meta);
3889 } catch (err) {
3890 if (typeof console !== "undefined") {
3891 console.error(
3892 `[desktop-mode] window-channel subscriber for "${channel}" threw:`,
3893 err
3894 );
3895 }
3896 }
3897 }
3898 }
3899 const wildcard = bucket(root, windowId, "*", false);
3900 if (wildcard) {
3901 for (const cb of Array.from(wildcard)) {
3902 try {
3903 cb(payload, meta);
3904 } catch (err) {
3905 if (typeof console !== "undefined") {
3906 console.error(
3907 `[desktop-mode] window-channel wildcard subscriber for "${windowId}" threw:`,
3908 err
3909 );
3910 }
3911 }
3912 }
3913 }
3914 }
3915 function addParentSubscriber(windowId, channel, cb) {
3916 const set = bucket(_parentSubs, windowId, channel, true);
3917 set.add(cb);
3918 let removed = false;
3919 return () => {
3920 if (removed) {
3921 return;
3922 }
3923 removed = true;
3924 set.delete(cb);
3925 };
3926 }
3927 function dispatchFromWindow(windowId, channel, payload) {
3928 dispatch(_parentSubs, windowId, channel, payload);
3929 }
3930 function dispatchToNative(windowId, channel, payload) {
3931 dispatch(_nativeSubs, windowId, channel, payload);
3932 }
3933 const _readyWindows = /* @__PURE__ */ new Set();
3934 const _loadingWindows = /* @__PURE__ */ new Set();
3935 const _pendingSends = /* @__PURE__ */ new Map();
3936 function markWindowContentReady(windowId) {
3937 if (!_readyWindows.has(windowId)) {
3938 _readyWindows.add(windowId);
3939 const queued = _pendingSends.get(windowId);
3940 if (queued) {
3941 _pendingSends.delete(windowId);
3942 for (const m of queued) {
3943 try {
3944 m.flush();
3945 } catch (err) {
3946 if (typeof console !== "undefined") {
3947 console.error(
3948 `[desktop-mode] flushing queued window-send for "${m.channel}" threw:`,
3949 err
3950 );
3951 }
3952 }
3953 }
3954 }
3955 }
3956 if (_loadingWindows.delete(windowId)) {
3957 doAction(HOOKS.WINDOW_CONTENT_LOADED, { windowId });
3958 if (typeof document !== "undefined") {
3959 document.dispatchEvent(
3960 new CustomEvent("desktop-mode-window-content-loaded", {
3961 detail: { windowId }
3962 })
3963 );
3964 }
3965 }
3966 }
3967 const WINDOW_CONFIG_KEY = Symbol.for("desktop-mode/window-config");
3968 function getWindowConfigFromElement(el) {
3969 return el[WINDOW_CONFIG_KEY];
3970 }
3971 function buildDefaultLoadingOverlay() {
3972 const overlay = document.createElement("div");
3973 overlay.className = "desktop-mode-window__loading";
3974 overlay.setAttribute("aria-hidden", "true");
3975 const spinner = document.createElement("wpd-spinner");
3976 spinner.setAttribute("preset", "classic");
3977 spinner.setAttribute("size", "clamp(96px, 14vw, 192px)");
3978 spinner.setAttribute("label", __("Loading window content"));
3979 overlay.appendChild(spinner);
3980 return overlay;
3981 }
3982 function createLoadingOverlay(config) {
3983 let overlay = buildDefaultLoadingOverlay();
3984 const ctx = { windowId: config.id, config };
3985 if (typeof config.loading?.render === "function") {
3986 try {
3987 config.loading.render(overlay, ctx);
3988 } catch (err) {
3989 if (typeof console !== "undefined") {
3990 console.error(
3991 `[desktop-mode] loading.render threw for "${config.id}":`,
3992 err
3993 );
3994 }
3995 }
3996 }
3997 try {
3998 const filtered = applyFilters(
3999 HOOKS.WINDOW_LOADING_OVERLAY,
4000 overlay,
4001 ctx
4002 );
4003 if (filtered instanceof HTMLElement) {
4004 overlay = filtered;
4005 }
4006 } catch (err) {
4007 if (typeof console !== "undefined") {
4008 console.error(
4009 `[desktop-mode] WINDOW_LOADING_OVERLAY filter threw for "${config.id}":`,
4010 err
4011 );
4012 }
4013 }
4014 if (overlay && !overlay.classList.contains("desktop-mode-window__loading")) {
4015 overlay.classList.add("desktop-mode-window__loading");
4016 }
4017 return overlay;
4018 }
4019 function removeLoadingOverlay(windowEl) {
4020 const overlay = windowEl.querySelector(":scope .desktop-mode-window__loading");
4021 overlay?.remove();
4022 }
4023 function ensureLoadingOverlay(windowEl) {
4024 const body = windowEl.querySelector(
4025 ":scope .desktop-mode-window__body"
4026 );
4027 if (!body) {
4028 return;
4029 }
4030 const existing = body.querySelector(":scope .desktop-mode-window__loading");
4031 if (existing) {
4032 return;
4033 }
4034 const config = getWindowConfigFromElement(windowEl);
4035 body.appendChild(config ? createLoadingOverlay(config) : buildDefaultLoadingOverlay());
4036 }
4037 const FADE_OUT_MS$1 = 250;
4038 let _installed$3 = false;
4039 function findWindowElement(windowId) {
4040 if (!windowId) {
4041 return null;
4042 }
4043 return document.getElementById(`wp-window-${windowId}`);
4044 }
4045 function installWindowLoadingTransitions() {
4046 if (_installed$3) {
4047 return;
4048 }
4049 _installed$3 = true;
4050 _installSubscriptions();
4051 }
4052 function _installSubscriptions() {
4053 addAction(
4054 HOOKS.WINDOW_CONTENT_LOADING,
4055 "desktop-mode/window-loading-enter",
4056 (e) => {
4057 const el = findWindowElement(e?.windowId ?? "");
4058 if (!el) {
4059 return;
4060 }
4061 const body = el.querySelector(
4062 ":scope .desktop-mode-window__body"
4063 );
4064 if (!body) {
4065 return;
4066 }
4067 body.classList.add("desktop-mode-window__body--loading");
4068 ensureLoadingOverlay(el);
4069 }
4070 );
4071 addAction(
4072 HOOKS.WINDOW_CONTENT_LOADED,
4073 "desktop-mode/window-loading-exit",
4074 (e) => {
4075 const el = findWindowElement(e?.windowId ?? "");
4076 if (!el) {
4077 return;
4078 }
4079 const body = el.querySelector(
4080 ":scope .desktop-mode-window__body"
4081 );
4082 if (!body) {
4083 return;
4084 }
4085 body.classList.remove("desktop-mode-window__body--loading");
4086 window.setTimeout(() => {
4087 if (!body.classList.contains("desktop-mode-window__body--loading")) {
4088 removeLoadingOverlay(el);
4089 }
4090 }, FADE_OUT_MS$1);
4091 }
4092 );
4093 addAction(
4094 HOOKS.INIT,
4095 "desktop-mode/loading-overlay-init-sweep",
4096 () => {
4097 queueMicrotask(() => repaintLoadingOverlays());
4098 }
4099 );
4100 }
4101 function repaintLoadingOverlays() {
4102 const bodies = document.querySelectorAll(
4103 ".desktop-mode-window__body--loading"
4104 );
4105 bodies.forEach((body) => {
4106 const windowEl = body.closest(".desktop-mode-window");
4107 if (!windowEl) {
4108 return;
4109 }
4110 body.querySelector(":scope .desktop-mode-window__loading")?.remove();
4111 ensureLoadingOverlay(windowEl);
4112 });
4113 }
4114 const SHARED_STORES_SLOT = "__desktopModeSharedStores";
4115 function resolveSlot() {
4116 const w = window;
4117 let slot = w[SHARED_STORES_SLOT];
4118 if (!slot) {
4119 slot = /* @__PURE__ */ new Map();
4120 w[SHARED_STORES_SLOT] = slot;
4121 }
4122 return slot;
4123 }
4124 function createSharedStore(key, initialState) {
4125 const slot = resolveSlot();
4126 let record = slot.get(key);
4127 if (!record) {
4128 record = {
4129 state: initialState(),
4130 listeners: /* @__PURE__ */ new Set(),
4131 rebuild: initialState
4132 };
4133 slot.set(key, record);
4134 }
4135 const handle = {
4136 // `record.state` is the live reference. The getter on the
4137 // `state` field reads the latest value even if `reset()`
4138 // reassigned it to a fresh object.
4139 get state() {
4140 return record.state;
4141 },
4142 set state(next) {
4143 record.state = next;
4144 },
4145 getState() {
4146 return record.state;
4147 },
4148 notify() {
4149 for (const cb of Array.from(record.listeners)) {
4150 try {
4151 cb(record.state);
4152 } catch (err) {
4153 console.error(
4154 `[desktop-mode/shared-store:${key}] subscriber threw:`,
4155 err
4156 );
4157 }
4158 }
4159 },
4160 subscribe(cb) {
4161 record.listeners.add(cb);
4162 return () => {
4163 record.listeners.delete(cb);
4164 };
4165 },
4166 setState(patch) {
4167 const cur = record.state;
4168 if (typeof cur !== "object" || cur === null) {
4169 console.warn(
4170 `[desktop-mode/shared-store:${key}] setState called on a primitive store; use the state setter instead.`
4171 );
4172 return;
4173 }
4174 Object.assign(cur, patch);
4175 handle.notify();
4176 },
4177 reset() {
4178 const fresh = record.rebuild();
4179 const cur = record.state;
4180 if (typeof cur === "object" && cur !== null && typeof fresh === "object" && fresh !== null) {
4181 const target2 = cur;
4182 for (const k of Object.keys(target2)) {
4183 delete target2[k];
4184 }
4185 Object.assign(target2, fresh);
4186 } else {
4187 record.state = fresh;
4188 }
4189 record.listeners.clear();
4190 }
4191 };
4192 return handle;
4193 }
4194 const remapStore = createSharedStore(
4195 "desktop-mode/native-url-remap",
4196 () => ({ remaps: [], deps: null })
4197 );
4198 function bindNativeUrlRemap(bound) {
4199 remapStore.state.deps = bound;
4200 }
4201 function registerNativeUrlRemap(entry) {
4202 if (!entry || typeof entry.id !== "string" || entry.id.trim() === "") {
4203 return () => {
4204 };
4205 }
4206 if (typeof entry.nativeWindowId !== "string" || entry.nativeWindowId === "") {
4207 return () => {
4208 };
4209 }
4210 if (typeof entry.matches !== "function") {
4211 return () => {
4212 };
4213 }
4214 const remaps = remapStore.state.remaps;
4215 const existingIdx = remaps.findIndex((r) => r.id === entry.id);
4216 if (existingIdx >= 0) {
4217 remaps.splice(existingIdx, 1);
4218 }
4219 remaps.push(entry);
4220 return () => unregisterNativeUrlRemap(entry.id);
4221 }
4222 function unregisterNativeUrlRemap(id) {
4223 const remaps = remapStore.state.remaps;
4224 const i = remaps.findIndex((r) => r.id === id);
4225 if (i >= 0) {
4226 remaps.splice(i, 1);
4227 }
4228 }
4229 function resolveNativeUrlRemap(url) {
4230 const { deps: deps2, remaps } = remapStore.state;
4231 if (!deps2 || !url) {
4232 return null;
4233 }
4234 let parsed;
4235 try {
4236 parsed = new URL(url, deps2.adminUrl);
4237 } catch {
4238 return null;
4239 }
4240 const snapshot = deps2.getSnapshot();
4241 for (const entry of remaps) {
4242 if (!entry.matches(url, parsed)) {
4243 continue;
4244 }
4245 if (entry.enabled && !entry.enabled(snapshot)) {
4246 continue;
4247 }
4248 return entry.nativeWindowId;
4249 }
4250 return null;
4251 }
4252 function tryNativeUrlRemap(url) {
4253 const { deps: deps2, remaps } = remapStore.state;
4254 if (!deps2 || !url) {
4255 return false;
4256 }
4257 let parsed;
4258 try {
4259 parsed = new URL(url, deps2.adminUrl);
4260 } catch {
4261 return false;
4262 }
4263 const snapshot = deps2.getSnapshot();
4264 for (const entry of remaps) {
4265 if (!entry.matches(url, parsed)) {
4266 continue;
4267 }
4268 if (entry.enabled && !entry.enabled(snapshot)) {
4269 continue;
4270 }
4271 if (entry.onMatch) {
4272 try {
4273 entry.onMatch(url, parsed);
4274 } catch (err) {
4275 console.warn(
4276 `[desktop-mode] URL remap onMatch hook threw for "${entry.id}":`,
4277 err
4278 );
4279 }
4280 }
4281 if (deps2.openById(entry.nativeWindowId)) {
4282 return true;
4283 }
4284 }
4285 return false;
4286 }
4287 const HOOK_PREFIX = "desktop-mode.activity.";
4288 function hookName(channel) {
4289 return `${HOOK_PREFIX}${String(channel)}`;
4290 }
4291 let subscribeSeq = 0;
4292 const activity = {
4293 publish(channel, payload) {
4294 doAction(hookName(channel), payload);
4295 },
4296 subscribe(channel, cb) {
4297 const ns = `desktop-mode/activity-sub/${++subscribeSeq}`;
4298 const hook = hookName(channel);
4299 addAction(
4300 hook,
4301 ns,
4302 (payload) => cb(payload)
4303 );
4304 let removed = false;
4305 return () => {
4306 if (removed) {
4307 return;
4308 }
4309 removed = true;
4310 removeAction(hook, ns);
4311 };
4312 },
4313 filter(channel, value, ...args) {
4314 return applyFilters(hookName(channel), value, ...args);
4315 }
4316 };
4317 const DEFAULT_DURATION_MS = 4e3;
4318 const FADE_OUT_MS = 200;
4319 function showToast(options) {
4320 const intent = activity.filter(
4321 "desktop-mode/toast-requested",
4322 { ...options }
4323 );
4324 if (!intent || intent.cancel === true) {
4325 return () => void 0;
4326 }
4327 let dismissRequested = false;
4328 let realDismiss = null;
4329 openWithShellOverlays(
4330 () => !dismissRequested,
4331 () => {
4332 realDismiss = renderToast(intent);
4333 }
4334 );
4335 return () => {
4336 dismissRequested = true;
4337 if (realDismiss) {
4338 realDismiss();
4339 }
4340 };
4341 }
4342 function renderToast(intent) {
4343 const container = ensureContainer();
4344 const toast = document.createElement("wpd-toast");
4345 toast.textContent = intent.message;
4346 if (intent.action) {
4347 toast.setAttribute("action", intent.action.label);
4348 toast.addEventListener("wpd-toast-action", () => {
4349 intent.action?.onClick();
4350 dismiss();
4351 });
4352 }
4353 container.appendChild(toast);
4354 let dismissed = false;
4355 let dismissTimer = null;
4356 const dismiss = () => {
4357 if (dismissed) {
4358 return;
4359 }
4360 dismissed = true;
4361 if (dismissTimer !== null) {
4362 window.clearTimeout(dismissTimer);
4363 dismissTimer = null;
4364 }
4365 toast.setAttribute("state", "out");
4366 window.setTimeout(() => {
4367 toast.remove();
4368 }, FADE_OUT_MS);
4369 };
4370 requestAnimationFrame(() => {
4371 toast.setAttribute("state", "in");
4372 });
4373 dismissTimer = window.setTimeout(
4374 dismiss,
4375 intent.duration ?? DEFAULT_DURATION_MS
4376 );
4377 activity.publish("desktop-mode/toast-shown", { ...intent });
4378 return dismiss;
4379 }
4380 function ensureContainer() {
4381 const existing = document.querySelector(
4382 "wpd-toast-container"
4383 );
4384 if (existing) {
4385 return existing;
4386 }
4387 const el = document.createElement("wpd-toast-container");
4388 document.body.appendChild(el);
4389 return el;
4390 }
4391 const store$e = createSharedStore(
4392 "desktop-mode/destructive-admin-actions",
4393 () => ({ entries: [] })
4394 );
4395 function registerDestructiveAdminAction(entry) {
4396 if (!entry || typeof entry.id !== "string" || entry.id.trim() === "") {
4397 return () => {
4398 };
4399 }
4400 if (typeof entry.matches !== "function") {
4401 return () => {
4402 };
4403 }
4404 const entries = store$e.state.entries;
4405 const idx = entries.findIndex((e) => e.id === entry.id);
4406 if (idx >= 0) {
4407 entries.splice(idx, 1);
4408 }
4409 entries.push(entry);
4410 return () => unregisterDestructiveAdminAction(entry.id);
4411 }
4412 function unregisterDestructiveAdminAction(id) {
4413 const entries = store$e.state.entries;
4414 const idx = entries.findIndex((e) => e.id === id);
4415 if (idx >= 0) {
4416 entries.splice(idx, 1);
4417 }
4418 }
4419 function listDestructiveAdminActions() {
4420 return store$e.state.entries.slice();
4421 }
4422 const adminLinkDepsStore = createSharedStore(
4423 "desktop-mode/admin-link-deps",
4424 () => ({ deps: null })
4425 );
4426 function bindAdminLinkDispatch(deps2) {
4427 adminLinkDepsStore.state.deps = deps2;
4428 }
4429 function collectRegistrationErrors(def, checks) {
4430 if (!def || typeof def !== "object") {
4431 return ["def (not an object)"];
4432 }
4433 const d = def;
4434 const errors = [];
4435 for (const check of checks) {
4436 if (!check.valid(d)) {
4437 errors.push(`${check.field} (${check.message})`);
4438 }
4439 }
4440 return errors;
4441 }
4442 class RegistrationError extends Error {
4443 constructor(kind, errors, def) {
4444 super(
4445 `[desktop-mode] ${kind} registration rejected — fields: ` + errors.join(", ") + "."
4446 );
4447 this.name = "RegistrationError";
4448 this.kind = kind;
4449 this.errors = errors;
4450 this.def = def;
4451 }
4452 }
4453 function throwOnRegistrationErrors(kind, errors, def) {
4454 if (errors.length === 0) {
4455 return;
4456 }
4457 throw new RegistrationError(kind, errors, def);
4458 }
4459 const store$d = createSharedStore(
4460 "desktop-mode/wallpaper-registry",
4461 () => ({
4462 seed: [],
4463 listeners: /* @__PURE__ */ new Set()
4464 })
4465 );
4466 const seed$3 = store$d.state.seed;
4467 const listeners$c = store$d.state.listeners;
4468 function register$2(def) {
4469 throwOnRegistrationErrors(
4470 "Wallpaper",
4471 collectRegistrationErrors(def, WALLPAPER_CHECKS),
4472 def
4473 );
4474 const idx = seed$3.findIndex((w) => w.id === def.id);
4475 if (idx >= 0) {
4476 seed$3[idx] = def;
4477 } else {
4478 seed$3.push(def);
4479 }
4480 notify$e();
4481 }
4482 function unregister$2(id) {
4483 const idx = seed$3.findIndex((w) => w.id === id);
4484 if (idx >= 0) {
4485 seed$3.splice(idx, 1);
4486 notify$e();
4487 }
4488 }
4489 function notify$e() {
4490 const snapshot = Array.from(listeners$c);
4491 for (const cb of snapshot) {
4492 try {
4493 cb();
4494 } catch (err) {
4495 if (typeof console !== "undefined") {
4496 console.error(
4497 "[desktop-mode] wallpaper registry listener threw:",
4498 err
4499 );
4500 }
4501 }
4502 }
4503 }
4504 function all$1() {
4505 const copy = seed$3.slice();
4506 const filtered = applyFilters(HOOKS.WALLPAPERS, copy);
4507 if (!Array.isArray(filtered)) {
4508 if (typeof console !== "undefined") {
4509 console.warn(
4510 "[desktop-mode] `desktop-mode.wallpapers` filter returned a non-array; falling back to seed list."
4511 );
4512 }
4513 return copy;
4514 }
4515 return filtered.filter(isValidDef$1);
4516 }
4517 function get$1(id) {
4518 return all$1().find((w) => w.id === id);
4519 }
4520 const WALLPAPER_CHECKS = [
4521 {
4522 field: "id",
4523 message: "missing or not a non-empty string",
4524 valid: (d) => typeof d.id === "string" && d.id !== ""
4525 },
4526 {
4527 field: "label",
4528 message: "missing or not a non-empty string",
4529 valid: (d) => typeof d.label === "string" && d.label !== ""
4530 },
4531 {
4532 field: "preview",
4533 message: "missing or not a non-empty string",
4534 valid: (d) => typeof d.preview === "string" && d.preview !== ""
4535 },
4536 {
4537 field: "type",
4538 message: 'must be "css" or "canvas"',
4539 valid: (d) => d.type === "css" || d.type === "canvas"
4540 },
4541 {
4542 field: "value/resolveValue/mount",
4543 message: "css types need `value` or `resolveValue`; canvas types need `mount`",
4544 valid: (d) => {
4545 if (d.type === "css") {
4546 return typeof d.value === "string" || typeof d.resolveValue === "function";
4547 }
4548 if (d.type === "canvas") {
4549 return typeof d.mount === "function";
4550 }
4551 return true;
4552 }
4553 }
4554 ];
4555 function isValidDef$1(def) {
4556 return collectRegistrationErrors(def, WALLPAPER_CHECKS).length === 0;
4557 }
4558 const STORAGE_KEY = "desktop-mode-os-settings";
4559 const CUSTOM_GRADIENT_ID = "custom-gradient";
4560 const CUSTOM_IMAGE_ID = "custom-image";
4561 const DEFAULT_WALLPAPER_ID = "dark";
4562 const DEFAULT_ACCENTS = [
4563 { id: "wp-blue", label: "WordPress Blue", value: "#2271b1" },
4564 { id: "indigo", label: "Indigo", value: "#3858e9" },
4565 { id: "teal", label: "Teal", value: "#04a4cc" },
4566 { id: "emerald", label: "Emerald", value: "#059669" },
4567 { id: "amber", label: "Amber", value: "#d97706" },
4568 { id: "rose", label: "Rose", value: "#e11d48" }
4569 ];
4570 function getAccents() {
4571 const config = window.wp?.desktop?.config;
4572 const raw = config?.accentColors;
4573 if (!Array.isArray(raw) || raw.length === 0) {
4574 return DEFAULT_ACCENTS;
4575 }
4576 const clean = [];
4577 for (const entry of raw) {
4578 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)) {
4579 clean.push({ id: entry.id, label: entry.label, value: entry.value });
4580 }
4581 }
4582 return clean.length > 0 ? clean : DEFAULT_ACCENTS;
4583 }
4584 function getDefaultWallpaperId() {
4585 const config = window.wp?.desktop?.config;
4586 const raw = config?.defaultWallpaper;
4587 if (typeof raw === "string" && raw !== "") {
4588 return raw;
4589 }
4590 return DEFAULT_WALLPAPER_ID;
4591 }
4592 const DOCK_SIZES = [
4593 { id: "compact", label: "Compact", width: 48, icon: 18 },
4594 { id: "default", label: "Default", width: 56, icon: 20 },
4595 { id: "large", label: "Large", width: 72, icon: 26 }
4596 ];
4597 const DESKTOP_LAYOUTS = [
4598 { id: "classic", label: "Classic" },
4599 { id: "unified", label: "Unified" },
4600 { id: "spatial", label: "Spatial" }
4601 ];
4602 const DEFAULTS = {
4603 wallpaper: DEFAULT_WALLPAPER_ID,
4604 accent: "wp-blue",
4605 dockSize: "default",
4606 desktopLayout: "classic",
4607 dockRailRenderer: "default",
4608 unfocusEffect: "darken",
4609 customGradient: {
4610 from: "#2271b1",
4611 to: "#7c3aed",
4612 angle: 135
4613 },
4614 customImage: null,
4615 libraryHdOnly: true,
4616 ai: {
4617 enabled: false,
4618 provider: "openai",
4619 apiKey: "",
4620 apiKeys: {},
4621 transport: "off"
4622 },
4623 // Opt-IN Beta as of 0.9.1. Fresh installs land on the classic
4624 // chromeless `edit.php` iframe; a user opts in via OS Settings →
4625 // Features → Beta features to get the native Posts window. The
4626 // native windows used to default ON (opt-out, 0.8.0) but are now
4627 // opt-in so the redesign is a deliberate choice, not imposed.
4628 heartbeatRate: 60,
4629 nativePostsEnabled: false,
4630 nativePostsHiddenColumns: [],
4631 // Same opt-in Beta posture as Posts — fresh installs keep the
4632 // iframe; users opt in to the native Pages window.
4633 nativePagesEnabled: false,
4634 // Native Users window — same opt-in Beta posture. Capability-gated
4635 // server-side (the window is only registered for users with
4636 // `list_users`), so this toggle only affects the small set of
4637 // users who can see the Users tile in the first place.
4638 nativeUsersEnabled: false,
4639 // Native Plugins window — replaces `plugins.php` and
4640 // `plugin-install.php`. Same opt-in Beta posture; cap-gated on
4641 // `activate_plugins` server-side, so this toggle only affects
4642 // users who could see the Plugins tile anyway.
4643 nativePluginsEnabled: false,
4644 // Native Comments window — replaces `edit-comments.php`. Same
4645 // opt-in Beta posture; cap-gated on `edit_posts` server-side.
4646 nativeCommentsEnabled: false,
4647 showDesktopOnWallpaperClick: false,
4648 showPostStatusRibbons: true,
4649 foldersSharingEnabled: true,
4650 itemVisibility: {},
4651 dockOrder: [],
4652 dockPromotedPositions: {}
4653 };
4654 const AI_TRANSPORTS = [
4655 { id: "off", label: "Off" },
4656 { id: "sse", label: "Streaming (SSE)" }
4657 ];
4658 const AI_PROVIDERS = [
4659 {
4660 id: "openai",
4661 label: "OpenAI",
4662 apiKeyLabel: "OpenAI API key",
4663 apiKeyLink: "https://platform.openai.com/api-keys"
4664 }
4665 ];
4666 function getAiProviders() {
4667 const cfg = window.desktopModeConfig;
4668 const list2 = cfg?.aiProviders;
4669 if (!Array.isArray(list2) || list2.length === 0) {
4670 return AI_PROVIDERS;
4671 }
4672 return list2.map((p) => ({
4673 id: p.id,
4674 label: p.label,
4675 description: p.description,
4676 apiKeyLabel: p.api_key_label,
4677 apiKeyLink: p.api_key_link
4678 }));
4679 }
4680 function isHexColor(value) {
4681 return typeof value === "string" && /^#[0-9a-f]{3,8}$/i.test(value);
4682 }
4683 const NONCE_HEADER = "X-WP-Nonce";
4684 function injectRestNonce(input, init2) {
4685 const nonce = readRestNonce$3();
4686 if (!nonce) {
4687 return init2;
4688 }
4689 const url = resolveUrl(input);
4690 if (!url || !isSameOriginRestUrl(url)) {
4691 return init2;
4692 }
4693 const baseHeaders = init2?.headers ?? (typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
4694 const headers = new Headers(baseHeaders ?? {});
4695 if (headers.has(NONCE_HEADER)) {
4696 return init2;
4697 }
4698 headers.set(NONCE_HEADER, nonce);
4699 return { ...init2 ?? {}, headers };
4700 }
4701 function readRestNonce$3() {
4702 if (typeof window === "undefined") {
4703 return void 0;
4704 }
4705 const cfg = window.desktopModeConfig;
4706 const value = cfg?.restNonce;
4707 return typeof value === "string" && value.length > 0 ? value : void 0;
4708 }
4709 function resolveUrl(input) {
4710 try {
4711 const base = typeof window !== "undefined" && window.location ? window.location.href : void 0;
4712 if (typeof input === "string") {
4713 return new URL(input, base);
4714 }
4715 if (input instanceof URL) {
4716 return input;
4717 }
4718 if (typeof Request !== "undefined" && input instanceof Request) {
4719 return new URL(input.url, base);
4720 }
4721 return null;
4722 } catch {
4723 return null;
4724 }
4725 }
4726 function isSameOriginRestUrl(url) {
4727 if (typeof window === "undefined" || !window.location || url.origin !== window.location.origin) {
4728 return false;
4729 }
4730 if (url.pathname.includes("/wp-json/")) {
4731 return true;
4732 }
4733 if (url.searchParams.has("rest_route")) {
4734 return true;
4735 }
4736 return false;
4737 }
4738 function trackedFetch$1(input, init2, opts = {}) {
4739 const fn = window.wp?.desktop?.fetch;
4740 if (typeof fn === "function") {
4741 return fn(input, init2, opts);
4742 }
4743 const finalInit = injectRestNonce(input, init2);
4744 return fetch(input, finalInit);
4745 }
4746 function loadState() {
4747 const serverRaw = _readServerSettings();
4748 if (serverRaw) {
4749 const state2 = _parseRaw(serverRaw);
4750 _writeLocalStorage(state2);
4751 return state2;
4752 }
4753 try {
4754 const cached = window.localStorage.getItem(STORAGE_KEY);
4755 if (cached) {
4756 return _parseRaw(JSON.parse(cached));
4757 }
4758 } catch {
4759 }
4760 return structuredDefaults();
4761 }
4762 function _readServerSettings() {
4763 const config = window.desktopModeConfig;
4764 const raw = config?.osSettings;
4765 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
4766 return null;
4767 }
4768 return raw;
4769 }
4770 function _parseRaw(parsed) {
4771 const accents = getAccents();
4772 return {
4773 wallpaper: typeof parsed.wallpaper === "string" && parsed.wallpaper !== "" ? parsed.wallpaper : getDefaultWallpaperId(),
4774 accent: accents.some((a) => a.id === parsed.accent) ? parsed.accent : DEFAULTS.accent,
4775 dockSize: DOCK_SIZES.some((d) => d.id === parsed.dockSize) ? parsed.dockSize : DEFAULTS.dockSize,
4776 desktopLayout: DESKTOP_LAYOUTS.some(
4777 (l) => l.id === parsed.desktopLayout
4778 ) ? parsed.desktopLayout : DEFAULTS.desktopLayout,
4779 // Dock rail renderer — any sanitize_key()-clean string
4780 // survives; the registry resolves at use time and falls back
4781 // to `'default'` when the picked renderer isn't registered.
4782 dockRailRenderer: typeof parsed.dockRailRenderer === "string" && /^[a-z0-9_-]+$/.test(parsed.dockRailRenderer) ? parsed.dockRailRenderer : DEFAULTS.dockRailRenderer,
4783 // Unfocus effect — any registry id (`vendor/sub-id` allowed) or
4784 // the `'none'` sentinel survives; the engine resolves at use
4785 // time and treats an unknown id as "no effect".
4786 unfocusEffect: typeof parsed.unfocusEffect === "string" && /^[a-z0-9_/-]+$/.test(parsed.unfocusEffect) ? parsed.unfocusEffect : DEFAULTS.unfocusEffect,
4787 customGradient: sanitizeCustomGradient(parsed.customGradient),
4788 customImage: sanitizeCustomImage(parsed.customImage),
4789 libraryHdOnly: typeof parsed.libraryHdOnly === "boolean" ? parsed.libraryHdOnly : DEFAULTS.libraryHdOnly,
4790 ai: sanitizeAi(parsed.ai),
4791 heartbeatRate: parsed.heartbeatRate === 15 || parsed.heartbeatRate === 30 || parsed.heartbeatRate === 45 || parsed.heartbeatRate === 60 ? parsed.heartbeatRate : DEFAULTS.heartbeatRate,
4792 nativePostsEnabled: typeof parsed.nativePostsEnabled === "boolean" ? parsed.nativePostsEnabled : DEFAULTS.nativePostsEnabled,
4793 nativePostsHiddenColumns: Array.isArray(parsed.nativePostsHiddenColumns) ? parsed.nativePostsHiddenColumns.filter((v) => typeof v === "string" && v !== "").slice(0, 32) : DEFAULTS.nativePostsHiddenColumns.slice(),
4794 nativePagesEnabled: typeof parsed.nativePagesEnabled === "boolean" ? parsed.nativePagesEnabled : DEFAULTS.nativePagesEnabled,
4795 nativeUsersEnabled: typeof parsed.nativeUsersEnabled === "boolean" ? parsed.nativeUsersEnabled : DEFAULTS.nativeUsersEnabled,
4796 nativePluginsEnabled: typeof parsed.nativePluginsEnabled === "boolean" ? parsed.nativePluginsEnabled : DEFAULTS.nativePluginsEnabled,
4797 nativeCommentsEnabled: typeof parsed.nativeCommentsEnabled === "boolean" ? parsed.nativeCommentsEnabled : DEFAULTS.nativeCommentsEnabled,
4798 showDesktopOnWallpaperClick: typeof parsed.showDesktopOnWallpaperClick === "boolean" ? parsed.showDesktopOnWallpaperClick : DEFAULTS.showDesktopOnWallpaperClick,
4799 showPostStatusRibbons: typeof parsed.showPostStatusRibbons === "boolean" ? parsed.showPostStatusRibbons : DEFAULTS.showPostStatusRibbons,
4800 foldersSharingEnabled: typeof parsed.foldersSharingEnabled === "boolean" ? parsed.foldersSharingEnabled : DEFAULTS.foldersSharingEnabled,
4801 itemVisibility: sanitizeItemVisibility(parsed.itemVisibility),
4802 dockOrder: sanitizeDockOrder(parsed.dockOrder),
4803 dockPromotedPositions: sanitizeDockPromotedPositions(
4804 parsed.dockPromotedPositions
4805 )
4806 };
4807 }
4808 function sanitizeItemVisibility(raw) {
4809 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
4810 return {};
4811 }
4812 const allowed = [
4813 "both",
4814 "dock",
4815 "desktop",
4816 "hidden"
4817 ];
4818 const out = {};
4819 let count = 0;
4820 for (const [k, v] of Object.entries(raw)) {
4821 if (count >= 256) {
4822 break;
4823 }
4824 if (typeof k !== "string" || k === "") {
4825 continue;
4826 }
4827 if (typeof v !== "string") {
4828 continue;
4829 }
4830 const placement = v;
4831 if (!allowed.includes(placement)) {
4832 continue;
4833 }
4834 out[k] = placement;
4835 count++;
4836 }
4837 return out;
4838 }
4839 function sanitizeDockOrder(raw) {
4840 if (!Array.isArray(raw)) {
4841 return [];
4842 }
4843 const out = [];
4844 const seen = /* @__PURE__ */ new Set();
4845 for (const id of raw) {
4846 if (typeof id !== "string" || id === "" || seen.has(id)) {
4847 continue;
4848 }
4849 seen.add(id);
4850 out.push(id);
4851 if (out.length >= 256) {
4852 break;
4853 }
4854 }
4855 return out;
4856 }
4857 function sanitizeDockPromotedPositions(raw) {
4858 if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
4859 return {};
4860 }
4861 const out = {};
4862 let count = 0;
4863 const MAX_COORD = 1e5;
4864 for (const [k, v] of Object.entries(raw)) {
4865 if (count >= 256) {
4866 break;
4867 }
4868 if (typeof k !== "string" || k === "") {
4869 continue;
4870 }
4871 if (!v || typeof v !== "object" || Array.isArray(v)) {
4872 continue;
4873 }
4874 const pos = v;
4875 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) {
4876 continue;
4877 }
4878 out[k] = { x: pos.x, y: pos.y };
4879 count++;
4880 }
4881 return out;
4882 }
4883 let _syncTimer = null;
4884 const SYNC_DEBOUNCE_MS = 250;
4885 let _lastConfirmedState = null;
4886 function setLastConfirmedState(state2) {
4887 _lastConfirmedState = _cloneState(state2);
4888 }
4889 function _cloneState(state2) {
4890 return {
4891 ...state2,
4892 customGradient: { ...state2.customGradient },
4893 customImage: state2.customImage ? { ...state2.customImage } : null,
4894 ai: { ...state2.ai, apiKeys: { ...state2.ai.apiKeys } },
4895 nativePostsHiddenColumns: state2.nativePostsHiddenColumns.slice(),
4896 itemVisibility: { ...state2.itemVisibility },
4897 dockOrder: state2.dockOrder.slice(),
4898 dockPromotedPositions: Object.fromEntries(
4899 Object.entries(state2.dockPromotedPositions).map(([k, v]) => [
4900 k,
4901 { ...v }
4902 ])
4903 )
4904 };
4905 }
4906 function saveState(state2, opts = {}) {
4907 _writeLocalStorage(state2);
4908 _scheduleSyncToServer(state2, opts.windowId);
4909 }
4910 function _writeLocalStorage(state2) {
4911 try {
4912 window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state2));
4913 } catch {
4914 }
4915 }
4916 function _scheduleSyncToServer(state2, windowId) {
4917 if (_syncTimer !== null) {
4918 clearTimeout(_syncTimer);
4919 }
4920 if (windowId) {
4921 _pendingActivityWindowId = windowId;
4922 }
4923 _emitSaveLifecycle("pending");
4924 _syncTimer = setTimeout(() => {
4925 _syncTimer = null;
4926 const id = _pendingActivityWindowId;
4927 _pendingActivityWindowId = null;
4928 _postToServer(state2, id);
4929 }, SYNC_DEBOUNCE_MS);
4930 }
4931 let _pendingActivityWindowId = null;
4932 function _postToServer(state2, windowId) {
4933 const config = window.desktopModeConfig;
4934 const url = config?.osSettingsUrl;
4935 const nonce = config?.restNonce;
4936 if (!url || !nonce) {
4937 _emitSaveLifecycle("saved");
4938 return;
4939 }
4940 _emitSaveLifecycle("saving");
4941 const attributedWindowId = windowId || "desktop-mode-os-settings";
4942 trackedFetch$1(
4943 url,
4944 {
4945 method: "POST",
4946 headers: {
4947 "Content-Type": "application/json",
4948 "X-WP-Nonce": nonce
4949 },
4950 body: JSON.stringify({ settings: state2 })
4951 },
4952 { windowId: attributedWindowId }
4953 ).then((res) => {
4954 if (!res.ok) {
4955 throw new Error(`${res.status} ${res.statusText}`);
4956 }
4957 _lastConfirmedState = _cloneState(state2);
4958 _emitSaveLifecycle("saved");
4959 }).catch((err) => {
4960 if (_lastConfirmedState) {
4961 _writeLocalStorage(_lastConfirmedState);
4962 _emitSaveLifecycle(
4963 "failed",
4964 err instanceof Error ? err.message : String(err),
4965 _cloneState(_lastConfirmedState)
4966 );
4967 } else {
4968 _emitSaveLifecycle(
4969 "failed",
4970 err instanceof Error ? err.message : String(err)
4971 );
4972 }
4973 });
4974 }
4975 function _emitSaveLifecycle(phase, error, rolledBackTo) {
4976 const detail = { phase };
4977 if (error) {
4978 detail.error = error;
4979 }
4980 if (rolledBackTo) {
4981 detail.rolledBackTo = rolledBackTo;
4982 }
4983 document.dispatchEvent(
4984 new CustomEvent("desktop-mode-os-settings-save-lifecycle", { detail })
4985 );
4986 }
4987 function structuredDefaults() {
4988 return {
4989 ...DEFAULTS,
4990 customGradient: { ...DEFAULTS.customGradient },
4991 customImage: null,
4992 ai: { ...DEFAULTS.ai },
4993 // Clone the collection fields too. A shallow `...DEFAULTS`
4994 // aliases these nested objects, so a later in-place mutation
4995 // (e.g. dragging the gradient editor after a Reset, which spreads
4996 // these defaults into live state) would corrupt the module-level
4997 // DEFAULTS singleton for the rest of the session.
4998 //
4999 // These are one-level clones, which is sufficient *because* all
5000 // three defaults are empty (`{}` / `[]`) — there are no inner
5001 // objects to share. If `DEFAULTS.dockPromotedPositions` ever
5002 // ships seeded entries, its `{ x, y }` values would need a
5003 // deeper clone here.
5004 itemVisibility: { ...DEFAULTS.itemVisibility },
5005 dockOrder: [...DEFAULTS.dockOrder],
5006 dockPromotedPositions: { ...DEFAULTS.dockPromotedPositions }
5007 };
5008 }
5009 function sanitizeAi(raw) {
5010 if (!raw || typeof raw !== "object") {
5011 return { ...DEFAULTS.ai, apiKeys: {} };
5012 }
5013 const { enabled, provider, apiKey, apiKeys, transport } = raw;
5014 const known = getAiProviders();
5015 const validProvider = typeof provider === "string" && known.some((p) => p.id === provider) ? provider : DEFAULTS.ai.provider;
5016 const cleanKeys = {};
5017 if (apiKeys && typeof apiKeys === "object") {
5018 for (const [pid, val] of Object.entries(apiKeys)) {
5019 if (typeof val === "string") {
5020 cleanKeys[pid] = val.slice(0, 512);
5021 }
5022 }
5023 }
5024 const validTransport = typeof transport === "string" && AI_TRANSPORTS.some((t) => t.id === transport) ? transport : DEFAULTS.ai.transport;
5025 return {
5026 enabled: typeof enabled === "boolean" ? enabled : DEFAULTS.ai.enabled,
5027 provider: validProvider,
5028 apiKey: typeof apiKey === "string" ? apiKey : DEFAULTS.ai.apiKey,
5029 apiKeys: cleanKeys,
5030 transport: validTransport
5031 };
5032 }
5033 function sanitizeCustomGradient(raw) {
5034 if (!raw || typeof raw !== "object") {
5035 return { ...DEFAULTS.customGradient };
5036 }
5037 const { from, to, angle } = raw;
5038 return {
5039 from: isHexColor(from) ? from : DEFAULTS.customGradient.from,
5040 to: isHexColor(to) ? to : DEFAULTS.customGradient.to,
5041 angle: typeof angle === "number" && Number.isFinite(angle) && angle >= 0 && angle <= 360 ? angle : DEFAULTS.customGradient.angle
5042 };
5043 }
5044 function sanitizeCustomImage(raw) {
5045 if (!raw || typeof raw !== "object") {
5046 return null;
5047 }
5048 const { id, url } = raw;
5049 if (typeof id !== "number" || !Number.isFinite(id) || id <= 0) {
5050 return null;
5051 }
5052 if (typeof url !== "string" || !/^https?:\/\//i.test(url)) {
5053 return null;
5054 }
5055 return { id, url };
5056 }
5057 const store$c = createSharedStore(
5058 "desktop-mode/dock-rail-registry",
5059 () => ({
5060 registry: /* @__PURE__ */ new Map(),
5061 listeners: /* @__PURE__ */ new Set(),
5062 activeId: "default"
5063 })
5064 );
5065 const registry$9 = store$c.state.registry;
5066 const listeners$b = store$c.state.listeners;
5067 const ID_RE = /^[a-z0-9_-]+$/;
5068 function register$1(renderer) {
5069 if (!renderer || typeof renderer !== "object") {
5070 throw new TypeError(
5071 "[desktop-mode] registerDockRailRenderer: renderer must be an object."
5072 );
5073 }
5074 if (typeof renderer.id !== "string" || !ID_RE.test(renderer.id)) {
5075 throw new TypeError(
5076 `[desktop-mode] registerDockRailRenderer: id must match /^[a-z0-9_-]+$/, got: ${String(renderer.id)}`
5077 );
5078 }
5079 if (typeof renderer.label !== "string" || renderer.label === "") {
5080 throw new TypeError(
5081 "[desktop-mode] registerDockRailRenderer: label must be a non-empty string."
5082 );
5083 }
5084 if (typeof renderer.mount !== "function") {
5085 throw new TypeError(
5086 "[desktop-mode] registerDockRailRenderer: mount must be a function."
5087 );
5088 }
5089 if (renderer.apiVersion !== void 0 && renderer.apiVersion !== 1) {
5090 throw new TypeError(
5091 `[desktop-mode] registerDockRailRenderer: unsupported apiVersion ${renderer.apiVersion} (this shell speaks v1).`
5092 );
5093 }
5094 registry$9.set(renderer.id, renderer);
5095 notify$d();
5096 }
5097 function unregister$1(id) {
5098 if (registry$9.delete(id)) {
5099 notify$d();
5100 }
5101 }
5102 function unregisterByOwner$1(owner) {
5103 if (!owner) {
5104 return 0;
5105 }
5106 let removed = 0;
5107 for (const [id, renderer] of Array.from(registry$9.entries())) {
5108 if (renderer.owner === owner) {
5109 registry$9.delete(id);
5110 removed++;
5111 }
5112 }
5113 if (removed > 0) {
5114 notify$d();
5115 }
5116 return removed;
5117 }
5118 function list() {
5119 return Array.from(registry$9.values());
5120 }
5121 function subscribe$3(cb) {
5122 listeners$b.add(cb);
5123 return () => {
5124 listeners$b.delete(cb);
5125 };
5126 }
5127 function setActiveRenderer(id) {
5128 if (store$c.state.activeId === id) {
5129 return;
5130 }
5131 store$c.state.activeId = id;
5132 notify$d();
5133 }
5134 function resolveActive() {
5135 return registry$9.get(store$c.state.activeId) ?? registry$9.get("default") ?? registry$9.values().next().value;
5136 }
5137 function notify$d() {
5138 const snapshot = Array.from(listeners$b);
5139 for (const cb of snapshot) {
5140 try {
5141 cb();
5142 } catch (err) {
5143 if (typeof console !== "undefined") {
5144 console.error(
5145 "[desktop-mode] dock-rail-renderer listener threw:",
5146 err
5147 );
5148 }
5149 }
5150 }
5151 }
5152 function hashTitleToHue(input) {
5153 if (!input) {
5154 return 214;
5155 }
5156 let hash2 = 5381;
5157 for (let i = 0; i < input.length; i++) {
5158 hash2 = Math.imul(hash2, 33) + input.charCodeAt(i);
5159 }
5160 return (hash2 % 360 + 360) % 360;
5161 }
5162 const SHOW_DELAY_MS = 180;
5163 const HIDE_DELAY_MS = 220;
5164 const STAGGER_MS = 32;
5165 function attachDockPeek(deps2) {
5166 const { tile: tile2 } = deps2;
5167 let popover = null;
5168 let showTimer = null;
5169 let hideTimer = null;
5170 let inside = false;
5171 const cancelShow = () => {
5172 if (showTimer !== null) {
5173 window.clearTimeout(showTimer);
5174 showTimer = null;
5175 }
5176 };
5177 const cancelHide = () => {
5178 if (hideTimer !== null) {
5179 window.clearTimeout(hideTimer);
5180 hideTimer = null;
5181 }
5182 };
5183 const tearDown = () => {
5184 cancelShow();
5185 cancelHide();
5186 if (popover) {
5187 popover.remove();
5188 popover = null;
5189 }
5190 deps2.suppressTooltip(false);
5191 };
5192 const onPointerEnterTile = (e) => {
5193 if (e.pointerType !== "mouse") {
5194 return;
5195 }
5196 if (!shouldShowPeek(deps2)) {
5197 return;
5198 }
5199 inside = true;
5200 cancelHide();
5201 if (popover) {
5202 return;
5203 }
5204 showTimer = window.setTimeout(() => {
5205 showTimer = null;
5206 if (!inside) {
5207 return;
5208 }
5209 showPeek();
5210 }, SHOW_DELAY_MS);
5211 };
5212 const onPointerLeaveTile = (e) => {
5213 if (popover && e.relatedTarget instanceof Node && popover.contains(e.relatedTarget)) {
5214 return;
5215 }
5216 inside = false;
5217 cancelShow();
5218 scheduleHide();
5219 };
5220 const scheduleHide = () => {
5221 cancelHide();
5222 hideTimer = window.setTimeout(() => {
5223 hideTimer = null;
5224 if (inside) {
5225 return;
5226 }
5227 tearDown();
5228 }, HIDE_DELAY_MS);
5229 };
5230 const showPeek = () => {
5231 deps2.suppressTooltip(true);
5232 popover = buildPopover(deps2, () => tearDown());
5233 document.body.appendChild(popover);
5234 inheritShellSchemeVars(popover);
5235 positionPopover(popover, tile2, deps2.getOrientation());
5236 requestAnimationFrame(() => {
5237 popover?.classList.add("desktop-mode-dock-peek--open");
5238 });
5239 popover.addEventListener("pointerenter", () => {
5240 inside = true;
5241 cancelHide();
5242 });
5243 popover.addEventListener("pointerleave", (e) => {
5244 if (e.relatedTarget instanceof Node && tile2.contains(e.relatedTarget)) {
5245 return;
5246 }
5247 inside = false;
5248 scheduleHide();
5249 });
5250 };
5251 tile2.addEventListener("pointerenter", onPointerEnterTile);
5252 tile2.addEventListener("pointerleave", onPointerLeaveTile);
5253 return () => {
5254 tile2.removeEventListener("pointerenter", onPointerEnterTile);
5255 tile2.removeEventListener("pointerleave", onPointerLeaveTile);
5256 tearDown();
5257 };
5258 }
5259 function shouldShowPeek(deps2) {
5260 return deps2.getInstances().length >= 1;
5261 }
5262 function buildPopover(deps2, dismiss) {
5263 const root = document.createElement("div");
5264 root.className = "desktop-mode-dock-peek";
5265 root.setAttribute("role", "menu");
5266 root.setAttribute("aria-label", sprintf(
5267 // translators: %s is the dock item's admin-page title (e.g., "Posts")
5268 __("%s — open windows"),
5269 deps2.item.title
5270 ));
5271 const cards = document.createElement("div");
5272 cards.className = "desktop-mode-dock-peek__cards";
5273 root.appendChild(cards);
5274 const instances = deps2.getInstances();
5275 let cardIndex = 0;
5276 for (const win of instances) {
5277 const card = buildInstanceCard(win, deps2, cardIndex++, dismiss);
5278 cards.appendChild(card);
5279 }
5280 if (deps2.enableGhost !== false) {
5281 const ghost = buildGhostCard(deps2, cardIndex, dismiss);
5282 cards.appendChild(ghost);
5283 }
5284 return root;
5285 }
5286 function buildInstanceCard(win, deps2, index2, dismiss) {
5287 const card = document.createElement("button");
5288 card.type = "button";
5289 card.setAttribute("role", "menuitem");
5290 card.className = "desktop-mode-dock-peek__card desktop-mode-dock-peek__card--instance";
5291 card.style.setProperty("--peek-card-index", String(index2));
5292 card.style.setProperty(
5293 "--peek-card-delay",
5294 `${index2 * STAGGER_MS}ms`
5295 );
5296 const title = win.config.title || deps2.item.title;
5297 card.style.setProperty(
5298 "--peek-card-hue",
5299 `${hashTitleToHue(win.id || title)}`
5300 );
5301 card.style.setProperty(
5302 "--peek-card-vt-name",
5303 `desktop-mode-peek-card-${win.id}`
5304 );
5305 const titlebar = document.createElement("span");
5306 titlebar.className = "desktop-mode-dock-peek__card-titlebar";
5307 const dots = document.createElement("span");
5308 dots.className = "desktop-mode-dock-peek__card-dots";
5309 dots.setAttribute("aria-hidden", "true");
5310 for (let i = 0; i < 3; i++) {
5311 dots.appendChild(document.createElement("i"));
5312 }
5313 titlebar.appendChild(dots);
5314 const iconHost = document.createElement("span");
5315 iconHost.className = "desktop-mode-dock-peek__card-icon";
5316 iconHost.setAttribute("aria-hidden", "true");
5317 const iconCls = win.config.icon || deps2.item.icon;
5318 if (iconCls.startsWith("dashicons-")) {
5319 iconHost.classList.add("dashicons", sanitizeClassName(iconCls));
5320 } else {
5321 iconHost.classList.add("dashicons", "dashicons-admin-generic");
5322 }
5323 titlebar.appendChild(iconHost);
5324 const label = document.createElement("span");
5325 label.className = "desktop-mode-dock-peek__card-label";
5326 label.textContent = title;
5327 titlebar.appendChild(label);
5328 card.appendChild(titlebar);
5329 const defaultBody = document.createElement("span");
5330 defaultBody.className = "desktop-mode-dock-peek__card-body";
5331 defaultBody.setAttribute("aria-hidden", "true");
5332 for (let i = 0; i < 3; i++) {
5333 const line = document.createElement("span");
5334 line.className = "desktop-mode-dock-peek__card-line";
5335 defaultBody.appendChild(line);
5336 }
5337 const ctx = { window: win, item: deps2.item };
5338 const body = applyFilters(
5339 HOOKS.DOCK_PEEK_CARD_CONTENT,
5340 defaultBody,
5341 ctx
5342 );
5343 if (body !== defaultBody) {
5344 body.classList.add("desktop-mode-dock-peek__card-body--custom");
5345 }
5346 card.appendChild(body);
5347 card.addEventListener("click", () => {
5348 spawnFocusViewTransition(deps2, win, card, dismiss);
5349 });
5350 card.addEventListener("pointerenter", () => {
5351 if (deps2.windowManager.getFocused() === win) {
5352 return;
5353 }
5354 deps2.windowManager.focus(win);
5355 });
5356 const finalCard = applyFilters(
5357 HOOKS.DOCK_PEEK_CARD_ELEMENT,
5358 card,
5359 ctx
5360 );
5361 return finalCard;
5362 }
5363 function spawnFocusViewTransition(deps2, win, card, dismiss) {
5364 const doc = document;
5365 const vtName = `desktop-mode-peek-card-${win.id}`;
5366 const focus = () => {
5367 dismiss();
5368 deps2.windowManager.focus(win);
5369 };
5370 if (typeof doc.startViewTransition !== "function") {
5371 focus();
5372 return;
5373 }
5374 const targetEl = win.element;
5375 card.style.setProperty("view-transition-name", vtName);
5376 targetEl.style.setProperty("view-transition-name", vtName);
5377 const transition = doc.startViewTransition(focus);
5378 const cleanup = () => {
5379 card.style.removeProperty("view-transition-name");
5380 targetEl.style.removeProperty("view-transition-name");
5381 };
5382 const t = transition;
5383 if (t.finished && typeof t.finished.then === "function") {
5384 t.finished.then(cleanup, cleanup);
5385 } else {
5386 Promise.resolve().then(cleanup);
5387 }
5388 }
5389 function buildGhostCard(deps2, index2, dismiss) {
5390 const card = document.createElement("button");
5391 card.type = "button";
5392 card.setAttribute("role", "menuitem");
5393 card.className = "desktop-mode-dock-peek__card desktop-mode-dock-peek__card--ghost";
5394 card.style.setProperty("--peek-card-index", String(index2));
5395 card.style.setProperty(
5396 "--peek-card-delay",
5397 `${index2 * STAGGER_MS}ms`
5398 );
5399 const plus = document.createElement("span");
5400 plus.className = "desktop-mode-dock-peek__card-plus";
5401 plus.setAttribute("aria-hidden", "true");
5402 plus.textContent = "+";
5403 card.appendChild(plus);
5404 const label = document.createElement("span");
5405 label.className = "desktop-mode-dock-peek__card-label";
5406 label.textContent = sprintf(
5407 // translators: %s is the admin-page title (e.g., "Posts")
5408 __("New %s"),
5409 deps2.item.title
5410 );
5411 card.appendChild(label);
5412 card.addEventListener("click", () => {
5413 spawnWithViewTransition(deps2, dismiss);
5414 });
5415 return card;
5416 }
5417 function spawnWithViewTransition(deps2, dismiss) {
5418 const doc = document;
5419 const spawn = () => {
5420 dismiss();
5421 deps2.openNew();
5422 };
5423 if (typeof doc.startViewTransition === "function") {
5424 doc.startViewTransition(spawn);
5425 return;
5426 }
5427 spawn();
5428 }
5429 const VIEWPORT_MARGIN_PX = 12;
5430 const SHELL_SCHEME_VARS = [
5431 "--wp-admin-theme-color",
5432 "--desktop-mode-titlebar-bg",
5433 "--desktop-mode-titlebar-bg-focused",
5434 "--desktop-mode-titlebar-color",
5435 "--desktop-mode-titlebar-color-focused"
5436 ];
5437 function inheritShellSchemeVars(popover) {
5438 const shell = document.querySelector(".desktop-mode-shell");
5439 if (!shell) {
5440 return;
5441 }
5442 const computed = window.getComputedStyle(shell);
5443 for (const name of SHELL_SCHEME_VARS) {
5444 const value = computed.getPropertyValue(name).trim();
5445 if (value) {
5446 popover.style.setProperty(name, value);
5447 }
5448 }
5449 }
5450 function positionPopover(popover, tile2, orientation) {
5451 const rect = tile2.getBoundingClientRect();
5452 popover.dataset.orientation = orientation;
5453 if (orientation === "bottom") {
5454 popover.style.left = `${rect.left + rect.width / 2}px`;
5455 popover.style.top = `${rect.top - 12}px`;
5456 } else if (orientation === "right") {
5457 popover.style.top = `${rect.top + rect.height / 2}px`;
5458 popover.style.left = `${rect.left - 12}px`;
5459 } else {
5460 popover.style.top = `${rect.top + rect.height / 2}px`;
5461 popover.style.left = `${rect.right + 12}px`;
5462 }
5463 requestAnimationFrame(() => clampToViewport$1(popover));
5464 }
5465 function clampToViewport$1(popover, orientation) {
5466 const rect = popover.getBoundingClientRect();
5467 const vh = window.innerHeight;
5468 const vw = window.innerWidth;
5469 const min = VIEWPORT_MARGIN_PX;
5470 let dy = 0;
5471 let dx = 0;
5472 if (rect.top < min) {
5473 dy = min - rect.top;
5474 } else if (rect.bottom > vh - min) {
5475 dy = vh - min - rect.bottom;
5476 }
5477 if (rect.left < min) {
5478 dx = min - rect.left;
5479 } else if (rect.right > vw - min) {
5480 dx = vw - min - rect.right;
5481 }
5482 if (dx === 0 && dy === 0) {
5483 return;
5484 }
5485 popover.style.setProperty("--peek-clamp-x", `${dx}px`);
5486 popover.style.setProperty("--peek-clamp-y", `${dy}px`);
5487 popover.classList.add("desktop-mode-dock-peek--clamped");
5488 }
5489 function tryOpenExternalUrl(url) {
5490 try {
5491 const parsed = new URL(url, window.location.origin);
5492 if (parsed.origin === window.location.origin) {
5493 return false;
5494 }
5495 window.open(parsed.toString(), "_blank", "noopener,noreferrer");
5496 return true;
5497 } catch {
5498 return false;
5499 }
5500 }
5501 function synthDockId(desktopIconId) {
5502 return `desktop:${desktopIconId}`;
5503 }
5504 function synthIconId(dockItemId) {
5505 return `dock:${dockItemId}`;
5506 }
5507 function canonicalItemId(id) {
5508 if (id.startsWith("dock:")) {
5509 return id.slice(5);
5510 }
5511 if (id.startsWith("desktop:")) {
5512 return id.slice(8);
5513 }
5514 return id;
5515 }
5516 function resolvePlacement(id, nativeRail, visibility) {
5517 const override = visibility[id];
5518 if (override) {
5519 return override;
5520 }
5521 return nativeRail;
5522 }
5523 function shouldShowOnDock(placement) {
5524 return placement === "dock" || placement === "both";
5525 }
5526 function shouldShowOnDesktop(placement) {
5527 return placement === "desktop" || placement === "both";
5528 }
5529 function applyDockPlacement(dockItems, desktopIcons, settings, dockedNativeWindows) {
5530 const visibility = settings.itemVisibility;
5531 const order = settings.dockOrder;
5532 const kept = [];
5533 for (const item of dockItems) {
5534 const placement = resolvePlacement(item.id, "dock", visibility);
5535 if (shouldShowOnDock(placement)) {
5536 kept.push(item);
5537 }
5538 }
5539 for (const icon of desktopIcons) {
5540 const placement = resolvePlacement(icon.id, "desktop", visibility);
5541 if (!shouldShowOnDock(placement)) {
5542 continue;
5543 }
5544 if (icon.window && dockedNativeWindows && dockedNativeWindows.has(icon.window)) {
5545 continue;
5546 }
5547 kept.push({
5548 id: synthIconId(icon.id),
5549 title: icon.title,
5550 icon: icon.icon,
5551 url: icon.url || "",
5552 // Carry the native-window id forward so the dock can light
5553 // the active-dot indicator + show the hover-peek card when
5554 // the target window is open. Without this, window-target
5555 // icons (no `url`) synthesize a tile whose only id-bearing
5556 // field is an empty string — deriveWindowId('') matches
5557 // nothing the window manager has stored.
5558 windowId: icon.window || void 0,
5559 badge: 0,
5560 submenu: [],
5561 isCore: false
5562 });
5563 }
5564 return applyOrder(kept, order);
5565 }
5566 function applyDesktopPlacement(desktopIcons, dockItems, visibility) {
5567 const out = [];
5568 for (const icon of desktopIcons) {
5569 const placement = resolvePlacement(icon.id, "desktop", visibility);
5570 if (shouldShowOnDesktop(placement)) {
5571 out.push(icon);
5572 }
5573 }
5574 let synthIndex = 0;
5575 for (const item of dockItems) {
5576 const placement = resolvePlacement(item.id, "dock", visibility);
5577 if (!shouldShowOnDesktop(placement)) {
5578 continue;
5579 }
5580 out.push({
5581 id: synthDockId(item.id),
5582 title: item.title,
5583 icon: item.icon,
5584 window: "",
5585 url: item.url || "",
5586 // Place synthesized dock-promoted icons after server-registered
5587 // ones. Stable ordering by source-list index inside the bucket.
5588 position: 2e3 + synthIndex++
5589 });
5590 }
5591 return out;
5592 }
5593 function applyOrder(items, order) {
5594 if (order.length === 0 || items.length <= 1) {
5595 return items;
5596 }
5597 const byId = /* @__PURE__ */ new Map();
5598 for (const item of items) {
5599 byId.set(item.id, item);
5600 }
5601 const out = [];
5602 const placed = /* @__PURE__ */ new Set();
5603 for (const id of order) {
5604 const item = byId.get(id);
5605 if (item) {
5606 out.push(item);
5607 placed.add(id);
5608 }
5609 }
5610 for (const item of items) {
5611 if (!placed.has(item.id)) {
5612 out.push(item);
5613 }
5614 }
5615 return out;
5616 }
5617 function html(strings, ...values) {
5618 return { __wpdHtml: true, strings, values };
5619 }
5620 function isTemplateResult(v) {
5621 return !!v && v.__wpdHtml === true;
5622 }
5623 const MARKER_PREFIX = "$$wpd$$";
5624 const MARKER_RE = /\$\$wpd\$\$(\d+)\$\$/g;
5625 function joinWithMarkers(strings) {
5626 let out = strings[0];
5627 for (let i = 1; i < strings.length; i++) {
5628 out += `${MARKER_PREFIX}${i - 1}$$` + strings[i];
5629 }
5630 return out;
5631 }
5632 const compiledCache = /* @__PURE__ */ new WeakMap();
5633 function compile(strings) {
5634 const cached = compiledCache.get(strings);
5635 if (cached) {
5636 return cached;
5637 }
5638 const template = document.createElement("template");
5639 template.innerHTML = joinWithMarkers(strings);
5640 const recipes = [];
5641 const walk2 = (node, path) => {
5642 if (node.nodeType === Node.ELEMENT_NODE) {
5643 const el = node;
5644 for (const attr of Array.from(el.attributes)) {
5645 const rawName = attr.name;
5646 const rawValue = attr.value;
5647 const prefix = rawName[0];
5648 if (MARKER_RE.test(rawValue)) {
5649 MARKER_RE.lastIndex = 0;
5650 if (prefix === "@") {
5651 const match = MARKER_RE.exec(rawValue);
5652 MARKER_RE.lastIndex = 0;
5653 recipes.push({
5654 path,
5655 kind: "event",
5656 name: rawName.slice(1),
5657 valueIndex: match ? Number(match[1]) : 0
5658 });
5659 el.removeAttribute(rawName);
5660 } else if (prefix === ".") {
5661 const match = MARKER_RE.exec(rawValue);
5662 MARKER_RE.lastIndex = 0;
5663 recipes.push({
5664 path,
5665 kind: "prop",
5666 name: rawName.slice(1),
5667 valueIndex: match ? Number(match[1]) : 0
5668 });
5669 el.removeAttribute(rawName);
5670 } else if (prefix === "?") {
5671 const match = MARKER_RE.exec(rawValue);
5672 MARKER_RE.lastIndex = 0;
5673 recipes.push({
5674 path,
5675 kind: "bool",
5676 name: rawName.slice(1),
5677 valueIndex: match ? Number(match[1]) : 0
5678 });
5679 el.removeAttribute(rawName);
5680 } else {
5681 const fragments = [];
5682 const indices = [];
5683 let lastEnd = 0;
5684 let m;
5685 MARKER_RE.lastIndex = 0;
5686 while ((m = MARKER_RE.exec(rawValue)) !== null) {
5687 fragments.push(rawValue.slice(lastEnd, m.index));
5688 indices.push(Number(m[1]));
5689 lastEnd = m.index + m[0].length;
5690 }
5691 fragments.push(rawValue.slice(lastEnd));
5692 recipes.push({
5693 path,
5694 kind: "attr",
5695 name: rawName,
5696 template: fragments,
5697 valueIndices: indices
5698 });
5699 el.setAttribute(rawName, "");
5700 }
5701 }
5702 }
5703 }
5704 const children = Array.from(node.childNodes);
5705 let shift = 0;
5706 for (let i = 0; i < children.length; i++) {
5707 const child = children[i];
5708 const liveIndex = i + shift;
5709 if (child.nodeType === Node.TEXT_NODE) {
5710 const text = child.textContent || "";
5711 if (!MARKER_RE.test(text)) {
5712 MARKER_RE.lastIndex = 0;
5713 continue;
5714 }
5715 MARKER_RE.lastIndex = 0;
5716 const parent = child.parentNode;
5717 let lastEnd = 0;
5718 let m;
5719 const newNodes = [];
5720 const newRecipes = [];
5721 MARKER_RE.lastIndex = 0;
5722 while ((m = MARKER_RE.exec(text)) !== null) {
5723 if (m.index > lastEnd) {
5724 newNodes.push(document.createTextNode(text.slice(lastEnd, m.index)));
5725 }
5726 const placeholder = document.createTextNode("");
5727 newNodes.push(placeholder);
5728 newRecipes.push({
5729 path: [...path, liveIndex + newNodes.length - 1],
5730 kind: "node",
5731 valueIndex: Number(m[1])
5732 });
5733 lastEnd = m.index + m[0].length;
5734 }
5735 if (lastEnd < text.length) {
5736 newNodes.push(document.createTextNode(text.slice(lastEnd)));
5737 }
5738 for (const nn of newNodes) {
5739 parent.insertBefore(nn, child);
5740 }
5741 parent.removeChild(child);
5742 shift += newNodes.length - 1;
5743 recipes.push(...newRecipes);
5744 } else {
5745 walk2(child, [...path, liveIndex]);
5746 }
5747 }
5748 };
5749 walk2(template.content, []);
5750 const buildParts = (fragment) => {
5751 const out = [];
5752 for (const r of recipes) {
5753 let node = fragment;
5754 for (const idx of r.path) {
5755 node = node.childNodes[idx];
5756 }
5757 if (r.kind === "node") {
5758 out.push({
5759 kind: "node",
5760 valueIndex: r.valueIndex,
5761 child: {
5762 anchor: node,
5763 state: null
5764 }
5765 });
5766 } else if (r.kind === "attr") {
5767 out.push({
5768 kind: "attr",
5769 element: node,
5770 name: r.name,
5771 template: r.template,
5772 valueIndices: r.valueIndices
5773 });
5774 } else if (r.kind === "event") {
5775 out.push({
5776 kind: "event",
5777 valueIndex: r.valueIndex,
5778 element: node,
5779 name: r.name
5780 });
5781 } else if (r.kind === "prop") {
5782 out.push({
5783 kind: "prop",
5784 valueIndex: r.valueIndex,
5785 element: node,
5786 name: r.name
5787 });
5788 } else if (r.kind === "bool") {
5789 out.push({
5790 kind: "bool",
5791 valueIndex: r.valueIndex,
5792 element: node,
5793 name: r.name
5794 });
5795 }
5796 }
5797 return out;
5798 };
5799 const entry = { template, buildParts };
5800 compiledCache.set(strings, entry);
5801 return entry;
5802 }
5803 const mountState = /* @__PURE__ */ new WeakMap();
5804 function render$1(result, container) {
5805 const existing = mountState.get(container);
5806 if (existing && existing.strings === result.strings) {
5807 applyValues(existing.parts, result.values);
5808 return;
5809 }
5810 const compiled = compile(result.strings);
5811 const fragment = compiled.template.content.cloneNode(true);
5812 const parts = compiled.buildParts(fragment);
5813 while (container.firstChild) {
5814 container.removeChild(container.firstChild);
5815 }
5816 container.appendChild(fragment);
5817 applyValues(parts, result.values);
5818 mountState.set(container, { strings: result.strings, parts });
5819 }
5820 function applyValues(parts, values) {
5821 for (const part of parts) {
5822 if (part.kind === "node") {
5823 updateChildPart(part.child, values[part.valueIndex]);
5824 } else if (part.kind === "attr") {
5825 let composed = part.template[0];
5826 for (let i = 0; i < part.valueIndices.length; i++) {
5827 composed += formatText(values[part.valueIndices[i]]);
5828 composed += part.template[i + 1];
5829 }
5830 if (composed !== part.last) {
5831 part.last = composed;
5832 if (composed === "") {
5833 part.element.removeAttribute(part.name);
5834 } else {
5835 part.element.setAttribute(part.name, composed);
5836 }
5837 }
5838 } else if (part.kind === "event") {
5839 const next = values[part.valueIndex];
5840 if (next !== part.current) {
5841 if (part.current) {
5842 part.element.removeEventListener(part.name, part.current);
5843 }
5844 if (next) {
5845 part.element.addEventListener(part.name, next);
5846 }
5847 part.current = next;
5848 }
5849 } else if (part.kind === "prop") {
5850 const next = values[part.valueIndex];
5851 if (next !== part.last) {
5852 part.last = next;
5853 part.element[part.name] = next;
5854 }
5855 } else if (part.kind === "bool") {
5856 const next = !!values[part.valueIndex];
5857 if (next !== part.last) {
5858 part.last = next;
5859 if (next) {
5860 part.element.setAttribute(part.name, "");
5861 } else {
5862 part.element.removeAttribute(part.name);
5863 }
5864 }
5865 }
5866 }
5867 }
5868 function updateChildPart(child, value) {
5869 if (value === null || value === void 0 || value === false) {
5870 if (child.state) {
5871 disposeChildState(child.state);
5872 child.state = null;
5873 }
5874 return;
5875 }
5876 if (Array.isArray(value)) {
5877 updateArrayChild(child, value);
5878 return;
5879 }
5880 if (isTemplateResult(value)) {
5881 updateTemplateChild(child, value);
5882 return;
5883 }
5884 if (value instanceof Node) {
5885 updateNodeChild(child, value);
5886 return;
5887 }
5888 updateTextChild(child, formatText(value));
5889 }
5890 function updateNodeChild(child, node) {
5891 const old = child.state;
5892 if (old?.shape === "node" && old.node === node) {
5893 return;
5894 }
5895 if (old) {
5896 disposeChildState(old);
5897 }
5898 insertBeforeAnchor(child, [node]);
5899 child.state = { shape: "node", node };
5900 }
5901 function updateTextChild(child, text) {
5902 const old = child.state;
5903 if (old?.shape === "text") {
5904 if (old.text !== text) {
5905 old.node.textContent = text;
5906 old.text = text;
5907 }
5908 return;
5909 }
5910 if (old) {
5911 disposeChildState(old);
5912 }
5913 const node = document.createTextNode(text);
5914 insertBeforeAnchor(child, [node]);
5915 child.state = { shape: "text", node, text };
5916 }
5917 function updateTemplateChild(child, result) {
5918 const old = child.state;
5919 if (old?.shape === "template" && old.strings === result.strings) {
5920 applyValues(old.parts, result.values);
5921 return;
5922 }
5923 if (old) {
5924 disposeChildState(old);
5925 }
5926 const compiled = compile(result.strings);
5927 const fragment = compiled.template.content.cloneNode(true);
5928 const parts = compiled.buildParts(fragment);
5929 const topNodes = Array.from(fragment.childNodes);
5930 insertBeforeAnchor(child, [fragment]);
5931 applyValues(parts, result.values);
5932 child.state = {
5933 shape: "template",
5934 strings: result.strings,
5935 parts,
5936 nodes: topNodes
5937 };
5938 }
5939 function updateArrayChild(child, arr) {
5940 const old = child.state;
5941 if (old?.shape === "array" && old.entries.length === arr.length) {
5942 for (let i = 0; i < arr.length; i++) {
5943 updateChildPart(old.entries[i], arr[i]);
5944 }
5945 return;
5946 }
5947 if (old) {
5948 disposeChildState(old);
5949 }
5950 const entries = [];
5951 for (const v of arr) {
5952 const entryAnchor = document.createTextNode("");
5953 insertBeforeAnchor(child, [entryAnchor]);
5954 const entry = { anchor: entryAnchor, state: null };
5955 updateChildPart(entry, v);
5956 entries.push(entry);
5957 }
5958 child.state = { shape: "array", entries };
5959 }
5960 function insertBeforeAnchor(child, nodes) {
5961 const parent = child.anchor.parentNode;
5962 if (!parent) {
5963 return;
5964 }
5965 for (const node of nodes) {
5966 parent.insertBefore(node, child.anchor);
5967 }
5968 }
5969 function disposeChildState(state2) {
5970 if (state2.shape === "text") {
5971 state2.node.remove();
5972 return;
5973 }
5974 if (state2.shape === "template") {
5975 for (const node of state2.nodes) {
5976 if (node.parentNode) {
5977 node.parentNode.removeChild(node);
5978 }
5979 }
5980 return;
5981 }
5982 if (state2.shape === "node") {
5983 if (state2.node.parentNode) {
5984 state2.node.parentNode.removeChild(state2.node);
5985 }
5986 return;
5987 }
5988 for (const entry of state2.entries) {
5989 if (entry.state) {
5990 disposeChildState(entry.state);
5991 }
5992 entry.anchor.remove();
5993 }
5994 }
5995 function formatText(v) {
5996 if (v === null || v === void 0 || v === false) {
5997 return "";
5998 }
5999 return String(v);
6000 }
6001 const _Component = class _Component extends HTMLElement {
6002 constructor() {
6003 super();
6004 this._renderScheduled = false;
6005 this._propValues = {};
6006 const ctor = this.constructor;
6007 if (ctor.shadow) {
6008 this.attachShadow({ mode: "open" });
6009 this._renderRoot = this.shadowRoot;
6010 } else {
6011 this._renderRoot = this;
6012 }
6013 this._installPropAccessors();
6014 }
6015 static get observedAttributes() {
6016 return this.props.map(kebab);
6017 }
6018 connectedCallback() {
6019 this._adoptStyles();
6020 this.requestUpdate();
6021 }
6022 attributeChangedCallback(name, oldValue, newValue) {
6023 if (oldValue === newValue) {
6024 return;
6025 }
6026 const prop = camel(name);
6027 this._propValues[prop] = newValue;
6028 this.requestUpdate();
6029 }
6030 /**
6031 * Declarative class-name setter. Assign an array (or a
6032 * space-separated string) and the host's `class` attribute is
6033 * rewritten to match. Intended for programmatic styling — when
6034 * a plugin has enqueued its own stylesheet and wants to apply
6035 * one of those classes to a shell component:
6036 *
6037 * ```js
6038 * element.classNames = [ 'my-plugin-brand', 'is-active' ];
6039 * // → <wpd-select class="my-plugin-brand is-active">
6040 * ```
6041 *
6042 * The plain HTML `class="…"` attribute works just the same and
6043 * is always preferred when writing markup by hand — this setter
6044 * exists for the JS-API case where the caller has an array of
6045 * conditional classes in hand.
6046 *
6047 * Getter returns the current `classList` as a plain array for
6048 * symmetric read/write.
6049 *
6050 * @since 0.5.0
6051 */
6052 get classNames() {
6053 return Array.from(this.classList);
6054 }
6055 set classNames(next) {
6056 if (next === null || next === void 0) {
6057 this.removeAttribute("class");
6058 return;
6059 }
6060 const list2 = Array.isArray(next) ? next : String(next).split(/\s+/);
6061 const cleaned = list2.map((s) => String(s).trim()).filter((s) => s !== "");
6062 this.className = cleaned.join(" ");
6063 }
6064 /**
6065 * Request a re-render explicitly. Components rarely need this —
6066 * declare state via props + attribute observers and the render
6067 * loop picks up changes automatically.
6068 */
6069 requestUpdate() {
6070 this._scheduleRender();
6071 }
6072 /**
6073 * Dispatch a `CustomEvent` with a `detail`. Bubbles + composed
6074 * by default (matches typical WC UX — events cross shadow
6075 * boundaries, parents can listen without knowing about internal
6076 * structure).
6077 */
6078 emit(name, detail) {
6079 return this.dispatchEvent(
6080 new CustomEvent(name, {
6081 detail,
6082 bubbles: true,
6083 composed: true
6084 })
6085 );
6086 }
6087 // ------------------------------------------------------------------
6088 // Internals
6089 // ------------------------------------------------------------------
6090 /**
6091 * Wire every `static props` entry to a matched property getter +
6092 * setter on the element. Setting the property reflects into the
6093 * attribute (so downstream observers + CSS selectors see it);
6094 * reading the property falls back to the attribute.
6095 */
6096 _installPropAccessors() {
6097 const ctor = this.constructor;
6098 for (const prop of ctor.props) {
6099 if (Object.getOwnPropertyDescriptor(this, prop)) {
6100 continue;
6101 }
6102 const attr = kebab(prop);
6103 Object.defineProperty(this, prop, {
6104 get: () => {
6105 if (prop in this._propValues) {
6106 return this._propValues[prop];
6107 }
6108 return this.getAttribute(attr);
6109 },
6110 set: (value) => {
6111 let str;
6112 if (value === null || value === void 0 || value === false) {
6113 str = null;
6114 } else if (value === true) {
6115 str = "";
6116 } else {
6117 str = String(value);
6118 }
6119 this._propValues[prop] = str;
6120 if (str === null) {
6121 this.removeAttribute(attr);
6122 } else {
6123 this.setAttribute(attr, str);
6124 }
6125 this.requestUpdate();
6126 },
6127 enumerable: true,
6128 configurable: true
6129 });
6130 }
6131 }
6132 /**
6133 * Schedule a render on the next microtask. Multiple property
6134 * assignments in the same tick collapse into a single render.
6135 */
6136 _scheduleRender() {
6137 if (this._renderScheduled || !this.isConnected) {
6138 return;
6139 }
6140 this._renderScheduled = true;
6141 queueMicrotask(() => {
6142 this._renderScheduled = false;
6143 if (!this.isConnected) {
6144 return;
6145 }
6146 render$1(this.render(), this._renderRoot);
6147 });
6148 }
6149 /**
6150 * Mount adoptable stylesheets onto the shadow root (via
6151 * `adoptedStyleSheets`) or the light DOM (via one `<style>`
6152 * tag per def). No-op if `static styles` is empty.
6153 */
6154 _adoptStyles() {
6155 const ctor = this.constructor;
6156 if (ctor.styles.length === 0) {
6157 return;
6158 }
6159 if (ctor.shadow && this.shadowRoot) {
6160 const sheets = ctor.styles.map((s) => s.sheet).filter((s) => s !== null);
6161 this.shadowRoot.adoptedStyleSheets = sheets;
6162 if (sheets.length !== ctor.styles.length) {
6163 for (const s of ctor.styles) {
6164 if (!s.sheet) {
6165 const tag = document.createElement("style");
6166 tag.textContent = s.cssText;
6167 this.shadowRoot.appendChild(tag);
6168 }
6169 }
6170 }
6171 } else {
6172 this._adoptLightStyles(ctor);
6173 }
6174 }
6175 _adoptLightStyles(ctor) {
6176 if (_Component._lightStylesAdopted.has(ctor)) {
6177 return;
6178 }
6179 _Component._lightStylesAdopted.add(ctor);
6180 for (const s of ctor.styles) {
6181 const tag = document.createElement("style");
6182 tag.dataset.wpdUi = this.tagName.toLowerCase();
6183 tag.textContent = s.cssText;
6184 document.head.appendChild(tag);
6185 }
6186 }
6187 };
6188 _Component.props = [];
6189 _Component.styles = [];
6190 _Component.shadow = true;
6191 _Component._lightStylesAdopted = /* @__PURE__ */ new WeakSet();
6192 let Component = _Component;
6193 function defineComponent(tag, ctor) {
6194 if (customElements.get(tag)) {
6195 return;
6196 }
6197 customElements.define(tag, ctor);
6198 }
6199 function kebab(s) {
6200 return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
6201 }
6202 function camel(s) {
6203 return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
6204 }
6205 const SUPPORTS_CONSTRUCTABLE_SHEETS = (() => {
6206 try {
6207 const s = new CSSStyleSheet();
6208 return typeof s.replaceSync === "function";
6209 } catch {
6210 return false;
6211 }
6212 })();
6213 function css(strings, ...values) {
6214 let text = strings[0];
6215 for (let i = 1; i < strings.length; i++) {
6216 const v = values[i - 1];
6217 if (typeof v === "string" || typeof v === "number") {
6218 text += String(v);
6219 } else if (v && v.__wpdCss) {
6220 text += v.cssText;
6221 } else {
6222 throw new TypeError(
6223 "[wpd-ui] css`` interpolations must be strings, numbers, or other css`` results. Got: " + typeof v
6224 );
6225 }
6226 text += strings[i];
6227 }
6228 if (SUPPORTS_CONSTRUCTABLE_SHEETS) {
6229 const sheet = new CSSStyleSheet();
6230 sheet.replaceSync(text);
6231 return { __wpdCss: true, sheet, cssText: text };
6232 }
6233 return { __wpdCss: true, sheet: null, cssText: text };
6234 }
6235 function computeAutoId(element) {
6236 const parts = [];
6237 const tabs = [];
6238 let windowId = null;
6239 let node = element.parentElement;
6240 while (node) {
6241 if (node === document.body || node === document.documentElement) {
6242 break;
6243 }
6244 const id = node.id || "";
6245 if (id.startsWith("wp-window-")) {
6246 windowId = id.slice("wp-window-".length);
6247 break;
6248 }
6249 if (node.tagName.toLowerCase() === "wpd-tabpanel") {
6250 const forValue = node.getAttribute("for");
6251 if (forValue) {
6252 tabs.unshift(forValue);
6253 }
6254 }
6255 node = node.parentElement;
6256 }
6257 if (windowId) {
6258 parts.push(slugify(windowId));
6259 }
6260 for (const tab of tabs) {
6261 parts.push("tab-" + slugify(tab));
6262 }
6263 const label = element.getAttribute("label");
6264 if (label) {
6265 parts.push(slugify(label));
6266 }
6267 if (parts.length === 0) {
6268 return "wpd-unnamed";
6269 }
6270 return "wpd-" + parts.filter((p) => p !== "").join("-");
6271 }
6272 function slugify(s) {
6273 return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
6274 }
6275 function ensureAutoId(element) {
6276 if (element.id) {
6277 return element.id;
6278 }
6279 const id = computeAutoId(element);
6280 element.id = id;
6281 return id;
6282 }
6283 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 )}`;
6284 const _WpdConfirmDialog = class _WpdConfirmDialog extends Component {
6285 constructor() {
6286 super(...arguments);
6287 this._onKey = (e) => {
6288 if (e.key === "Escape") {
6289 e.preventDefault();
6290 this._cancel();
6291 }
6292 if (e.key === "Enter" && !e.isComposing) {
6293 e.preventDefault();
6294 this._confirm();
6295 }
6296 };
6297 this._onBackdrop = (e) => {
6298 const path = e.composedPath();
6299 const original = path.length > 0 ? path[0] : e.target;
6300 if (original === this) {
6301 this._cancel();
6302 }
6303 };
6304 this._confirm = () => {
6305 this.emit("wpd-confirm", { confirmed: true });
6306 this.removeAttribute("open");
6307 };
6308 this._cancel = () => {
6309 this.emit("wpd-cancel", { confirmed: false });
6310 this.removeAttribute("open");
6311 };
6312 }
6313 connectedCallback() {
6314 super.connectedCallback();
6315 this.setAttribute("role", "dialog");
6316 this.setAttribute("aria-modal", "true");
6317 this.addEventListener("keydown", this._onKey);
6318 this.addEventListener("click", this._onBackdrop);
6319 }
6320 disconnectedCallback() {
6321 this.removeEventListener("keydown", this._onKey);
6322 this.removeEventListener("click", this._onBackdrop);
6323 }
6324 render() {
6325 const title = this.title ?? "";
6326 const message = this.message ?? "";
6327 const confirmLabel = this["confirm-label"] || "Confirm";
6328 const cancelLabel = this["cancel-label"] || "Cancel";
6329 const isDanger = this.hasAttribute("danger");
6330 const hideCancel = this.hasAttribute("hide-cancel");
6331 const isDismissable = this.hasAttribute("dismissable");
6332 return html`
6333 <div class="dialog" tabindex="-1">
6334 ${isDismissable ? html`<button
6335 type="button"
6336 class="close"
6337 aria-label="Close"
6338 @click=${() => this._cancel()}
6339 >&times;</button>` : html``}
6340 ${title ? html`<h2 class="title">${title}</h2>` : html``}
6341 ${message ? html`<p class="message">${message}</p>` : html``}
6342 <div class="actions">
6343 ${hideCancel ? html`` : html`<button
6344 type="button"
6345 class="btn btn--secondary"
6346 @click=${() => this._cancel()}
6347 >
6348 ${cancelLabel}
6349 </button>`}
6350 <button
6351 type="button"
6352 class="btn ${isDanger ? "btn--danger" : "btn--primary"}"
6353 @click=${() => this._confirm()}
6354 >
6355 ${confirmLabel}
6356 </button>
6357 </div>
6358 </div>
6359 `;
6360 }
6361 };
6362 _WpdConfirmDialog.props = [
6363 "open",
6364 "title",
6365 "message",
6366 "confirm-label",
6367 "cancel-label",
6368 "danger",
6369 "hide-cancel",
6370 "dismissable"
6371 ];
6372 _WpdConfirmDialog.styles = [dialogStyles];
6373 _WpdConfirmDialog.help = {
6374 title: "Confirm dialog",
6375 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.",
6376 status: "experimental",
6377 since: "0.9.0",
6378 props: [
6379 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
6380 { name: "title", type: "string", description: "Heading shown at the top." },
6381 { name: "message", type: "string", description: "Body copy. Newlines preserved." },
6382 { name: "confirm-label", type: "string", default: "Confirm", description: "Confirm-button label." },
6383 { name: "cancel-label", type: "string", default: "Cancel", description: "Cancel-button label." },
6384 { name: "danger", type: "boolean attribute", description: "Renders the confirm button red." },
6385 { 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." },
6386 { name: "dismissable", type: "boolean attribute", description: "Renders an X close button in the top-right corner. Click emits `wpd-cancel`." }
6387 ],
6388 events: [
6389 {
6390 name: "wpd-confirm",
6391 description: "Fires on confirm. Detail: `{ confirmed: true }`."
6392 },
6393 {
6394 name: "wpd-cancel",
6395 description: "Fires on cancel (Cancel button, Escape, backdrop click). Detail: `{ confirmed: false }`."
6396 }
6397 ]
6398 };
6399 let WpdConfirmDialog = _WpdConfirmDialog;
6400 defineComponent("wpd-confirm-dialog", WpdConfirmDialog);
6401 function wpdConfirm$1(options) {
6402 return new Promise((resolve2) => {
6403 const dialog2 = document.createElement("wpd-confirm-dialog");
6404 dialog2.setAttribute("open", "");
6405 if (options.title) {
6406 dialog2.setAttribute("title", options.title);
6407 }
6408 dialog2.setAttribute("message", options.message);
6409 if (options.confirmLabel) {
6410 dialog2.setAttribute("confirm-label", options.confirmLabel);
6411 }
6412 if (options.cancelLabel) {
6413 dialog2.setAttribute("cancel-label", options.cancelLabel);
6414 }
6415 if (options.danger) {
6416 dialog2.setAttribute("danger", "");
6417 }
6418 if (options.hideCancel) {
6419 dialog2.setAttribute("hide-cancel", "");
6420 }
6421 if (options.dismissable) {
6422 dialog2.setAttribute("dismissable", "");
6423 }
6424 const cleanup = (ok) => {
6425 dialog2.remove();
6426 resolve2(ok);
6427 };
6428 dialog2.addEventListener("wpd-confirm", () => cleanup(true));
6429 dialog2.addEventListener("wpd-cancel", () => cleanup(false));
6430 document.body.appendChild(dialog2);
6431 const inner = dialog2.shadowRoot?.querySelector(".dialog");
6432 (inner ?? dialog2).focus?.();
6433 });
6434 }
6435 const FALLBACK_BASE = "http://localhost/";
6436 function joinRestUrl(restRoot2, path) {
6437 const base = typeof window !== "undefined" && window.location ? window.location.href : FALLBACK_BASE;
6438 const url = new URL(restRoot2, base);
6439 const trimmed = path.replace(/^\/+/, "");
6440 const queryAt = trimmed.indexOf("?");
6441 const route = queryAt === -1 ? trimmed : trimmed.slice(0, queryAt);
6442 const extraQuery = queryAt === -1 ? "" : trimmed.slice(queryAt + 1);
6443 if (url.searchParams.has("rest_route")) {
6444 const existing = url.searchParams.get("rest_route") ?? "/";
6445 const prefix = existing.endsWith("/") ? existing : existing + "/";
6446 url.searchParams.set("rest_route", prefix + route);
6447 } else {
6448 const pathname = url.pathname.endsWith("/") ? url.pathname : url.pathname + "/";
6449 url.pathname = pathname + route;
6450 }
6451 if (extraQuery) {
6452 const extras = new URLSearchParams(extraQuery);
6453 extras.forEach((value, key) => {
6454 url.searchParams.append(key, value);
6455 });
6456 }
6457 return url.toString();
6458 }
6459 function getApi() {
6460 const w = window;
6461 return w.wp?.desktop ?? null;
6462 }
6463 let activeMenu$3 = null;
6464 function closeMenu$1() {
6465 if (activeMenu$3) {
6466 activeMenu$3.remove();
6467 activeMenu$3 = null;
6468 }
6469 }
6470 function writeVisibility(canonicalId, placement) {
6471 const api = getApi();
6472 if (!api?.getOsSettings || !api?.updateOsSettings) {
6473 return;
6474 }
6475 const snap = api.getOsSettings();
6476 const next = { ...snap.itemVisibility };
6477 next[canonicalId] = placement;
6478 api.updateOsSettings({ itemVisibility: next });
6479 }
6480 function railFromId(id, surface) {
6481 if (id.startsWith("dock:")) {
6482 return "dock";
6483 }
6484 if (id.startsWith("desktop:")) {
6485 return "desktop";
6486 }
6487 return surface;
6488 }
6489 function computeHideTarget(canonicalId, nativeRail, hideSurface, visibility) {
6490 const current = resolvePlacement(canonicalId, nativeRail, visibility);
6491 if (current === "both") {
6492 return hideSurface === "dock" ? "desktop" : "dock";
6493 }
6494 return "hidden";
6495 }
6496 let openGeneration$2 = 0;
6497 function openItemVisibilityMenu(opts) {
6498 closeMenu$1();
6499 const myGen = ++openGeneration$2;
6500 openWithShellOverlays(
6501 () => myGen === openGeneration$2,
6502 () => openItemVisibilityMenuImmediate(opts)
6503 );
6504 }
6505 function openItemVisibilityMenuImmediate(opts) {
6506 closeMenu$1();
6507 const canonical = canonicalItemId(opts.id);
6508 const nativeRail = railFromId(opts.id, opts.surface);
6509 const currentPlacement = resolvePlacement(
6510 canonical,
6511 nativeRail,
6512 getApi()?.getOsSettings?.().itemVisibility ?? {}
6513 );
6514 const options = [];
6515 if (opts.surface === "dock") {
6516 options.push({
6517 id: "hide-from-dock",
6518 label: __("Hide from dock"),
6519 icon: "dashicons-hidden",
6520 onPick: () => writeVisibility(
6521 canonical,
6522 computeHideTarget(
6523 canonical,
6524 nativeRail,
6525 "dock",
6526 getApi()?.getOsSettings?.().itemVisibility ?? {}
6527 )
6528 )
6529 });
6530 if (currentPlacement !== "both") {
6531 options.push({
6532 id: "show-on-desktop-too",
6533 label: __("Also show on desktop"),
6534 icon: "dashicons-desktop",
6535 onPick: () => writeVisibility(canonical, "both")
6536 });
6537 }
6538 } else {
6539 options.push({
6540 id: "hide-from-desktop",
6541 label: __("Hide from desktop"),
6542 icon: "dashicons-hidden",
6543 onPick: () => writeVisibility(
6544 canonical,
6545 computeHideTarget(
6546 canonical,
6547 nativeRail,
6548 "desktop",
6549 getApi()?.getOsSettings?.().itemVisibility ?? {}
6550 )
6551 )
6552 });
6553 if (currentPlacement !== "both") {
6554 options.push({
6555 id: "show-on-dock-too",
6556 label: __("Also show on dock"),
6557 icon: "dashicons-menu",
6558 onPick: () => writeVisibility(canonical, "both")
6559 });
6560 }
6561 }
6562 options.push({
6563 id: "hide-everywhere",
6564 label: __("Hide everywhere"),
6565 icon: "dashicons-no",
6566 danger: true,
6567 onPick: () => writeVisibility(canonical, "hidden")
6568 });
6569 options.push({
6570 id: "open-settings",
6571 label: __("Apps & Icons settings…"),
6572 icon: "dashicons-admin-generic",
6573 onPick: () => {
6574 const api = getApi();
6575 api?.openOsSettings?.({ tabId: "apps-icons" });
6576 }
6577 });
6578 if (opts.pluginFile) {
6579 const pluginFile = opts.pluginFile;
6580 const pluginLabel = opts.pluginName || opts.title;
6581 options.push({ kind: "separator" });
6582 options.push({
6583 id: "deactivate-plugin",
6584 // translators: %s is the owning plugin's display name.
6585 label: sprintf(__("Deactivate %s…"), pluginLabel),
6586 icon: "dashicons-trash",
6587 danger: true,
6588 onPick: () => {
6589 void confirmAndDeactivatePlugin(pluginFile, pluginLabel);
6590 }
6591 });
6592 }
6593 const menu = document.createElement("wpd-context-menu");
6594 menu.setAttribute("open", "");
6595 menu.classList.add("desktop-mode-item-visibility-menu");
6596 menu.dataset.itemId = opts.id;
6597 menu.style.position = "fixed";
6598 menu.style.left = "-9999px";
6599 menu.style.top = "-9999px";
6600 menu.style.visibility = "hidden";
6601 menu.style.zIndex = "1000000";
6602 const byKey = /* @__PURE__ */ new Map();
6603 for (const opt of options) {
6604 if (opt.kind === "separator") {
6605 const hr = document.createElement("hr");
6606 hr.style.cssText = "border: 0; border-top: 1px solid var( --wpd-context-menu-separator-color, rgba(255,255,255,0.12) ); margin: 4px 6px;";
6607 menu.appendChild(hr);
6608 continue;
6609 }
6610 byKey.set(opt.id, opt);
6611 const node = document.createElement("wpd-context-menu-option");
6612 node.dataset.menuItemId = opt.id;
6613 node.setAttribute("value", opt.id);
6614 if (opt.icon) {
6615 node.setAttribute("icon", opt.icon);
6616 }
6617 if (opt.danger) {
6618 node.setAttribute("danger", "");
6619 }
6620 node.textContent = opt.label;
6621 menu.appendChild(node);
6622 }
6623 menu.addEventListener("wpd-context-menu-pick", (e) => {
6624 const detail = e.detail;
6625 const key = detail?.id || detail?.value || "";
6626 const opt = byKey.get(key);
6627 closeMenu$1();
6628 try {
6629 opt?.onPick();
6630 } catch {
6631 }
6632 });
6633 document.body.appendChild(menu);
6634 activeMenu$3 = menu;
6635 const positionMenu = () => {
6636 if (menu !== activeMenu$3) {
6637 return;
6638 }
6639 const rect = menu.getBoundingClientRect();
6640 const margin = 8;
6641 let left = opts.x;
6642 let top;
6643 if (opts.surface === "dock") {
6644 top = Math.max(margin, opts.y - rect.height - margin);
6645 } else {
6646 top = opts.y;
6647 if (top + rect.height + margin > window.innerHeight) {
6648 top = Math.max(margin, opts.y - rect.height);
6649 }
6650 }
6651 if (left + rect.width + margin > window.innerWidth) {
6652 left = Math.max(margin, opts.x - rect.width);
6653 }
6654 menu.style.left = `${left}px`;
6655 menu.style.top = `${top}px`;
6656 menu.style.visibility = "";
6657 };
6658 requestAnimationFrame(positionMenu);
6659 const onOutside = (ev) => {
6660 if (!activeMenu$3) {
6661 return;
6662 }
6663 if (!activeMenu$3.contains(ev.target)) {
6664 closeMenu$1();
6665 document.removeEventListener("mousedown", onOutside, true);
6666 document.removeEventListener("keydown", onKey, true);
6667 }
6668 };
6669 const onKey = (ev) => {
6670 if (ev.key === "Escape") {
6671 closeMenu$1();
6672 document.removeEventListener("mousedown", onOutside, true);
6673 document.removeEventListener("keydown", onKey, true);
6674 }
6675 };
6676 document.addEventListener("mousedown", onOutside, true);
6677 document.addEventListener("keydown", onKey, true);
6678 }
6679 async function confirmAndDeactivatePlugin(pluginFile, title) {
6680 const confirmed = await wpdConfirm$1({
6681 /* translators: %s: plugin title. */
6682 title: sprintf(__("Deactivate %s?"), title),
6683 message: __(
6684 "This plugin will stop running on the site. You can re-activate it later from the Plugins screen."
6685 ),
6686 confirmLabel: __("Deactivate"),
6687 cancelLabel: __("Cancel"),
6688 danger: true
6689 });
6690 if (!confirmed) {
6691 return;
6692 }
6693 const cfg = window.desktopModeConfig ?? {};
6694 const restRoot2 = typeof cfg.restRoot === "string" && cfg.restRoot ? cfg.restRoot : `${window.location.origin}/wp-json/`;
6695 const restNonce = typeof cfg.restNonce === "string" && cfg.restNonce ? cfg.restNonce : "";
6696 const stripped = pluginFile.endsWith(".php") ? pluginFile.slice(0, -4) : pluginFile;
6697 const encoded = stripped.split("/").map(encodeURIComponent).join("/");
6698 const url = joinRestUrl(restRoot2, `wp/v2/plugins/${encoded}`);
6699 try {
6700 const res = await trackedFetch$1(
6701 url,
6702 {
6703 method: "PUT",
6704 headers: {
6705 "Content-Type": "application/json",
6706 "X-WP-Nonce": restNonce
6707 },
6708 body: JSON.stringify({ status: "inactive" }),
6709 credentials: "same-origin"
6710 },
6711 { source: "desktop-mode/dock-deactivate-plugin" }
6712 );
6713 if (!res.ok) {
6714 throw new Error(`HTTP ${res.status}`);
6715 }
6716 } catch (err) {
6717 showToast({
6718 message: sprintf(
6719 /* translators: %s: plugin title. */
6720 __("Could not deactivate %s."),
6721 title
6722 ),
6723 duration: 4e3
6724 });
6725 console.error("[desktop-mode] deactivate plugin failed", err);
6726 return;
6727 }
6728 const closedTitles = closeWindowsForPlugin(pluginFile);
6729 const deactivatedMsg = closedTitles.length > 0 ? sprintf(
6730 /* translators: 1: plugin title. 2: number of windows that were closed. */
6731 __("%1$s deactivated. Closed %2$d window(s)."),
6732 title,
6733 closedTitles.length
6734 ) : sprintf(
6735 /* translators: %s: plugin title. */
6736 __("%s deactivated."),
6737 title
6738 );
6739 showToast({ message: deactivatedMsg, duration: 3e3 });
6740 const w = window;
6741 w.wp?.desktop?.refreshMenu?.();
6742 }
6743 function closeWindowsForPlugin(pluginFile) {
6744 const api = window.wp?.desktop;
6745 if (!api?.windowManager?.getAll) {
6746 return [];
6747 }
6748 const items = api.getMenuItems?.() ?? [];
6749 const owned = items.filter((i) => i.pluginFile === pluginFile);
6750 if (owned.length === 0) {
6751 return [];
6752 }
6753 const ownedKeys = /* @__PURE__ */ new Set();
6754 for (const item of owned) {
6755 ownedKeys.add(item.id);
6756 if (api.deriveWindowId) {
6757 ownedKeys.add(api.deriveWindowId(item.url));
6758 }
6759 }
6760 const toClose = /* @__PURE__ */ new Map();
6761 const windows = api.windowManager.getAll() ?? [];
6762 const derive = api.deriveWindowId;
6763 for (const w of windows) {
6764 if (ownedKeys.has(w.id)) {
6765 toClose.set(w.id, w);
6766 continue;
6767 }
6768 if (w.config?.baseId && ownedKeys.has(w.config.baseId)) {
6769 toClose.set(w.id, w);
6770 continue;
6771 }
6772 if (derive && w.config?.url) {
6773 const derivedFromConfig = derive(w.config.url);
6774 if (ownedKeys.has(derivedFromConfig)) {
6775 toClose.set(w.id, w);
6776 continue;
6777 }
6778 }
6779 if (derive && w.iframe) {
6780 let liveUrl = "";
6781 try {
6782 liveUrl = w.iframe.src || "";
6783 } catch {
6784 }
6785 if (liveUrl) {
6786 const derivedFromLive = derive(liveUrl);
6787 if (ownedKeys.has(derivedFromLive)) {
6788 toClose.set(w.id, w);
6789 }
6790 }
6791 }
6792 }
6793 const titles = [];
6794 for (const w of toClose.values()) {
6795 titles.push(w.config?.title ?? w.id);
6796 try {
6797 w.close();
6798 } catch {
6799 }
6800 }
6801 return titles;
6802 }
6803 const _Dock = class _Dock {
6804 constructor(container, windowManager, items, adminUrl, orientation = "left") {
6805 this.itemElements = /* @__PURE__ */ new Map();
6806 this.systemItems = [];
6807 this.systemItemElements = /* @__PURE__ */ new Map();
6808 this.systemSeparator = null;
6809 this.badgeOverrides = /* @__PURE__ */ new Map();
6810 this.attentionTimers = /* @__PURE__ */ new Map();
6811 this.peekTeardowns = /* @__PURE__ */ new Map();
6812 this.boundRefresh = () => void 0;
6813 this.container = container;
6814 this.windowManager = windowManager;
6815 this.items = items;
6816 this.adminUrl = adminUrl;
6817 this.orientation = orientation;
6818 this.rail = orientation === "bottom" ? "taskbar" : "dock";
6819 this.hooksNamespace = `desktop-mode/dock/${++_Dock.instanceCounter}`;
6820 this.container.setAttribute(
6821 "data-desktop-mode-dock-placement",
6822 orientation
6823 );
6824 const scroll = document.createElement("div");
6825 scroll.className = "desktop-mode-dock__scroll";
6826 const pinned = document.createElement("div");
6827 pinned.className = "desktop-mode-dock__pinned";
6828 container.appendChild(scroll);
6829 container.appendChild(pinned);
6830 this.itemHost = scroll;
6831 this.systemHost = pinned;
6832 this.tooltip = document.createElement("div");
6833 this.tooltip.className = "desktop-mode-dock__tooltip";
6834 this.tooltip.setAttribute("role", "tooltip");
6835 if (orientation === "bottom") {
6836 this.tooltip.classList.add("desktop-mode-dock__tooltip--above");
6837 } else if (orientation === "right") {
6838 this.tooltip.classList.add("desktop-mode-dock__tooltip--before");
6839 } else {
6840 this.tooltip.classList.add("desktop-mode-dock__tooltip--after");
6841 }
6842 document.body.appendChild(this.tooltip);
6843 this.render();
6844 this.bindWindowEvents();
6845 }
6846 /**
6847 * Build the base context object every dock decoration hook
6848 * receives. Read from `this` so a single subscriber can
6849 * disambiguate two coexisting rails by `dockId`.
6850 */
6851 buildHookContextBase() {
6852 return {
6853 rail: this.rail,
6854 orientation: this.orientation,
6855 dockId: this.container.id,
6856 container: this.container
6857 };
6858 }
6859 /**
6860 * Replace the menu-derived tile list with a fresh one, preserving
6861 * any JS-registered system tiles. Used by the live menu-refresh
6862 * path: after a plugin is activated or deactivated, the chromeless
6863 * bridge postMessages a fresh payload built from real admin
6864 * context, and the shell calls this so the dock repaints without
6865 * a tab reload.
6866 *
6867 * Old menu tiles are removed from both the DOM and the lookup
6868 * map; new tiles are inserted before the system separator (or
6869 * appended at the end if none exists yet), so the menu-items →
6870 * hairline → system-items ordering stays intact. Active-state
6871 * classes are re-computed once the new tiles are in place so
6872 * window indicators survive the swap.
6873 *
6874 * @param items New DockItem list. Pass `[]` to clear everything
6875 * menu-derived.
6876 */
6877 /**
6878 * Update the dock's orientation. Writes the new value to the
6879 * dock element's `data-desktop-mode-dock-placement` attribute (CSS
6880 * keys off it for layout) and keeps the tooltip anchor in sync.
6881 *
6882 * In practice, the layout dispatcher in `desktop.ts` rebuilds the
6883 * dock(s) from scratch on a layout change rather than re-orienting
6884 * a live instance — but this stays correct in case any caller
6885 * wants to flip orientation without the rebuild.
6886 */
6887 setOrientation(orientation) {
6888 if (this.orientation === orientation) {
6889 return;
6890 }
6891 this.orientation = orientation;
6892 this.container.setAttribute(
6893 "data-desktop-mode-dock-placement",
6894 orientation
6895 );
6896 this.tooltip.classList.remove(
6897 "desktop-mode-dock__tooltip--above",
6898 "desktop-mode-dock__tooltip--before",
6899 "desktop-mode-dock__tooltip--after"
6900 );
6901 if (orientation === "bottom") {
6902 this.tooltip.classList.add("desktop-mode-dock__tooltip--above");
6903 } else if (orientation === "right") {
6904 this.tooltip.classList.add("desktop-mode-dock__tooltip--before");
6905 } else {
6906 this.tooltip.classList.add("desktop-mode-dock__tooltip--after");
6907 }
6908 }
6909 replaceItems(items) {
6910 for (const itemId of this.itemElements.keys()) {
6911 const teardown = this.peekTeardowns.get(itemId);
6912 if (teardown) {
6913 teardown();
6914 this.peekTeardowns.delete(itemId);
6915 }
6916 }
6917 for (const el of this.itemElements.values()) {
6918 el.remove();
6919 }
6920 this.itemHost.querySelectorAll(
6921 ".desktop-mode-dock__separator--group"
6922 ).forEach((el) => el.remove());
6923 this.itemElements.clear();
6924 this.items = items;
6925 const base = this.buildHookContextBase();
6926 doAction(HOOKS.DOCK_BEFORE_RENDER, {
6927 ...base,
6928 items,
6929 tileElements: this.itemElements
6930 });
6931 let insertedGroupSeparator = false;
6932 let tilesInsertedThisPass = 0;
6933 for (const item of items) {
6934 if (!insertedGroupSeparator && item.isCore === false) {
6935 if (tilesInsertedThisPass > 0) {
6936 const sep = document.createElement("div");
6937 sep.className = "desktop-mode-dock__separator desktop-mode-dock__separator--group";
6938 sep.setAttribute("aria-hidden", "true");
6939 this.itemHost.appendChild(sep);
6940 }
6941 insertedGroupSeparator = true;
6942 }
6943 const btn = this.createItemButton(item);
6944 this.itemElements.set(item.id, btn);
6945 this.itemHost.appendChild(btn);
6946 tilesInsertedThisPass++;
6947 const override = this.badgeOverrides.get(item.id);
6948 if (override !== void 0) {
6949 const primary = btn.querySelector(
6950 ".desktop-mode-dock__item-primary"
6951 );
6952 _applyBadgeNode(primary ?? btn, override);
6953 }
6954 doAction(HOOKS.DOCK_TILE_RENDERED, {
6955 ...base,
6956 item,
6957 isSystem: false,
6958 el: btn
6959 });
6960 }
6961 this.updateActiveStates();
6962 doAction(HOOKS.DOCK_AFTER_RENDER, {
6963 ...base,
6964 items,
6965 tileElements: this.itemElements
6966 });
6967 }
6968 /**
6969 * True when the rail currently has ANY renderable tile —
6970 * either a menu-derived item or a JS-registered system item.
6971 * Lets callers (the shell's live-refresh path) decide whether
6972 * to hide the whole rail without having to peek into two
6973 * internal maps. "System tiles keep the rail alive even when
6974 * menu items are empty" is the user-visible contract we enforce.
6975 */
6976 hasItems() {
6977 return this.itemElements.size > 0 || this.systemItemElements.size > 0;
6978 }
6979 /**
6980 * Remove a previously-registered system item. Used by the
6981 * server-driven native-window sync path — when a plugin is
6982 * deactivated, its native-window entry disappears from the
6983 * server's payload and the shell calls this to pull the tile
6984 * back off the rail without a reload.
6985 *
6986 * Idempotent: an unknown id is a silent no-op. The system
6987 * separator is kept in place as long as at least one system
6988 * item remains; removing the last system item also strips the
6989 * separator so the rail doesn't dangle a divider under nothing.
6990 */
6991 removeSystemItem(id) {
6992 const tile2 = this.systemItemElements.get(id);
6993 if (!tile2) {
6994 return;
6995 }
6996 tile2.remove();
6997 this.systemItemElements.delete(id);
6998 this.systemItems = this.systemItems.filter((s) => s.id !== id);
6999 this.badgeOverrides.delete(id);
7000 if (this.systemItemElements.size === 0 && this.systemSeparator) {
7001 this.systemSeparator.remove();
7002 this.systemSeparator = null;
7003 }
7004 doAction(HOOKS.DOCK_ITEM_REMOVED, { id, placement: this.rail });
7005 }
7006 /**
7007 * Set the badge count on a tile. Live-updates without a full
7008 * dock re-render — the existing tile's badge node is mutated in
7009 * place (or created if missing). Pass `0` to remove the badge.
7010 *
7011 * Resolves the tile in id order: menu items (`data-menu-slug`)
7012 * first, then system items (`data-system-id`), so callers can
7013 * use the same id surface regardless of which rail the tile
7014 * happens to live on.
7015 *
7016 * Idempotent: applying the same count is a no-op (no DOM mutation).
7017 *
7018 * @since 0.6.0
7019 *
7020 * @param itemId Tile id (menu slug for admin pages, system id
7021 * for `appendSystemItem` / `registerSystemTile`).
7022 * @param count Non-negative integer. `>99` renders as `99+`.
7023 */
7024 setBadge(itemId, count) {
7025 const tile2 = this._resolveTileElement(itemId);
7026 if (!tile2) {
7027 return;
7028 }
7029 const safe = Math.max(0, Math.floor(Number(count) || 0));
7030 if (safe === 0) {
7031 this.badgeOverrides.delete(itemId);
7032 } else {
7033 this.badgeOverrides.set(itemId, safe);
7034 }
7035 const primary = tile2.querySelector(
7036 ".desktop-mode-dock__item-primary"
7037 );
7038 _applyBadgeNode(primary ?? tile2, safe);
7039 activity.publish("desktop-mode/badge-changed", {
7040 itemId,
7041 count: safe,
7042 rail: this.rail
7043 });
7044 }
7045 /**
7046 * Clear the badge on a tile. Equivalent to `setBadge( id, 0 )`.
7047 *
7048 * @since 0.6.0
7049 */
7050 clearBadge(itemId) {
7051 this.setBadge(itemId, 0);
7052 }
7053 /**
7054 * Apply or clear an attention animation on a tile.
7055 *
7056 * - `'pulse'` — soft halo + scale, ~1.4 s loop. Default.
7057 * - `'shake'` — short horizontal jiggle.
7058 * - `'bounce'` — vertical bob, attention-grabbing.
7059 * - `null` — clear any active attention.
7060 *
7061 * Animations are gated on `prefers-reduced-motion: no-preference`;
7062 * the reduced-motion fallback shows a static accent ring for the
7063 * same duration so the affordance still works. `durationMs` of
7064 * `0` keeps the attention until the next call clears it.
7065 *
7066 * @since 0.6.0
7067 *
7068 * @param itemId Tile id.
7069 * @param mode Animation mode or `null` to clear.
7070 * @param opts Optional duration / intensity overrides.
7071 */
7072 setAttention(itemId, mode, opts = {}) {
7073 const tile2 = this._resolveTileElement(itemId);
7074 if (!tile2) {
7075 return;
7076 }
7077 const pending2 = this.attentionTimers.get(itemId);
7078 if (pending2 !== void 0) {
7079 window.clearTimeout(pending2);
7080 this.attentionTimers.delete(itemId);
7081 }
7082 tile2.classList.remove(
7083 "desktop-mode-dock__item--attention-pulse",
7084 "desktop-mode-dock__item--attention-shake",
7085 "desktop-mode-dock__item--attention-bounce",
7086 "desktop-mode-dock__item--intensity-subtle",
7087 "desktop-mode-dock__item--intensity-normal",
7088 "desktop-mode-dock__item--intensity-strong"
7089 );
7090 if (mode === null) {
7091 return;
7092 }
7093 tile2.classList.add(`desktop-mode-dock__item--attention-${mode}`);
7094 const intensity = opts.intensity ?? "normal";
7095 tile2.classList.add(`desktop-mode-dock__item--intensity-${intensity}`);
7096 const duration = opts.durationMs ?? 4e3;
7097 if (duration > 0) {
7098 const handle = window.setTimeout(() => {
7099 this.attentionTimers.delete(itemId);
7100 this.setAttention(itemId, null);
7101 }, duration);
7102 this.attentionTimers.set(itemId, handle);
7103 }
7104 }
7105 /**
7106 * Resolve a tile element by id — checks menu items first
7107 * (`data-menu-slug`), then system items (`data-system-id`). Used
7108 * by `setBadge` / `setAttention` so callers can reach either rail
7109 * with one id surface.
7110 */
7111 _resolveTileElement(itemId) {
7112 return this.itemElements.get(itemId) ?? this.systemItemElements.get(itemId) ?? null;
7113 }
7114 /**
7115 * Append a JS-registered system item to the dock.
7116 *
7117 * System items render after the menu-derived items, separated by a
7118 * hairline divider. Use for shell affordances that don't live in
7119 * the admin menu: OS Settings today, Jorvy and desktop widgets
7120 * later. Callers supply their own `onOpen` — the dock doesn't
7121 * assume the item opens a window at all.
7122 */
7123 appendSystemItem(item) {
7124 this.systemItems.push(item);
7125 if (!this.systemSeparator) {
7126 this.systemSeparator = document.createElement("div");
7127 this.systemSeparator.className = "desktop-mode-dock__separator";
7128 this.systemSeparator.setAttribute("aria-hidden", "true");
7129 this.systemHost.appendChild(this.systemSeparator);
7130 }
7131 const tile2 = this.createSystemItemButton(item);
7132 this.systemItemElements.set(item.id, tile2);
7133 this.systemHost.appendChild(tile2);
7134 this.updateActiveStates();
7135 doAction(HOOKS.DOCK_TILE_RENDERED, {
7136 ...this.buildHookContextBase(),
7137 item,
7138 isSystem: true,
7139 el: tile2
7140 });
7141 }
7142 /**
7143 * Render the dock contents.
7144 *
7145 * Items are ordered server-side with core WordPress menus first and
7146 * plugin-contributed menus after. We insert a `--group` separator
7147 * at the first core→plugin transition so the two clusters read as
7148 * distinct groups of tiles — "default apps" and "installed apps"
7149 * in macOS-dock parlance. The separator is skipped when the menu
7150 * contains only one kind (no plugin menus, or a theme's filter
7151 * reordered everything into one class).
7152 */
7153 render() {
7154 if (_Dock.activeDragReset) {
7155 const prev = _Dock.activeDragReset;
7156 _Dock.activeDragReset = null;
7157 prev();
7158 }
7159 for (const teardown of this.peekTeardowns.values()) {
7160 teardown();
7161 }
7162 this.peekTeardowns.clear();
7163 this.itemHost.innerHTML = "";
7164 const base = this.buildHookContextBase();
7165 doAction(HOOKS.DOCK_BEFORE_RENDER, {
7166 ...base,
7167 items: this.items,
7168 tileElements: this.itemElements
7169 });
7170 let insertedGroupSeparator = false;
7171 for (const item of this.items) {
7172 if (!insertedGroupSeparator && item.isCore === false) {
7173 if (this.itemHost.childElementCount > 0) {
7174 const sep = document.createElement("div");
7175 sep.className = "desktop-mode-dock__separator desktop-mode-dock__separator--group";
7176 sep.setAttribute("aria-hidden", "true");
7177 this.itemHost.appendChild(sep);
7178 }
7179 insertedGroupSeparator = true;
7180 }
7181 const btn = this.createItemButton(item);
7182 this.itemElements.set(item.id, btn);
7183 this.itemHost.appendChild(btn);
7184 doAction(HOOKS.DOCK_TILE_RENDERED, {
7185 ...base,
7186 item,
7187 isSystem: false,
7188 el: btn
7189 });
7190 }
7191 doAction(HOOKS.DOCK_AFTER_RENDER, {
7192 ...base,
7193 items: this.items,
7194 tileElements: this.itemElements
7195 });
7196 }
7197 /**
7198 * Create a tile for a JS-registered system item. Structurally simpler
7199 * than a menu tile — no submenu, no multi-instance rail, no badge —
7200 * but uses the same base classes so the hover / focus / active
7201 * styling is shared.
7202 */
7203 createSystemItemButton(item) {
7204 const ctx = {
7205 ...this.buildHookContextBase(),
7206 item,
7207 isSystem: true
7208 };
7209 const tile2 = document.createElement("div");
7210 const baseClasses = [
7211 "desktop-mode-dock__item",
7212 "desktop-mode-dock__item--system"
7213 ];
7214 const filteredClasses = applyFilters(
7215 HOOKS.DOCK_TILE_CLASS,
7216 baseClasses,
7217 ctx
7218 );
7219 tile2.className = filteredClasses.join(" ");
7220 tile2.dataset.systemId = item.id;
7221 const primary = document.createElement("button");
7222 primary.className = "desktop-mode-dock__item-primary";
7223 primary.setAttribute("type", "button");
7224 primary.setAttribute("aria-label", item.title);
7225 primary.appendChild(this.resolveIcon(item.icon, item.title));
7226 primary.addEventListener("click", () => item.onOpen());
7227 tile2.appendChild(primary);
7228 this.bindTooltipFiltered(tile2, item.title, ctx);
7229 const teardown = attachDockPeek({
7230 tile: tile2,
7231 item: {
7232 id: item.id,
7233 title: item.title,
7234 icon: item.icon,
7235 url: ""
7236 },
7237 // System tiles target a single native-window id; that id
7238 // is also the baseId the manager stores duplicates under
7239 // when the user opens additional instances via the Ghost
7240 // Card. `getAllByBaseId` returns `[]` / `[one]` for the
7241 // singleton cases and the full set when a multi-capable
7242 // system tile (`multi: true`) has been duplicated.
7243 getInstances: () => this.windowManager.getAllByBaseId(item.id),
7244 enableGhost: !!item.multi,
7245 windowManager: this.windowManager,
7246 getOrientation: () => this.orientation,
7247 openNew: () => {
7248 const fn = item.onOpenNew ?? item.onOpen;
7249 fn();
7250 },
7251 suppressTooltip: (on) => {
7252 if (on) {
7253 this.tooltip.classList.remove(
7254 "desktop-mode-dock__tooltip--visible"
7255 );
7256 }
7257 }
7258 });
7259 this.peekTeardowns.set(`system:${item.id}`, teardown);
7260 return applyFilters(
7261 HOOKS.DOCK_TILE_ELEMENT,
7262 tile2,
7263 ctx
7264 );
7265 }
7266 /**
7267 * Create a single dock icon tile.
7268 *
7269 * A tile is a vertical stack: the primary icon button, plus — for
7270 * multi-capable pages — an instance rail rendered below it showing one
7271 * dot per open window and a trailing "+" to open another. The rail is
7272 * hydrated by {@link updateActiveStates}; here we only place the empty
7273 * container so the DOM is stable.
7274 */
7275 createItemButton(item) {
7276 const ctx = {
7277 ...this.buildHookContextBase(),
7278 item,
7279 isSystem: false
7280 };
7281 const tile2 = document.createElement("div");
7282 const baseClasses = ["desktop-mode-dock__item"];
7283 if (item.multi) {
7284 baseClasses.push("desktop-mode-dock__item--multi");
7285 }
7286 const filteredClasses = applyFilters(
7287 HOOKS.DOCK_TILE_CLASS,
7288 baseClasses,
7289 ctx
7290 );
7291 tile2.className = filteredClasses.join(" ");
7292 tile2.dataset.menuSlug = item.id;
7293 const primary = document.createElement("button");
7294 primary.className = "desktop-mode-dock__item-primary";
7295 primary.setAttribute("type", "button");
7296 primary.setAttribute("aria-label", item.title);
7297 const iconEl = this.resolveIcon(item.icon, item.title, item.url);
7298 primary.appendChild(iconEl);
7299 if (item.badge > 0) {
7300 const displayCount = item.badge > 99 ? "99+" : String(item.badge);
7301 const badge = document.createElement("span");
7302 badge.className = "desktop-mode-dock__badge";
7303 badge.textContent = displayCount;
7304 badge.setAttribute(
7305 "aria-label",
7306 sprintf(
7307 // translators: %d is the number of pending updates / items.
7308 _n("%d update", "%d updates", item.badge),
7309 item.badge
7310 )
7311 );
7312 primary.appendChild(badge);
7313 }
7314 primary.addEventListener("click", () => {
7315 this.openPage(item);
7316 });
7317 tile2.addEventListener("contextmenu", (ev) => {
7318 ev.preventDefault();
7319 openItemVisibilityMenu({
7320 x: ev.clientX,
7321 y: ev.clientY,
7322 id: item.id,
7323 title: item.title,
7324 surface: "dock",
7325 pluginFile: item.pluginFile ?? null,
7326 pluginName: item.pluginName ?? null
7327 });
7328 });
7329 tile2.appendChild(primary);
7330 this.bindTooltipFiltered(tile2, item.title, ctx);
7331 const baseId = this.resolveItemBaseId(item);
7332 const teardown = attachDockPeek({
7333 tile: tile2,
7334 item: {
7335 id: item.id,
7336 title: item.title,
7337 icon: item.icon,
7338 url: item.url
7339 },
7340 // Source instances from `getAllByBaseId` regardless of
7341 // `item.multi`. The Ghost Card spawns duplicates on every
7342 // tile (the `enableGhost: true` below), so any tile —
7343 // including ones synthesized from a desktop icon, where
7344 // `multi` is never set — can end up with >1 open instance.
7345 // A `multi`-gated singleton lookup would only return the
7346 // first window and the peek would silently underreport.
7347 // For genuine singletons that never get duplicated, the
7348 // returned array is just `[one]` (or `[]`), same shape the
7349 // old branch produced.
7350 getInstances: () => this.windowManager.getAllByBaseId(baseId),
7351 // Ghost Card on EVERY tile, regardless of `multi`. The
7352 // affordance reads consistently across the dock — every
7353 // hover-peek surfaces a "+ open another <Page>" card. For
7354 // multi-capable items, clicking it spawns a fresh
7355 // instance. For singletons it falls through to the same
7356 // open-or-focus path the tile click takes — usually a
7357 // no-op (focuses the existing window) but cheap and
7358 // visually consistent.
7359 enableGhost: true,
7360 windowManager: this.windowManager,
7361 getOrientation: () => this.orientation,
7362 openNew: () => this.openNewInstance(item),
7363 suppressTooltip: (on) => {
7364 if (on) {
7365 this.tooltip.classList.remove(
7366 "desktop-mode-dock__tooltip--visible"
7367 );
7368 }
7369 }
7370 });
7371 this.peekTeardowns.set(item.id, teardown);
7372 this.attachDragReorder(tile2, item.id);
7373 return applyFilters(
7374 HOOKS.DOCK_TILE_ELEMENT,
7375 tile2,
7376 ctx
7377 );
7378 }
7379 /**
7380 * Drag-to-reorder for menu tiles. Fixed slots — no interpolated
7381 * positioning. While dragging:
7382 *
7383 * 1. Pointer down on the primary button starts a tentative drag.
7384 * Click handling is preserved by requiring movement past a
7385 * small threshold before we claim the gesture.
7386 * 2. Once claimed, the tile gets a `--dragging` modifier so CSS
7387 * can lift it visually. Every `pointermove` checks which other
7388 * menu tile the cursor is currently over; if it's a different
7389 * tile, we splice the dragged tile in front of it (so adjacent
7390 * tiles slide into the vacated slot).
7391 * 3. On `pointerup` we read the resulting DOM order, persist the
7392 * new id list to `dockOrder` via the public settings writer,
7393 * and the layout-dispatcher subscriber re-applies. Cancellation
7394 * (Escape, pointercancel) reverts to the original order.
7395 *
7396 * @since 0.8.2
7397 */
7398 attachDragReorder(tile2, itemId) {
7399 const THRESHOLD = 5;
7400 const FLIP_MS = 200;
7401 let active2 = false;
7402 let startX = 0;
7403 let startY = 0;
7404 let originalOrder = [];
7405 let originalNext = null;
7406 let pointerId = -1;
7407 let originRect = null;
7408 let justDragged = false;
7409 const hardReset = () => {
7410 active2 = false;
7411 tile2.classList.remove("desktop-mode-dock__item--dragging");
7412 tile2.style.transform = "";
7413 tile2.style.transition = "";
7414 document.removeEventListener("pointermove", onMove);
7415 document.removeEventListener("pointerup", onUp);
7416 document.removeEventListener("pointercancel", onCancel);
7417 document.removeEventListener("keydown", onKey, true);
7418 window.removeEventListener("blur", onBlur);
7419 document.removeEventListener("visibilitychange", onVisibility);
7420 pointerId = -1;
7421 originRect = null;
7422 };
7423 const isMenuTile = (el) => {
7424 return !!el && el instanceof HTMLElement && el.classList.contains("desktop-mode-dock__item") && !el.classList.contains("desktop-mode-dock__item--system") && !!el.dataset.menuSlug;
7425 };
7426 const eachSiblingTile = (fn) => {
7427 for (const child of Array.from(this.itemHost.children)) {
7428 if (child instanceof HTMLElement && child !== tile2 && isMenuTile(child)) {
7429 fn(child);
7430 }
7431 }
7432 };
7433 const snapshotMenuOrder = () => {
7434 const ids = [];
7435 for (const child of Array.from(this.itemHost.children)) {
7436 if (isMenuTile(child)) {
7437 ids.push(child.dataset.menuSlug);
7438 }
7439 }
7440 return ids;
7441 };
7442 const flipSiblings = (prevRects) => {
7443 eachSiblingTile((sib) => {
7444 const prev = prevRects.get(sib);
7445 if (!prev) {
7446 return;
7447 }
7448 const now = sib.getBoundingClientRect();
7449 const dx = prev.left - now.left;
7450 const dy = prev.top - now.top;
7451 if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) {
7452 return;
7453 }
7454 sib.style.transition = "none";
7455 sib.style.transform = `translate(${dx}px, ${dy}px)`;
7456 void sib.offsetHeight;
7457 sib.style.transition = `transform ${FLIP_MS}ms cubic-bezier(0.2, 0.7, 0.3, 1)`;
7458 sib.style.transform = "";
7459 const onEnd = () => {
7460 sib.style.transition = "";
7461 sib.style.transform = "";
7462 sib.removeEventListener("transitionend", onEnd);
7463 };
7464 sib.addEventListener("transitionend", onEnd);
7465 });
7466 };
7467 const onMove = (ev) => {
7468 if (pointerId !== -1 && ev.pointerId !== pointerId) {
7469 return;
7470 }
7471 if (!active2) {
7472 const dx2 = ev.clientX - startX;
7473 const dy2 = ev.clientY - startY;
7474 if (dx2 * dx2 + dy2 * dy2 < THRESHOLD * THRESHOLD) {
7475 return;
7476 }
7477 active2 = true;
7478 originalOrder = snapshotMenuOrder();
7479 originalNext = tile2.nextSibling;
7480 originRect = tile2.getBoundingClientRect();
7481 tile2.classList.add("desktop-mode-dock__item--dragging");
7482 this.tooltip.classList.remove(
7483 "desktop-mode-dock__tooltip--visible"
7484 );
7485 }
7486 if (!originRect) {
7487 return;
7488 }
7489 const dx = ev.clientX - startX;
7490 const dy = ev.clientY - startY;
7491 tile2.style.transform = `translate(${dx}px, ${dy}px)`;
7492 const under = document.elementFromPoint(ev.clientX, ev.clientY);
7493 const targetTile = under?.closest(
7494 ".desktop-mode-dock__item"
7495 );
7496 if (!targetTile || targetTile === tile2) {
7497 return;
7498 }
7499 if (!isMenuTile(targetTile)) {
7500 return;
7501 }
7502 const rect = targetTile.getBoundingClientRect();
7503 let insertBefore;
7504 if (this.orientation === "bottom") {
7505 insertBefore = ev.clientX < rect.left + rect.width / 2;
7506 } else {
7507 insertBefore = ev.clientY < rect.top + rect.height / 2;
7508 }
7509 const prevRects = /* @__PURE__ */ new Map();
7510 eachSiblingTile((sib) => {
7511 prevRects.set(sib, sib.getBoundingClientRect());
7512 });
7513 let reordered = false;
7514 if (insertBefore) {
7515 if (targetTile !== tile2.nextSibling) {
7516 this.itemHost.insertBefore(tile2, targetTile);
7517 reordered = true;
7518 }
7519 } else if (targetTile.nextSibling !== tile2) {
7520 this.itemHost.insertBefore(tile2, targetTile.nextSibling);
7521 reordered = true;
7522 }
7523 if (reordered) {
7524 tile2.style.transform = "";
7525 const fresh = tile2.getBoundingClientRect();
7526 startX = fresh.left + fresh.width / 2;
7527 startY = fresh.top + fresh.height / 2;
7528 tile2.style.transform = `translate(${ev.clientX - startX}px, ${ev.clientY - startY}px)`;
7529 flipSiblings(prevRects);
7530 }
7531 };
7532 const cleanup = () => {
7533 tile2.classList.remove("desktop-mode-dock__item--dragging");
7534 tile2.style.transform = "";
7535 tile2.style.transition = "";
7536 document.removeEventListener("pointermove", onMove);
7537 document.removeEventListener("pointerup", onUp);
7538 document.removeEventListener("pointercancel", onCancel);
7539 document.removeEventListener("keydown", onKey, true);
7540 window.removeEventListener("blur", onBlur);
7541 document.removeEventListener("visibilitychange", onVisibility);
7542 pointerId = -1;
7543 originRect = null;
7544 active2 = false;
7545 if (_Dock.activeDragReset === hardReset) {
7546 _Dock.activeDragReset = null;
7547 }
7548 };
7549 const animateHome = () => {
7550 tile2.style.transition = `transform ${FLIP_MS}ms cubic-bezier(0.2, 0.7, 0.3, 1)`;
7551 tile2.style.transform = "";
7552 const onEnd = () => {
7553 tile2.style.transition = "";
7554 tile2.removeEventListener("transitionend", onEnd);
7555 };
7556 tile2.addEventListener("transitionend", onEnd);
7557 };
7558 const persistDockOrder = (finalOrder) => {
7559 const api = window.wp?.desktop;
7560 if (!api?.getOsSettings || !api?.updateOsSettings) {
7561 return;
7562 }
7563 const existing = api.getOsSettings().dockOrder;
7564 const finalSet = new Set(finalOrder);
7565 const merged = [];
7566 let injected = false;
7567 for (const id of existing) {
7568 if (finalSet.has(id)) {
7569 if (!injected) {
7570 merged.push(...finalOrder);
7571 injected = true;
7572 }
7573 continue;
7574 }
7575 merged.push(id);
7576 }
7577 if (!injected) {
7578 merged.push(...finalOrder);
7579 }
7580 api.updateOsSettings({ dockOrder: merged });
7581 };
7582 const onUp = (ev) => {
7583 if (pointerId !== -1 && ev.pointerId !== pointerId) {
7584 return;
7585 }
7586 if (!active2) {
7587 cleanup();
7588 return;
7589 }
7590 justDragged = true;
7591 const finalOrder = snapshotMenuOrder();
7592 animateHome();
7593 cleanup();
7594 const same = finalOrder.length === originalOrder.length && finalOrder.every((id, i) => id === originalOrder[i]);
7595 if (!same) {
7596 persistDockOrder(finalOrder);
7597 }
7598 setTimeout(() => {
7599 justDragged = false;
7600 }, 200);
7601 };
7602 const onCancel = (ev) => {
7603 if (ev && pointerId !== -1 && ev.pointerId !== pointerId) {
7604 return;
7605 }
7606 if (active2 && originalNext !== void 0) {
7607 const prevRects = /* @__PURE__ */ new Map();
7608 eachSiblingTile((sib) => {
7609 prevRects.set(sib, sib.getBoundingClientRect());
7610 });
7611 this.itemHost.insertBefore(tile2, originalNext);
7612 flipSiblings(prevRects);
7613 }
7614 animateHome();
7615 cleanup();
7616 };
7617 const onKey = (ev) => {
7618 if (ev.key === "Escape") {
7619 onCancel();
7620 }
7621 };
7622 const onBlur = () => onCancel();
7623 const onVisibility = () => {
7624 if (document.visibilityState !== "visible") {
7625 onCancel();
7626 }
7627 };
7628 tile2.addEventListener("pointerdown", (ev) => {
7629 if (ev.button !== 0) {
7630 return;
7631 }
7632 if (_Dock.activeDragReset) {
7633 const prev = _Dock.activeDragReset;
7634 _Dock.activeDragReset = null;
7635 prev();
7636 }
7637 if (active2 || pointerId !== -1) {
7638 hardReset();
7639 }
7640 startX = ev.clientX;
7641 startY = ev.clientY;
7642 pointerId = ev.pointerId;
7643 active2 = false;
7644 _Dock.activeDragReset = hardReset;
7645 document.addEventListener("pointermove", onMove);
7646 document.addEventListener("pointerup", onUp);
7647 document.addEventListener("pointercancel", onCancel);
7648 document.addEventListener("keydown", onKey, true);
7649 window.addEventListener("blur", onBlur);
7650 document.addEventListener("visibilitychange", onVisibility);
7651 });
7652 tile2.addEventListener(
7653 "click",
7654 (ev) => {
7655 if (justDragged) {
7656 ev.preventDefault();
7657 ev.stopImmediatePropagation();
7658 }
7659 },
7660 true
7661 );
7662 }
7663 /**
7664 * Resolve a registered icon value into a DOM element.
7665 *
7666 * Priority: dashicons class → inline SVG data URI → image URL →
7667 * letter badge derived from the item's title. The letter fallback is
7668 * important for plugin tiles: plugin authors routinely register
7669 * top-level menus with `add_menu_page()` and omit the icon argument
7670 * (defaulting to `'div'` or empty), which would otherwise render as
7671 * an indistinguishable wall of generic wrenches. A colored letter
7672 * tile gives each plugin a stable, unique-ish visual identity with
7673 * zero plugin-side effort — the hue derives deterministically from
7674 * the title so the same plugin always gets the same color.
7675 *
7676 * @param icon The icon value from the menu entry.
7677 * @param title Human-readable title, used when falling back to a
7678 * letter badge.
7679 */
7680 resolveIcon(icon, title, url) {
7681 if (icon.startsWith("dashicons-") && icon !== "dashicons-admin-generic") {
7682 const el = document.createElement("span");
7683 el.className = `dashicons ${icon}`;
7684 el.setAttribute("aria-hidden", "true");
7685 return el;
7686 }
7687 if (icon.startsWith("data:image/svg+xml;base64,")) {
7688 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
7689 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
7690 return this._makeSvgIcon(icon);
7691 }
7692 }
7693 if (icon.startsWith("url(")) {
7694 return this._makeSvgIcon(icon);
7695 }
7696 if (icon.startsWith("http://") || icon.startsWith("https://")) {
7697 const img = document.createElement("img");
7698 img.className = "desktop-mode-dock__item-img";
7699 img.src = icon;
7700 img.alt = "";
7701 img.setAttribute("aria-hidden", "true");
7702 return img;
7703 }
7704 if (url) {
7705 const native = this._extractNativeMenuIcon(url);
7706 if (native) {
7707 return native;
7708 }
7709 }
7710 if (icon === "dashicons-admin-generic") {
7711 const el = document.createElement("span");
7712 el.className = "dashicons dashicons-admin-generic";
7713 el.setAttribute("aria-hidden", "true");
7714 return el;
7715 }
7716 return this.createLetterBadge(title);
7717 }
7718 /**
7719 * Build an SVG-background icon tile. Shared between the data-URI
7720 * branch of {@link resolveIcon} and the native-menu extractor.
7721 */
7722 _makeSvgIcon(bgValue) {
7723 const el = document.createElement("span");
7724 el.className = "desktop-mode-dock__item-svg";
7725 el.style.backgroundImage = bgValue.startsWith("url(") ? bgValue : `url("${bgValue}")`;
7726 el.style.backgroundSize = "contain";
7727 el.style.backgroundRepeat = "no-repeat";
7728 el.style.backgroundPosition = "center";
7729 el.setAttribute("aria-hidden", "true");
7730 return el;
7731 }
7732 /**
7733 * Extract a plugin's icon from the hidden `#adminmenu` that still
7734 * exists in the parent shell DOM (display:none'd by desktop.css).
7735 * Handles the three shapes plugins commonly use when the menu-page
7736 * icon_url is 'none' or 'div':
7737 *
7738 * (a) `<img src="...">` nested inside `.wp-menu-image`
7739 * (b) a dashicon class on `.wp-menu-image` itself
7740 * (c) a CSS background-image on `.wp-menu-image::before` (the
7741 * `menu-icon-XYZ` pattern Yoast, WooCommerce, Jetpack, etc. use)
7742 *
7743 * Returns null when the URL doesn't match any admin-menu entry or
7744 * none of the three shapes are detectable.
7745 */
7746 _extractNativeMenuIcon(url) {
7747 const adminMenu = document.getElementById("adminmenu");
7748 if (!adminMenu) {
7749 return null;
7750 }
7751 let target2;
7752 try {
7753 const u = new URL(url, window.location.href);
7754 const filename = u.pathname.split("/").pop() || "";
7755 target2 = filename + u.search;
7756 } catch {
7757 return null;
7758 }
7759 if (!target2) {
7760 return null;
7761 }
7762 const links = adminMenu.querySelectorAll("li.menu-top > a");
7763 let matchLi = null;
7764 for (const link of Array.from(links)) {
7765 if (link.href.endsWith(target2)) {
7766 matchLi = link.closest("li.menu-top");
7767 break;
7768 }
7769 }
7770 if (!matchLi) {
7771 return null;
7772 }
7773 const imgWrap = matchLi.querySelector(".wp-menu-image");
7774 if (!imgWrap) {
7775 return null;
7776 }
7777 const img = imgWrap.querySelector("img");
7778 if (img && img.src) {
7779 const el = document.createElement("img");
7780 el.className = "desktop-mode-dock__item-img";
7781 el.src = img.src;
7782 el.alt = "";
7783 el.setAttribute("aria-hidden", "true");
7784 return el;
7785 }
7786 const dashMatch = imgWrap.className.match(/\bdashicons-[\w-]+\b/);
7787 if (dashMatch && dashMatch[0] !== "dashicons-before") {
7788 const el = document.createElement("span");
7789 el.className = `dashicons ${dashMatch[0]}`;
7790 el.setAttribute("aria-hidden", "true");
7791 return el;
7792 }
7793 const before = window.getComputedStyle(imgWrap, "::before");
7794 const bg = before.backgroundImage;
7795 if (bg && bg !== "none" && !bg.includes('url("")')) {
7796 return this._makeSvgIcon(bg);
7797 }
7798 const bgWrap = window.getComputedStyle(imgWrap).backgroundImage;
7799 if (bgWrap && bgWrap !== "none" && !bgWrap.includes('url("")')) {
7800 return this._makeSvgIcon(bgWrap);
7801 }
7802 return null;
7803 }
7804 /**
7805 * Create a letter-badge icon — a rounded square tinted with a
7806 * deterministic hue derived from the title, displaying the first
7807 * letter of the title. Mirrors the "app icon placeholder" look
7808 * macOS uses when an app ships without artwork.
7809 *
7810 * The title always drives both the letter and the hue — same plugin,
7811 * same color across reloads. An empty title falls through to a `?`
7812 * on a neutral gray tile, but the menu builder upstream guards
7813 * against empty titles, so this is a defensive branch.
7814 */
7815 createLetterBadge(title) {
7816 const el = document.createElement("span");
7817 el.className = "desktop-mode-dock__item-letter";
7818 el.setAttribute("aria-hidden", "true");
7819 const trimmed = title.trim();
7820 const firstCodePoint = trimmed ? Array.from(trimmed)[0] : "?";
7821 el.textContent = firstCodePoint.toUpperCase();
7822 const hue = hashTitleToHue(trimmed);
7823 el.style.background = `linear-gradient(135deg, hsl(${hue} 62% 55%), hsl(${(hue + 24) % 360} 58% 42%))`;
7824 return el;
7825 }
7826 /**
7827 * Bind tooltip show/hide on hover. Tooltip anchor differs per
7828 * orientation: left dock → tile's right side, right dock → tile's
7829 * left side, bottom dock → above the tile. We set the relevant
7830 * coordinate inline each enter; the CSS takes care of the rest.
7831 */
7832 /**
7833 * Resolves the tooltip text through {@link HOOKS.DOCK_TILE_TOOLTIP}
7834 * once at bind time (so the dock doesn't re-filter on every
7835 * pointerenter) and stashes the resolved text on
7836 * `tile.dataset.dockTooltip` so the multi-instance chip can
7837 * restore it on its own pointerleave without going through the
7838 * filter again.
7839 *
7840 * Returning an empty string from the filter suppresses the
7841 * tooltip — the listener short-circuits and never adds the
7842 * `--visible` class.
7843 */
7844 bindTooltipFiltered(tile2, text, ctx) {
7845 const filtered = applyFilters(
7846 HOOKS.DOCK_TILE_TOOLTIP,
7847 text,
7848 ctx
7849 );
7850 tile2.dataset.dockTooltip = filtered;
7851 if (filtered === "") {
7852 return;
7853 }
7854 tile2.addEventListener("pointerenter", () => {
7855 this.positionTooltip(tile2, filtered);
7856 this.tooltip.classList.add("desktop-mode-dock__tooltip--visible");
7857 });
7858 tile2.addEventListener("pointerleave", () => {
7859 this.tooltip.classList.remove("desktop-mode-dock__tooltip--visible");
7860 });
7861 }
7862 /**
7863 * Write the tooltip text + anchor coordinate for `el`. Split out
7864 * because the multi-instance chip's pointerenter handler also
7865 * needs to anchor to a specific element (the chip, not the tile).
7866 */
7867 positionTooltip(el, text) {
7868 const rect = el.getBoundingClientRect();
7869 this.tooltip.textContent = text;
7870 if (this.orientation === "bottom") {
7871 this.tooltip.style.left = `${rect.left + rect.width / 2}px`;
7872 this.tooltip.style.top = `${rect.top - 14}px`;
7873 } else if (this.orientation === "right") {
7874 this.tooltip.style.top = `${rect.top + rect.height / 2 - 14}px`;
7875 this.tooltip.style.left = `${rect.left}px`;
7876 } else {
7877 this.tooltip.style.top = `${rect.top + rect.height / 2 - 14}px`;
7878 this.tooltip.style.left = `${rect.right + 8}px`;
7879 }
7880 }
7881 /**
7882 * Open an admin page in a window (or focus if already open).
7883 *
7884 * Consults the native URL-remap registry first — when an opt-in
7885 * native window has registered itself as the replacement for this
7886 * admin URL (e.g. the native Posts window for `edit.php` when the
7887 * user has flipped `nativePostsEnabled`), the click is rerouted
7888 * to that window and the iframe path is skipped. The dock item
7889 * itself is untouched: same icon, same tooltip, same position —
7890 * only the destination changes.
7891 */
7892 openPage(item) {
7893 if (item.id.startsWith("dock:")) {
7894 const iconId = item.id.slice(5);
7895 const cfg = window.desktopModeConfig;
7896 const icon = cfg?.desktopIcons?.find((i) => i.id === iconId);
7897 if (icon?.window) {
7898 const wp = window.wp?.desktop;
7899 wp?.openWindow?.(icon.window);
7900 return;
7901 }
7902 if (icon?.url) {
7903 if (tryOpenExternalUrl(icon.url)) {
7904 return;
7905 }
7906 const baseId2 = this.deriveWindowId(icon.url);
7907 this.windowManager.open({
7908 id: baseId2,
7909 baseId: baseId2,
7910 url: icon.url,
7911 parentUrl: icon.url,
7912 title: icon.title,
7913 icon: icon.icon.startsWith("dashicons-") ? icon.icon : "dashicons-admin-generic",
7914 submenu: [],
7915 multi: false
7916 });
7917 return;
7918 }
7919 return;
7920 }
7921 if (tryOpenExternalUrl(item.url)) {
7922 return;
7923 }
7924 if (tryNativeUrlRemap(item.url)) {
7925 return;
7926 }
7927 const baseId = this.deriveWindowId(item.url);
7928 this.windowManager.open({
7929 id: baseId,
7930 baseId,
7931 url: item.url,
7932 parentUrl: item.url,
7933 title: item.title,
7934 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
7935 submenu: item.submenu,
7936 multi: !!item.multi
7937 });
7938 }
7939 /**
7940 * Open a brand-new instance of a page, even if one is already
7941 * open. Invoked by the "+" ghost card in the dock peek.
7942 *
7943 * The user explicitly asked for "another window of this thing,"
7944 * so we honour the request even when {@link tryNativeUrlRemap}
7945 * would otherwise route the click into a native-window
7946 * singleton. Result: clicking + while a native Posts window is
7947 * open opens a fresh iframe of `edit.php` alongside it. Two
7948 * windows of Posts is the explicit ask — that's what + is for.
7949 */
7950 openNewInstance(item) {
7951 if (tryOpenExternalUrl(item.url)) {
7952 return;
7953 }
7954 const openNewWindow = window.wp?.desktop?.openNewWindow;
7955 if (item.windowId && !item.url) {
7956 if (openNewWindow?.(item.windowId, { source: "dock-peek" })) {
7957 return;
7958 }
7959 }
7960 const remappedId = resolveNativeUrlRemap(item.url);
7961 if (remappedId) {
7962 if (openNewWindow?.(remappedId, { source: "dock-peek" })) {
7963 return;
7964 }
7965 }
7966 const baseId = this.deriveWindowId(item.url);
7967 void this.windowManager.openNew({
7968 id: baseId,
7969 baseId,
7970 url: item.url,
7971 parentUrl: item.url,
7972 title: item.title,
7973 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
7974 submenu: item.submenu,
7975 multi: true
7976 });
7977 }
7978 /**
7979 * Derive a window ID from an admin page URL.
7980 */
7981 deriveWindowId(url) {
7982 return deriveWindowId(url, this.adminUrl);
7983 }
7984 /**
7985 * Resolve the window-manager key for a dock tile, in this order:
7986 *
7987 * 1. `item.windowId` — set by `applyDockPlacement` when the tile
7988 * is synthesized from a `desktop_mode_register_icon()` entry
7989 * whose target is a native window. Native-window ids never
7990 * pass through the URL → native-window remap layer, so we
7991 * short-circuit before touching it.
7992 * 2. {@link resolveNativeUrlRemap} on `item.url` — captures the
7993 * `nativePostsEnabled` / `nativePagesEnabled` opt-ins that
7994 * repoint a URL-based tile at a native window.
7995 * 3. {@link deriveWindowId} on `item.url` — the URL-based
7996 * fallback for ordinary admin-menu tiles.
7997 *
7998 * Shared by the hover-peek card and the active/focused-dot
7999 * indicator; the two stayed in lockstep before this method existed
8000 * by hand-rolling the same chain at each call site.
8001 */
8002 resolveItemBaseId(item) {
8003 if (item.windowId) {
8004 return item.windowId;
8005 }
8006 const remapped = resolveNativeUrlRemap(item.url);
8007 return remapped ?? this.deriveWindowId(item.url);
8008 }
8009 /**
8010 * Listen to window events to update active/focused/minimized
8011 * indicators on dock items, plus the global Show Desktop body class.
8012 *
8013 * The event detail isn't used — we just need to re-query the
8014 * window manager on every change — so the handlers take no
8015 * argument and the type cast is gone with it.
8016 *
8017 * `WINDOW_MINIMIZED` / `WINDOW_RESTORED` route through the hook bus
8018 * (no DOM CustomEvent equivalent today). Without these, minimizing
8019 * a window via Show Desktop / the title-bar minimize button left
8020 * the dock's active-dot rendering stuck on "visible window" — the
8021 * user had no cue that everything had collapsed to minimized.
8022 */
8023 bindWindowEvents() {
8024 const refresh = () => this.updateActiveStates();
8025 this.boundRefresh = refresh;
8026 document.addEventListener("desktop-mode-window-opened", refresh);
8027 document.addEventListener("desktop-mode-window-closed", refresh);
8028 document.addEventListener("desktop-mode-window-focused", refresh);
8029 window.wp?.hooks?.addAction?.(
8030 "desktop-mode.desktop.switched",
8031 this.hooksNamespace,
8032 refresh
8033 );
8034 window.wp?.hooks?.addAction?.(
8035 "desktop-mode.desktop.closed",
8036 this.hooksNamespace,
8037 refresh
8038 );
8039 window.wp?.hooks?.addAction?.(
8040 HOOKS.WINDOW_MINIMIZED,
8041 this.hooksNamespace,
8042 refresh
8043 );
8044 window.wp?.hooks?.addAction?.(
8045 HOOKS.WINDOW_RESTORED,
8046 this.hooksNamespace,
8047 refresh
8048 );
8049 }
8050 /**
8051 * Tear the dock down: detach window-lifecycle listeners, clear
8052 * pending attention timers, remove the floating tooltip from
8053 * `document.body`, and empty the container's children. Used by
8054 * the layout dispatcher when the user switches `desktopLayout`
8055 * in OS Settings — old dock(s) get destroyed and a fresh set is
8056 * constructed for the new layout.
8057 *
8058 * Idempotent: calling twice is safe.
8059 */
8060 destroy() {
8061 document.removeEventListener(
8062 "desktop-mode-window-opened",
8063 this.boundRefresh
8064 );
8065 document.removeEventListener(
8066 "desktop-mode-window-closed",
8067 this.boundRefresh
8068 );
8069 document.removeEventListener(
8070 "desktop-mode-window-focused",
8071 this.boundRefresh
8072 );
8073 window.wp?.hooks?.removeAction?.(
8074 "desktop-mode.desktop.switched",
8075 this.hooksNamespace
8076 );
8077 window.wp?.hooks?.removeAction?.(
8078 "desktop-mode.desktop.closed",
8079 this.hooksNamespace
8080 );
8081 window.wp?.hooks?.removeAction?.(
8082 HOOKS.WINDOW_MINIMIZED,
8083 this.hooksNamespace
8084 );
8085 window.wp?.hooks?.removeAction?.(
8086 HOOKS.WINDOW_RESTORED,
8087 this.hooksNamespace
8088 );
8089 for (const handle of this.attentionTimers.values()) {
8090 window.clearTimeout(handle);
8091 }
8092 this.attentionTimers.clear();
8093 for (const teardown of this.peekTeardowns.values()) {
8094 teardown();
8095 }
8096 this.peekTeardowns.clear();
8097 this.tooltip.remove();
8098 while (this.container.firstChild) {
8099 this.container.removeChild(this.container.firstChild);
8100 }
8101 this.itemElements.clear();
8102 this.systemItemElements.clear();
8103 this.systemItems = [];
8104 this.systemSeparator = null;
8105 this.container.removeAttribute("data-desktop-mode-dock-placement");
8106 }
8107 /**
8108 * Update the active/focused/minimized classes on every dock item in
8109 * response to a window lifecycle event, and toggle the global Show
8110 * Desktop body class.
8111 *
8112 * For singletons the rail is absent; "active" means "the one window
8113 * is open". For multi-capable items, active means "≥1 instance is
8114 * open" and focused means "the focused window belongs to this item".
8115 *
8116 * `--all-minimized` is layered on top of `--active` and fires only
8117 * when EVERY open instance of the tile is minimized — so a partial
8118 * minimize (one of two windows hidden) keeps the solid dot. CSS
8119 * swaps the dot for a hollow ring on minimized-only tiles so the
8120 * user can tell at a glance "I have something here, it's just
8121 * hidden right now."
8122 */
8123 updateActiveStates() {
8124 const focused = this.windowManager.getFocused();
8125 const focusedBaseId = focused ? focused.config.baseId || focused.id : null;
8126 const activeDesktopId = this.windowManager.getActiveDesktopId();
8127 const onActiveDesktop = (w) => (w.config.desktopId || activeDesktopId) === activeDesktopId;
8128 const isMinimized = (w) => w.state === "minimized";
8129 for (const item of this.items) {
8130 const tile2 = this.itemElements.get(item.id);
8131 if (!tile2) {
8132 continue;
8133 }
8134 const baseId = this.resolveItemBaseId(item);
8135 const instances = this.windowManager.getAllByBaseId(baseId).filter(onActiveDesktop);
8136 const isOpen = instances.length > 0;
8137 const allMinimized = isOpen && instances.every(isMinimized);
8138 const isFocused = focusedBaseId === baseId && !!focused && onActiveDesktop(focused) && !isMinimized(focused);
8139 tile2.classList.toggle("desktop-mode-dock__item--active", isOpen);
8140 tile2.classList.toggle("desktop-mode-dock__item--focused", isFocused);
8141 tile2.classList.toggle(
8142 "desktop-mode-dock__item--all-minimized",
8143 allMinimized
8144 );
8145 }
8146 for (const sys of this.systemItems) {
8147 const tile2 = this.systemItemElements.get(sys.id);
8148 if (!tile2) {
8149 continue;
8150 }
8151 const sysWin = this.windowManager.getById(sys.id);
8152 const isOpen = sys.isOpen ? sys.isOpen() : !!sysWin;
8153 const allMinimized = !!sysWin && isMinimized(sysWin);
8154 const isFocused = !!focused && focused.id === sys.id && !isMinimized(focused);
8155 tile2.classList.toggle("desktop-mode-dock__item--active", isOpen);
8156 tile2.classList.toggle("desktop-mode-dock__item--focused", isFocused);
8157 tile2.classList.toggle(
8158 "desktop-mode-dock__item--all-minimized",
8159 allMinimized
8160 );
8161 }
8162 this.updateShowDesktopBodyClass();
8163 }
8164 /**
8165 * Toggle `body.desktop-mode-show-desktop-active` based on whether
8166 * every live window on the active desktop is minimized. Mirrors
8167 * the heuristic inside {@link WindowManager.toggleShowDesktop} so
8168 * the visual cue tracks the actual state — set by Show Desktop
8169 * gestures, restored when any window is brought back, automatically
8170 * cleared when no windows exist.
8171 *
8172 * @internal
8173 */
8174 updateShowDesktopBodyClass() {
8175 const activeDesktopId = this.windowManager.getActiveDesktopId();
8176 const live = this.windowManager.getAll().filter(
8177 (w) => (w.config.desktopId || activeDesktopId) === activeDesktopId
8178 );
8179 const showDesktop = live.length > 0 && live.every((w) => w.state === "minimized");
8180 document.body.classList.toggle(
8181 "desktop-mode-show-desktop-active",
8182 showDesktop
8183 );
8184 }
8185 };
8186 _Dock.instanceCounter = 0;
8187 _Dock.activeDragReset = null;
8188 let Dock = _Dock;
8189 function _applyBadgeNode(host, count) {
8190 const existing = host.querySelector(
8191 ":scope > .desktop-mode-dock__badge"
8192 );
8193 if (count <= 0) {
8194 existing?.remove();
8195 return;
8196 }
8197 const display = count > 99 ? "99+" : String(count);
8198 if (existing) {
8199 if (existing.textContent !== display) {
8200 existing.textContent = display;
8201 }
8202 existing.setAttribute(
8203 "aria-label",
8204 sprintf(
8205 // translators: %d is the number of pending items in a dock badge.
8206 _n("%d notification", "%d notifications", count),
8207 count
8208 )
8209 );
8210 return;
8211 }
8212 const badge = document.createElement("span");
8213 badge.className = "desktop-mode-dock__badge";
8214 badge.textContent = display;
8215 badge.setAttribute(
8216 "aria-label",
8217 sprintf(
8218 // translators: %d is the number of pending items in a dock badge.
8219 _n("%d notification", "%d notifications", count),
8220 count
8221 )
8222 );
8223 host.appendChild(badge);
8224 }
8225 const DEFAULT_RENDERER_DOCK = Symbol.for(
8226 "desktop-mode/default-dock-rail-renderer/dock"
8227 );
8228 const defaultDockRailRenderer = {
8229 id: "default",
8230 label: "Icon strip",
8231 description: "The shipped baseline — icon tiles with badges, tooltips, multi-instance chips, and attention animations.",
8232 icon: "dashicons-menu-alt",
8233 apiVersion: 1,
8234 mount(deps2) {
8235 const dock = new Dock(
8236 deps2.container,
8237 deps2.windowManager,
8238 deps2.items,
8239 deps2.adminUrl,
8240 deps2.orientation
8241 );
8242 const controller = {
8243 [DEFAULT_RENDERER_DOCK]: dock,
8244 replaceItems: (items) => dock.replaceItems(items),
8245 appendSystemItem: (item) => dock.appendSystemItem(item),
8246 removeSystemItem: (id) => dock.removeSystemItem(id),
8247 setBadge: (itemId, count) => dock.setBadge(itemId, count),
8248 setAttention: (itemId, mode, opts) => dock.setAttention(itemId, mode, opts),
8249 setOrientation: (orientation) => dock.setOrientation(orientation),
8250 destroy: () => dock.destroy()
8251 };
8252 return controller;
8253 }
8254 };
8255 function unwrapDefaultDock(controller) {
8256 if (!controller) {
8257 return null;
8258 }
8259 const probe = controller;
8260 const dock = probe[DEFAULT_RENDERER_DOCK];
8261 return dock instanceof Dock ? dock : null;
8262 }
8263 function installDefaultDockRailRenderer() {
8264 register$1(defaultDockRailRenderer);
8265 }
8266 function customGradientCss(state2) {
8267 const { from, to, angle } = state2.customGradient;
8268 return `linear-gradient(${angle}deg, ${from}, ${to})`;
8269 }
8270 function registerCustomGradient(ctx) {
8271 register$2({
8272 id: CUSTOM_GRADIENT_ID,
8273 label: __("Custom gradient"),
8274 type: "css",
8275 preview: customGradientCss(ctx.state),
8276 resolveValue: () => customGradientCss(ctx.state)
8277 });
8278 }
8279 function registerCustomImageIfPresent(state2) {
8280 if (!state2.customImage) {
8281 unregister$2(CUSTOM_IMAGE_ID);
8282 return;
8283 }
8284 const safeUrl = encodeURI(state2.customImage.url);
8285 const value = `url("${safeUrl}") center/cover no-repeat, #1d2327`;
8286 register$2({
8287 id: CUSTOM_IMAGE_ID,
8288 label: __("Custom image"),
8289 type: "css",
8290 value,
8291 preview: value
8292 });
8293 }
8294 let _panelLoadPromise = null;
8295 function loadOsSettingsPanelBundle(scriptUrl) {
8296 if (window.desktopModeRenderOsSettingsPanel) {
8297 return Promise.resolve(window.desktopModeRenderOsSettingsPanel);
8298 }
8299 if (_panelLoadPromise) {
8300 return _panelLoadPromise;
8301 }
8302 _panelLoadPromise = new Promise((resolve2, reject) => {
8303 const existing = document.querySelector(
8304 'script[data-desktop-mode-os-settings-panel="1"]'
8305 );
8306 const finish = () => {
8307 const fn = window.desktopModeRenderOsSettingsPanel;
8308 if (!fn) {
8309 reject(
8310 new Error(
8311 "[desktop-mode] os-settings-panel bundle loaded but did not register desktopModeRenderOsSettingsPanel"
8312 )
8313 );
8314 return;
8315 }
8316 resolve2(fn);
8317 };
8318 if (existing) {
8319 if (window.desktopModeRenderOsSettingsPanel) {
8320 finish();
8321 } else {
8322 existing.addEventListener("load", finish);
8323 existing.addEventListener(
8324 "error",
8325 () => reject(new Error("failed to load os-settings-panel bundle"))
8326 );
8327 }
8328 return;
8329 }
8330 const s = document.createElement("script");
8331 s.src = scriptUrl;
8332 s.async = true;
8333 s.dataset.desktopModeOsSettingsPanel = "1";
8334 s.addEventListener("load", finish);
8335 s.addEventListener(
8336 "error",
8337 () => reject(new Error("failed to load os-settings-panel bundle"))
8338 );
8339 document.head.appendChild(s);
8340 });
8341 return _panelLoadPromise;
8342 }
8343 class OsSettings {
8344 constructor(config, layer) {
8345 this.activeEditorTeardown = null;
8346 this.tabRegistryUnsubscribe = null;
8347 this.activeTabId = null;
8348 this.osSettingsListeners = /* @__PURE__ */ new Set();
8349 this._lastRenderedBody = null;
8350 this.config = config;
8351 this.layer = layer;
8352 this.state = loadState();
8353 setLastConfirmedState(this.state);
8354 document.addEventListener(
8355 "desktop-mode-os-settings-save-lifecycle",
8356 (e) => {
8357 const detail = e.detail;
8358 if (!detail || detail.phase !== "failed" || !detail.rolledBackTo) {
8359 return;
8360 }
8361 this.state = detail.rolledBackTo;
8362 this.apply();
8363 if (this._lastRenderedBody?.isConnected) {
8364 this.renderPanel(this._lastRenderedBody);
8365 }
8366 }
8367 );
8368 registerCustomGradient(this);
8369 registerCustomImageIfPresent(this.state);
8370 }
8371 /** Project the private state into the public snapshot shape. */
8372 getOsSettingsSnapshot() {
8373 return {
8374 wallpaper: this.state.wallpaper,
8375 accent: this.state.accent,
8376 dockSize: this.state.dockSize,
8377 desktopLayout: this.state.desktopLayout,
8378 dockRailRenderer: this.state.dockRailRenderer,
8379 unfocusEffect: this.state.unfocusEffect,
8380 ai: { ...this.state.ai },
8381 nativePostsEnabled: this.state.nativePostsEnabled,
8382 nativePostsHiddenColumns: this.state.nativePostsHiddenColumns.slice(),
8383 nativePagesEnabled: this.state.nativePagesEnabled,
8384 nativeUsersEnabled: this.state.nativeUsersEnabled,
8385 nativePluginsEnabled: this.state.nativePluginsEnabled,
8386 nativeCommentsEnabled: this.state.nativeCommentsEnabled,
8387 foldersSharingEnabled: this.state.foldersSharingEnabled,
8388 itemVisibility: { ...this.state.itemVisibility },
8389 dockOrder: this.state.dockOrder.slice(),
8390 dockPromotedPositions: Object.fromEntries(
8391 Object.entries(this.state.dockPromotedPositions).map(
8392 ([k, v]) => [k, { ...v }]
8393 )
8394 )
8395 };
8396 }
8397 subscribeOsSettings(cb) {
8398 this.osSettingsListeners.add(cb);
8399 return () => {
8400 this.osSettingsListeners.delete(cb);
8401 };
8402 }
8403 /**
8404 * Apply the current state: wallpaper via the layer, accent + dock
8405 * size as CSS custom properties on the shell.
8406 *
8407 * Safe to call repeatedly — calls into `layer.apply` dedupe via
8408 * generation counter; CSS property writes are idempotent.
8409 */
8410 apply() {
8411 const shell = document.getElementById("desktop-mode-shell");
8412 if (!shell) {
8413 return;
8414 }
8415 const def = get$1(this.state.wallpaper) || get$1(getDefaultWallpaperId()) || get$1(DEFAULT_WALLPAPER_ID) || all$1()[0];
8416 if (def) {
8417 this.layer.apply(def);
8418 }
8419 const accents = getAccents();
8420 const accent = accents.find((a) => a.id === this.state.accent) ?? accents[0];
8421 const dockSize = DOCK_SIZES.find((d) => d.id === this.state.dockSize) ?? DOCK_SIZES[1];
8422 const root = document.documentElement;
8423 root.style.setProperty("--wp-admin-theme-color", accent.value);
8424 root.style.setProperty("--desktop-mode-dock-width", `${dockSize.width}px`);
8425 root.style.setProperty("--desktop-mode-dock-icon-size", `${dockSize.icon}px`);
8426 shell.setAttribute(
8427 "data-desktop-mode-layout",
8428 this.state.desktopLayout
8429 );
8430 setActiveRenderer(this.state.dockRailRenderer);
8431 }
8432 save(opts = {}) {
8433 saveState(this.state, opts);
8434 if (this.osSettingsListeners.size > 0) {
8435 const snapshot = this.getOsSettingsSnapshot();
8436 const listeners2 = Array.from(this.osSettingsListeners);
8437 for (const cb of listeners2) {
8438 try {
8439 cb(snapshot);
8440 } catch (err) {
8441 if (typeof console !== "undefined") {
8442 console.error(
8443 "[desktop-mode] os-settings listener threw:",
8444 err
8445 );
8446 }
8447 }
8448 }
8449 }
8450 }
8451 /**
8452 * Render the settings panel into the given native-window body.
8453 *
8454 * Builds three sections (wallpaper, accent, dock size) and wires
8455 * each to save/apply on change. The panel is a one-shot build per
8456 * window open — closing and re-opening renders a fresh tree.
8457 */
8458 /**
8459 * Render the settings panel into the given native-window body.
8460 *
8461 * Lazy since 0.8.4 — the actual rendering logic plus every
8462 * `<wpd-*>` component the panel uses lives in
8463 * `src/settings/panel.ts`, compiled into its own Vite target
8464 * `os-settings-panel[.min].js`. The script is injected on the
8465 * first call below and the matching
8466 * `window.desktopModeRenderOsSettingsPanel( ctx, body )` global
8467 * is then invoked. Subsequent calls (registry-driven re-render,
8468 * save-failure rollback) skip the load and forward immediately.
8469 *
8470 * Why this is a `<script>`-injected sibling bundle rather than
8471 * an in-bundle dynamic import: Vite IIFE lib mode inlines
8472 * `import()` calls, so an in-bundle lazy import would give zero
8473 * byte savings. A separate Vite target is the only mechanism
8474 * that actually shrinks `desktop.min.js`. See the Stage 8
8475 * section of `BUNDLE-SIZE-REPORT.md` for the full picture.
8476 */
8477 /**
8478 * Switch the active settings tab. Records the choice on
8479 * {@link activeTabId} (so the next render mounts on it) and, when
8480 * the panel is currently mounted, flips the live `<wpd-tabs>` value
8481 * in place so an already-open OS Settings window jumps to the tab
8482 * without a full re-render. Deep-linking entry points
8483 * (`openOsSettings({ tabId })`) call this after opening the window.
8484 *
8485 * @param tabId Settings tab id, e.g. `'ai'`, `'apps-icons'`.
8486 */
8487 focusTab(tabId) {
8488 this.activeTabId = tabId;
8489 const body = this._lastRenderedBody;
8490 if (!body?.isConnected) {
8491 return;
8492 }
8493 const tabs = body.querySelector("wpd-tabs");
8494 if (tabs) {
8495 tabs.value = tabId;
8496 }
8497 }
8498 renderPanel(body) {
8499 this._lastRenderedBody = body;
8500 const fn = window.desktopModeRenderOsSettingsPanel;
8501 if (fn) {
8502 fn(this, body);
8503 return;
8504 }
8505 void loadOsSettingsPanelBundle(
8506 this.config.osSettingsPanelBundleUrl ?? ""
8507 ).then((render2) => {
8508 if (!body.isConnected) {
8509 return;
8510 }
8511 render2(this, body);
8512 }).catch((err) => {
8513 if (typeof console !== "undefined") {
8514 console.error(
8515 "[desktop-mode] OS Settings panel failed to load:",
8516 err
8517 );
8518 }
8519 });
8520 }
8521 }
8522 const EXIT_DESKTOP_MODE_TILE_ID = "desktop-mode-exit";
8523 function getExitDesktopModeTileDef() {
8524 return {
8525 id: EXIT_DESKTOP_MODE_TILE_ID,
8526 title: __("Exit Desktop Mode"),
8527 // `dashicons-exit` (door with arrow) is the clearest "leave"
8528 // glyph in the WordPress set, distinct from `dashicons-desktop`
8529 // used by OS Settings.
8530 icon: "dashicons-exit",
8531 onOpen: () => {
8532 void exitDesktopMode();
8533 }
8534 };
8535 }
8536 async function exitDesktopMode() {
8537 const cfg = window.desktopModeAdminBar;
8538 const fallback = cfg?.classicUrl || "/wp-admin/";
8539 if (!cfg?.ajaxUrl || !cfg?.nonce) {
8540 navigateTop(fallback);
8541 return;
8542 }
8543 const body = new URLSearchParams();
8544 body.set("action", "save-desktop-mode");
8545 body.set("nonce", cfg.nonce);
8546 body.set("enabled", "");
8547 let target2 = fallback;
8548 try {
8549 const res = await fetch(cfg.ajaxUrl, {
8550 method: "POST",
8551 headers: {
8552 "Content-Type": "application/x-www-form-urlencoded"
8553 },
8554 body: body.toString(),
8555 credentials: "same-origin"
8556 });
8557 if (res.ok) {
8558 const json = await res.json();
8559 if (json?.success && json.data?.redirect) {
8560 target2 = json.data.redirect;
8561 }
8562 }
8563 } catch {
8564 }
8565 navigateTop(target2);
8566 }
8567 function navigateTop(url) {
8568 try {
8569 window.top.location.href = url;
8570 } catch {
8571 window.location.href = url;
8572 }
8573 }
8574 const _initial$1 = {
8575 userId: null,
8576 requestedAt: 0,
8577 tabRequested: false
8578 };
8579 let _store$2 = null;
8580 function getStore$1() {
8581 if (_store$2) {
8582 return _store$2;
8583 }
8584 const w = window;
8585 const factory = w.wp?.desktop?.createSharedStore;
8586 if (typeof factory !== "function") {
8587 return null;
8588 }
8589 _store$2 = factory(
8590 "desktop-mode/user-edit/target",
8591 () => ({ ..._initial$1 })
8592 );
8593 return _store$2;
8594 }
8595 function setUserEditTarget(userId) {
8596 const store2 = getStore$1();
8597 if (store2) {
8598 store2.state.userId = userId;
8599 store2.state.requestedAt = Date.now();
8600 store2.state.tabRequested = true;
8601 store2.notify();
8602 return;
8603 }
8604 const w = window;
8605 w._wpdUserEditTarget = {
8606 userId,
8607 requestedAt: Date.now(),
8608 tabRequested: true
8609 };
8610 }
8611 const pending = /* @__PURE__ */ new Map();
8612 function loadVendorScript(url, extras) {
8613 const existing = pending.get(url);
8614 if (existing) {
8615 return existing;
8616 }
8617 const promise = new Promise((resolve2, reject) => {
8618 const selector = `script[data-desktop-mode-vendor="${cssEscape(url)}"]`;
8619 const preexisting = document.querySelector(selector);
8620 if (preexisting) {
8621 if (preexisting.dataset.loaded === "1") {
8622 resolve2();
8623 return;
8624 }
8625 preexisting.addEventListener("load", () => resolve2(), { once: true });
8626 preexisting.addEventListener(
8627 "error",
8628 () => reject(new Error(`Failed to load ${url}`)),
8629 { once: true }
8630 );
8631 return;
8632 }
8633 if (extras?.translations) {
8634 injectInline(extras.translations);
8635 }
8636 for (const code of extras?.l10n ?? []) {
8637 injectInline(code);
8638 }
8639 for (const code of extras?.before ?? []) {
8640 injectInline(code);
8641 }
8642 const script = document.createElement("script");
8643 script.src = url;
8644 script.async = true;
8645 script.dataset.desktopModeVendor = url;
8646 script.addEventListener(
8647 "load",
8648 () => {
8649 script.dataset.loaded = "1";
8650 for (const code of extras?.after ?? []) {
8651 injectInline(code);
8652 }
8653 resolve2();
8654 },
8655 { once: true }
8656 );
8657 script.addEventListener(
8658 "error",
8659 () => {
8660 pending.delete(url);
8661 script.remove();
8662 reject(new Error(`Failed to load ${url}`));
8663 },
8664 { once: true }
8665 );
8666 document.head.appendChild(script);
8667 });
8668 pending.set(url, promise);
8669 return promise;
8670 }
8671 function injectInline(code) {
8672 if (!code) {
8673 return;
8674 }
8675 const tag = document.createElement("script");
8676 tag.textContent = code;
8677 tag.dataset.desktopModeVendorInline = "1";
8678 document.head.appendChild(tag);
8679 }
8680 function cssEscape(value) {
8681 if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
8682 return CSS.escape(value);
8683 }
8684 return value.replace(/["\\]/g, "\\$&");
8685 }
8686 const registry$8 = /* @__PURE__ */ new Map();
8687 function registerModule(def) {
8688 if (!def || typeof def.id !== "string" || def.id === "") {
8689 if (typeof console !== "undefined") {
8690 console.warn("[desktop-mode] Ignored invalid module registration:", def);
8691 }
8692 return;
8693 }
8694 if (typeof def.url !== "string" || def.url === "") {
8695 if (typeof console !== "undefined") {
8696 console.warn(
8697 `[desktop-mode] Module "${def.id}" has no url; ignored.`
8698 );
8699 }
8700 return;
8701 }
8702 registry$8.set(def.id, def);
8703 }
8704 function moduleIds() {
8705 return Array.from(registry$8.keys());
8706 }
8707 async function loadModules(ids) {
8708 if (!ids || ids.length === 0) {
8709 return;
8710 }
8711 const unknown = ids.filter((id) => !registry$8.has(id));
8712 if (unknown.length > 0) {
8713 throw new Error(
8714 `[desktop-mode] Unknown module(s) in needs: ${unknown.map((id) => `"${id}"`).join(", ")}. Known modules: ${moduleIds().join(", ") || "(none)"}.`
8715 );
8716 }
8717 await Promise.all(
8718 ids.map((id) => {
8719 const def = registry$8.get(id);
8720 if (!def) {
8721 return Promise.resolve();
8722 }
8723 if (def.isReady && def.isReady()) {
8724 return Promise.resolve();
8725 }
8726 return loadVendorScript(def.url);
8727 })
8728 );
8729 }
8730 function createContext(id, pluginUrl) {
8731 return {
8732 id,
8733 pluginUrl,
8734 prefersReducedMotion: prefersReducedMotion(),
8735 visible: !document.hidden
8736 };
8737 }
8738 function prefersReducedMotion() {
8739 if (typeof window.matchMedia !== "function") {
8740 return false;
8741 }
8742 return window.matchMedia("( prefers-reduced-motion: reduce )").matches;
8743 }
8744 class WallpaperLayer {
8745 constructor(element, pluginUrl) {
8746 this.generation = 0;
8747 this.active = null;
8748 this.boundVisibilityChange = () => {
8749 if (!this.active) {
8750 return;
8751 }
8752 doAction(HOOKS.WALLPAPER_VISIBILITY, {
8753 id: this.active.id,
8754 state: document.hidden ? "hidden" : "visible"
8755 });
8756 };
8757 this.element = element;
8758 this.pluginUrl = pluginUrl;
8759 document.addEventListener("visibilitychange", this.boundVisibilityChange);
8760 }
8761 /**
8762 * Apply a wallpaper definition. Safe to call from any event
8763 * handler — handles type dispatch, teardown of the prior active
8764 * canvas, and race-safe async mounts.
8765 */
8766 apply(def) {
8767 const gen = ++this.generation;
8768 this.teardownActive();
8769 if (def.type === "css") {
8770 this.applyCss(def);
8771 return;
8772 }
8773 this.applyCanvas(def, gen);
8774 }
8775 /**
8776 * Imperative teardown entry point — called from desktop.ts on
8777 * `pagehide` so a canvas wallpaper's ticker doesn't compete with
8778 * the session-beacon flush at unload.
8779 */
8780 teardownActive() {
8781 if (!this.active) {
8782 return;
8783 }
8784 const { id, teardown } = this.active;
8785 this.active = null;
8786 doAction(HOOKS.WALLPAPER_UNMOUNTING, { id });
8787 try {
8788 teardown();
8789 } catch (err) {
8790 doAction(HOOKS.SHELL_ERROR, { scope: "wallpaper-teardown", id, error: err });
8791 if (typeof console !== "undefined") {
8792 console.error(
8793 `[desktop-mode] Wallpaper "${id}" teardown threw:`,
8794 err
8795 );
8796 }
8797 }
8798 this.element.innerHTML = "";
8799 }
8800 /** Remove listeners. Not called in normal flow — reserved for tests. */
8801 dispose() {
8802 this.teardownActive();
8803 document.removeEventListener("visibilitychange", this.boundVisibilityChange);
8804 }
8805 applyCss(def) {
8806 const value = def.resolveValue ? def.resolveValue(createContext(def.id, this.pluginUrl)) : def.value;
8807 if (typeof value === "string") {
8808 this.element.style.setProperty("--desktop-mode-bg", value);
8809 const shell = document.getElementById("desktop-mode-shell");
8810 shell?.style.setProperty("--desktop-mode-bg", value);
8811 }
8812 }
8813 applyCanvas(def, gen) {
8814 const ctx = createContext(def.id, this.pluginUrl);
8815 doAction(HOOKS.WALLPAPER_MOUNTING, { id: def.id, container: this.element, ctx });
8816 const depsReady = def.needs && def.needs.length > 0 ? loadModules(def.needs) : Promise.resolve();
8817 const onResolve = (teardown) => {
8818 if (gen !== this.generation) {
8819 try {
8820 teardown();
8821 } catch {
8822 }
8823 return;
8824 }
8825 this.active = { id: def.id, teardown };
8826 doAction(HOOKS.WALLPAPER_MOUNTED, { id: def.id, container: this.element, ctx });
8827 };
8828 depsReady.then(
8829 () => {
8830 if (gen !== this.generation) {
8831 return;
8832 }
8833 let result;
8834 try {
8835 result = def.mount(this.element, ctx);
8836 } catch (err) {
8837 this.handleMountFailure(def.id, err);
8838 return;
8839 }
8840 if (isThenable$1(result)) {
8841 result.then(onResolve, (err) => {
8842 if (gen !== this.generation) {
8843 return;
8844 }
8845 this.handleMountFailure(def.id, err);
8846 });
8847 return;
8848 }
8849 onResolve(result);
8850 },
8851 (err) => {
8852 if (gen !== this.generation) {
8853 return;
8854 }
8855 this.handleMountFailure(def.id, err);
8856 }
8857 );
8858 }
8859 handleMountFailure(id, err) {
8860 this.element.innerHTML = "";
8861 doAction(HOOKS.WALLPAPER_MOUNT_FAILED, { id, error: err });
8862 doAction(HOOKS.SHELL_ERROR, { scope: "wallpaper-mount", id, error: err });
8863 if (typeof console !== "undefined") {
8864 console.error(
8865 `[desktop-mode] Wallpaper "${id}" failed to mount:`,
8866 err
8867 );
8868 }
8869 }
8870 }
8871 function isThenable$1(value) {
8872 return !!value && typeof value === "object" && typeof value.then === "function";
8873 }
8874 function createWallpaperRegistrySync(deps2) {
8875 const { osSettings } = deps2;
8876 const registered = /* @__PURE__ */ new Set();
8877 const loadedScripts = /* @__PURE__ */ new Set();
8878 const ensureScript = async (entry) => {
8879 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
8880 return;
8881 }
8882 try {
8883 await loadVendorScript(entry.scriptUrl, {
8884 translations: entry.scriptTranslations,
8885 l10n: entry.scriptL10n,
8886 before: entry.scriptBefore,
8887 after: entry.scriptAfter
8888 });
8889 } catch (err) {
8890 doAction(HOOKS.SHELL_ERROR, {
8891 scope: "wallpaper-script-load",
8892 id: entry.id,
8893 error: err
8894 });
8895 return;
8896 }
8897 loadedScripts.add(entry.scriptUrl);
8898 };
8899 const readDef = (id) => {
8900 const globals = window.desktopModeWallpapers || {};
8901 return globals[id] ?? null;
8902 };
8903 const defFromCssEntry = (entry) => {
8904 if (entry.type !== "css" || entry.value === "") {
8905 return null;
8906 }
8907 return {
8908 id: entry.id,
8909 label: entry.label,
8910 type: "css",
8911 value: entry.value,
8912 preview: entry.preview !== "" ? entry.preview : entry.value
8913 };
8914 };
8915 const registerEntry = async (entry) => {
8916 if (registered.has(entry.id)) {
8917 return;
8918 }
8919 const cssDef = defFromCssEntry(entry);
8920 if (cssDef) {
8921 register$2(cssDef);
8922 registered.add(entry.id);
8923 osSettings.apply();
8924 return;
8925 }
8926 await ensureScript(entry);
8927 const def = readDef(entry.id);
8928 if (!def) {
8929 doAction(HOOKS.SHELL_ERROR, {
8930 scope: "wallpaper-missing-def",
8931 id: entry.id,
8932 error: new Error(
8933 `[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.`
8934 )
8935 });
8936 return;
8937 }
8938 try {
8939 register$2(def);
8940 } catch (err) {
8941 doAction(HOOKS.SHELL_ERROR, {
8942 scope: "wallpaper-register",
8943 id: entry.id,
8944 error: err
8945 });
8946 return;
8947 }
8948 registered.add(entry.id);
8949 osSettings.apply();
8950 };
8951 const unregisterEntry = (id) => {
8952 if (!registered.has(id)) {
8953 return;
8954 }
8955 unregister$2(id);
8956 registered.delete(id);
8957 osSettings.apply();
8958 };
8959 return async (list2) => {
8960 const incoming = /* @__PURE__ */ new Set();
8961 for (const entry of list2) {
8962 incoming.add(entry.id);
8963 }
8964 for (const id of Array.from(registered)) {
8965 if (!incoming.has(id)) {
8966 unregisterEntry(id);
8967 }
8968 }
8969 for (const entry of list2) {
8970 if (!registered.has(entry.id)) {
8971 await registerEntry(entry);
8972 }
8973 }
8974 };
8975 }
8976 const COMMAND_SLUG = /^[a-z0-9_/-]+$/;
8977 const commandRegistryStore = createSharedStore(
8978 "desktop-mode/commands-registry",
8979 () => ({
8980 registry: /* @__PURE__ */ new Map(),
8981 listeners: /* @__PURE__ */ new Set()
8982 })
8983 );
8984 const registry$7 = commandRegistryStore.state.registry;
8985 const listeners$a = commandRegistryStore.state.listeners;
8986 function registerCommand(cmd) {
8987 const errors = [];
8988 const slug = typeof cmd?.slug === "string" ? cmd.slug.trim().toLowerCase() : "";
8989 if (!cmd || typeof cmd !== "object") {
8990 errors.push("def (not an object)");
8991 } else {
8992 if (typeof cmd.slug !== "string" || cmd.slug.trim() === "") {
8993 errors.push("slug (missing)");
8994 } else if (!COMMAND_SLUG.test(slug)) {
8995 errors.push(
8996 `slug (must match ${COMMAND_SLUG} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
8997 );
8998 }
8999 if (typeof cmd.label !== "string" || cmd.label.trim() === "") {
9000 errors.push("label (missing)");
9001 }
9002 if (typeof cmd.run !== "function") {
9003 errors.push("run (must be a function)");
9004 }
9005 }
9006 throwOnRegistrationErrors("Command", errors, cmd);
9007 registry$7.set(slug, { ...cmd, slug });
9008 notify$c();
9009 }
9010 function unregisterCommand(slug) {
9011 if (registry$7.delete(slug.toLowerCase())) {
9012 notify$c();
9013 }
9014 }
9015 function unregisterByOwner(owner) {
9016 if (!owner) {
9017 return 0;
9018 }
9019 let removed = 0;
9020 for (const [slug, cmd] of Array.from(registry$7.entries())) {
9021 if (cmd.owner === owner) {
9022 registry$7.delete(slug);
9023 removed++;
9024 }
9025 }
9026 if (removed > 0) {
9027 notify$c();
9028 }
9029 return removed;
9030 }
9031 function listCommands() {
9032 return Array.from(registry$7.values());
9033 }
9034 function listAiCallableCommands() {
9035 const out = [];
9036 for (const cmd of registry$7.values()) {
9037 if (cmd.aiCallable !== true) {
9038 continue;
9039 }
9040 out.push({
9041 slug: cmd.slug,
9042 label: cmd.label,
9043 description: cmd.description ?? "",
9044 hint: cmd.hint ?? ""
9045 });
9046 }
9047 return out;
9048 }
9049 function findCommand(slug) {
9050 return registry$7.get(slug.toLowerCase()) ?? null;
9051 }
9052 function notify$c() {
9053 const snapshot = Array.from(listeners$a);
9054 for (const cb of snapshot) {
9055 try {
9056 cb();
9057 } catch (err) {
9058 if (typeof console !== "undefined") {
9059 console.error("[desktop-mode] command-registry listener threw:", err);
9060 }
9061 }
9062 }
9063 }
9064 function createCommandRegistrySync() {
9065 const loadedHandles = /* @__PURE__ */ new Set();
9066 const loadedUrls = /* @__PURE__ */ new Set();
9067 let prevSlugsByHandle = /* @__PURE__ */ new Map();
9068 const ensureScript = async (entry) => {
9069 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9070 loadedHandles.add(entry.handle);
9071 return;
9072 }
9073 try {
9074 await loadVendorScript(entry.scriptUrl, {
9075 translations: entry.scriptTranslations,
9076 l10n: entry.scriptL10n,
9077 before: entry.scriptBefore,
9078 after: entry.scriptAfter
9079 });
9080 } catch (err) {
9081 doAction(HOOKS.SHELL_ERROR, {
9082 scope: "command-script-load",
9083 handle: entry.handle,
9084 url: entry.scriptUrl,
9085 error: err
9086 });
9087 return;
9088 }
9089 loadedUrls.add(entry.scriptUrl);
9090 loadedHandles.add(entry.handle);
9091 };
9092 const slugsByHandleFrom = (commands) => {
9093 const map = /* @__PURE__ */ new Map();
9094 if (!commands) {
9095 return map;
9096 }
9097 for (const entry of commands) {
9098 if (!entry.scriptHandle || !entry.slug) {
9099 continue;
9100 }
9101 let set = map.get(entry.scriptHandle);
9102 if (!set) {
9103 set = /* @__PURE__ */ new Set();
9104 map.set(entry.scriptHandle, set);
9105 }
9106 set.add(entry.slug);
9107 }
9108 return map;
9109 };
9110 const collectSlugsToRemove = (handle) => {
9111 const slugs = /* @__PURE__ */ new Set();
9112 for (const cmd of listCommands()) {
9113 if (cmd.owner === handle) {
9114 slugs.add(cmd.slug);
9115 }
9116 }
9117 const declared = prevSlugsByHandle.get(handle);
9118 if (declared) {
9119 for (const slug of declared) {
9120 slugs.add(slug);
9121 }
9122 }
9123 return slugs;
9124 };
9125 return async (scripts, commands) => {
9126 const incomingHandles = /* @__PURE__ */ new Set();
9127 for (const entry of scripts) {
9128 if (entry.handle) {
9129 incomingHandles.add(entry.handle);
9130 }
9131 }
9132 for (const handle of Array.from(loadedHandles)) {
9133 if (incomingHandles.has(handle)) {
9134 continue;
9135 }
9136 for (const slug of collectSlugsToRemove(handle)) {
9137 unregisterCommand(slug);
9138 }
9139 loadedHandles.delete(handle);
9140 }
9141 for (const entry of scripts) {
9142 if (!entry.handle || loadedHandles.has(entry.handle)) {
9143 continue;
9144 }
9145 await ensureScript(entry);
9146 }
9147 prevSlugsByHandle = slugsByHandleFrom(commands);
9148 };
9149 }
9150 const store$b = createSharedStore(
9151 "desktop-mode/settings-tab-registry",
9152 () => ({
9153 registry: /* @__PURE__ */ new Map(),
9154 listeners: /* @__PURE__ */ new Set()
9155 })
9156 );
9157 const registry$6 = store$b.state.registry;
9158 const listeners$9 = store$b.state.listeners;
9159 function registerSettingsTab(tab) {
9160 if (!tab || typeof tab.id !== "string" || tab.id.trim() === "") {
9161 return;
9162 }
9163 if (typeof tab.label !== "string" || tab.label.trim() === "") {
9164 return;
9165 }
9166 if (typeof tab.render !== "function") {
9167 return;
9168 }
9169 const id = tab.id.trim().toLowerCase();
9170 if (!/^[a-z0-9_\-]+$/.test(id)) {
9171 if (typeof console !== "undefined") {
9172 console.warn(
9173 "[desktop-mode] registerSettingsTab: id must be [a-z0-9_-]+, got",
9174 tab.id
9175 );
9176 }
9177 return;
9178 }
9179 registry$6.set(id, { ...tab, id });
9180 notify$b();
9181 }
9182 function unregisterSettingsTab(id) {
9183 if (registry$6.delete(id.toLowerCase())) {
9184 notify$b();
9185 }
9186 }
9187 function unregisterSettingsTabsByOwner(owner) {
9188 if (!owner) {
9189 return 0;
9190 }
9191 let removed = 0;
9192 for (const [id, tab] of Array.from(registry$6.entries())) {
9193 if (tab.owner === owner) {
9194 registry$6.delete(id);
9195 removed++;
9196 }
9197 }
9198 if (removed > 0) {
9199 notify$b();
9200 }
9201 return removed;
9202 }
9203 function listSettingsTabs() {
9204 return Array.from(registry$6.values()).sort(
9205 (a, b) => (a.order ?? 100) - (b.order ?? 100)
9206 );
9207 }
9208 function notify$b() {
9209 const snapshot = Array.from(listeners$9);
9210 for (const cb of snapshot) {
9211 try {
9212 cb();
9213 } catch (err) {
9214 if (typeof console !== "undefined") {
9215 console.error(
9216 "[desktop-mode] settings-tab-registry listener threw:",
9217 err
9218 );
9219 }
9220 }
9221 }
9222 }
9223 function createSettingsTabRegistrySync() {
9224 const loadedHandles = /* @__PURE__ */ new Set();
9225 const loadedUrls = /* @__PURE__ */ new Set();
9226 let prevIdsByHandle = /* @__PURE__ */ new Map();
9227 const ensureScript = async (entry) => {
9228 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9229 loadedHandles.add(entry.handle);
9230 return;
9231 }
9232 try {
9233 await loadVendorScript(entry.scriptUrl, {
9234 translations: entry.scriptTranslations,
9235 l10n: entry.scriptL10n,
9236 before: entry.scriptBefore,
9237 after: entry.scriptAfter
9238 });
9239 } catch (err) {
9240 doAction(HOOKS.SHELL_ERROR, {
9241 scope: "settings-tab-script-load",
9242 handle: entry.handle,
9243 url: entry.scriptUrl,
9244 error: err
9245 });
9246 return;
9247 }
9248 loadedUrls.add(entry.scriptUrl);
9249 loadedHandles.add(entry.handle);
9250 };
9251 const idsByHandleFrom = (tabs) => {
9252 const map = /* @__PURE__ */ new Map();
9253 if (!tabs) {
9254 return map;
9255 }
9256 for (const entry of tabs) {
9257 if (!entry.scriptHandle || !entry.id) {
9258 continue;
9259 }
9260 let set = map.get(entry.scriptHandle);
9261 if (!set) {
9262 set = /* @__PURE__ */ new Set();
9263 map.set(entry.scriptHandle, set);
9264 }
9265 set.add(entry.id);
9266 }
9267 return map;
9268 };
9269 const removeByHandle = (handle) => {
9270 unregisterSettingsTabsByOwner(handle);
9271 const declared = prevIdsByHandle.get(handle);
9272 if (declared) {
9273 const present = new Set(
9274 listSettingsTabs().map((t) => t.id)
9275 );
9276 for (const id of declared) {
9277 if (present.has(id)) {
9278 unregisterSettingsTab(id);
9279 }
9280 }
9281 }
9282 };
9283 return async (scripts, tabs) => {
9284 const incomingHandles = /* @__PURE__ */ new Set();
9285 for (const entry of scripts) {
9286 if (entry.handle) {
9287 incomingHandles.add(entry.handle);
9288 }
9289 }
9290 for (const handle of Array.from(loadedHandles)) {
9291 if (incomingHandles.has(handle)) {
9292 continue;
9293 }
9294 removeByHandle(handle);
9295 loadedHandles.delete(handle);
9296 }
9297 for (const entry of scripts) {
9298 if (!entry.handle || loadedHandles.has(entry.handle)) {
9299 continue;
9300 }
9301 await ensureScript(entry);
9302 }
9303 prevIdsByHandle = idsByHandleFrom(tabs);
9304 };
9305 }
9306 const store$a = createSharedStore(
9307 "desktop-mode/title-bar-buttons-registry",
9308 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9309 );
9310 const registry$5 = store$a.state.registry;
9311 const listeners$8 = store$a.state.listeners;
9312 const TITLE_BAR_BUTTON_ID = /^[a-z0-9_/-]+$/;
9313 function registerTitleBarButton(def) {
9314 const errors = [];
9315 if (!def || typeof def !== "object") {
9316 errors.push("def (not an object)");
9317 } else {
9318 if (typeof def.id !== "string" || def.id.trim() === "") {
9319 errors.push("id (missing)");
9320 } else if (!TITLE_BAR_BUTTON_ID.test(def.id.trim().toLowerCase())) {
9321 errors.push(
9322 `id (must match ${TITLE_BAR_BUTTON_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9323 );
9324 }
9325 if (typeof def.label !== "string" || def.label.trim() === "") {
9326 errors.push("label (missing)");
9327 }
9328 if (typeof def.icon !== "string" || def.icon.trim() === "") {
9329 errors.push("icon (missing)");
9330 }
9331 if (typeof def.match !== "function") {
9332 errors.push("match (must be a function)");
9333 }
9334 if (typeof def.onClick !== "function" && typeof def.render !== "function") {
9335 errors.push("onClick|render (at least one must be a function)");
9336 }
9337 }
9338 throwOnRegistrationErrors("TitleBarButton", errors, def);
9339 const id = def.id.trim().toLowerCase();
9340 registry$5.set(id, { ...def, id });
9341 notify$a();
9342 }
9343 function unregisterTitleBarButton(id) {
9344 if (registry$5.delete(id.toLowerCase())) {
9345 notify$a();
9346 }
9347 }
9348 function unregisterTitleBarButtonsByOwner(owner) {
9349 if (!owner) {
9350 return 0;
9351 }
9352 let removed = 0;
9353 for (const [id, def] of Array.from(registry$5.entries())) {
9354 if (def.owner === owner) {
9355 registry$5.delete(id);
9356 removed++;
9357 }
9358 }
9359 if (removed > 0) {
9360 notify$a();
9361 }
9362 return removed;
9363 }
9364 function listTitleBarButtons() {
9365 return Array.from(registry$5.values()).sort(
9366 (a, b) => (a.order ?? 100) - (b.order ?? 100)
9367 );
9368 }
9369 function notify$a() {
9370 const snapshot = Array.from(listeners$8);
9371 for (const cb of snapshot) {
9372 try {
9373 cb();
9374 } catch (err) {
9375 if (typeof console !== "undefined") {
9376 console.error(
9377 "[desktop-mode] title-bar-button registry listener threw:",
9378 err
9379 );
9380 }
9381 }
9382 }
9383 }
9384 function createTitleBarButtonRegistrySync() {
9385 const loadedHandles = /* @__PURE__ */ new Set();
9386 const loadedUrls = /* @__PURE__ */ new Set();
9387 const ensureScript = async (entry) => {
9388 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9389 loadedHandles.add(entry.handle);
9390 return;
9391 }
9392 try {
9393 await loadVendorScript(entry.scriptUrl, {
9394 translations: entry.scriptTranslations,
9395 l10n: entry.scriptL10n,
9396 before: entry.scriptBefore,
9397 after: entry.scriptAfter
9398 });
9399 } catch (err) {
9400 doAction(HOOKS.SHELL_ERROR, {
9401 scope: "titlebar-button-script-load",
9402 handle: entry.handle,
9403 url: entry.scriptUrl,
9404 error: err
9405 });
9406 return;
9407 }
9408 loadedUrls.add(entry.scriptUrl);
9409 loadedHandles.add(entry.handle);
9410 };
9411 return async (scripts) => {
9412 const incomingHandles = /* @__PURE__ */ new Set();
9413 for (const entry of scripts) {
9414 if (entry.handle) {
9415 incomingHandles.add(entry.handle);
9416 }
9417 }
9418 for (const handle of Array.from(loadedHandles)) {
9419 if (incomingHandles.has(handle)) {
9420 continue;
9421 }
9422 unregisterTitleBarButtonsByOwner(handle);
9423 loadedHandles.delete(handle);
9424 }
9425 for (const entry of scripts) {
9426 if (!entry.handle || loadedHandles.has(entry.handle)) {
9427 continue;
9428 }
9429 await ensureScript(entry);
9430 }
9431 };
9432 }
9433 const UNFOCUS_EFFECT_NONE = "none";
9434 const store$9 = createSharedStore(
9435 "desktop-mode/unfocus-effect-registry",
9436 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9437 );
9438 const registry$4 = store$9.state.registry;
9439 const listeners$7 = store$9.state.listeners;
9440 const UNFOCUS_EFFECT_ID = /^[a-z0-9_/-]+$/;
9441 function registerUnfocusEffect(def) {
9442 const errors = [];
9443 if (!def || typeof def !== "object") {
9444 errors.push("def (not an object)");
9445 } else {
9446 if (typeof def.id !== "string" || def.id.trim() === "") {
9447 errors.push("id (missing)");
9448 } else if (!UNFOCUS_EFFECT_ID.test(def.id.trim().toLowerCase())) {
9449 errors.push(
9450 `id (must match ${UNFOCUS_EFFECT_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9451 );
9452 } else if (def.id.trim().toLowerCase() === UNFOCUS_EFFECT_NONE) {
9453 errors.push('id ("none" is reserved)');
9454 }
9455 if (typeof def.label !== "string" || def.label.trim() === "") {
9456 errors.push("label (missing)");
9457 }
9458 if (typeof def.className !== "string" && typeof def.apply !== "function") {
9459 errors.push(
9460 "className|apply (at least one must be provided — a CSS class to toggle or an apply callback)"
9461 );
9462 }
9463 }
9464 throwOnRegistrationErrors("UnfocusEffect", errors, def);
9465 const id = def.id.trim().toLowerCase();
9466 registry$4.set(id, { ...def, id });
9467 notify$9();
9468 }
9469 function unregisterUnfocusEffect(id) {
9470 if (registry$4.delete(id.toLowerCase())) {
9471 notify$9();
9472 }
9473 }
9474 function unregisterUnfocusEffectsByOwner(owner) {
9475 if (!owner) {
9476 return 0;
9477 }
9478 let removed = 0;
9479 for (const [id, def] of Array.from(registry$4.entries())) {
9480 if (def.owner === owner) {
9481 registry$4.delete(id);
9482 removed++;
9483 }
9484 }
9485 if (removed > 0) {
9486 notify$9();
9487 }
9488 return removed;
9489 }
9490 function listUnfocusEffects() {
9491 const copy = Array.from(registry$4.values());
9492 const filtered = applyFilters(
9493 HOOKS.UNFOCUS_EFFECTS,
9494 copy
9495 );
9496 if (!Array.isArray(filtered)) {
9497 if (typeof console !== "undefined") {
9498 console.warn(
9499 "[desktop-mode] `desktop-mode.unfocus-effects` filter returned a non-array; falling back to registry list."
9500 );
9501 }
9502 return copy;
9503 }
9504 return filtered;
9505 }
9506 function getUnfocusEffect(id) {
9507 return listUnfocusEffects().find((e) => e.id === id);
9508 }
9509 function subscribeUnfocusEffects(cb) {
9510 listeners$7.add(cb);
9511 return () => {
9512 listeners$7.delete(cb);
9513 };
9514 }
9515 function notify$9() {
9516 const snapshot = Array.from(listeners$7);
9517 for (const cb of snapshot) {
9518 try {
9519 cb();
9520 } catch (err) {
9521 if (typeof console !== "undefined") {
9522 console.error(
9523 "[desktop-mode] unfocus-effect registry listener threw:",
9524 err
9525 );
9526 }
9527 }
9528 }
9529 }
9530 registerUnfocusEffect({
9531 id: "darken",
9532 label: __("Darken"),
9533 description: __("Dim unfocused windows so the focused one stands out."),
9534 className: "desktop-mode-window--fx-darken"
9535 });
9536 registerUnfocusEffect({
9537 id: "frost",
9538 label: __("Frost"),
9539 description: __(
9540 "Throw unfocused windows out of focus — a soft, frosted-glass blur, as if you were looking at them through an iced-over pane."
9541 ),
9542 className: "desktop-mode-window--fx-frost"
9543 });
9544 registerUnfocusEffect({
9545 id: "grayscale",
9546 label: __("Grayscale"),
9547 description: __(
9548 "Drain the colour from unfocused windows so the focused one is the only thing still in colour — your eye snaps right to it."
9549 ),
9550 className: "desktop-mode-window--fx-grayscale"
9551 });
9552 function createUnfocusEffectRegistrySync() {
9553 const loadedHandles = /* @__PURE__ */ new Set();
9554 const loadedUrls = /* @__PURE__ */ new Set();
9555 const ensureScript = async (entry) => {
9556 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9557 loadedHandles.add(entry.handle);
9558 return;
9559 }
9560 try {
9561 await loadVendorScript(entry.scriptUrl, {
9562 translations: entry.scriptTranslations,
9563 l10n: entry.scriptL10n,
9564 before: entry.scriptBefore,
9565 after: entry.scriptAfter
9566 });
9567 } catch (err) {
9568 doAction(HOOKS.SHELL_ERROR, {
9569 scope: "unfocus-effect-script-load",
9570 handle: entry.handle,
9571 url: entry.scriptUrl,
9572 error: err
9573 });
9574 return;
9575 }
9576 loadedUrls.add(entry.scriptUrl);
9577 loadedHandles.add(entry.handle);
9578 };
9579 return async (scripts) => {
9580 const incomingHandles = /* @__PURE__ */ new Set();
9581 for (const entry of scripts) {
9582 if (entry.handle) {
9583 incomingHandles.add(entry.handle);
9584 }
9585 }
9586 for (const handle of Array.from(loadedHandles)) {
9587 if (incomingHandles.has(handle)) {
9588 continue;
9589 }
9590 unregisterUnfocusEffectsByOwner(handle);
9591 loadedHandles.delete(handle);
9592 }
9593 for (const entry of scripts) {
9594 if (!entry.handle || loadedHandles.has(entry.handle)) {
9595 continue;
9596 }
9597 await ensureScript(entry);
9598 }
9599 };
9600 }
9601 const EFFECT_ATTR = "data-desktop-unfocus-effect";
9602 const EFFECT_CLASS_ATTR = "data-desktop-unfocus-effect-class";
9603 let _started = false;
9604 function hostsCanvas(el) {
9605 return el.querySelector("canvas") !== null;
9606 }
9607 function startUnfocusEngine({ manager, osSettings }) {
9608 if (_started) {
9609 return;
9610 }
9611 _started = true;
9612 let currentId = osSettings.getOsSettingsSnapshot().unfocusEffect;
9613 const clear = (el, allEffects) => {
9614 const storedClass = el.getAttribute(EFFECT_CLASS_ATTR);
9615 if (storedClass) {
9616 el.classList.remove(storedClass);
9617 el.removeAttribute(EFFECT_CLASS_ATTR);
9618 }
9619 const priorId = el.getAttribute(EFFECT_ATTR);
9620 if (priorId) {
9621 getUnfocusEffect(priorId)?.clear?.(el);
9622 }
9623 for (const def of allEffects) {
9624 if (def.className) {
9625 el.classList.remove(def.className);
9626 }
9627 }
9628 el.removeAttribute(EFFECT_ATTR);
9629 };
9630 const apply = (el, def) => {
9631 if (def.className) {
9632 el.classList.add(def.className);
9633 el.setAttribute(EFFECT_CLASS_ATTR, def.className);
9634 }
9635 el.setAttribute(EFFECT_ATTR, def.id);
9636 def.apply?.(el);
9637 };
9638 const recompute = () => {
9639 const def = currentId === UNFOCUS_EFFECT_NONE ? void 0 : getUnfocusEffect(currentId);
9640 const allEffects = listUnfocusEffects();
9641 for (const win of manager.getAll()) {
9642 const el = win.element;
9643 if (!el) {
9644 continue;
9645 }
9646 clear(el, allEffects);
9647 if (!def || win.isFocused() || win.state === "minimized") {
9648 continue;
9649 }
9650 if (hostsCanvas(el)) {
9651 continue;
9652 }
9653 apply(el, def);
9654 }
9655 };
9656 for (const name of [
9657 "desktop-mode-window-opened",
9658 "desktop-mode-window-reopened",
9659 "desktop-mode-window-closed",
9660 "desktop-mode-window-focused",
9661 "desktop-mode-window-blurred"
9662 ]) {
9663 document.addEventListener(name, () => recompute());
9664 }
9665 osSettings.subscribeOsSettings((snapshot) => {
9666 currentId = snapshot.unfocusEffect;
9667 recompute();
9668 });
9669 subscribeUnfocusEffects(() => recompute());
9670 recompute();
9671 }
9672 function createDockRailRendererSync() {
9673 const loadedHandles = /* @__PURE__ */ new Set();
9674 const loadedUrls = /* @__PURE__ */ new Set();
9675 const ensureScript = async (entry) => {
9676 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9677 loadedHandles.add(entry.handle);
9678 return;
9679 }
9680 try {
9681 await loadVendorScript(entry.scriptUrl, {
9682 translations: entry.scriptTranslations,
9683 l10n: entry.scriptL10n,
9684 before: entry.scriptBefore,
9685 after: entry.scriptAfter
9686 });
9687 } catch (err) {
9688 doAction(HOOKS.SHELL_ERROR, {
9689 scope: "dock-rail-renderer-script-load",
9690 handle: entry.handle,
9691 url: entry.scriptUrl,
9692 error: err
9693 });
9694 return;
9695 }
9696 loadedUrls.add(entry.scriptUrl);
9697 loadedHandles.add(entry.handle);
9698 };
9699 return async (scripts) => {
9700 const incomingHandles = /* @__PURE__ */ new Set();
9701 for (const entry of scripts) {
9702 if (entry.handle) {
9703 incomingHandles.add(entry.handle);
9704 }
9705 }
9706 for (const handle of Array.from(loadedHandles)) {
9707 if (incomingHandles.has(handle)) {
9708 continue;
9709 }
9710 unregisterByOwner$1(handle);
9711 loadedHandles.delete(handle);
9712 }
9713 for (const entry of scripts) {
9714 if (!entry.handle || loadedHandles.has(entry.handle)) {
9715 continue;
9716 }
9717 await ensureScript(entry);
9718 }
9719 };
9720 }
9721 const store$8 = createSharedStore(
9722 "desktop-mode/window-themes-registry",
9723 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9724 );
9725 const registry$3 = store$8.state.registry;
9726 const listeners$6 = store$8.state.listeners;
9727 const WINDOW_THEME_ID = /^[a-z0-9_/-]+$/;
9728 function registerWindowTheme(def) {
9729 const errors = [];
9730 if (!def || typeof def !== "object") {
9731 errors.push("def (not an object)");
9732 } else {
9733 if (typeof def.id !== "string" || def.id.trim() === "") {
9734 errors.push("id (missing)");
9735 } else if (!WINDOW_THEME_ID.test(def.id.trim().toLowerCase())) {
9736 errors.push(
9737 `id (must match ${WINDOW_THEME_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9738 );
9739 }
9740 if (!def.tokens || typeof def.tokens !== "object") {
9741 errors.push("tokens (must be an object of CSS custom-property → value)");
9742 } else {
9743 for (const key of Object.keys(def.tokens)) {
9744 if (!key.startsWith("--")) {
9745 errors.push(
9746 `tokens.${key} (CSS custom-property keys must start with "--")`
9747 );
9748 break;
9749 }
9750 }
9751 }
9752 if (typeof def.match !== "function") {
9753 errors.push("match (must be a function)");
9754 }
9755 }
9756 throwOnRegistrationErrors("WindowTheme", errors, def);
9757 const id = def.id.trim().toLowerCase();
9758 registry$3.set(id, { ...def, id });
9759 notify$8();
9760 }
9761 function unregisterWindowTheme(id) {
9762 if (registry$3.delete(id.toLowerCase())) {
9763 notify$8();
9764 }
9765 }
9766 function unregisterWindowThemesByOwner(owner) {
9767 if (!owner) {
9768 return 0;
9769 }
9770 let removed = 0;
9771 for (const [id, def] of Array.from(registry$3.entries())) {
9772 if (def.owner === owner) {
9773 registry$3.delete(id);
9774 removed++;
9775 }
9776 }
9777 if (removed > 0) {
9778 notify$8();
9779 }
9780 return removed;
9781 }
9782 function listWindowThemes() {
9783 return Array.from(registry$3.values()).sort(
9784 (a, b) => (a.priority ?? 100) - (b.priority ?? 100)
9785 );
9786 }
9787 function notify$8() {
9788 const snapshot = Array.from(listeners$6);
9789 for (const cb of snapshot) {
9790 try {
9791 cb();
9792 } catch (err) {
9793 if (typeof console !== "undefined") {
9794 console.error(
9795 "[desktop-mode] window-theme registry listener threw:",
9796 err
9797 );
9798 }
9799 }
9800 }
9801 }
9802 function createWindowThemeRegistrySync() {
9803 const loadedHandles = /* @__PURE__ */ new Set();
9804 const loadedUrls = /* @__PURE__ */ new Set();
9805 let prevIdsByHandle = /* @__PURE__ */ new Map();
9806 const shellRegistered = /* @__PURE__ */ new Set();
9807 const ensureScript = async (entry) => {
9808 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
9809 loadedHandles.add(entry.handle);
9810 return;
9811 }
9812 try {
9813 await loadVendorScript(entry.scriptUrl, {
9814 translations: entry.scriptTranslations,
9815 l10n: entry.scriptL10n,
9816 before: entry.scriptBefore,
9817 after: entry.scriptAfter
9818 });
9819 } catch (err) {
9820 doAction(HOOKS.SHELL_ERROR, {
9821 scope: "window-theme-script-load",
9822 handle: entry.handle,
9823 url: entry.scriptUrl,
9824 error: err
9825 });
9826 return;
9827 }
9828 loadedUrls.add(entry.scriptUrl);
9829 loadedHandles.add(entry.handle);
9830 };
9831 const idsByHandleFrom = (themes) => {
9832 const map = /* @__PURE__ */ new Map();
9833 if (!themes) {
9834 return map;
9835 }
9836 for (const entry of themes) {
9837 if (!entry.scriptHandle || !entry.id) {
9838 continue;
9839 }
9840 let set = map.get(entry.scriptHandle);
9841 if (!set) {
9842 set = /* @__PURE__ */ new Set();
9843 map.set(entry.scriptHandle, set);
9844 }
9845 set.add(entry.id);
9846 }
9847 return map;
9848 };
9849 const collectIdsToRemove = (handle) => {
9850 const ids = /* @__PURE__ */ new Set();
9851 for (const def of listWindowThemes()) {
9852 if (def.owner === handle) {
9853 ids.add(def.id);
9854 }
9855 }
9856 const declared = prevIdsByHandle.get(handle);
9857 if (declared) {
9858 for (const id of declared) {
9859 ids.add(id);
9860 }
9861 }
9862 return ids;
9863 };
9864 const applyMetadata = (themes) => {
9865 if (!themes) {
9866 return;
9867 }
9868 for (const entry of themes) {
9869 if (!entry.id || !entry.tokens) {
9870 continue;
9871 }
9872 try {
9873 registerWindowTheme({
9874 id: entry.id,
9875 label: entry.label,
9876 tokens: entry.tokens,
9877 priority: entry.priority,
9878 match: () => true,
9879 owner: entry.scriptHandle || void 0
9880 });
9881 shellRegistered.add(entry.id);
9882 } catch (err) {
9883 doAction(HOOKS.SHELL_ERROR, {
9884 scope: "window-theme-shell-register",
9885 id: entry.id,
9886 error: err
9887 });
9888 }
9889 }
9890 };
9891 return async (scripts, themes) => {
9892 const incomingHandles = /* @__PURE__ */ new Set();
9893 for (const entry of scripts) {
9894 if (entry.handle) {
9895 incomingHandles.add(entry.handle);
9896 }
9897 }
9898 for (const handle of Array.from(loadedHandles)) {
9899 if (incomingHandles.has(handle)) {
9900 continue;
9901 }
9902 const ids = collectIdsToRemove(handle);
9903 for (const id of ids) {
9904 unregisterWindowTheme(id);
9905 shellRegistered.delete(id);
9906 }
9907 unregisterWindowThemesByOwner(handle);
9908 loadedHandles.delete(handle);
9909 }
9910 applyMetadata(themes);
9911 for (const entry of scripts) {
9912 if (!entry.handle || loadedHandles.has(entry.handle)) {
9913 continue;
9914 }
9915 await ensureScript(entry);
9916 }
9917 prevIdsByHandle = idsByHandleFrom(themes);
9918 };
9919 }
9920 const store$7 = createSharedStore(
9921 "desktop-mode/window-controls-registry",
9922 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
9923 );
9924 const registry$2 = store$7.state.registry;
9925 const listeners$5 = store$7.state.listeners;
9926 const WINDOW_CONTROL_ID = /^[a-z0-9_/-]+$/;
9927 function registerWindowControl(def) {
9928 const errors = [];
9929 if (!def || typeof def !== "object") {
9930 errors.push("def (not an object)");
9931 } else {
9932 if (typeof def.id !== "string" || def.id.trim() === "") {
9933 errors.push("id (missing)");
9934 } else if (!WINDOW_CONTROL_ID.test(def.id.trim().toLowerCase())) {
9935 errors.push(
9936 `id (must match ${WINDOW_CONTROL_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
9937 );
9938 }
9939 if (typeof def.label !== "string" || def.label.trim() === "") {
9940 errors.push("label (missing)");
9941 }
9942 if (typeof def.onClick !== "function" && typeof def.render !== "function") {
9943 errors.push("onClick|render (at least one must be a function)");
9944 }
9945 if (typeof def.render !== "function") {
9946 if (typeof def.icon !== "string" || def.icon.trim() === "") {
9947 errors.push("icon (required when render is omitted)");
9948 }
9949 }
9950 if (typeof def.match !== "function") {
9951 errors.push("match (must be a function)");
9952 }
9953 if (def.placement !== void 0 && def.placement !== "left" && def.placement !== "right" && def.placement !== "controls") {
9954 errors.push('placement (must be "left", "right", or "controls")');
9955 }
9956 }
9957 throwOnRegistrationErrors("WindowControl", errors, def);
9958 const id = def.id.trim().toLowerCase();
9959 registry$2.set(id, { ...def, id });
9960 notify$7();
9961 }
9962 function unregisterWindowControl(id) {
9963 if (registry$2.delete(id.toLowerCase())) {
9964 notify$7();
9965 }
9966 }
9967 function unregisterWindowControlsByOwner(owner) {
9968 if (!owner) {
9969 return 0;
9970 }
9971 let removed = 0;
9972 for (const [id, def] of Array.from(registry$2.entries())) {
9973 if (def.owner === owner) {
9974 registry$2.delete(id);
9975 removed++;
9976 }
9977 }
9978 if (removed > 0) {
9979 notify$7();
9980 }
9981 return removed;
9982 }
9983 function listWindowControls() {
9984 return Array.from(registry$2.values()).sort((a, b) => {
9985 const oa = a.order ?? 100;
9986 const ob = b.order ?? 100;
9987 if (oa !== ob) {
9988 return oa - ob;
9989 }
9990 return a.id.localeCompare(b.id);
9991 });
9992 }
9993 function notify$7() {
9994 const snapshot = Array.from(listeners$5);
9995 for (const cb of snapshot) {
9996 try {
9997 cb();
9998 } catch (err) {
9999 if (typeof console !== "undefined") {
10000 console.error(
10001 "[desktop-mode] window-control registry listener threw:",
10002 err
10003 );
10004 }
10005 }
10006 }
10007 }
10008 function registerBuiltInControls() {
10009 registerWindowControl({
10010 id: "core/minimize",
10011 label: __("Minimize"),
10012 icon: "minimize",
10013 placement: "controls",
10014 order: 10,
10015 core: true,
10016 match: () => true,
10017 onClick: (win) => {
10018 win.minimize();
10019 }
10020 });
10021 registerWindowControl({
10022 id: "core/maximize",
10023 label: __("Maximize"),
10024 icon: "maximize",
10025 placement: "controls",
10026 order: 20,
10027 core: true,
10028 match: () => true,
10029 onClick: (win) => {
10030 win.toggleMaximize();
10031 }
10032 });
10033 registerWindowControl({
10034 id: "core/focus-tab",
10035 label: __("Enter fullscreen"),
10036 icon: "fullscreen",
10037 placement: "controls",
10038 order: 30,
10039 core: true,
10040 match: () => true,
10041 onClick: (win) => {
10042 win.toggleFullscreen();
10043 }
10044 });
10045 registerWindowControl({
10046 id: "core/close",
10047 label: __("Close"),
10048 icon: "close",
10049 placement: "controls",
10050 order: 50,
10051 core: true,
10052 match: () => true,
10053 onClick: (win) => {
10054 win.close();
10055 }
10056 });
10057 }
10058 function createWindowControlRegistrySync() {
10059 const loadedHandles = /* @__PURE__ */ new Set();
10060 const loadedUrls = /* @__PURE__ */ new Set();
10061 let prevIdsByHandle = /* @__PURE__ */ new Map();
10062 const ensureScript = async (entry) => {
10063 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
10064 loadedHandles.add(entry.handle);
10065 return;
10066 }
10067 try {
10068 await loadVendorScript(entry.scriptUrl, {
10069 translations: entry.scriptTranslations,
10070 l10n: entry.scriptL10n,
10071 before: entry.scriptBefore,
10072 after: entry.scriptAfter
10073 });
10074 } catch (err) {
10075 doAction(HOOKS.SHELL_ERROR, {
10076 scope: "window-control-script-load",
10077 handle: entry.handle,
10078 url: entry.scriptUrl,
10079 error: err
10080 });
10081 return;
10082 }
10083 loadedUrls.add(entry.scriptUrl);
10084 loadedHandles.add(entry.handle);
10085 };
10086 const idsByHandleFrom = (controls) => {
10087 const map = /* @__PURE__ */ new Map();
10088 if (!controls) {
10089 return map;
10090 }
10091 for (const entry of controls) {
10092 if (!entry.scriptHandle || !entry.id) {
10093 continue;
10094 }
10095 let set = map.get(entry.scriptHandle);
10096 if (!set) {
10097 set = /* @__PURE__ */ new Set();
10098 map.set(entry.scriptHandle, set);
10099 }
10100 set.add(entry.id);
10101 }
10102 return map;
10103 };
10104 const collectIdsToRemove = (handle) => {
10105 const ids = /* @__PURE__ */ new Set();
10106 for (const def of listWindowControls()) {
10107 if (def.owner === handle) {
10108 ids.add(def.id);
10109 }
10110 }
10111 const declared = prevIdsByHandle.get(handle);
10112 if (declared) {
10113 for (const id of declared) {
10114 ids.add(id);
10115 }
10116 }
10117 return ids;
10118 };
10119 return async (scripts, controls) => {
10120 const incomingHandles = /* @__PURE__ */ new Set();
10121 for (const entry of scripts) {
10122 if (entry.handle) {
10123 incomingHandles.add(entry.handle);
10124 }
10125 }
10126 for (const handle of Array.from(loadedHandles)) {
10127 if (incomingHandles.has(handle)) {
10128 continue;
10129 }
10130 for (const id of collectIdsToRemove(handle)) {
10131 unregisterWindowControl(id);
10132 }
10133 unregisterWindowControlsByOwner(handle);
10134 loadedHandles.delete(handle);
10135 }
10136 for (const entry of scripts) {
10137 if (!entry.handle || loadedHandles.has(entry.handle)) {
10138 continue;
10139 }
10140 await ensureScript(entry);
10141 }
10142 prevIdsByHandle = idsByHandleFrom(controls);
10143 };
10144 }
10145 const store$6 = createSharedStore(
10146 "desktop-mode/window-slots-registry",
10147 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
10148 );
10149 const registry$1 = store$6.state.registry;
10150 const listeners$4 = store$6.state.listeners;
10151 const WINDOW_SLOT_ID = /^[a-z0-9_/-]+$/;
10152 const KNOWN_SLOTS = /* @__PURE__ */ new Set([
10153 "before-titlebar",
10154 "before-icon",
10155 "icon",
10156 "title",
10157 "after-title",
10158 "before-controls",
10159 "controls",
10160 "after-controls",
10161 "after-titlebar"
10162 ]);
10163 function registerWindowSlot(def) {
10164 const errors = [];
10165 if (!def || typeof def !== "object") {
10166 errors.push("def (not an object)");
10167 } else {
10168 if (typeof def.id !== "string" || def.id.trim() === "") {
10169 errors.push("id (missing)");
10170 } else if (!WINDOW_SLOT_ID.test(def.id.trim().toLowerCase())) {
10171 errors.push(
10172 `id (must match ${WINDOW_SLOT_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
10173 );
10174 }
10175 if (typeof def.slot !== "string" || def.slot.trim() === "") {
10176 errors.push("slot (missing)");
10177 } else if (!KNOWN_SLOTS.has(def.slot)) {
10178 errors.push(
10179 `slot (must be one of ${Array.from(KNOWN_SLOTS).join(", ")})`
10180 );
10181 }
10182 if (typeof def.match !== "function") {
10183 errors.push("match (must be a function)");
10184 }
10185 if (typeof def.render !== "function") {
10186 errors.push("render (must be a function)");
10187 }
10188 }
10189 throwOnRegistrationErrors("WindowSlot", errors, def);
10190 const id = def.id.trim().toLowerCase();
10191 registry$1.set(id, { ...def, id });
10192 notify$6();
10193 }
10194 function unregisterWindowSlot(id) {
10195 if (registry$1.delete(id.toLowerCase())) {
10196 notify$6();
10197 }
10198 }
10199 function unregisterWindowSlotsByOwner(owner) {
10200 if (!owner) {
10201 return 0;
10202 }
10203 let removed = 0;
10204 for (const [id, def] of Array.from(registry$1.entries())) {
10205 if (def.owner === owner) {
10206 registry$1.delete(id);
10207 removed++;
10208 }
10209 }
10210 if (removed > 0) {
10211 notify$6();
10212 }
10213 return removed;
10214 }
10215 function listWindowSlots() {
10216 return Array.from(registry$1.values()).sort((a, b) => {
10217 const oa = a.order ?? 100;
10218 const ob = b.order ?? 100;
10219 if (oa !== ob) {
10220 return oa - ob;
10221 }
10222 return a.id.localeCompare(b.id);
10223 });
10224 }
10225 function notify$6() {
10226 const snapshot = Array.from(listeners$4);
10227 for (const cb of snapshot) {
10228 try {
10229 cb();
10230 } catch (err) {
10231 if (typeof console !== "undefined") {
10232 console.error(
10233 "[desktop-mode] window-slot registry listener threw:",
10234 err
10235 );
10236 }
10237 }
10238 }
10239 }
10240 function createWindowSlotRegistrySync() {
10241 const loadedHandles = /* @__PURE__ */ new Set();
10242 const loadedUrls = /* @__PURE__ */ new Set();
10243 let prevIdsByHandle = /* @__PURE__ */ new Map();
10244 const ensureScript = async (entry) => {
10245 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
10246 loadedHandles.add(entry.handle);
10247 return;
10248 }
10249 try {
10250 await loadVendorScript(entry.scriptUrl, {
10251 translations: entry.scriptTranslations,
10252 l10n: entry.scriptL10n,
10253 before: entry.scriptBefore,
10254 after: entry.scriptAfter
10255 });
10256 } catch (err) {
10257 doAction(HOOKS.SHELL_ERROR, {
10258 scope: "window-slot-script-load",
10259 handle: entry.handle,
10260 url: entry.scriptUrl,
10261 error: err
10262 });
10263 return;
10264 }
10265 loadedUrls.add(entry.scriptUrl);
10266 loadedHandles.add(entry.handle);
10267 };
10268 const idsByHandleFrom = (slots) => {
10269 const map = /* @__PURE__ */ new Map();
10270 if (!slots) {
10271 return map;
10272 }
10273 for (const entry of slots) {
10274 if (!entry.scriptHandle || !entry.id) {
10275 continue;
10276 }
10277 let set = map.get(entry.scriptHandle);
10278 if (!set) {
10279 set = /* @__PURE__ */ new Set();
10280 map.set(entry.scriptHandle, set);
10281 }
10282 set.add(entry.id);
10283 }
10284 return map;
10285 };
10286 const collectIdsToRemove = (handle) => {
10287 const ids = /* @__PURE__ */ new Set();
10288 for (const def of listWindowSlots()) {
10289 if (def.owner === handle) {
10290 ids.add(def.id);
10291 }
10292 }
10293 const declared = prevIdsByHandle.get(handle);
10294 if (declared) {
10295 for (const id of declared) {
10296 ids.add(id);
10297 }
10298 }
10299 return ids;
10300 };
10301 return async (scripts, slots) => {
10302 const incomingHandles = /* @__PURE__ */ new Set();
10303 for (const entry of scripts) {
10304 if (entry.handle) {
10305 incomingHandles.add(entry.handle);
10306 }
10307 }
10308 for (const handle of Array.from(loadedHandles)) {
10309 if (incomingHandles.has(handle)) {
10310 continue;
10311 }
10312 for (const id of collectIdsToRemove(handle)) {
10313 unregisterWindowSlot(id);
10314 }
10315 unregisterWindowSlotsByOwner(handle);
10316 loadedHandles.delete(handle);
10317 }
10318 for (const entry of scripts) {
10319 if (!entry.handle || loadedHandles.has(entry.handle)) {
10320 continue;
10321 }
10322 await ensureScript(entry);
10323 }
10324 prevIdsByHandle = idsByHandleFrom(slots);
10325 };
10326 }
10327 const KEY_PREFIX = "desktop-mode-notice-dismissed";
10328 function currentUserSuffix() {
10329 const w = window.wp;
10330 const uid = w?.desktop?.config?.currentUserId;
10331 if (typeof uid === "number" && uid > 0) {
10332 return String(uid);
10333 }
10334 return "anon";
10335 }
10336 function storageKey() {
10337 return `${KEY_PREFIX}:${currentUserSuffix()}`;
10338 }
10339 function readMap() {
10340 try {
10341 const raw = window.localStorage.getItem(storageKey());
10342 if (!raw) {
10343 return {};
10344 }
10345 const parsed = JSON.parse(raw);
10346 if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
10347 return parsed;
10348 }
10349 } catch {
10350 }
10351 return {};
10352 }
10353 function writeMap(map) {
10354 try {
10355 window.localStorage.setItem(storageKey(), JSON.stringify(map));
10356 } catch {
10357 }
10358 }
10359 function isNoticeDismissed(id) {
10360 if (!id) {
10361 return false;
10362 }
10363 return readMap()[id] === true;
10364 }
10365 function markNoticeDismissed(id) {
10366 if (!id) {
10367 return;
10368 }
10369 const map = readMap();
10370 map[id] = true;
10371 writeMap(map);
10372 }
10373 function clearNoticeDismissed(id) {
10374 if (!id) {
10375 return;
10376 }
10377 const map = readMap();
10378 if (map[id]) {
10379 delete map[id];
10380 writeMap(map);
10381 }
10382 }
10383 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 ) )}`;
10384 const _WpdNotice = class _WpdNotice extends Component {
10385 connectedCallback() {
10386 super.connectedCallback();
10387 if (!this.hasAttribute("role")) {
10388 this.setAttribute("role", "status");
10389 }
10390 if (!this.hasAttribute("tone")) {
10391 this.setAttribute("tone", "info");
10392 }
10393 const id = this.getAttribute("notice-id");
10394 if (id && isNoticeDismissed(id)) {
10395 this.hidden = true;
10396 }
10397 }
10398 /**
10399 * Imperatively dismiss the notice — hides the host and records
10400 * the dismissal in localStorage when `notice-id` is set.
10401 */
10402 dismiss() {
10403 this.hidden = true;
10404 const id = this.getAttribute("notice-id");
10405 if (id) {
10406 markNoticeDismissed(id);
10407 }
10408 this.emit("wpd-notice-dismiss", { noticeId: id ?? void 0 });
10409 }
10410 /**
10411 * Clear a previously recorded dismissal and re-show the notice.
10412 * Useful in tests and for "Show again" affordances.
10413 */
10414 undismiss() {
10415 const id = this.getAttribute("notice-id");
10416 if (id) {
10417 clearNoticeDismissed(id);
10418 }
10419 this.hidden = false;
10420 }
10421 render() {
10422 const icon = this.getAttribute("icon");
10423 const dismissible = !this.hasAttribute("not-dismissible");
10424 return html`
10425 <span
10426 class="wpd-notice__icon dashicons ${icon ?? ""}"
10427 ?hidden=${!icon}
10428 aria-hidden="true"
10429 ></span>
10430 <span class="wpd-notice__label"><slot></slot></span>
10431 <button
10432 type="button"
10433 class="wpd-notice__close"
10434 ?hidden=${!dismissible}
10435 aria-label=${__("Dismiss notice")}
10436 @click=${(e) => this._onDismiss(e)}
10437 >
10438 <svg viewBox="0 0 14 14" aria-hidden="true">
10439 <path
10440 d="M3 3 L11 11 M11 3 L3 11"
10441 stroke="currentColor"
10442 stroke-width="1.6"
10443 stroke-linecap="round"
10444 fill="none"
10445 ></path>
10446 </svg>
10447 </button>
10448 `;
10449 }
10450 _onDismiss(e) {
10451 e.preventDefault();
10452 e.stopPropagation();
10453 this.dismiss();
10454 }
10455 };
10456 _WpdNotice.props = ["tone", "notDismissible", "icon", "noticeId"];
10457 _WpdNotice.styles = [styles$6];
10458 _WpdNotice.help = {
10459 title: "Notice",
10460 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.",
10461 status: "experimental",
10462 since: "0.8.6",
10463 props: [
10464 {
10465 name: "tone",
10466 type: '"info" | "success" | "warning" | "error" | "danger" | "neutral"',
10467 description: "Color palette. Defaults to `info`. `error` and `danger` are aliases."
10468 },
10469 {
10470 name: "not-dismissible",
10471 type: "boolean",
10472 description: "Suppress the trailing close button. Defaults to dismissible."
10473 },
10474 {
10475 name: "icon",
10476 type: "string",
10477 description: "Optional Dashicons class for a leading glyph (e.g. `dashicons-info`)."
10478 },
10479 {
10480 name: "notice-id",
10481 type: "string",
10482 description: "Persistence key. When set, the notice records its dismissed state in localStorage so it stays closed across reloads for the same user."
10483 }
10484 ],
10485 slots: [
10486 {
10487 name: "(default)",
10488 description: "Message HTML. Links, `<strong>`, `<em>`, and other inline formatting are allowed."
10489 }
10490 ],
10491 events: [
10492 {
10493 name: "wpd-notice-dismiss",
10494 description: "Fires after the user clicks the close button.",
10495 detail: "{ noticeId?: string }"
10496 }
10497 ],
10498 cssProps: [
10499 { name: "--wpd-notice-bg", description: "Background color." },
10500 { name: "--wpd-notice-accent", description: "Left-edge stripe + icon color." },
10501 { name: "--wpd-notice-color", description: "Text color." },
10502 { name: "--wpd-notice-border", description: "Bottom border color." },
10503 { name: "--wpd-notice-link", description: "Color for slotted <a> elements." }
10504 ],
10505 example: html`
10506 <wpd-notice tone="warning" notice-id="docs/example">
10507 Heads up — this is a demo notice.
10508 <a href="#">Learn more</a>.
10509 </wpd-notice>
10510 `
10511 };
10512 let WpdNotice = _WpdNotice;
10513 defineComponent("wpd-notice", WpdNotice);
10514 const store$5 = createSharedStore(
10515 "desktop-mode/window-notices",
10516 () => ({ entries: /* @__PURE__ */ new Map() })
10517 );
10518 const ID_PATTERN = /^[a-z0-9_/-]+$/;
10519 function slotIdFor(id) {
10520 return `desktop-mode-notice/${id.toLowerCase()}`;
10521 }
10522 function buildNoticeElement(entry) {
10523 const el = document.createElement("wpd-notice");
10524 el.setAttribute("tone", entry.tone ?? "info");
10525 el.setAttribute("notice-id", entry.id);
10526 if (entry.dismissible === false) {
10527 el.setAttribute("not-dismissible", "");
10528 }
10529 if (entry.icon) {
10530 el.setAttribute("icon", entry.icon);
10531 }
10532 el.innerHTML = entry.message;
10533 return el;
10534 }
10535 function registerWindowNotice(entry) {
10536 if (!entry || typeof entry !== "object") {
10537 return () => {
10538 };
10539 }
10540 const id = String(entry.id ?? "").trim().toLowerCase();
10541 if (!id || !ID_PATTERN.test(id)) {
10542 return () => {
10543 };
10544 }
10545 if (typeof entry.message !== "string" || entry.message === "") {
10546 return () => {
10547 };
10548 }
10549 const normalised = { ...entry, id };
10550 store$5.state.entries.set(id, normalised);
10551 const slotId = slotIdFor(id);
10552 registerWindowSlot({
10553 id: slotId,
10554 slot: "after-titlebar",
10555 order: normalised.order ?? 100,
10556 // Append rather than clear — every notice slot entry appends
10557 // its own `<wpd-notice>` so multiple notices stack.
10558 replace: false,
10559 owner: normalised.owner,
10560 match: (win) => {
10561 const def = store$5.state.entries.get(id);
10562 if (!def) {
10563 return false;
10564 }
10565 if (typeof def.match !== "function") {
10566 return true;
10567 }
10568 try {
10569 return def.match(win) === true;
10570 } catch {
10571 return false;
10572 }
10573 },
10574 render: (host) => {
10575 const def = store$5.state.entries.get(id);
10576 if (!def) {
10577 return;
10578 }
10579 host.appendChild(buildNoticeElement(def));
10580 }
10581 });
10582 return () => unregisterWindowNotice(id);
10583 }
10584 function unregisterWindowNotice(id) {
10585 const key = String(id ?? "").trim().toLowerCase();
10586 if (!key) {
10587 return;
10588 }
10589 if (store$5.state.entries.delete(key)) {
10590 unregisterWindowSlot(slotIdFor(key));
10591 }
10592 }
10593 function listWindowNotices() {
10594 return Array.from(store$5.state.entries.values()).sort((a, b) => {
10595 const oa = a.order ?? 100;
10596 const ob = b.order ?? 100;
10597 if (oa !== ob) {
10598 return oa - ob;
10599 }
10600 return a.id.localeCompare(b.id);
10601 });
10602 }
10603 function dismissWindowNotice(id) {
10604 const key = String(id ?? "").trim().toLowerCase();
10605 if (!key) {
10606 return;
10607 }
10608 markNoticeDismissed(key);
10609 }
10610 function undismissWindowNotice(id) {
10611 const key = String(id ?? "").trim().toLowerCase();
10612 if (!key) {
10613 return;
10614 }
10615 clearNoticeDismissed(key);
10616 }
10617 function buildMatcher(match) {
10618 if (!match) {
10619 return void 0;
10620 }
10621 const ids = /* @__PURE__ */ new Set();
10622 if (typeof match.window === "string" && match.window !== "") {
10623 ids.add(match.window);
10624 }
10625 if (Array.isArray(match.windows)) {
10626 for (const id of match.windows) {
10627 if (typeof id === "string" && id !== "") {
10628 ids.add(id);
10629 }
10630 }
10631 }
10632 const needle = typeof match.urlContains === "string" && match.urlContains !== "" ? match.urlContains.toLowerCase() : null;
10633 if (ids.size === 0 && needle === null) {
10634 return void 0;
10635 }
10636 return (w) => {
10637 if (ids.size > 0 && !ids.has(w.id)) {
10638 return false;
10639 }
10640 if (needle !== null) {
10641 const url = typeof w.config.url === "string" ? w.config.url.toLowerCase() : "";
10642 if (!url.includes(needle)) {
10643 return false;
10644 }
10645 }
10646 return true;
10647 };
10648 }
10649 function applyServerWindowNotices(entries) {
10650 const wanted = /* @__PURE__ */ new Set();
10651 for (const entry of entries) {
10652 if (!entry || typeof entry.id !== "string" || !entry.id) {
10653 continue;
10654 }
10655 wanted.add(entry.id.toLowerCase());
10656 registerWindowNotice({
10657 id: entry.id,
10658 message: entry.message,
10659 tone: entry.tone,
10660 dismissible: entry.dismissible !== false,
10661 icon: entry.icon,
10662 match: buildMatcher(entry.match),
10663 order: typeof entry.order === "number" ? entry.order : void 0,
10664 // `owner` tag marks every server-shipped notice so a
10665 // targeted cleanup is trivial if/when we surface a sweep
10666 // helper later. Matches the convention used by the
10667 // command / settings-tab sync modules.
10668 owner: "__server__"
10669 });
10670 }
10671 for (const existing of listWindowNotices()) {
10672 if (existing.owner !== "__server__") {
10673 continue;
10674 }
10675 if (!wanted.has(existing.id)) {
10676 unregisterWindowNotice(existing.id);
10677 }
10678 }
10679 }
10680 const store$4 = createSharedStore(
10681 "desktop-mode/window-chrome-registry",
10682 () => ({ registry: /* @__PURE__ */ new Map(), listeners: /* @__PURE__ */ new Set() })
10683 );
10684 const registry = store$4.state.registry;
10685 const listeners$3 = store$4.state.listeners;
10686 const WINDOW_CHROME_ID = /^[a-z0-9_/-]+$/;
10687 function registerWindowChrome(def) {
10688 const errors = [];
10689 if (!def || typeof def !== "object") {
10690 errors.push("def (not an object)");
10691 } else {
10692 if (typeof def.id !== "string" || def.id.trim() === "") {
10693 errors.push("id (missing)");
10694 } else if (!WINDOW_CHROME_ID.test(def.id.trim().toLowerCase())) {
10695 errors.push(
10696 `id (must match ${WINDOW_CHROME_ID} — lowercase alphanum, hyphens, underscores, slashes for vendor/sub-id)`
10697 );
10698 }
10699 if (typeof def.match !== "function") {
10700 errors.push("match (must be a function)");
10701 }
10702 if (typeof def.render !== "function") {
10703 errors.push("render (must be a function)");
10704 }
10705 }
10706 throwOnRegistrationErrors("WindowChrome", errors, def);
10707 const id = def.id.trim().toLowerCase();
10708 registry.set(id, { ...def, id });
10709 notify$5();
10710 }
10711 function unregisterWindowChrome(id) {
10712 if (registry.delete(id.toLowerCase())) {
10713 notify$5();
10714 }
10715 }
10716 function unregisterWindowChromesByOwner(owner) {
10717 if (!owner) {
10718 return 0;
10719 }
10720 let removed = 0;
10721 for (const [id, def] of Array.from(registry.entries())) {
10722 if (def.owner === owner) {
10723 registry.delete(id);
10724 removed++;
10725 }
10726 }
10727 if (removed > 0) {
10728 notify$5();
10729 }
10730 return removed;
10731 }
10732 function listWindowChromes() {
10733 return Array.from(registry.values()).sort(
10734 (a, b) => a.id.localeCompare(b.id)
10735 );
10736 }
10737 function notify$5() {
10738 const snapshot = Array.from(listeners$3);
10739 for (const cb of snapshot) {
10740 try {
10741 cb();
10742 } catch (err) {
10743 if (typeof console !== "undefined") {
10744 console.error(
10745 "[desktop-mode] window-chrome registry listener threw:",
10746 err
10747 );
10748 }
10749 }
10750 }
10751 }
10752 function createWindowChromeRegistrySync() {
10753 const loadedHandles = /* @__PURE__ */ new Set();
10754 const loadedUrls = /* @__PURE__ */ new Set();
10755 let prevIdsByHandle = /* @__PURE__ */ new Map();
10756 const ensureScript = async (entry) => {
10757 if (!entry.scriptUrl || loadedUrls.has(entry.scriptUrl)) {
10758 loadedHandles.add(entry.handle);
10759 return;
10760 }
10761 try {
10762 await loadVendorScript(entry.scriptUrl, {
10763 translations: entry.scriptTranslations,
10764 l10n: entry.scriptL10n,
10765 before: entry.scriptBefore,
10766 after: entry.scriptAfter
10767 });
10768 } catch (err) {
10769 doAction(HOOKS.SHELL_ERROR, {
10770 scope: "window-chrome-script-load",
10771 handle: entry.handle,
10772 url: entry.scriptUrl,
10773 error: err
10774 });
10775 return;
10776 }
10777 loadedUrls.add(entry.scriptUrl);
10778 loadedHandles.add(entry.handle);
10779 };
10780 const idsByHandleFrom = (chromes) => {
10781 const map = /* @__PURE__ */ new Map();
10782 if (!chromes) {
10783 return map;
10784 }
10785 for (const entry of chromes) {
10786 if (!entry.scriptHandle || !entry.id) {
10787 continue;
10788 }
10789 let set = map.get(entry.scriptHandle);
10790 if (!set) {
10791 set = /* @__PURE__ */ new Set();
10792 map.set(entry.scriptHandle, set);
10793 }
10794 set.add(entry.id);
10795 }
10796 return map;
10797 };
10798 const collectIdsToRemove = (handle) => {
10799 const ids = /* @__PURE__ */ new Set();
10800 for (const def of listWindowChromes()) {
10801 if (def.owner === handle) {
10802 ids.add(def.id);
10803 }
10804 }
10805 const declared = prevIdsByHandle.get(handle);
10806 if (declared) {
10807 for (const id of declared) {
10808 ids.add(id);
10809 }
10810 }
10811 return ids;
10812 };
10813 return async (scripts, chromes) => {
10814 const incomingHandles = /* @__PURE__ */ new Set();
10815 for (const entry of scripts) {
10816 if (entry.handle) {
10817 incomingHandles.add(entry.handle);
10818 }
10819 }
10820 for (const handle of Array.from(loadedHandles)) {
10821 if (incomingHandles.has(handle)) {
10822 continue;
10823 }
10824 for (const id of collectIdsToRemove(handle)) {
10825 unregisterWindowChrome(id);
10826 }
10827 unregisterWindowChromesByOwner(handle);
10828 loadedHandles.delete(handle);
10829 }
10830 for (const entry of scripts) {
10831 if (!entry.handle || loadedHandles.has(entry.handle)) {
10832 continue;
10833 }
10834 await ensureScript(entry);
10835 }
10836 prevIdsByHandle = idsByHandleFrom(chromes);
10837 };
10838 }
10839 const INITIAL_ORIGIN$2 = window.location.origin;
10840 let _connSeq = 0;
10841 const _connections = /* @__PURE__ */ new Map();
10842 const _connectionsByTarget = /* @__PURE__ */ new Map();
10843 const _syntheticIframes = /* @__PURE__ */ new Map();
10844 function registerSyntheticIframe(windowId, iframe) {
10845 _syntheticIframes.set(windowId, iframe);
10846 return () => {
10847 if (_syntheticIframes.get(windowId) === iframe) {
10848 _syntheticIframes.delete(windowId);
10849 }
10850 };
10851 }
10852 function nextId() {
10853 return `desktop-mode-conn-${++_connSeq}`;
10854 }
10855 function createConnectionBridge(manager) {
10856 const sendToIframe = (win, message) => {
10857 try {
10858 win.contentWindow?.postMessage(message, INITIAL_ORIGIN$2);
10859 } catch (err) {
10860 if (typeof console !== "undefined") {
10861 console.error(
10862 "[desktop-mode] connection: postMessage failed",
10863 err
10864 );
10865 }
10866 }
10867 };
10868 const connect = (targetWindowId, opts = {}) => {
10869 const id = nextId();
10870 const topics = Array.isArray(opts.topics) ? [...opts.topics] : [];
10871 const subs = /* @__PURE__ */ new Map();
10872 const queue = [];
10873 let isOpen = false;
10874 let destroyed = false;
10875 const targetIframe = () => {
10876 const synth = _syntheticIframes.get(targetWindowId);
10877 if (synth) {
10878 return synth;
10879 }
10880 const w = manager.getById(targetWindowId);
10881 return w?.iframe ?? null;
10882 };
10883 const isNativeTarget = () => {
10884 if (targetIframe()) {
10885 return false;
10886 }
10887 const w = manager.getById(targetWindowId);
10888 return !!w && w.config?.native === true;
10889 };
10890 const nativeSubUnsubs = [];
10891 const flushQueue = () => {
10892 const iframe2 = targetIframe();
10893 if (!iframe2) {
10894 return;
10895 }
10896 while (queue.length) {
10897 const msg = queue.shift();
10898 sendToIframe(iframe2, {
10899 type: "desktop-mode-bridge-publish",
10900 connectionId: id,
10901 topic: msg.topic,
10902 payload: msg.payload
10903 });
10904 }
10905 };
10906 const conn = {
10907 id,
10908 target: targetWindowId,
10909 isOpen: () => isOpen,
10910 subscribe(topic, cb) {
10911 const wrapped = cb;
10912 if (isNativeTarget()) {
10913 const off = addParentSubscriber(
10914 targetWindowId,
10915 topic,
10916 (payload, meta) => {
10917 doAction(HOOKS.CONNECTION_MESSAGE, {
10918 connectionId: id,
10919 topic: meta.channel,
10920 direction: "in"
10921 });
10922 try {
10923 wrapped(payload, { topic: meta.channel });
10924 } catch (err) {
10925 if (typeof console !== "undefined") {
10926 console.error(
10927 "[desktop-mode] connection subscriber threw:",
10928 err
10929 );
10930 }
10931 }
10932 }
10933 );
10934 nativeSubUnsubs.push(off);
10935 return off;
10936 }
10937 let bucket22 = subs.get(topic);
10938 if (!bucket22) {
10939 bucket22 = /* @__PURE__ */ new Set();
10940 subs.set(topic, bucket22);
10941 }
10942 bucket22.add(wrapped);
10943 return () => {
10944 bucket22?.delete(wrapped);
10945 };
10946 },
10947 send(topic, payload) {
10948 if (destroyed) {
10949 return;
10950 }
10951 doAction(HOOKS.CONNECTION_MESSAGE, {
10952 connectionId: id,
10953 topic,
10954 direction: "out"
10955 });
10956 if (isNativeTarget()) {
10957 dispatchToNative(targetWindowId, topic, payload);
10958 return;
10959 }
10960 if (!isOpen) {
10961 queue.push({ topic, payload });
10962 return;
10963 }
10964 const iframe2 = targetIframe();
10965 if (!iframe2) {
10966 return;
10967 }
10968 sendToIframe(iframe2, {
10969 type: "desktop-mode-bridge-publish",
10970 connectionId: id,
10971 topic,
10972 payload
10973 });
10974 },
10975 disconnect() {
10976 conn._destroy("disconnect");
10977 },
10978 _targetWindow: targetIframe,
10979 _handleIframeMessage(data) {
10980 if (!data || typeof data !== "object") {
10981 return;
10982 }
10983 const msg = data;
10984 if (msg.type === "desktop-mode-bridge-handshake-ack") {
10985 if (isOpen) {
10986 return;
10987 }
10988 isOpen = true;
10989 doAction(HOOKS.CONNECTION_OPENED, {
10990 connectionId: id,
10991 targetWindowId,
10992 topics,
10993 // Ship the live Connection alongside the id so
10994 // iframe-initiated connections can be subscribed
10995 // to directly from the hook handler — without
10996 // `wp.desktop.getConnection(id)` plumbing the
10997 // payload would carry the id but no way to call
10998 // `.subscribe()` against it.
10999 connection: conn
11000 });
11001 try {
11002 opts.onOpen?.();
11003 } catch (err) {
11004 if (typeof console !== "undefined") {
11005 console.error(
11006 "[desktop-mode] connection.onOpen threw:",
11007 err
11008 );
11009 }
11010 }
11011 flushQueue();
11012 return;
11013 }
11014 if (msg.type === "desktop-mode-bridge-publish") {
11015 const m = data;
11016 const topic = typeof m.topic === "string" ? m.topic : "";
11017 if (!topic) {
11018 return;
11019 }
11020 doAction(HOOKS.CONNECTION_MESSAGE, {
11021 connectionId: id,
11022 topic,
11023 direction: "in"
11024 });
11025 const exact = subs.get(topic);
11026 if (exact) {
11027 for (const cb of Array.from(exact)) {
11028 try {
11029 cb(m.payload, { topic });
11030 } catch (err) {
11031 if (typeof console !== "undefined") {
11032 console.error(
11033 "[desktop-mode] connection subscriber threw:",
11034 err
11035 );
11036 }
11037 }
11038 }
11039 }
11040 const wildcard = subs.get("*");
11041 if (wildcard) {
11042 for (const cb of Array.from(wildcard)) {
11043 try {
11044 cb(m.payload, { topic });
11045 } catch (err) {
11046 if (typeof console !== "undefined") {
11047 console.error(
11048 "[desktop-mode] connection wildcard subscriber threw:",
11049 err
11050 );
11051 }
11052 }
11053 }
11054 }
11055 return;
11056 }
11057 if (msg.type === "desktop-mode-bridge-disconnect") {
11058 conn._destroy("disconnect");
11059 }
11060 },
11061 _destroy(reason) {
11062 if (destroyed) {
11063 return;
11064 }
11065 destroyed = true;
11066 const wasOpen = isOpen;
11067 isOpen = false;
11068 _connections.delete(id);
11069 const targetSet = _connectionsByTarget.get(targetWindowId);
11070 if (targetSet) {
11071 targetSet.delete(id);
11072 if (targetSet.size === 0) {
11073 _connectionsByTarget.delete(targetWindowId);
11074 }
11075 }
11076 for (const off of nativeSubUnsubs.splice(0)) {
11077 try {
11078 off();
11079 } catch {
11080 }
11081 }
11082 if (wasOpen) {
11083 const iframe2 = targetIframe();
11084 if (iframe2) {
11085 sendToIframe(iframe2, {
11086 type: "desktop-mode-bridge-disconnect",
11087 connectionId: id
11088 });
11089 }
11090 }
11091 doAction(HOOKS.CONNECTION_CLOSED, {
11092 connectionId: id,
11093 reason
11094 });
11095 try {
11096 opts.onClose?.(reason);
11097 } catch (err) {
11098 if (typeof console !== "undefined") {
11099 console.error(
11100 "[desktop-mode] connection.onClose threw:",
11101 err
11102 );
11103 }
11104 }
11105 }
11106 };
11107 _connections.set(id, conn);
11108 let bucket2 = _connectionsByTarget.get(targetWindowId);
11109 if (!bucket2) {
11110 bucket2 = /* @__PURE__ */ new Set();
11111 _connectionsByTarget.set(targetWindowId, bucket2);
11112 }
11113 bucket2.add(id);
11114 if (isNativeTarget()) {
11115 Promise.resolve().then(() => {
11116 if (destroyed || isOpen) {
11117 return;
11118 }
11119 isOpen = true;
11120 doAction(HOOKS.CONNECTION_OPENED, {
11121 connectionId: id,
11122 targetWindowId,
11123 topics
11124 });
11125 try {
11126 opts.onOpen?.();
11127 } catch (err) {
11128 if (typeof console !== "undefined") {
11129 console.error(
11130 "[desktop-mode] connection.onOpen threw:",
11131 err
11132 );
11133 }
11134 }
11135 });
11136 return conn;
11137 }
11138 const iframe = targetIframe();
11139 if (iframe) {
11140 sendToIframe(iframe, {
11141 type: "desktop-mode-bridge-handshake",
11142 connectionId: id,
11143 targetWindowId,
11144 topics
11145 });
11146 }
11147 return conn;
11148 };
11149 const routeIncomingFromIframe = (data, windowId) => {
11150 if (!data || typeof data !== "object") {
11151 return;
11152 }
11153 const msg = data;
11154 if (typeof msg.type !== "string" || !msg.type.startsWith("desktop-mode-bridge-")) {
11155 return;
11156 }
11157 if (msg.type === "desktop-mode-bridge-connection-request" && typeof msg.requestId === "string" && typeof windowId === "string" && windowId !== "") {
11158 handleConnectionRequest(windowId, msg.requestId, Array.isArray(msg.topics) ? msg.topics : []);
11159 return;
11160 }
11161 if (typeof msg.connectionId !== "string") {
11162 return;
11163 }
11164 const conn = _connections.get(msg.connectionId);
11165 conn?._handleIframeMessage(data);
11166 };
11167 const handleConnectionRequest = (windowId, requestId, topics) => {
11168 const synth = _syntheticIframes.get(windowId);
11169 const iframe = synth ?? manager.getById(windowId)?.iframe ?? null;
11170 if (!iframe) {
11171 return;
11172 }
11173 const decision = applyFilters(
11174 HOOKS.IFRAME_CONNECTION_REQUEST,
11175 true,
11176 { windowId, requestId, topics: topics.slice() }
11177 );
11178 if (decision === false) {
11179 try {
11180 iframe.contentWindow?.postMessage({
11181 type: "desktop-mode-bridge-connection-ack",
11182 requestId,
11183 accepted: false,
11184 reason: "rejected"
11185 }, INITIAL_ORIGIN$2);
11186 } catch {
11187 }
11188 return;
11189 }
11190 const finalTopics = decision && typeof decision === "object" && Array.isArray(decision.topics) ? decision.topics : topics;
11191 const conn = connect(windowId, { topics: finalTopics });
11192 try {
11193 iframe.contentWindow?.postMessage({
11194 type: "desktop-mode-bridge-connection-ack",
11195 requestId,
11196 accepted: true,
11197 connectionId: conn.id
11198 }, INITIAL_ORIGIN$2);
11199 } catch {
11200 }
11201 };
11202 const onIframeReady = (windowId) => {
11203 const bucket2 = _connectionsByTarget.get(windowId);
11204 if (!bucket2) {
11205 return;
11206 }
11207 for (const connId of Array.from(bucket2)) {
11208 const conn = _connections.get(connId);
11209 if (!conn || conn.isOpen()) {
11210 continue;
11211 }
11212 const iframe = conn._targetWindow();
11213 if (!iframe) {
11214 continue;
11215 }
11216 sendToIframe(iframe, {
11217 type: "desktop-mode-bridge-handshake",
11218 connectionId: conn.id,
11219 targetWindowId: conn.target,
11220 topics: []
11221 // already negotiated client-side; iframe re-uses
11222 });
11223 }
11224 };
11225 const onWindowClosed = (windowId) => {
11226 const bucket2 = _connectionsByTarget.get(windowId);
11227 if (!bucket2) {
11228 return;
11229 }
11230 for (const connId of Array.from(bucket2)) {
11231 const conn = _connections.get(connId);
11232 conn?._destroy("window-closed");
11233 }
11234 };
11235 const getConnection = (connectionId) => {
11236 const conn = _connections.get(connectionId);
11237 return conn ?? null;
11238 };
11239 return {
11240 connect,
11241 getConnection,
11242 routeIncomingFromIframe,
11243 onIframeReady,
11244 onWindowClosed
11245 };
11246 }
11247 const __vite_import_meta_env__ = {};
11248 function devLog(...args) {
11249 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;
11250 if (mode !== "production") {
11251 console.log(...args);
11252 }
11253 }
11254 const OWNER_PREFIX = "iframe:";
11255 function ownerFor(windowId) {
11256 return OWNER_PREFIX + windowId;
11257 }
11258 function iconFor(harvested) {
11259 if (harvested.icon && typeof harvested.icon === "string" && harvested.icon.startsWith("dashicons-")) {
11260 return harvested.icon;
11261 }
11262 return harvested.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
11263 }
11264 function slugFor(windowId, name) {
11265 const safeName = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
11266 const safeWin = windowId.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
11267 return `win-${safeWin}-${safeName}`;
11268 }
11269 class IframeCommandBridge {
11270 constructor(opts) {
11271 this.subscribedWindowId = null;
11272 this.manager = opts.manager;
11273 this.adminUrl = opts.adminUrl;
11274 }
11275 /** Wire up the focus / close / message listeners. Idempotent. */
11276 install() {
11277 document.addEventListener("desktop-mode-window-focused", (e) => {
11278 const detail = e.detail;
11279 if (detail && typeof detail.windowId === "string") {
11280 this.onFocused(detail.windowId);
11281 }
11282 });
11283 document.addEventListener("desktop-mode-window-closed", (e) => {
11284 const detail = e.detail;
11285 if (detail && typeof detail.windowId === "string") {
11286 unregisterByOwner(ownerFor(detail.windowId));
11287 if (this.subscribedWindowId === detail.windowId) {
11288 this.subscribedWindowId = null;
11289 }
11290 }
11291 });
11292 document.addEventListener("desktop-mode-window-changed", (e) => {
11293 const detail = e.detail;
11294 if (!detail || typeof detail.windowId !== "string") {
11295 return;
11296 }
11297 if (detail.reason !== "state") {
11298 return;
11299 }
11300 if (detail.state !== "minimized") {
11301 return;
11302 }
11303 if (this.subscribedWindowId === detail.windowId) {
11304 this.subscribedWindowId = null;
11305 }
11306 });
11307 window.addEventListener("message", (e) => {
11308 if (e.origin !== window.location.origin) {
11309 return;
11310 }
11311 const data = e.data;
11312 if (!data || typeof data.type !== "string") {
11313 return;
11314 }
11315 if (data.type === "desktop-mode-bridge-ready") {
11316 const win2 = this.manager.findByIframeSource(e.source);
11317 if (win2 && win2.id === this.subscribedWindowId) {
11318 this.sendSubscribe(win2.id);
11319 }
11320 return;
11321 }
11322 if (data.type !== "desktop-mode-commands-list") {
11323 return;
11324 }
11325 if (!Array.isArray(data.commands)) {
11326 return;
11327 }
11328 const win = this.manager.findByIframeSource(e.source);
11329 if (!win) {
11330 return;
11331 }
11332 if (win.id !== this.subscribedWindowId) {
11333 return;
11334 }
11335 this.applyList(win.id, data.commands);
11336 });
11337 const focused = this.manager.getFocused();
11338 if (focused) {
11339 this.onFocused(focused.id);
11340 }
11341 }
11342 onFocused(windowId) {
11343 if (this.subscribedWindowId === windowId) {
11344 return;
11345 }
11346 if (this.subscribedWindowId) {
11347 const prev = this.manager.getById(this.subscribedWindowId);
11348 if (prev && prev.iframe && prev.iframe.contentWindow) {
11349 try {
11350 prev.iframe.contentWindow.postMessage(
11351 { type: "desktop-mode-commands-unsubscribe" },
11352 window.location.origin
11353 );
11354 } catch {
11355 }
11356 }
11357 unregisterByOwner(ownerFor(this.subscribedWindowId));
11358 }
11359 this.subscribedWindowId = windowId;
11360 this.sendSubscribe(windowId);
11361 }
11362 sendSubscribe(windowId) {
11363 const win = this.manager.getById(windowId);
11364 if (!win) {
11365 return;
11366 }
11367 if (!win.iframe) {
11368 return;
11369 }
11370 if (!win.iframe.contentWindow) {
11371 return;
11372 }
11373 try {
11374 win.iframe.contentWindow.postMessage(
11375 { type: "desktop-mode-commands-subscribe" },
11376 window.location.origin
11377 );
11378 } catch (err) {
11379 devLog("[wpd-cmd:parent] sendSubscribe: postMessage threw", err);
11380 }
11381 }
11382 applyList(windowId, commands) {
11383 const owner = ownerFor(windowId);
11384 unregisterByOwner(owner);
11385 for (const cmd of commands) {
11386 if (!cmd || !cmd.name || !cmd.label) {
11387 continue;
11388 }
11389 const slug = slugFor(windowId, cmd.name);
11390 const safeSvg = typeof cmd.iconSvg === "string" && cmd.iconSvg !== "" ? sanitizeIconSvg(cmd.iconSvg) : "";
11391 const def = {
11392 slug,
11393 label: cmd.label,
11394 icon: iconFor(cmd),
11395 iconSvg: safeSvg !== "" ? safeSvg : void 0,
11396 owner,
11397 // Harvested commands are contextual by construction —
11398 // they come from whichever window has focus. Surface
11399 // them eagerly so the user sees "Duplicate block" /
11400 // "Toggle distraction free" without having to type `/`
11401 // first.
11402 eager: true,
11403 run: cmd.kind === "navigate" && cmd.url ? this.runNavigate(cmd.url, cmd.label, iconFor(cmd)) : this.runProxy(windowId, cmd.name)
11404 };
11405 try {
11406 registerCommand(def);
11407 } catch (err) {
11408 console.error(
11409 "[desktop-mode] iframe-bridge: dropping bad command",
11410 def,
11411 err
11412 );
11413 }
11414 }
11415 }
11416 runNavigate(url, title, icon) {
11417 return (_args, ctx) => {
11418 ctx.close();
11419 if (tryNativeUrlRemap(url)) {
11420 return;
11421 }
11422 const id = deriveWindowId(url, this.adminUrl);
11423 this.manager.open({ id, baseId: id, url, title, icon });
11424 };
11425 }
11426 runProxy(windowId, name) {
11427 return (_args, ctx) => {
11428 ctx.close();
11429 const win = this.manager.getById(windowId);
11430 if (!win || !win.iframe || !win.iframe.contentWindow) {
11431 return;
11432 }
11433 try {
11434 win.iframe.contentWindow.postMessage(
11435 { type: "desktop-mode-commands-invoke", name },
11436 window.location.origin
11437 );
11438 } catch {
11439 }
11440 this.manager.focus(win);
11441 };
11442 }
11443 }
11444 const OWNER = "global";
11445 const NAV_HREF_LITERAL_RE = /(?:document\.location\.href|window\.location\.href|location\.href)\s*=\s*['"]([^'"$]+?)['"]/;
11446 const NAV_ASSIGN_LITERAL_RE = /(?:document\.location|window\.location|location)\s*=\s*['"]([^'"$]+?)['"]/;
11447 const NAV_CALL_LITERAL_RE = /location\.(?:assign|replace)\s*\(\s*['"]([^'"$]+?)['"]\s*\)/;
11448 const NAV_INTENT_RE = /(?:document\.location|window\.location|location)\s*(?:\.href\s*)?=|location\.(?:assign|replace)\s*\(/;
11449 const SITE_EDITOR_INTENT_RE = /getSiteEditorPage\s*\(|site-editor\.php/;
11450 const SITE_EDITOR_NAME_RE = /^(wp_template_part|wp_template|wp_navigation|wp_block)-(.+)$/;
11451 function lookupMenuCommand(name) {
11452 const list2 = window.__desktopModeMenuCommands;
11453 if (!Array.isArray(list2)) {
11454 return null;
11455 }
11456 for (const entry of list2) {
11457 if (entry && typeof entry === "object" && entry.name === name && typeof entry.url === "string" && entry.url !== "") {
11458 return {
11459 label: typeof entry.label === "string" ? entry.label : "",
11460 url: entry.url
11461 };
11462 }
11463 }
11464 return null;
11465 }
11466 class ShellCommandHarvester {
11467 constructor(opts) {
11468 this.mounted = false;
11469 this.host = null;
11470 this.root = null;
11471 this.kindCache = /* @__PURE__ */ Object.create(null);
11472 this.callbackCache = /* @__PURE__ */ Object.create(null);
11473 this.lastFingerprint = "";
11474 this.manager = opts.manager;
11475 this.adminUrl = opts.adminUrl;
11476 }
11477 /** Mount the harvester. Idempotent. Safe to call before `wp.data` loads. */
11478 install() {
11479 this.tryMount(0);
11480 }
11481 tryMount(attempt) {
11482 if (this.mounted) {
11483 return;
11484 }
11485 const wp = window.wp;
11486 if (!wp || !wp.data || !wp.element || typeof wp.data.subscribe !== "function") {
11487 if (attempt < 40) {
11488 window.setTimeout(() => this.tryMount(attempt + 1), 150);
11489 }
11490 return;
11491 }
11492 this.mount();
11493 }
11494 mount() {
11495 const wp = window.wp;
11496 const el = wp.element;
11497 const data = wp.data;
11498 const createEl = el.createElement;
11499 const useEffect = el.useEffect;
11500 const useRef = el.useRef;
11501 const useMemo = el.useMemo;
11502 const useSelect = data.useSelect;
11503 if (typeof createEl !== "function" || typeof useEffect !== "function" || typeof useRef !== "function" || typeof useMemo !== "function" || typeof useSelect !== "function" || typeof el.createRoot !== "function") {
11504 return;
11505 }
11506 this.mounted = true;
11507 const host = document.createElement("div");
11508 host.setAttribute("aria-hidden", "true");
11509 host.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;left:-9999px;top:-9999px;";
11510 (document.body || document.documentElement).appendChild(host);
11511 this.host = host;
11512 const bucket2 = {
11513 perLoader: {},
11514 statics: [],
11515 loadersList: []
11516 };
11517 const fingerprint2 = (cmds) => {
11518 if (!Array.isArray(cmds) || cmds.length === 0) {
11519 return "";
11520 }
11521 const keys = new Array(cmds.length);
11522 for (let i = 0; i < cmds.length; i++) {
11523 const c = cmds[i];
11524 keys[i] = c && c.name ? c.name : "";
11525 }
11526 return keys.join("|");
11527 };
11528 const mergeAndPublish = () => {
11529 let merged = [];
11530 for (const name of bucket2.loadersList) {
11531 const slice = bucket2.perLoader[name];
11532 if (Array.isArray(slice)) {
11533 merged = merged.concat(slice);
11534 }
11535 }
11536 if (Array.isArray(bucket2.statics)) {
11537 merged = merged.concat(bucket2.statics);
11538 }
11539 this.callbackCache = /* @__PURE__ */ Object.create(null);
11540 for (const cc of merged) {
11541 if (cc && cc.name && typeof cc.callback === "function") {
11542 this.callbackCache[cc.name] = cc.callback;
11543 }
11544 }
11545 this.publish(merged);
11546 };
11547 const LoaderSlot = (props) => {
11548 const loader = props.loader;
11549 let result = null;
11550 try {
11551 result = loader.hook({ search: "" });
11552 } catch {
11553 }
11554 const cmds = result && Array.isArray(result.commands) ? result.commands : [];
11555 const key = useMemo(() => fingerprint2(cmds), [cmds]);
11556 useEffect(() => {
11557 bucket2.perLoader[loader.name] = cmds;
11558 mergeAndPublish();
11559 }, [key]);
11560 useEffect(() => {
11561 return () => {
11562 delete bucket2.perLoader[loader.name];
11563 mergeAndPublish();
11564 };
11565 }, []);
11566 return null;
11567 };
11568 const Harvester = () => {
11569 const loaders = useSelect((s) => {
11570 const ss = s("core/commands");
11571 if (!ss || typeof ss.getCommandLoaders !== "function") {
11572 return [];
11573 }
11574 return [
11575 ...ss.getCommandLoaders(false) || [],
11576 ...ss.getCommandLoaders(true) || []
11577 ];
11578 }, []);
11579 const staticCmds = useSelect((s) => {
11580 const ss = s("core/commands");
11581 if (!ss || typeof ss.getCommands !== "function") {
11582 return [];
11583 }
11584 return [
11585 ...ss.getCommands(false) || [],
11586 ...ss.getCommands(true) || []
11587 ];
11588 }, []);
11589 const loadersNames = useMemo(() => {
11590 return Array.isArray(loaders) ? loaders.map((l) => l ? l.name || "" : "") : [];
11591 }, [loaders]);
11592 const loadersKey = loadersNames.join("|");
11593 useEffect(() => {
11594 bucket2.loadersList = loadersNames;
11595 mergeAndPublish();
11596 }, [loadersKey]);
11597 const staticKey = useMemo(
11598 () => fingerprint2(Array.isArray(staticCmds) ? staticCmds : []),
11599 [staticCmds]
11600 );
11601 useEffect(() => {
11602 bucket2.statics = Array.isArray(staticCmds) ? staticCmds : [];
11603 mergeAndPublish();
11604 }, [staticKey]);
11605 if (!Array.isArray(loaders) || loaders.length === 0) {
11606 return null;
11607 }
11608 const children = [];
11609 for (const loader of loaders) {
11610 if (!loader || typeof loader.hook !== "function") {
11611 continue;
11612 }
11613 children.push(
11614 createEl(LoaderSlot, { key: loader.name, loader })
11615 );
11616 }
11617 return createEl(el.Fragment || "div", null, children);
11618 };
11619 try {
11620 this.root = el.createRoot(host);
11621 this.root.render(createEl(Harvester));
11622 } catch {
11623 this.mounted = false;
11624 this.root = null;
11625 if (this.host && this.host.parentNode) {
11626 this.host.parentNode.removeChild(this.host);
11627 }
11628 this.host = null;
11629 }
11630 }
11631 publish(raw) {
11632 const seen = /* @__PURE__ */ Object.create(null);
11633 const classified = [];
11634 for (const cmd of raw) {
11635 if (!cmd || !cmd.name || !cmd.label) {
11636 continue;
11637 }
11638 if (cmd.disabled) {
11639 continue;
11640 }
11641 if (seen[cmd.name]) {
11642 continue;
11643 }
11644 seen[cmd.name] = true;
11645 classified.push(this.classify(cmd));
11646 }
11647 let key = "";
11648 for (const c of classified) {
11649 key += `${c.name}|${c.kind}|${c.url || ""}
11650 `;
11651 }
11652 if (key === this.lastFingerprint) {
11653 return;
11654 }
11655 this.lastFingerprint = key;
11656 unregisterByOwner(OWNER);
11657 for (const c of classified) {
11658 if (c.kind === "skip") {
11659 continue;
11660 }
11661 const slug = `global-${c.name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-")}`;
11662 const icon = this.iconFor(c);
11663 const def = {
11664 slug,
11665 label: c.label,
11666 icon,
11667 iconSvg: c.iconSvg && c.iconSvg !== "" ? sanitizeIconSvg(c.iconSvg) : void 0,
11668 owner: OWNER,
11669 // NOT eager. The palette splits the registry into two
11670 // disjoint surfaces: `eager` commands show on empty
11671 // input (and are excluded from slash search at
11672 // `src/ai-assistant/impl.ts:494`); non-eager commands
11673 // show when the user types `/<query>`. The WP baseline
11674 // is large (~150 entries) and meant to be searched —
11675 // surfacing it eagerly would drown the iframe-harvested
11676 // contextual shortcuts on every open. Slash-search is
11677 // the right surface for it, matching the native WP
11678 // palette UX (open, type, find).
11679 run: c.kind === "navigate" && c.url ? this.runNavigate(c.url, c.windowTitle || c.label, icon) : this.runInvoke(c.name, c.label, icon)
11680 };
11681 try {
11682 registerCommand(def);
11683 } catch (err) {
11684 console.error(
11685 "[desktop-mode] shell-harvester: dropping bad command",
11686 def,
11687 err
11688 );
11689 }
11690 }
11691 }
11692 classify(cmd) {
11693 const out = {
11694 name: String(cmd.name),
11695 label: String(cmd.label),
11696 icon: typeof cmd.icon === "string" ? cmd.icon : void 0,
11697 iconSvg: void 0,
11698 kind: "action",
11699 url: void 0,
11700 callback: typeof cmd.callback === "function" ? cmd.callback : void 0
11701 };
11702 const cached = this.kindCache[out.name];
11703 if (cached) {
11704 out.kind = cached.kind;
11705 out.url = cached.url;
11706 out.iconSvg = cached.iconSvg;
11707 return out;
11708 }
11709 if (cmd.icon && typeof cmd.icon !== "string") {
11710 out.iconSvg = this.renderIcon(cmd.icon);
11711 }
11712 const menuEntry = lookupMenuCommand(out.name);
11713 if (menuEntry) {
11714 try {
11715 out.url = new URL(menuEntry.url, this.adminUrl).toString();
11716 out.kind = "navigate";
11717 if (menuEntry.label !== "") {
11718 out.windowTitle = menuEntry.label;
11719 }
11720 } catch {
11721 out.kind = "skip";
11722 }
11723 this.kindCache[out.name] = {
11724 kind: out.kind,
11725 url: out.url,
11726 iconSvg: out.iconSvg
11727 };
11728 return out;
11729 }
11730 if (typeof cmd.callback === "function") {
11731 let src = "";
11732 try {
11733 src = Function.prototype.toString.call(cmd.callback);
11734 } catch {
11735 src = "";
11736 }
11737 const literal = src.match(NAV_HREF_LITERAL_RE) || src.match(NAV_ASSIGN_LITERAL_RE) || src.match(NAV_CALL_LITERAL_RE);
11738 if (literal && literal[1]) {
11739 try {
11740 out.url = new URL(literal[1], window.location.href).toString();
11741 out.kind = "navigate";
11742 } catch {
11743 out.kind = "action";
11744 }
11745 } else if (NAV_INTENT_RE.test(src)) {
11746 const isSiteEditorIntent = SITE_EDITOR_INTENT_RE.test(src);
11747 const nameMatch = isSiteEditorIntent ? out.name.match(SITE_EDITOR_NAME_RE) : null;
11748 if (nameMatch) {
11749 const entityType = nameMatch[1];
11750 const entityId = nameMatch[2];
11751 const p = `/${entityType}/${entityId}`;
11752 try {
11753 const siteEditor = new URL("site-editor.php", this.adminUrl);
11754 siteEditor.searchParams.set("p", p);
11755 siteEditor.searchParams.set("canvas", "edit");
11756 out.url = siteEditor.toString();
11757 out.kind = "navigate";
11758 } catch {
11759 out.kind = "skip";
11760 }
11761 } else {
11762 out.kind = "skip";
11763 }
11764 }
11765 }
11766 this.kindCache[out.name] = {
11767 kind: out.kind,
11768 url: out.url,
11769 iconSvg: out.iconSvg
11770 };
11771 return out;
11772 }
11773 renderIcon(icon) {
11774 const wp = window.wp;
11775 if (!wp || !wp.element || typeof wp.element.renderToString !== "function") {
11776 return "";
11777 }
11778 try {
11779 const rendered = wp.element.renderToString(icon);
11780 if (typeof rendered === "string" && rendered.toLowerCase().startsWith("<svg")) {
11781 return rendered;
11782 }
11783 } catch {
11784 }
11785 return "";
11786 }
11787 iconFor(c) {
11788 if (c.icon && c.icon.startsWith("dashicons-")) {
11789 return c.icon;
11790 }
11791 return c.kind === "navigate" ? "dashicons-external" : "dashicons-arrow-right-alt";
11792 }
11793 runNavigate(url, title, icon) {
11794 return (_args, ctx) => {
11795 ctx.close();
11796 if (tryNativeUrlRemap(url)) {
11797 return;
11798 }
11799 const id = deriveWindowId(url, this.adminUrl);
11800 this.manager.open({ id, baseId: id, url, title, icon });
11801 };
11802 }
11803 runInvoke(name, title, icon) {
11804 return (_args, ctx) => {
11805 ctx.close();
11806 const cb = this.callbackCache[name];
11807 if (typeof cb !== "function") {
11808 return;
11809 }
11810 const captured = this.runWithNavCapture(cb);
11811 if (captured) {
11812 const id = deriveWindowId(captured, this.adminUrl);
11813 this.manager.open({ id, baseId: id, url: captured, title, icon });
11814 }
11815 };
11816 }
11817 /**
11818 * Invoke `cb` with navigation sinks (`document.location`,
11819 * `window.location`, `location.assign`, `location.replace`)
11820 * shadowed so any assignment is captured instead of navigating
11821 * the shell. Returns the captured URL or `null` if the callback
11822 * was a pure JS action.
11823 *
11824 * The shadow uses `Object.defineProperty` on the document /
11825 * window instance to override the prototype's accessor for the
11826 * duration of the call. `delete` afterwards unshadows so the
11827 * native setter is restored.
11828 */
11829 runWithNavCapture(cb) {
11830 let captured = null;
11831 const setCaptured = (v) => {
11832 if (captured === null && typeof v === "string" && v !== "") {
11833 captured = v;
11834 }
11835 };
11836 const realLocation = window.location;
11837 const locationProxy = new Proxy(realLocation, {
11838 get(target2, prop) {
11839 const value = target2[prop];
11840 if (prop === "assign" || prop === "replace") {
11841 return (url) => setCaptured(url);
11842 }
11843 if (typeof value === "function") {
11844 return value.bind(target2);
11845 }
11846 return value;
11847 },
11848 set(_target, prop, value) {
11849 if (prop === "href") {
11850 setCaptured(value);
11851 return true;
11852 }
11853 return true;
11854 }
11855 });
11856 const shadowed = [];
11857 const installShadow = (obj) => {
11858 try {
11859 Object.defineProperty(obj, "location", {
11860 configurable: true,
11861 get: () => locationProxy,
11862 set: (v) => setCaptured(v)
11863 });
11864 shadowed.push({ obj, key: "location" });
11865 } catch {
11866 }
11867 };
11868 installShadow(document);
11869 installShadow(window);
11870 try {
11871 cb({ close: () => {
11872 } });
11873 } catch {
11874 } finally {
11875 for (const s of shadowed) {
11876 try {
11877 delete s.obj[s.key];
11878 } catch {
11879 }
11880 }
11881 }
11882 return captured;
11883 }
11884 }
11885 const seed$2 = [];
11886 function register(def) {
11887 throwOnRegistrationErrors(
11888 "Widget",
11889 collectRegistrationErrors(def, WIDGET_CHECKS),
11890 def
11891 );
11892 const idx = seed$2.findIndex((w) => w.id === def.id);
11893 if (idx >= 0) {
11894 seed$2[idx] = def;
11895 } else {
11896 seed$2.push(def);
11897 }
11898 }
11899 function unregister(id) {
11900 const idx = seed$2.findIndex((w) => w.id === id);
11901 if (idx >= 0) {
11902 seed$2.splice(idx, 1);
11903 }
11904 }
11905 function all() {
11906 const copy = seed$2.slice();
11907 const filtered = applyFilters(HOOKS.WIDGETS, copy);
11908 if (!Array.isArray(filtered)) {
11909 if (typeof console !== "undefined") {
11910 console.warn(
11911 "[desktop-mode] `desktop-mode.widgets` filter returned a non-array; falling back to seed list."
11912 );
11913 }
11914 return copy;
11915 }
11916 return filtered.filter(isValidDef);
11917 }
11918 function get(id) {
11919 return all().find((w) => w.id === id);
11920 }
11921 const WIDGET_CHECKS = [
11922 {
11923 field: "id",
11924 message: "missing or not a non-empty string",
11925 valid: (d) => typeof d.id === "string" && d.id !== ""
11926 },
11927 {
11928 field: "label",
11929 message: "missing or not a non-empty string",
11930 valid: (d) => typeof d.label === "string" && d.label !== ""
11931 },
11932 {
11933 field: "description",
11934 message: "not a string",
11935 valid: (d) => typeof d.description === "string"
11936 },
11937 {
11938 field: "icon",
11939 message: "missing or not a non-empty string",
11940 valid: (d) => typeof d.icon === "string" && d.icon !== ""
11941 },
11942 {
11943 field: "mount",
11944 message: "not a function",
11945 valid: (d) => typeof d.mount === "function"
11946 }
11947 ];
11948 function isValidDef(def) {
11949 return collectRegistrationErrors(def, WIDGET_CHECKS).length === 0;
11950 }
11951 let active$2 = null;
11952 function openWidgetPicker(options) {
11953 if (active$2) {
11954 return;
11955 }
11956 const panel2 = document.createElement("div");
11957 panel2.className = "desktop-mode-widget-picker";
11958 panel2.setAttribute("role", "menu");
11959 panel2.setAttribute("aria-label", __("Add widget"));
11960 const title = document.createElement("div");
11961 title.className = "desktop-mode-widget-picker__title";
11962 title.textContent = __("Add widget");
11963 panel2.appendChild(title);
11964 const list2 = document.createElement("div");
11965 list2.className = "desktop-mode-widget-picker__list";
11966 panel2.appendChild(list2);
11967 paintList(list2, options);
11968 document.body.appendChild(panel2);
11969 positionPanel(panel2, options.anchor);
11970 const onOutsidePointerDown = (e) => {
11971 const target2 = e.target;
11972 if (!target2) {
11973 return;
11974 }
11975 if (panel2.contains(target2) || options.anchor.contains(target2)) {
11976 return;
11977 }
11978 closeWidgetPicker();
11979 };
11980 window.setTimeout(() => {
11981 document.addEventListener("pointerdown", onOutsidePointerDown, true);
11982 }, 0);
11983 const onKeyDown = (e) => {
11984 if (e.key === "Escape") {
11985 closeWidgetPicker();
11986 }
11987 };
11988 document.addEventListener("keydown", onKeyDown);
11989 active$2 = { panel: panel2, options, onOutsidePointerDown, onKeyDown };
11990 const first = list2.querySelector(
11991 "button:not([disabled])"
11992 );
11993 first?.focus();
11994 }
11995 function refreshWidgetPicker() {
11996 if (!active$2) {
11997 return;
11998 }
11999 const list2 = active$2.panel.querySelector(
12000 ".desktop-mode-widget-picker__list"
12001 );
12002 if (list2) {
12003 paintList(list2, active$2.options);
12004 }
12005 }
12006 function closeWidgetPicker() {
12007 if (!active$2) {
12008 return;
12009 }
12010 document.removeEventListener(
12011 "pointerdown",
12012 active$2.onOutsidePointerDown,
12013 true
12014 );
12015 document.removeEventListener("keydown", active$2.onKeyDown);
12016 active$2.panel.remove();
12017 active$2 = null;
12018 }
12019 function paintList(list2, options) {
12020 list2.innerHTML = "";
12021 const enabled = new Set(options.enabledIds());
12022 const defs = options.registry();
12023 if (defs.length === 0) {
12024 const empty = document.createElement("div");
12025 empty.className = "desktop-mode-widget-picker__empty";
12026 empty.textContent = __(
12027 "No widgets available. Activate a plugin that registers one, or see the docs for the registerWidget API."
12028 );
12029 list2.appendChild(empty);
12030 return;
12031 }
12032 for (const def of defs) {
12033 const entry = document.createElement("button");
12034 entry.type = "button";
12035 entry.className = "desktop-mode-widget-picker__entry";
12036 const isAdded = enabled.has(def.id);
12037 if (isAdded) {
12038 entry.classList.add(
12039 "desktop-mode-widget-picker__entry--added"
12040 );
12041 entry.disabled = true;
12042 entry.setAttribute("aria-disabled", "true");
12043 }
12044 entry.setAttribute("role", "menuitem");
12045 let ariaLabel;
12046 if (isAdded) {
12047 ariaLabel = sprintf(__("%s (already added)"), def.label);
12048 } else {
12049 ariaLabel = sprintf(__("Add %s"), def.label);
12050 }
12051 entry.setAttribute("aria-label", ariaLabel);
12052 const icon = document.createElement("span");
12053 icon.className = `desktop-mode-widget-picker__entry-icon dashicons ${def.icon}`;
12054 icon.setAttribute("aria-hidden", "true");
12055 entry.appendChild(icon);
12056 const textWrap = document.createElement("span");
12057 textWrap.className = "desktop-mode-widget-picker__entry-text";
12058 const label = document.createElement("span");
12059 label.className = "desktop-mode-widget-picker__entry-label";
12060 label.textContent = def.label;
12061 textWrap.appendChild(label);
12062 if (def.description) {
12063 const desc = document.createElement("span");
12064 desc.className = "desktop-mode-widget-picker__entry-description";
12065 desc.textContent = def.description;
12066 textWrap.appendChild(desc);
12067 }
12068 entry.appendChild(textWrap);
12069 if (isAdded) {
12070 const status = document.createElement("span");
12071 status.className = "desktop-mode-widget-picker__entry-status";
12072 status.textContent = __("Added");
12073 entry.appendChild(status);
12074 }
12075 if (!isAdded) {
12076 entry.addEventListener("click", (e) => {
12077 e.preventDefault();
12078 e.stopPropagation();
12079 options.onAdd(def.id);
12080 });
12081 }
12082 list2.appendChild(entry);
12083 }
12084 }
12085 function positionPanel(panel2, anchor) {
12086 const rect = anchor.getBoundingClientRect();
12087 panel2.style.position = "fixed";
12088 panel2.style.left = "0px";
12089 panel2.style.top = "0px";
12090 panel2.style.visibility = "hidden";
12091 const panelRect = panel2.getBoundingClientRect();
12092 const width = panelRect.width || 320;
12093 const height = panelRect.height || 200;
12094 const gap = 6;
12095 let left = rect.right - width;
12096 let top = rect.top - height - gap;
12097 if (left < 8) {
12098 left = 8;
12099 }
12100 if (top < 8) {
12101 top = rect.bottom + gap;
12102 }
12103 panel2.style.left = `${Math.round(left)}px`;
12104 panel2.style.top = `${Math.round(top)}px`;
12105 panel2.style.visibility = "";
12106 }
12107 const FLOATING_CLASS = "desktop-mode-widgets__card--floating";
12108 const MOVABLE_CLASS = "desktop-mode-widgets__card--movable";
12109 const RESIZABLE_CLASS = "desktop-mode-widgets__card--resizable";
12110 const DRAGGING_CLASS = "desktop-mode-widgets__card--dragging";
12111 const RESIZING_CLASS = "desktop-mode-widgets__card--resizing";
12112 const DEFAULT_MIN_WIDTH = 160;
12113 const DEFAULT_MIN_HEIGHT = 80;
12114 const DEFAULT_WIDTH$1 = 280;
12115 const DEFAULT_HEIGHT$1 = 180;
12116 const VIEWPORT_MARGIN = 20;
12117 const DRAG_THRESHOLD_PX$1 = 5;
12118 const DRAG_THRESHOLD_SQUARED = DRAG_THRESHOLD_PX$1 * DRAG_THRESHOLD_PX$1;
12119 const DRAG_EXCLUDED_SELECTORS = 'input, textarea, select, button, a, [contenteditable="true"]';
12120 function buildFrame(def, ctx, handlers) {
12121 const card = document.createElement("div");
12122 card.className = "desktop-mode-widgets__card";
12123 card.dataset.widgetId = def.id;
12124 const movable = def.movable === true;
12125 const resizable = def.resizable === true;
12126 if (movable) {
12127 card.classList.add(MOVABLE_CLASS);
12128 }
12129 if (resizable) {
12130 card.classList.add(RESIZABLE_CLASS);
12131 }
12132 if (movable) {
12133 card.appendChild(buildChrome(def, handlers.onRemove, handlers.onRedock));
12134 } else {
12135 card.appendChild(buildCornerClose(def, handlers.onRemove));
12136 }
12137 const body = document.createElement("div");
12138 body.className = "desktop-mode-widgets__card-body";
12139 card.appendChild(body);
12140 let isFloating = false;
12141 if (ctx.geometry) {
12142 applyGeometry(card, ctx.geometry);
12143 card.classList.add(FLOATING_CLASS);
12144 isFloating = true;
12145 }
12146 const resizeCleanups = [];
12147 if (resizable) {
12148 for (const dir of allHandleDirs()) {
12149 const handle = document.createElement("div");
12150 handle.className = `desktop-mode-widgets__resize desktop-mode-widgets__resize--${dir}`;
12151 handle.setAttribute("aria-hidden", "true");
12152 handle.dataset.dir = dir;
12153 card.appendChild(handle);
12154 resizeCleanups.push(
12155 attachResize(card, handle, dir, def, ctx, handlers, () => isFloating)
12156 );
12157 }
12158 }
12159 let dragCleanup = null;
12160 if (movable) {
12161 const chrome = card.querySelector(
12162 ".desktop-mode-widgets__chrome"
12163 );
12164 if (chrome) {
12165 dragCleanup = attachDrag(card, chrome, def, ctx, handlers, (next) => {
12166 isFloating = next;
12167 });
12168 }
12169 }
12170 return {
12171 card,
12172 body,
12173 dispose: () => {
12174 for (const fn of resizeCleanups) {
12175 try {
12176 fn();
12177 } catch {
12178 }
12179 }
12180 if (dragCleanup) {
12181 try {
12182 dragCleanup();
12183 } catch {
12184 }
12185 }
12186 card.remove();
12187 }
12188 };
12189 }
12190 function buildChrome(def, onRemove, onRedock) {
12191 const chrome = document.createElement("header");
12192 chrome.className = "desktop-mode-widgets__chrome";
12193 const grip = document.createElement("span");
12194 grip.className = "desktop-mode-widgets__grip";
12195 grip.setAttribute("aria-hidden", "true");
12196 chrome.appendChild(grip);
12197 const title = document.createElement("span");
12198 title.className = "desktop-mode-widgets__title";
12199 title.textContent = def.label;
12200 chrome.appendChild(title);
12201 chrome.appendChild(buildRedockButton(def, onRedock));
12202 const close = buildCloseButton(def, onRemove);
12203 chrome.appendChild(close);
12204 return chrome;
12205 }
12206 function buildRedockButton(def, onRedock) {
12207 const btn = document.createElement("button");
12208 btn.type = "button";
12209 btn.className = "desktop-mode-widgets__card-redock";
12210 btn.setAttribute(
12211 "aria-label",
12212 // translators: %s is the widget label (e.g., "Clock")
12213 sprintf(__("Dock %s back to widget column"), def.label)
12214 );
12215 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>';
12216 btn.addEventListener("click", (e) => {
12217 e.preventDefault();
12218 e.stopPropagation();
12219 onRedock();
12220 });
12221 btn.dataset.noDrag = "true";
12222 return btn;
12223 }
12224 function buildCornerClose(def, onRemove) {
12225 const close = buildCloseButton(def, onRemove);
12226 close.classList.add("desktop-mode-widgets__card-close--corner");
12227 return close;
12228 }
12229 function buildCloseButton(def, onRemove) {
12230 const close = document.createElement("button");
12231 close.type = "button";
12232 close.className = "desktop-mode-widgets__card-close";
12233 close.setAttribute("aria-label", sprintf(__("Remove %s"), def.label));
12234 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>';
12235 close.addEventListener("click", (e) => {
12236 e.preventDefault();
12237 e.stopPropagation();
12238 onRemove();
12239 });
12240 return close;
12241 }
12242 function attachDrag(card, chrome, def, ctx, handlers, setFloating) {
12243 let pointerId = null;
12244 let startX = 0;
12245 let startY = 0;
12246 let initialLeft = 0;
12247 let initialTop = 0;
12248 let committed = false;
12249 const onDown = (e) => {
12250 if (e.button !== 0) {
12251 return;
12252 }
12253 const target2 = e.target;
12254 if (target2 && target2.closest(DRAG_EXCLUDED_SELECTORS)) {
12255 return;
12256 }
12257 e.preventDefault();
12258 pointerId = e.pointerId;
12259 startX = e.clientX;
12260 startY = e.clientY;
12261 committed = false;
12262 initialLeft = parseFloat(card.style.left) || 0;
12263 initialTop = parseFloat(card.style.top) || 0;
12264 chrome.setPointerCapture(pointerId);
12265 };
12266 const commitDrag = () => {
12267 if (!card.classList.contains(FLOATING_CLASS)) {
12268 const parentRect = ctx.floatingParent.getBoundingClientRect();
12269 const rect = card.getBoundingClientRect();
12270 const initial = {
12271 x: rect.left - parentRect.left,
12272 y: rect.top - parentRect.top,
12273 width: rect.width || def.defaultWidth || DEFAULT_WIDTH$1,
12274 height: rect.height || def.defaultHeight || DEFAULT_HEIGHT$1
12275 };
12276 applyGeometry(card, initial);
12277 card.classList.add(FLOATING_CLASS);
12278 setFloating(true);
12279 handlers.onLiberate(initial);
12280 initialLeft = parseFloat(card.style.left) || 0;
12281 initialTop = parseFloat(card.style.top) || 0;
12282 }
12283 card.classList.add(DRAGGING_CLASS);
12284 };
12285 const onMove = (e) => {
12286 if (pointerId === null || e.pointerId !== pointerId) {
12287 return;
12288 }
12289 const dx = e.clientX - startX;
12290 const dy = e.clientY - startY;
12291 if (!committed) {
12292 if (dx * dx + dy * dy < DRAG_THRESHOLD_SQUARED) {
12293 return;
12294 }
12295 committed = true;
12296 commitDrag();
12297 }
12298 const clamped = clampToParent(
12299 initialLeft + dx,
12300 initialTop + dy,
12301 card.offsetWidth,
12302 card.offsetHeight,
12303 ctx.floatingParent
12304 );
12305 card.style.left = `${clamped.x}px`;
12306 card.style.top = `${clamped.y}px`;
12307 };
12308 const onUp = (e) => {
12309 if (pointerId === null || e.pointerId !== pointerId) {
12310 return;
12311 }
12312 try {
12313 chrome.releasePointerCapture(pointerId);
12314 } catch {
12315 }
12316 pointerId = null;
12317 if (!committed) {
12318 return;
12319 }
12320 committed = false;
12321 card.classList.remove(DRAGGING_CLASS);
12322 handlers.onGeometryChanged(currentGeometry(card));
12323 };
12324 chrome.addEventListener("pointerdown", onDown);
12325 chrome.addEventListener("pointermove", onMove);
12326 chrome.addEventListener("pointerup", onUp);
12327 chrome.addEventListener("pointercancel", onUp);
12328 return () => {
12329 chrome.removeEventListener("pointerdown", onDown);
12330 chrome.removeEventListener("pointermove", onMove);
12331 chrome.removeEventListener("pointerup", onUp);
12332 chrome.removeEventListener("pointercancel", onUp);
12333 };
12334 }
12335 function attachResize(card, handle, dir, def, ctx, handlers, isFloating) {
12336 let pointerId = null;
12337 let startX = 0;
12338 let startY = 0;
12339 let startLeft = 0;
12340 let startTop = 0;
12341 let startW = 0;
12342 let startH = 0;
12343 const onDown = (e) => {
12344 if (e.button !== 0) {
12345 return;
12346 }
12347 if (!isFloating() && !isHeightOnlyDir(dir)) {
12348 return;
12349 }
12350 e.preventDefault();
12351 e.stopPropagation();
12352 pointerId = e.pointerId;
12353 startX = e.clientX;
12354 startY = e.clientY;
12355 const rect = card.getBoundingClientRect();
12356 const parentRect = ctx.floatingParent.getBoundingClientRect();
12357 startLeft = rect.left - parentRect.left;
12358 startTop = rect.top - parentRect.top;
12359 startW = rect.width;
12360 startH = rect.height;
12361 handle.setPointerCapture(pointerId);
12362 card.classList.add(RESIZING_CLASS);
12363 };
12364 const onMove = (e) => {
12365 if (pointerId === null || e.pointerId !== pointerId) {
12366 return;
12367 }
12368 const dx = e.clientX - startX;
12369 const dy = e.clientY - startY;
12370 const next = computeResize(
12371 dir,
12372 dx,
12373 dy,
12374 startLeft,
12375 startTop,
12376 startW,
12377 startH,
12378 def,
12379 ctx.floatingParent,
12380 isFloating()
12381 );
12382 if (isFloating()) {
12383 card.style.left = `${next.x}px`;
12384 card.style.top = `${next.y}px`;
12385 card.style.width = `${next.width}px`;
12386 }
12387 card.style.height = `${next.height}px`;
12388 };
12389 const onUp = (e) => {
12390 if (pointerId === null || e.pointerId !== pointerId) {
12391 return;
12392 }
12393 try {
12394 handle.releasePointerCapture(pointerId);
12395 } catch {
12396 }
12397 pointerId = null;
12398 card.classList.remove(RESIZING_CLASS);
12399 handlers.onGeometryChanged(currentGeometry(card));
12400 };
12401 handle.addEventListener("pointerdown", onDown);
12402 handle.addEventListener("pointermove", onMove);
12403 handle.addEventListener("pointerup", onUp);
12404 handle.addEventListener("pointercancel", onUp);
12405 return () => {
12406 handle.removeEventListener("pointerdown", onDown);
12407 handle.removeEventListener("pointermove", onMove);
12408 handle.removeEventListener("pointerup", onUp);
12409 handle.removeEventListener("pointercancel", onUp);
12410 };
12411 }
12412 function allHandleDirs() {
12413 return ["n", "e", "s", "w", "ne", "nw", "se", "sw"];
12414 }
12415 function isHeightOnlyDir(dir) {
12416 return dir === "s";
12417 }
12418 function applyGeometry(card, geometry) {
12419 card.style.left = `${geometry.x}px`;
12420 card.style.top = `${geometry.y}px`;
12421 card.style.width = `${geometry.width}px`;
12422 card.style.height = `${geometry.height}px`;
12423 }
12424 function currentGeometry(card) {
12425 return {
12426 x: parseFloat(card.style.left) || 0,
12427 y: parseFloat(card.style.top) || 0,
12428 width: card.offsetWidth,
12429 height: card.offsetHeight
12430 };
12431 }
12432 function clampToParent(x, y, width, height, parent) {
12433 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
12434 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
12435 const maxX = Math.max(0, parentWidth - width - VIEWPORT_MARGIN);
12436 const maxY = Math.max(0, parentHeight - height - VIEWPORT_MARGIN);
12437 return {
12438 x: Math.min(Math.max(VIEWPORT_MARGIN, x), maxX),
12439 y: Math.min(Math.max(VIEWPORT_MARGIN, y), maxY)
12440 };
12441 }
12442 function computeResize(dir, dx, dy, startLeft, startTop, startW, startH, def, parent, floating) {
12443 const minW = def.minWidth ?? DEFAULT_MIN_WIDTH;
12444 const minH = def.minHeight ?? DEFAULT_MIN_HEIGHT;
12445 const maxW = def.maxWidth ?? Infinity;
12446 const maxH = def.maxHeight ?? Infinity;
12447 const parentWidth = parent.clientWidth || parent.getBoundingClientRect().width;
12448 const parentHeight = parent.clientHeight || parent.getBoundingClientRect().height;
12449 let x = startLeft;
12450 let y = startTop;
12451 let width = startW;
12452 let height = startH;
12453 if (dir === "e" || dir === "ne" || dir === "se") {
12454 width = clamp$1(startW + dx, minW, Math.min(maxW, parentWidth - startLeft));
12455 }
12456 if (dir === "w" || dir === "nw" || dir === "sw") {
12457 const nextWidth = clamp$1(startW - dx, minW, Math.min(maxW, startLeft + startW));
12458 x = startLeft + (startW - nextWidth);
12459 width = nextWidth;
12460 }
12461 if (dir === "s" || dir === "se" || dir === "sw") {
12462 height = clamp$1(
12463 startH + dy,
12464 minH,
12465 Math.min(maxH, parentHeight - startTop)
12466 );
12467 }
12468 if (dir === "n" || dir === "ne" || dir === "nw") {
12469 const nextHeight = clamp$1(startH - dy, minH, Math.min(maxH, startTop + startH));
12470 y = startTop + (startH - nextHeight);
12471 height = nextHeight;
12472 }
12473 if (!floating) {
12474 width = startW;
12475 x = startLeft;
12476 }
12477 return { x, y, width, height };
12478 }
12479 function clamp$1(value, min, max) {
12480 if (max < min) {
12481 return min;
12482 }
12483 return Math.min(Math.max(value, min), max);
12484 }
12485 const IDS_KEY = "desktop-mode-widgets";
12486 const GEOMETRY_KEY$1 = "desktop-mode-widgets-geometry";
12487 function readRawEnabled() {
12488 try {
12489 return window.localStorage.getItem(IDS_KEY);
12490 } catch {
12491 return null;
12492 }
12493 }
12494 function loadEnabledIds() {
12495 const raw = readRawEnabled();
12496 if (raw === null) {
12497 return [];
12498 }
12499 try {
12500 const parsed = JSON.parse(raw);
12501 if (!Array.isArray(parsed)) {
12502 return [];
12503 }
12504 return parsed.filter((x) => typeof x === "string");
12505 } catch {
12506 return [];
12507 }
12508 }
12509 function saveEnabledIds(ids) {
12510 try {
12511 window.localStorage.setItem(IDS_KEY, JSON.stringify(ids));
12512 } catch {
12513 }
12514 }
12515 function loadGeometry$1() {
12516 try {
12517 const raw = window.localStorage.getItem(GEOMETRY_KEY$1);
12518 if (!raw) {
12519 return {};
12520 }
12521 const parsed = JSON.parse(raw);
12522 if (!parsed || typeof parsed !== "object") {
12523 return {};
12524 }
12525 const out = {};
12526 for (const [id, rawEntry] of Object.entries(parsed)) {
12527 const entry = sanitizeGeometry(rawEntry);
12528 if (entry) {
12529 out[id] = entry;
12530 }
12531 }
12532 return out;
12533 } catch {
12534 return {};
12535 }
12536 }
12537 function saveGeometry$1(geometry) {
12538 try {
12539 window.localStorage.setItem(GEOMETRY_KEY$1, JSON.stringify(geometry));
12540 } catch {
12541 }
12542 }
12543 function sanitizeGeometry(raw) {
12544 if (!raw || typeof raw !== "object") {
12545 return null;
12546 }
12547 const { x, y, width, height } = raw;
12548 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) {
12549 return null;
12550 }
12551 return { x, y, width, height };
12552 }
12553 function createWidgetStorage(widgetId) {
12554 const prefix = `desktop-mode.widget.${widgetId}.`;
12555 const safeGet = (key) => {
12556 try {
12557 return localStorage.getItem(prefix + key);
12558 } catch {
12559 return null;
12560 }
12561 };
12562 return {
12563 get(key) {
12564 const raw = safeGet(key);
12565 if (raw === null) {
12566 return null;
12567 }
12568 try {
12569 return JSON.parse(raw);
12570 } catch {
12571 return null;
12572 }
12573 },
12574 set(key, value) {
12575 try {
12576 localStorage.setItem(prefix + key, JSON.stringify(value));
12577 } catch {
12578 }
12579 },
12580 remove(key) {
12581 try {
12582 localStorage.removeItem(prefix + key);
12583 } catch {
12584 }
12585 },
12586 clear() {
12587 try {
12588 for (let i = localStorage.length - 1; i >= 0; i--) {
12589 const key = localStorage.key(i);
12590 if (key && key.startsWith(prefix)) {
12591 localStorage.removeItem(key);
12592 }
12593 }
12594 } catch {
12595 }
12596 }
12597 };
12598 }
12599 const DEFAULT_ENABLED_IDS = ["clock"];
12600 class WidgetLayer {
12601 /**
12602 * @param root The column element (`#desktop-mode-widgets`).
12603 * @param pluginUrl Absolute plugin URL — passed to widget ctx.
12604 * @param floatingHost Parent for liberated (floating) widgets.
12605 * Defaults to the column's parent (the desktop
12606 * area) so floats are bounded by the visible
12607 * desktop, not the 320 px-wide column.
12608 */
12609 constructor(root, pluginUrl, floatingHost) {
12610 this.mounted = /* @__PURE__ */ new Map();
12611 this.generation = 0;
12612 this.root = root;
12613 this.pluginUrl = pluginUrl;
12614 this.enabledIds = loadEnabledIds();
12615 this.geometry = loadGeometry$1();
12616 this.floatingHost = floatingHost ?? root.parentElement ?? root;
12617 this.listEl = document.createElement("div");
12618 this.listEl.className = "desktop-mode-widgets__list";
12619 this.root.appendChild(this.listEl);
12620 this.addTile = this.buildAddTile();
12621 this.root.appendChild(this.addTile);
12622 this.paintEmptyState();
12623 }
12624 /**
12625 * Mount every widget the user has enabled (per localStorage).
12626 * Called once during shell boot, AFTER the registry seed has run
12627 * so built-ins are available. Safe to call multiple times — the
12628 * `mounted` map dedupes.
12629 */
12630 hydrate() {
12631 if (readRawEnabled() === null) {
12632 this.enabledIds = DEFAULT_ENABLED_IDS.filter(
12633 (id) => !!get(id)
12634 );
12635 saveEnabledIds(this.enabledIds);
12636 }
12637 for (const id of this.enabledIds) {
12638 if (this.mounted.has(id)) {
12639 continue;
12640 }
12641 this.mountById(id);
12642 }
12643 this.paintEmptyState();
12644 }
12645 /**
12646 * Add a widget by id — called by the picker after the user
12647 * selects an available entry. Idempotent.
12648 */
12649 add(id) {
12650 if (this.enabledIds.includes(id)) {
12651 return;
12652 }
12653 if (!get(id)) {
12654 return;
12655 }
12656 this.enabledIds.push(id);
12657 saveEnabledIds(this.enabledIds);
12658 this.mountById(id);
12659 this.paintEmptyState();
12660 doAction(HOOKS.WIDGET_ADDED, { id });
12661 refreshWidgetPicker();
12662 }
12663 /**
12664 * Remove a widget by id — called from the card's × button and
12665 * from the picker. Idempotent.
12666 */
12667 remove(id) {
12668 const before = this.enabledIds.length;
12669 this.enabledIds = this.enabledIds.filter((e) => e !== id);
12670 if (this.enabledIds.length === before) {
12671 return;
12672 }
12673 saveEnabledIds(this.enabledIds);
12674 if (this.geometry[id]) {
12675 delete this.geometry[id];
12676 saveGeometry$1(this.geometry);
12677 }
12678 this.unmountById(id);
12679 this.paintEmptyState();
12680 doAction(HOOKS.WIDGET_REMOVED, { id });
12681 refreshWidgetPicker();
12682 }
12683 /** Public read for the picker / external callers. */
12684 getEnabledIds() {
12685 return [...this.enabledIds];
12686 }
12687 /**
12688 * Mount a widget ONLY if it's already in the user's enabled
12689 * list AND not currently mounted. No-op when the widget isn't
12690 * enabled (user never opted in) and no-op when it's already on
12691 * screen. Used by the server-driven sync: when a plugin
12692 * activates mid-session, its widget def registers via the
12693 * sync's path; if the user had previously enabled that widget
12694 * (in a prior session or before the plugin was deactivated),
12695 * we want to bring it back on screen without toggling the
12696 * "enabled" state or firing a `WIDGET_ADDED` action.
12697 *
12698 * The net behaviour is "rehydrate this one widget now that
12699 * its def is finally registered," which is subtly different
12700 * from `ensureMounted` (which OPT-INs the user into enabling
12701 * the widget for the first time).
12702 */
12703 mountIfEnabled(id) {
12704 if (!get(id)) {
12705 return;
12706 }
12707 if (!this.enabledIds.includes(id)) {
12708 return;
12709 }
12710 if (this.mounted.has(id)) {
12711 return;
12712 }
12713 this.mountById(id);
12714 this.paintEmptyState();
12715 }
12716 /**
12717 * Unmount a widget without touching the persisted enablement.
12718 * Used by the server-driven widget-registry sync: when a plugin
12719 * deactivates mid-session, its widget defs disappear from the
12720 * registry and we need to pull any mounted instance off the
12721 * screen — but we deliberately KEEP the id in the user's
12722 * enabled list so re-activating the plugin re-mounts it
12723 * automatically through `hydrate()`.
12724 *
12725 * Idempotent; a no-op when the widget isn't currently mounted.
12726 */
12727 unmount(id) {
12728 if (!this.mounted.has(id)) {
12729 return;
12730 }
12731 this.unmountById(id);
12732 this.paintEmptyState();
12733 }
12734 /**
12735 * Guarantee the widget identified by `id` is currently mounted,
12736 * adding it to the enabled list if it isn't. No-op when the
12737 * widget is already on screen. Intended for companion plugins
12738 * that want to pin their widget programmatically — a monitor
12739 * plugin that auto-pins itself on the first error burst, a
12740 * first-run onboarding flow that ensures the quick-start widget
12741 * is present, etc.
12742 *
12743 * Returns `true` when the widget is mounted (either newly added
12744 * or already present), `false` when the id isn't registered —
12745 * callers can branch on the failure without having to maintain
12746 * their own registry snapshot.
12747 */
12748 ensureMounted(id) {
12749 if (!get(id)) {
12750 return false;
12751 }
12752 if (this.enabledIds.includes(id)) {
12753 return true;
12754 }
12755 this.add(id);
12756 return true;
12757 }
12758 /**
12759 * Tear down every widget. Called on shell unload via `pagehide`
12760 * so intervals / RAF loops stop before the beacon flush.
12761 */
12762 disposeAll() {
12763 for (const id of Array.from(this.mounted.keys())) {
12764 this.unmountById(id);
12765 }
12766 }
12767 // --- Internal ---------------------------------------------------
12768 mountById(id) {
12769 const def = get(id);
12770 if (!def) {
12771 return;
12772 }
12773 const gen = ++this.generation;
12774 const initialGeometry = def.movable === true ? this.geometry[id] : void 0;
12775 const frame = buildFrame(
12776 def,
12777 { floatingParent: this.floatingHost, geometry: initialGeometry },
12778 {
12779 onRemove: () => this.remove(id),
12780 onGeometryChanged: (geom) => this.persistGeometry(id, geom),
12781 onLiberate: (geom) => this.liberate(id, geom),
12782 onRedock: () => this.redock(id)
12783 }
12784 );
12785 const floating = !!initialGeometry;
12786 const record = {
12787 id,
12788 frame,
12789 generation: gen,
12790 teardown: null,
12791 floating
12792 };
12793 this.mounted.set(id, record);
12794 this.placeCard(frame.card, floating);
12795 const ctx = {
12796 id,
12797 pluginUrl: this.pluginUrl,
12798 storage: createWidgetStorage(id)
12799 };
12800 doAction(HOOKS.WIDGET_MOUNTING, { id, container: frame.body, ctx });
12801 const onResolve = (teardown) => {
12802 const current = this.mounted.get(id);
12803 if (!current || current.generation !== gen) {
12804 try {
12805 teardown();
12806 } catch {
12807 }
12808 return;
12809 }
12810 current.teardown = teardown;
12811 doAction(HOOKS.WIDGET_MOUNTED, { id, container: frame.body, ctx });
12812 };
12813 let result;
12814 try {
12815 result = def.mount(frame.body, ctx);
12816 } catch (err) {
12817 this.handleMountFailure(id, err);
12818 return;
12819 }
12820 if (isThenable(result)) {
12821 result.then(onResolve, (err) => {
12822 if (this.mounted.get(id)?.generation === gen) {
12823 this.handleMountFailure(id, err);
12824 }
12825 });
12826 return;
12827 }
12828 onResolve(result);
12829 }
12830 unmountById(id) {
12831 const record = this.mounted.get(id);
12832 if (!record) {
12833 return;
12834 }
12835 doAction(HOOKS.WIDGET_UNMOUNTING, { id });
12836 try {
12837 record.teardown?.();
12838 } catch (err) {
12839 doAction(HOOKS.SHELL_ERROR, { scope: "widget-teardown", id, error: err });
12840 if (typeof console !== "undefined") {
12841 console.error(
12842 `[desktop-mode] Widget "${id}" teardown threw:`,
12843 err
12844 );
12845 }
12846 }
12847 this.generation++;
12848 record.frame.dispose();
12849 this.mounted.delete(id);
12850 }
12851 handleMountFailure(id, err) {
12852 const record = this.mounted.get(id);
12853 if (record) {
12854 record.frame.dispose();
12855 this.mounted.delete(id);
12856 }
12857 doAction(HOOKS.WIDGET_MOUNT_FAILED, { id, error: err });
12858 doAction(HOOKS.SHELL_ERROR, { scope: "widget-mount", id, error: err });
12859 if (typeof console !== "undefined") {
12860 console.error(
12861 `[desktop-mode] Widget "${id}" failed to mount:`,
12862 err
12863 );
12864 }
12865 }
12866 buildAddTile() {
12867 const tile2 = document.createElement("button");
12868 tile2.type = "button";
12869 tile2.className = "desktop-mode-widgets__add";
12870 tile2.setAttribute("aria-label", __("Add widget"));
12871 const plus = document.createElement("span");
12872 plus.className = "desktop-mode-widgets__add-plus";
12873 plus.setAttribute("aria-hidden", "true");
12874 plus.textContent = "+";
12875 const label = document.createElement("span");
12876 label.className = "desktop-mode-widgets__add-label";
12877 label.textContent = __("Add widget");
12878 tile2.appendChild(plus);
12879 tile2.appendChild(label);
12880 tile2.addEventListener("click", (e) => {
12881 e.preventDefault();
12882 e.stopPropagation();
12883 openWidgetPicker({
12884 anchor: tile2,
12885 registry: () => all(),
12886 enabledIds: () => [...this.enabledIds],
12887 onAdd: (id) => this.add(id)
12888 });
12889 });
12890 return tile2;
12891 }
12892 /**
12893 * Drop a card into the right parent based on its floating state.
12894 * Docked cards append to the column list above the `+` tile;
12895 * floating cards append to the desktop-area-level host so they
12896 * sit above the wallpaper and can range across the viewport.
12897 */
12898 placeCard(card, floating) {
12899 if (floating) {
12900 this.floatingHost.appendChild(card);
12901 } else {
12902 this.listEl.appendChild(card);
12903 }
12904 }
12905 /**
12906 * Move a widget from the column into the floating host. Called by
12907 * the frame on the user's first drag of a movable widget.
12908 */
12909 liberate(id, geometry) {
12910 const record = this.mounted.get(id);
12911 if (!record || record.floating) {
12912 return;
12913 }
12914 record.floating = true;
12915 this.floatingHost.appendChild(record.frame.card);
12916 applyGeometry(record.frame.card, geometry);
12917 this.persistGeometry(id, geometry);
12918 this.paintEmptyState();
12919 }
12920 /**
12921 * Inverse of {@link liberate}: move a floating card back into
12922 * the column and drop its persisted geometry so a subsequent
12923 * shell boot brings it up docked. Called when the user clicks
12924 * the re-dock button in the card's chrome header, or
12925 * programmatically by companion plugins via
12926 * `wp.desktop.widgets.redock( id )` /
12927 * `wp.desktop.widgetLayer.redock( id )`.
12928 *
12929 * Idempotent — a docked widget silently no-ops, an unknown id
12930 * silently no-ops. The `--floating` class on the card is
12931 * removed as part of the same write so CSS rules that depend
12932 * on it (re-dock button visibility, absolute positioning) flip
12933 * back in one paint.
12934 *
12935 * @since 0.7.0 (private)
12936 * @since 0.8.6 (public)
12937 */
12938 redock(id) {
12939 const record = this.mounted.get(id);
12940 if (!record || !record.floating) {
12941 return;
12942 }
12943 record.floating = false;
12944 if (this.geometry[id]) {
12945 delete this.geometry[id];
12946 saveGeometry$1(this.geometry);
12947 }
12948 const card = record.frame.card;
12949 card.classList.remove("desktop-mode-widgets__card--floating");
12950 card.style.left = "";
12951 card.style.top = "";
12952 card.style.width = "";
12953 card.style.height = "";
12954 this.listEl.appendChild(card);
12955 this.paintEmptyState();
12956 }
12957 persistGeometry(id, geometry) {
12958 this.geometry[id] = geometry;
12959 saveGeometry$1(this.geometry);
12960 }
12961 /**
12962 * Toggle a `--has-widgets` modifier so CSS can hide the column's
12963 * decorative backdrop when nothing's mounted (keeps the empty
12964 * state clean — just the `+` tile floating in the corner).
12965 *
12966 * Floating widgets don't count toward "has widgets" in the column
12967 * sense — if every enabled widget is floating, the column itself
12968 * shows only the empty state + add tile.
12969 */
12970 paintEmptyState() {
12971 let docked = 0;
12972 for (const record of this.mounted.values()) {
12973 if (!record.floating) {
12974 docked++;
12975 }
12976 }
12977 this.root.classList.toggle(
12978 "desktop-mode-widgets--has-widgets",
12979 docked > 0
12980 );
12981 }
12982 }
12983 function isThenable(x) {
12984 return !!x && (typeof x === "object" || typeof x === "function") && typeof x.then === "function";
12985 }
12986 const DEFAULT_NATIVE_MIN_WIDTH = 280;
12987 const DEFAULT_NATIVE_MIN_HEIGHT = 220;
12988 const DEFAULT_NATIVE_WIDTH = 520;
12989 const DEFAULT_NATIVE_HEIGHT = 400;
12990 function buildIframeContentRender(cfg, cleanups, windowId) {
12991 return (body) => {
12992 const iframe = document.createElement("iframe");
12993 iframe.style.width = "100%";
12994 iframe.style.height = "100%";
12995 iframe.style.border = "0";
12996 iframe.setAttribute("src", cfg.url);
12997 if (typeof cfg.sandbox === "string" && cfg.sandbox !== "") {
12998 iframe.setAttribute("sandbox", cfg.sandbox);
12999 }
13000 body.style.padding = "0";
13001 body.appendChild(iframe);
13002 const unregisterSynth = registerSyntheticIframe(windowId, iframe);
13003 cleanups.push(unregisterSynth);
13004 let targetOrigin;
13005 try {
13006 targetOrigin = new URL(cfg.url, window.location.origin).origin;
13007 } catch {
13008 targetOrigin = window.location.origin;
13009 }
13010 let resolveReady = null;
13011 const readyPromise = new Promise((resolve2) => {
13012 resolveReady = resolve2;
13013 });
13014 const onLoad = () => {
13015 if (cfg.bridge) {
13016 try {
13017 const doc = iframe.contentDocument;
13018 if (doc && !doc.querySelector("script[data-desktop-mode-iframe-bridge]")) {
13019 const bridgeUrl = window.desktopModeConfig?.iframeBridgeUrl;
13020 if (bridgeUrl) {
13021 const s = doc.createElement("script");
13022 s.src = bridgeUrl;
13023 s.setAttribute("data-desktop-mode-iframe-bridge", "1");
13024 doc.head?.appendChild(s);
13025 }
13026 }
13027 } catch {
13028 }
13029 }
13030 markWindowContentReady(windowId);
13031 resolveReady?.();
13032 };
13033 iframe.addEventListener("load", onLoad);
13034 const onMessage = (e) => {
13035 if (!iframe.contentWindow || e.source !== iframe.contentWindow) {
13036 return;
13037 }
13038 if (e.origin !== targetOrigin && e.origin !== window.location.origin) {
13039 return;
13040 }
13041 const data = e.data;
13042 if (data && typeof data === "object" && typeof data.type === "string" && data.type.startsWith("desktop-mode-bridge-")) {
13043 const bridgeRouter = window.__desktopModeConnectionBridge;
13044 bridgeRouter?.routeIncomingFromIframe(data, windowId);
13045 }
13046 if (data && typeof data === "object" && data.type === "desktop-mode-window-publish" && typeof data.channel === "string" && data.channel !== "") {
13047 dispatchFromWindow(
13048 windowId,
13049 data.channel,
13050 data.payload
13051 );
13052 }
13053 try {
13054 cfg.onMessage?.(e.data);
13055 } catch (err) {
13056 if (typeof console !== "undefined") {
13057 console.error(
13058 "[desktop-mode] iframeContent.onMessage threw:",
13059 err
13060 );
13061 }
13062 }
13063 };
13064 window.addEventListener("message", onMessage);
13065 cleanups.push(() => {
13066 window.removeEventListener("message", onMessage);
13067 iframe.removeEventListener("load", onLoad);
13068 });
13069 return readyPromise;
13070 };
13071 }
13072 function createRegisterWindow(manager) {
13073 return async (def) => {
13074 const userRender = def.render;
13075 let render2 = userRender;
13076 const cleanups = [];
13077 if (def.iframeContent) {
13078 if (userRender && typeof console !== "undefined") {
13079 console.warn(
13080 "[desktop-mode] registerWindow: both `render` and `iframeContent` provided — ignoring `render` and using the iframe shorthand. Drop one."
13081 );
13082 }
13083 render2 = buildIframeContentRender(
13084 def.iframeContent,
13085 cleanups,
13086 def.id
13087 );
13088 }
13089 const userOnClose = def.onClose;
13090 const onClose = cleanups.length ? () => {
13091 for (const fn of cleanups) {
13092 try {
13093 fn();
13094 } catch {
13095 }
13096 }
13097 userOnClose?.();
13098 } : userOnClose;
13099 const win = await manager.open({
13100 id: def.id,
13101 baseId: def.baseId || def.id,
13102 native: true,
13103 url: def.url || `#${def.id}`,
13104 title: def.title,
13105 icon: def.icon,
13106 x: def.x ?? 0,
13107 y: def.y ?? 0,
13108 width: def.width ?? DEFAULT_NATIVE_WIDTH,
13109 height: def.height ?? DEFAULT_NATIVE_HEIGHT,
13110 minWidth: def.minWidth ?? DEFAULT_NATIVE_MIN_WIDTH,
13111 minHeight: def.minHeight ?? DEFAULT_NATIVE_MIN_HEIGHT,
13112 render: render2,
13113 onClose,
13114 onResize: def.onResize,
13115 autofocus: def.autofocus,
13116 initialState: def.initialState,
13117 ownerHandle: def.ownerHandle,
13118 multi: def.multi,
13119 desktopId: def.desktopId
13120 });
13121 return win;
13122 };
13123 }
13124 let onWindowInstanceCounter = 0;
13125 function onWindow(id, handlers, options = {}) {
13126 const namespace = `desktop-mode/on-window/${id}/${++onWindowInstanceCounter}`;
13127 const persistent = options.persistent === true;
13128 const bindings = [
13129 ["opened", HOOKS.WINDOW_OPENED],
13130 ["reopened", HOOKS.WINDOW_REOPENED],
13131 ["focused", HOOKS.WINDOW_FOCUSED],
13132 ["blurred", HOOKS.WINDOW_BLURRED],
13133 ["closing", HOOKS.WINDOW_CLOSING],
13134 ["closed", HOOKS.WINDOW_CLOSED],
13135 ["minimized", HOOKS.WINDOW_MINIMIZED],
13136 ["restored", HOOKS.WINDOW_RESTORED],
13137 ["maximized", HOOKS.WINDOW_MAXIMIZED],
13138 ["unmaximized", HOOKS.WINDOW_UNMAXIMIZED],
13139 ["fullscreenEntered", HOOKS.WINDOW_FULLSCREEN_ENTERED],
13140 ["fullscreenExited", HOOKS.WINDOW_FULLSCREEN_EXITED],
13141 ["resized", HOOKS.WINDOW_RESIZED],
13142 ["bodyResized", HOOKS.WINDOW_BODY_RESIZED],
13143 ["boundsChanged", HOOKS.WINDOW_BOUNDS_CHANGED]
13144 ];
13145 const registered = [];
13146 let disposed = false;
13147 const unsubscribe = () => {
13148 if (disposed) {
13149 return;
13150 }
13151 disposed = true;
13152 for (const hookName2 of registered) {
13153 removeAction(hookName2, namespace);
13154 }
13155 };
13156 for (const [key, hookName2] of bindings) {
13157 const handler = handlers[key];
13158 if (!handler) {
13159 continue;
13160 }
13161 registered.push(hookName2);
13162 addAction(hookName2, namespace, (payload) => {
13163 const p = payload;
13164 if (p.windowId !== id) {
13165 return;
13166 }
13167 const { windowId: _w, ...rest } = p;
13168 handler(rest);
13169 if (key === "closed" && !persistent) {
13170 unsubscribe();
13171 }
13172 });
13173 }
13174 return unsubscribe;
13175 }
13176 function readGlobalRegistry() {
13177 const g = window;
13178 return {
13179 ...g.wpDesktopNativeWindows || {},
13180 ...g.desktopModeNativeWindows || {}
13181 };
13182 }
13183 function createNativeWindowSync(deps2) {
13184 const { manager, appendSystemTile, removeSystemTile } = deps2;
13185 const registered = /* @__PURE__ */ new Set();
13186 const injectedTemplates = /* @__PURE__ */ new Set();
13187 const loadedScripts = /* @__PURE__ */ new Set();
13188 const loadedStyles = /* @__PURE__ */ new Set();
13189 const entriesById = /* @__PURE__ */ new Map();
13190 const resolveSizeForEntry = (entry) => {
13191 const saved = loadNativeWindowGeometry(entry.id);
13192 if (!saved) {
13193 return { width: entry.width, height: entry.height };
13194 }
13195 return {
13196 width: Math.max(saved.width, entry.minWidth),
13197 height: Math.max(saved.height, entry.minHeight)
13198 };
13199 };
13200 const ensureTemplate = (entry) => {
13201 if (injectedTemplates.has(entry.templateId)) {
13202 return;
13203 }
13204 if (document.getElementById(entry.templateId)) {
13205 injectedTemplates.add(entry.templateId);
13206 return;
13207 }
13208 if (!entry.templateHtml) {
13209 return;
13210 }
13211 const tpl = document.createElement("template");
13212 tpl.id = entry.templateId;
13213 tpl.innerHTML = entry.templateHtml;
13214 document.body.appendChild(tpl);
13215 injectedTemplates.add(entry.templateId);
13216 };
13217 const ensureStyle = (entry) => {
13218 const url = entry.styleUrl;
13219 if (!url || loadedStyles.has(url)) {
13220 return;
13221 }
13222 const safeUrl = url.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
13223 const existing = document.head.querySelector(
13224 `link[rel="stylesheet"][href="${safeUrl}"]`
13225 );
13226 if (!existing) {
13227 const link = document.createElement("link");
13228 link.rel = "stylesheet";
13229 link.href = url;
13230 if (entry.styleHandle) {
13231 link.dataset.desktopModeStyleHandle = entry.styleHandle;
13232 }
13233 document.head.appendChild(link);
13234 }
13235 if (Array.isArray(entry.styleInline)) {
13236 for (const css2 of entry.styleInline) {
13237 if (typeof css2 !== "string" || css2 === "") {
13238 continue;
13239 }
13240 const style = document.createElement("style");
13241 if (entry.styleHandle) {
13242 style.dataset.desktopModeStyleHandle = entry.styleHandle;
13243 }
13244 style.textContent = css2;
13245 document.head.appendChild(style);
13246 }
13247 }
13248 loadedStyles.add(url);
13249 };
13250 const ensureScript = async (entry) => {
13251 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
13252 return;
13253 }
13254 try {
13255 await loadVendorScript(entry.scriptUrl, {
13256 translations: entry.scriptTranslations,
13257 l10n: entry.scriptL10n,
13258 before: entry.scriptBefore,
13259 after: entry.scriptAfter
13260 });
13261 } catch (err) {
13262 doAction(HOOKS.SHELL_ERROR, {
13263 scope: "native-window-script-load",
13264 id: entry.id,
13265 error: err
13266 });
13267 }
13268 loadedScripts.add(entry.scriptUrl);
13269 };
13270 const openFromEntry = (entry) => {
13271 const render2 = readGlobalRegistry()[entry.id];
13272 const finalRender = (body, ctx) => {
13273 body.appendChild(cloneTemplate(entry.templateId));
13274 return render2?.(body, ctx);
13275 };
13276 const size = resolveSizeForEntry(entry);
13277 void manager.open({
13278 id: entry.id,
13279 baseId: entry.id,
13280 native: true,
13281 url: `#${entry.id}`,
13282 title: entry.title,
13283 icon: entry.icon,
13284 width: size.width,
13285 height: size.height,
13286 minWidth: entry.minWidth,
13287 minHeight: entry.minHeight,
13288 render: finalRender,
13289 autofocus: entry.autofocus,
13290 ownerHandle: entry.ownerHandle || entry.scriptHandle
13291 });
13292 };
13293 const openNewFromEntry = (entry) => {
13294 const render2 = readGlobalRegistry()[entry.id];
13295 const finalRender = (body, ctx) => {
13296 body.appendChild(cloneTemplate(entry.templateId));
13297 return render2?.(body, ctx);
13298 };
13299 const size = resolveSizeForEntry(entry);
13300 void manager.openNew({
13301 id: entry.id,
13302 baseId: entry.id,
13303 native: true,
13304 url: `#${entry.id}`,
13305 title: entry.title,
13306 icon: entry.icon,
13307 width: size.width,
13308 height: size.height,
13309 minWidth: entry.minWidth,
13310 minHeight: entry.minHeight,
13311 initialState: "normal",
13312 render: finalRender,
13313 autofocus: entry.autofocus,
13314 ownerHandle: entry.ownerHandle || entry.scriptHandle
13315 });
13316 };
13317 const registerTile = async (entry) => {
13318 if (registered.has(entry.id)) {
13319 return;
13320 }
13321 if ("none" === entry.placement) {
13322 ensureTemplate(entry);
13323 ensureStyle(entry);
13324 await ensureScript(entry);
13325 registered.add(entry.id);
13326 return;
13327 }
13328 ensureTemplate(entry);
13329 ensureStyle(entry);
13330 await ensureScript(entry);
13331 appendSystemTile({
13332 id: entry.id,
13333 title: entry.title,
13334 icon: entry.icon,
13335 isOpen: () => !!manager.getById(entry.id),
13336 onOpen: () => openFromEntry(entry)
13337 });
13338 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: entry.id });
13339 registered.add(entry.id);
13340 };
13341 const unregisterTile = (id) => {
13342 if (!registered.has(id)) {
13343 return;
13344 }
13345 removeSystemTile(id);
13346 registered.delete(id);
13347 entriesById.delete(id);
13348 };
13349 const sync = async (list2) => {
13350 const incoming = /* @__PURE__ */ new Set();
13351 for (const entry of list2) {
13352 incoming.add(entry.id);
13353 entriesById.set(entry.id, entry);
13354 }
13355 for (const id of Array.from(registered)) {
13356 if (!incoming.has(id)) {
13357 unregisterTile(id);
13358 }
13359 }
13360 for (const entry of list2) {
13361 if (!registered.has(entry.id)) {
13362 await registerTile(entry);
13363 }
13364 }
13365 };
13366 const openById = (id, opts = {}) => {
13367 const entry = entriesById.get(id);
13368 if (!entry) {
13369 return false;
13370 }
13371 activity.publish("desktop-mode/open-requested", {
13372 windowId: id,
13373 source: opts.source ?? "api"
13374 });
13375 openFromEntry(entry);
13376 return true;
13377 };
13378 const openNewById = (id, opts = {}) => {
13379 const entry = entriesById.get(id);
13380 if (!entry) {
13381 return false;
13382 }
13383 activity.publish("desktop-mode/open-requested", {
13384 windowId: id,
13385 source: opts.source ?? "api"
13386 });
13387 openNewFromEntry(entry);
13388 return true;
13389 };
13390 addAction(
13391 HOOKS.WINDOW_RESIZE_END,
13392 "desktop-mode-native-window-geometry",
13393 (payload) => {
13394 const p = payload;
13395 const windowId = p?.windowId;
13396 const width = p?.width;
13397 const height = p?.height;
13398 if (!windowId || typeof width !== "number" || typeof height !== "number") {
13399 return;
13400 }
13401 const win = manager.getById(windowId);
13402 if (!win) {
13403 return;
13404 }
13405 if (win.state !== "normal") {
13406 return;
13407 }
13408 const baseId = win.config.baseId || win.id;
13409 saveNativeWindowGeometry(baseId, { width, height });
13410 if (win.element) {
13411 saveNativeWindowPosition(baseId, {
13412 x: win.element.offsetLeft,
13413 y: win.element.offsetTop
13414 });
13415 }
13416 }
13417 );
13418 addAction(
13419 HOOKS.WINDOW_DRAG_END,
13420 "desktop-mode-native-window-geometry",
13421 (payload) => {
13422 const windowId = payload?.windowId;
13423 if (!windowId) {
13424 return;
13425 }
13426 const win = manager.getById(windowId);
13427 if (!win) {
13428 return;
13429 }
13430 if (win.state !== "normal") {
13431 return;
13432 }
13433 if (!win.element) {
13434 return;
13435 }
13436 const baseId = win.config.baseId || win.id;
13437 saveNativeWindowGeometry(baseId, {
13438 width: win.element.offsetWidth,
13439 height: win.element.offsetHeight
13440 });
13441 saveNativeWindowPosition(baseId, {
13442 x: win.element.offsetLeft,
13443 y: win.element.offsetTop
13444 });
13445 }
13446 );
13447 addAction(
13448 HOOKS.WINDOW_MAXIMIZED,
13449 "desktop-mode-native-window-geometry",
13450 (payload) => {
13451 const windowId = payload?.windowId;
13452 if (!windowId) {
13453 return;
13454 }
13455 const win = manager.getById(windowId);
13456 if (!win) {
13457 return;
13458 }
13459 const baseId = win.config.baseId || win.id;
13460 const entry = entriesById.get(baseId);
13461 const defaults = entry ? { width: entry.width, height: entry.height } : { width: win.config.width, height: win.config.height };
13462 setNativeWindowSavedState(baseId, "maximized", defaults);
13463 }
13464 );
13465 addAction(
13466 HOOKS.WINDOW_UNMAXIMIZED,
13467 "desktop-mode-native-window-geometry",
13468 (payload) => {
13469 const windowId = payload?.windowId;
13470 if (!windowId) {
13471 return;
13472 }
13473 const win = manager.getById(windowId);
13474 if (!win) {
13475 return;
13476 }
13477 const baseId = win.config.baseId || win.id;
13478 setNativeWindowSavedState(baseId, null);
13479 }
13480 );
13481 return { sync, openById, openNewById };
13482 }
13483 function cloneTemplate(template) {
13484 let tpl = null;
13485 if (typeof template === "string") {
13486 const found = document.getElementById(template);
13487 if (found instanceof HTMLTemplateElement) {
13488 tpl = found;
13489 }
13490 } else {
13491 tpl = template;
13492 }
13493 if (!tpl) {
13494 throw new Error(
13495 `[desktop-mode] cloneTemplate: no <template> found for ${typeof template === "string" ? `#${template}` : "<reference>"}`
13496 );
13497 }
13498 return tpl.content.cloneNode(true);
13499 }
13500 function renderIcon(icon, opts) {
13501 const className = opts.className ?? "";
13502 const title = opts.title ?? "";
13503 if (typeof icon === "string" && icon.startsWith("dashicons-")) {
13504 const el = document.createElement("span");
13505 el.className = `dashicons ${icon} ${className}`.trim();
13506 el.setAttribute("aria-hidden", "true");
13507 return el;
13508 }
13509 if (typeof icon === "string" && icon.startsWith("data:image/svg+xml;base64,")) {
13510 const base64Part = icon.slice("data:image/svg+xml;base64,".length);
13511 if (/^[A-Za-z0-9+/=]+$/.test(base64Part)) {
13512 const el = document.createElement("span");
13513 el.className = className;
13514 el.setAttribute("aria-hidden", "true");
13515 el.style.backgroundImage = `url("${icon}")`;
13516 el.style.backgroundRepeat = "no-repeat";
13517 el.style.backgroundPosition = "center";
13518 el.style.backgroundSize = "contain";
13519 el.style.display = "inline-block";
13520 return el;
13521 }
13522 }
13523 if (typeof icon === "string" && /^data:image\/(png|jpeg|jpg|gif|webp|x-icon|vnd\.microsoft\.icon);base64,/i.test(icon)) {
13524 const commaIdx = icon.indexOf(",");
13525 const payload = commaIdx >= 0 ? icon.slice(commaIdx + 1) : "";
13526 if (/^[A-Za-z0-9+/=]+$/.test(payload)) {
13527 return makeImgIcon(icon, className);
13528 }
13529 }
13530 if (typeof icon === "string" && (icon.startsWith("http://") || icon.startsWith("https://"))) {
13531 return makeImgIcon(icon, className);
13532 }
13533 const span = document.createElement("span");
13534 span.className = `${className} desktop-mode-icon-letter`.trim();
13535 span.setAttribute("aria-hidden", "true");
13536 const letters = letterFromTitle(title);
13537 span.textContent = letters;
13538 const hue = hashTitleToHue(title);
13539 span.style.backgroundColor = `hsl( ${hue}, 60%, 45% )`;
13540 span.style.color = "#fff";
13541 span.style.display = "inline-flex";
13542 span.style.alignItems = "center";
13543 span.style.justifyContent = "center";
13544 span.style.fontWeight = "600";
13545 span.style.borderRadius = "4px";
13546 return span;
13547 }
13548 function makeImgIcon(src, className) {
13549 const img = document.createElement("img");
13550 img.className = className;
13551 img.src = src;
13552 img.alt = "";
13553 img.setAttribute("aria-hidden", "true");
13554 img.draggable = false;
13555 return img;
13556 }
13557 function letterFromTitle(title) {
13558 const trimmed = (title ?? "").trim();
13559 if (trimmed === "") {
13560 return "?";
13561 }
13562 const words = trimmed.split(/\s+/);
13563 if (words.length >= 2) {
13564 return (words[0][0] + words[1][0]).toUpperCase();
13565 }
13566 const first = words[0];
13567 if (first.length >= 2) {
13568 return first.slice(0, 2).toUpperCase();
13569 }
13570 return first.toUpperCase();
13571 }
13572 const BADGE_CLASS = "desktop-mode-icon__badge";
13573 const _badges = /* @__PURE__ */ new Map();
13574 function _safeBadge(count) {
13575 return Math.max(0, Math.floor(Number(count) || 0));
13576 }
13577 function setIconBadge(iconId, count) {
13578 if (!iconId) {
13579 return;
13580 }
13581 const tile2 = _findIconTile(iconId);
13582 if (!tile2) {
13583 return;
13584 }
13585 const safe = _safeBadge(count);
13586 const previous = _badges.get(iconId) ?? 0;
13587 if (safe === previous) {
13588 return;
13589 }
13590 if (safe === 0) {
13591 _badges.delete(iconId);
13592 } else {
13593 _badges.set(iconId, safe);
13594 }
13595 _paintBadgeNode(tile2, safe);
13596 activity.publish("desktop-mode/badge-changed", {
13597 itemId: iconId,
13598 count: safe,
13599 rail: "icon"
13600 });
13601 doAction(HOOKS.ICON_BADGE_CHANGED, {
13602 iconId,
13603 count: safe,
13604 previousCount: previous
13605 });
13606 }
13607 function clearIconBadge(iconId) {
13608 setIconBadge(iconId, 0);
13609 }
13610 function getIconBadge(iconId) {
13611 return _badges.get(iconId) ?? 0;
13612 }
13613 const iconsApi = {
13614 setBadge: setIconBadge,
13615 clearBadge: clearIconBadge,
13616 getBadge: getIconBadge
13617 };
13618 function fingerprintIcons(icons) {
13619 if (!icons || icons.length === 0) {
13620 return "";
13621 }
13622 return icons.map(
13623 (i) => `${i.id}|${i.title}|${i.icon}|${i.window ?? ""}|${i.url ?? ""}|${i.position ?? 0}|${i.pinned ? 1 : 0}`
13624 ).join(";");
13625 }
13626 let _lastFingerprint = "";
13627 function renderDesktopIcons(host, icons, deps2) {
13628 const fp = fingerprintIcons(icons);
13629 if (fp === _lastFingerprint && host.querySelector(":scope > .desktop-mode-icons")) {
13630 return;
13631 }
13632 _lastFingerprint = fp;
13633 const existing = host.querySelector(":scope > .desktop-mode-icons");
13634 if (existing) {
13635 existing.remove();
13636 }
13637 if (!icons || icons.length === 0) {
13638 return;
13639 }
13640 const container = document.createElement("div");
13641 container.className = "desktop-mode-icons";
13642 container.setAttribute("role", "list");
13643 container.setAttribute("aria-label", __("Desktop icons"));
13644 const ordered = [...icons].sort((a, b) => {
13645 const ap = a.pinned ? 0 : 1;
13646 const bp = b.pinned ? 0 : 1;
13647 return ap - bp;
13648 });
13649 const tiles = /* @__PURE__ */ new Map();
13650 for (const entry of ordered) {
13651 const tile2 = buildIcon(entry, deps2);
13652 const stored = _badges.get(entry.id) ?? 0;
13653 if (stored > 0) {
13654 _paintBadgeNode(tile2, stored);
13655 }
13656 container.appendChild(tile2);
13657 tiles.set(entry.id, tile2);
13658 }
13659 host.appendChild(container);
13660 doAction(HOOKS.DESKTOP_ICONS_RENDERED, {
13661 ids: (icons ?? []).map((i) => i.id),
13662 container,
13663 tiles
13664 });
13665 }
13666 function _findIconTile(iconId) {
13667 if (!iconId) {
13668 return null;
13669 }
13670 const container = document.querySelector(
13671 ".desktop-mode-icons"
13672 );
13673 if (!container) {
13674 return null;
13675 }
13676 return container.querySelector(
13677 `[data-icon-id="${_cssEscape(iconId)}"]`
13678 );
13679 }
13680 function _paintBadgeNode(host, count) {
13681 const existing = host.querySelector(
13682 `:scope > .${BADGE_CLASS}`
13683 );
13684 if (count <= 0) {
13685 existing?.remove();
13686 return;
13687 }
13688 const display = count > 99 ? "99+" : String(count);
13689 const ariaLabel = sprintf(
13690 // translators: %d is the number of pending items in a desktop-icon badge.
13691 _n("%d notification", "%d notifications", count),
13692 count
13693 );
13694 if (existing) {
13695 if (existing.textContent !== display) {
13696 existing.textContent = display;
13697 }
13698 existing.setAttribute("aria-label", ariaLabel);
13699 return;
13700 }
13701 const badge = document.createElement("span");
13702 badge.className = BADGE_CLASS;
13703 badge.textContent = display;
13704 badge.setAttribute("aria-label", ariaLabel);
13705 host.appendChild(badge);
13706 }
13707 function _cssEscape(value) {
13708 const c = window.CSS;
13709 return c?.escape ? c.escape(value) : value;
13710 }
13711 function buildIcon(entry, deps2) {
13712 const tile2 = document.createElement("button");
13713 tile2.type = "button";
13714 tile2.className = entry.pinned ? "desktop-mode-icon desktop-mode-icon--pinned" : "desktop-mode-icon";
13715 tile2.dataset.iconId = entry.id;
13716 if (entry.pinned) {
13717 tile2.dataset.pinned = "1";
13718 }
13719 tile2.setAttribute("role", "listitem");
13720 tile2.setAttribute("aria-label", entry.title);
13721 const icon = renderIcon(entry.icon, {
13722 title: entry.title,
13723 className: "desktop-mode-icon__image"
13724 });
13725 tile2.appendChild(icon);
13726 const label = document.createElement("span");
13727 label.className = "desktop-mode-icon__label";
13728 label.textContent = entry.title;
13729 tile2.appendChild(label);
13730 tile2.addEventListener("click", (e) => {
13731 e.stopPropagation();
13732 doAction(HOOKS.DESKTOP_ICON_CLICKED, {
13733 id: entry.id,
13734 target: entry.window ? "window" : "url"
13735 });
13736 openTarget(entry, deps2);
13737 });
13738 tile2.addEventListener("contextmenu", (e) => {
13739 if (entry.pinned) {
13740 return;
13741 }
13742 e.preventDefault();
13743 e.stopPropagation();
13744 openItemVisibilityMenu({
13745 x: e.clientX,
13746 y: e.clientY,
13747 id: entry.id,
13748 title: entry.title,
13749 surface: "desktop"
13750 });
13751 });
13752 return tile2;
13753 }
13754 function openTarget(entry, deps2) {
13755 if (entry.window) {
13756 const opened = deps2.openWindow(entry.window);
13757 if (!opened) {
13758 return;
13759 }
13760 return;
13761 }
13762 if (entry.url) {
13763 if (tryOpenExternalUrl(entry.url)) {
13764 return;
13765 }
13766 try {
13767 const parsed = new URL(entry.url, window.location.origin);
13768 const windowId = deps2.deriveWindowId(parsed.toString());
13769 void deps2.manager.open({
13770 id: windowId,
13771 baseId: windowId,
13772 url: parsed.toString(),
13773 title: entry.title,
13774 icon: entry.icon
13775 });
13776 } catch {
13777 }
13778 }
13779 }
13780 const SIDE_DOCK_ID = "desktop-mode-side-dock";
13781 function coreItemToIconEntry(item, index2) {
13782 return {
13783 id: `dock-core:${item.id}`,
13784 title: item.title,
13785 icon: item.icon,
13786 window: "",
13787 url: item.url,
13788 // Synthesized icons render after server-registered ones; the
13789 // large offset leaves headroom for plugin authors who set
13790 // explicit `position` values.
13791 position: 1e3 + index2
13792 };
13793 }
13794 function createLayoutDispatcher(deps2, initialLayout, initialDockItems, initialServerIcons) {
13795 let layout = initialLayout;
13796 let items = initialDockItems;
13797 let serverIcons = initialServerIcons ?? [];
13798 let primary = null;
13799 let side = null;
13800 let primaryDock = null;
13801 let sideDock = null;
13802 let sideDockEl = null;
13803 const systemTiles = /* @__PURE__ */ new Map();
13804 const railFor = (affinity) => {
13805 if (affinity === "core" && side) {
13806 return side;
13807 }
13808 return primary;
13809 };
13810 const ensureSideDockEl = () => {
13811 const existing = document.getElementById(
13812 SIDE_DOCK_ID
13813 );
13814 if (existing) {
13815 return existing;
13816 }
13817 const el = document.createElement("nav");
13818 el.id = SIDE_DOCK_ID;
13819 el.className = "desktop-mode-dock";
13820 el.setAttribute("role", "toolbar");
13821 el.setAttribute("aria-label", "Core admin navigation");
13822 deps2.shellBody.insertBefore(el, deps2.shellBody.firstChild);
13823 return el;
13824 };
13825 const removeSideDockEl = () => {
13826 if (sideDockEl && sideDockEl.parentNode) {
13827 sideDockEl.parentNode.removeChild(sideDockEl);
13828 }
13829 sideDockEl = null;
13830 };
13831 const readSettings = () => deps2.getSettings?.() ?? { itemVisibility: {}, dockOrder: [] };
13832 const effectiveDockItems = () => {
13833 const dockedNativeWindows = /* @__PURE__ */ new Set();
13834 for (const entry of systemTiles.values()) {
13835 dockedNativeWindows.add(entry.item.id);
13836 }
13837 return applyDockPlacement(
13838 items,
13839 serverIcons,
13840 readSettings(),
13841 dockedNativeWindows
13842 );
13843 };
13844 const partition = () => {
13845 const effective = effectiveDockItems();
13846 const core = [];
13847 const plugin = [];
13848 for (const item of effective) {
13849 if (item.isCore) {
13850 core.push(item);
13851 } else {
13852 plugin.push(item);
13853 }
13854 }
13855 return { core, plugin };
13856 };
13857 const repaintIcons = () => {
13858 const settings = readSettings();
13859 if (layout !== "spatial") {
13860 deps2.renderIcons(
13861 applyDesktopPlacement(serverIcons, items, settings.itemVisibility)
13862 );
13863 return;
13864 }
13865 const { core } = partition();
13866 const synthesized = core.map(coreItemToIconEntry);
13867 const keptServerIcons = serverIcons.filter((icon) => {
13868 const override = settings.itemVisibility[icon.id];
13869 if (override) {
13870 return override === "desktop" || override === "both";
13871 }
13872 return Boolean(icon.pinned);
13873 });
13874 const explicitlyPromoted = [];
13875 let synthIndex = 0;
13876 for (const item of items) {
13877 const placement = settings.itemVisibility[item.id];
13878 if (placement === "desktop" || placement === "both") {
13879 explicitlyPromoted.push({
13880 id: `dock:${item.id}`,
13881 title: item.title,
13882 icon: item.icon,
13883 window: "",
13884 url: item.url || "",
13885 position: 2e3 + synthIndex++
13886 });
13887 }
13888 }
13889 deps2.renderIcons([
13890 ...synthesized,
13891 ...keptServerIcons,
13892 ...explicitlyPromoted
13893 ]);
13894 };
13895 const tearDownDocks = () => {
13896 if (primary) {
13897 try {
13898 primary.destroy();
13899 } catch (err) {
13900 doAction(HOOKS.SHELL_ERROR, {
13901 scope: "dock-rail-renderer/destroy",
13902 error: err
13903 });
13904 }
13905 primary = null;
13906 primaryDock = null;
13907 }
13908 if (side) {
13909 try {
13910 side.destroy();
13911 } catch (err) {
13912 doAction(HOOKS.SHELL_ERROR, {
13913 scope: "dock-rail-renderer/destroy",
13914 error: err
13915 });
13916 }
13917 side = null;
13918 sideDock = null;
13919 }
13920 };
13921 const mountRail = (mountDeps) => {
13922 const renderer = resolveActive();
13923 if (!renderer) {
13924 doAction(HOOKS.SHELL_ERROR, {
13925 scope: "dock-rail-renderer",
13926 message: "No dock rail renderer is registered."
13927 });
13928 return null;
13929 }
13930 try {
13931 return renderer.mount(mountDeps);
13932 } catch (err) {
13933 doAction(HOOKS.SHELL_ERROR, {
13934 scope: "dock-rail-renderer/mount",
13935 rendererId: renderer.id,
13936 error: err
13937 });
13938 if (renderer === defaultDockRailRenderer) {
13939 return null;
13940 }
13941 try {
13942 return defaultDockRailRenderer.mount(mountDeps);
13943 } catch {
13944 return null;
13945 }
13946 }
13947 };
13948 const buildMountDeps = (container, railItems, orientation) => ({
13949 container,
13950 items: railItems,
13951 // `fullMenu` is the complete admin-menu list. Renderers that
13952 // want to ignore the layout's partitioning (e.g., paint
13953 // every menu item in one ring regardless of `isCore`) read
13954 // this instead of `items`. Snapshot per-mount so a renderer
13955 // holding the array sees a stable list; live updates flow
13956 // through `replaceItems`.
13957 fullMenu: items.slice(),
13958 // Same idea for system tiles — OS Settings, plugin-owned
13959 // native-window launchers, etc. Lets a renderer apply
13960 // uniform treatment across menu + system cohorts in one
13961 // pass. Live updates flow through `appendSystemItem` /
13962 // `removeSystemItem`.
13963 fullSystemTiles: Array.from(systemTiles.values()).map(
13964 (entry) => entry.item
13965 ),
13966 orientation,
13967 windowManager: deps2.windowManager,
13968 adminUrl: deps2.adminUrl,
13969 // `openItem` / `openSubmenuPick` / `openSystemItem` are
13970 // routing callbacks for custom renderers. They mirror
13971 // exactly what the default renderer (`Dock.openPage` /
13972 // `Dock.openSubmenuPick`) does internally — same
13973 // `deriveWindowId(url, adminUrl)` call, same window-
13974 // config shape — so a custom renderer addresses the same
13975 // window with the same id at runtime. Switching renderer
13976 // mid-session doesn't lose the user's open windows.
13977 openItem: (item) => {
13978 const baseId = deriveWindowId(item.url, deps2.adminUrl);
13979 deps2.windowManager.open({
13980 id: baseId,
13981 baseId,
13982 url: item.url,
13983 parentUrl: item.url,
13984 title: item.title,
13985 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
13986 submenu: item.submenu,
13987 multi: !!item.multi
13988 });
13989 },
13990 openSubmenuPick: (item, sub) => {
13991 deps2.windowManager.open({
13992 id: deriveWindowId(sub.url, deps2.adminUrl),
13993 baseId: deriveWindowId(item.url, deps2.adminUrl),
13994 url: sub.url,
13995 // Pin the synthetic parent tab to the dock landing
13996 // page, not to the sub-page the user picked. Without
13997 // this, a submenu-pick (e.g. clicking "Editor" inside
13998 // Appearance's submenu popover) would open at
13999 // site-editor.php with no way back to themes.php.
14000 parentUrl: item.url,
14001 title: item.title,
14002 icon: item.icon.startsWith("dashicons-") ? item.icon : "dashicons-admin-generic",
14003 submenu: item.submenu,
14004 multi: !!item.multi
14005 });
14006 },
14007 openSystemItem: (item) => item.onOpen()
14008 });
14009 const buildDocksForCurrentLayout = () => {
14010 tearDownDocks();
14011 const { core, plugin } = partition();
14012 if (layout === "classic") {
14013 sideDockEl = ensureSideDockEl();
14014 side = mountRail(
14015 buildMountDeps(sideDockEl, core, "left")
14016 );
14017 sideDock = unwrapDefaultDock(side);
14018 primary = mountRail(
14019 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
14020 );
14021 primaryDock = unwrapDefaultDock(primary);
14022 } else if (layout === "unified") {
14023 removeSideDockEl();
14024 primary = mountRail(
14025 buildMountDeps(deps2.bottomDockEl, effectiveDockItems(), "bottom")
14026 );
14027 primaryDock = unwrapDefaultDock(primary);
14028 } else {
14029 removeSideDockEl();
14030 primary = mountRail(
14031 buildMountDeps(deps2.bottomDockEl, plugin, "bottom")
14032 );
14033 primaryDock = unwrapDefaultDock(primary);
14034 }
14035 for (const entry of systemTiles.values()) {
14036 railFor(entry.affinity)?.appendSystemItem(entry.item);
14037 }
14038 };
14039 const dispatcher = {
14040 getLayout: () => layout,
14041 getPrimary: () => primaryDock,
14042 getSide: () => sideDock,
14043 setLayout: (next) => {
14044 if (next === layout) {
14045 return;
14046 }
14047 layout = next;
14048 deps2.shellRoot.setAttribute("data-desktop-mode-layout", next);
14049 buildDocksForCurrentLayout();
14050 repaintIcons();
14051 document.dispatchEvent(
14052 new CustomEvent("desktop-mode-layout-changed", {
14053 detail: {
14054 layout: next,
14055 primary: primaryDock,
14056 side: sideDock
14057 }
14058 })
14059 );
14060 },
14061 applyDockItems: (nextItems) => {
14062 items = nextItems;
14063 const { core, plugin } = partition();
14064 if (layout === "classic") {
14065 side?.replaceItems(core);
14066 primary?.replaceItems(plugin);
14067 } else if (layout === "unified") {
14068 primary?.replaceItems(effectiveDockItems());
14069 } else {
14070 primary?.replaceItems(plugin);
14071 }
14072 repaintIcons();
14073 },
14074 applyDesktopIcons: (next) => {
14075 serverIcons = next ?? [];
14076 repaintIcons();
14077 },
14078 appendSystemTile: (item, affinity = "plugin") => {
14079 systemTiles.set(item.id, { item, affinity });
14080 railFor(affinity)?.appendSystemItem(item);
14081 },
14082 removeSystemTile: (id) => {
14083 const entry = systemTiles.get(id);
14084 if (!entry) {
14085 return;
14086 }
14087 systemTiles.delete(id);
14088 railFor(entry.affinity)?.removeSystemItem(id);
14089 },
14090 listSystemTiles: () => Array.from(systemTiles.values()).map((entry) => ({
14091 id: entry.item.id,
14092 title: entry.item.title,
14093 icon: entry.item.icon,
14094 affinity: entry.affinity
14095 })),
14096 getSystemTile: (id) => systemTiles.get(id)?.item ?? null,
14097 getMenuItems: () => items.slice(),
14098 refresh: () => {
14099 const { core, plugin } = partition();
14100 if (layout === "classic") {
14101 side?.replaceItems(core);
14102 primary?.replaceItems(plugin);
14103 } else if (layout === "unified") {
14104 primary?.replaceItems(effectiveDockItems());
14105 } else {
14106 primary?.replaceItems(plugin);
14107 }
14108 repaintIcons();
14109 },
14110 destroy: () => {
14111 tearDownDocks();
14112 removeSideDockEl();
14113 }
14114 };
14115 deps2.shellRoot.setAttribute("data-desktop-mode-layout", layout);
14116 buildDocksForCurrentLayout();
14117 repaintIcons();
14118 let lastResolvedId = resolveActive()?.id ?? null;
14119 subscribe$3(() => {
14120 const nextId2 = resolveActive()?.id ?? null;
14121 if (nextId2 === lastResolvedId) {
14122 return;
14123 }
14124 lastResolvedId = nextId2;
14125 buildDocksForCurrentLayout();
14126 repaintIcons();
14127 document.dispatchEvent(
14128 new CustomEvent("desktop-mode-layout-changed", {
14129 detail: {
14130 layout,
14131 primary: primaryDock,
14132 side: sideDock
14133 }
14134 })
14135 );
14136 });
14137 return dispatcher;
14138 }
14139 function loadImpl(scriptUrl) {
14140 if (window.desktopModeCreateAiAssistant) {
14141 return Promise.resolve(window.desktopModeCreateAiAssistant);
14142 }
14143 return new Promise((resolve2, reject) => {
14144 const existing = document.querySelector(
14145 `script[data-desktop-mode-ai="1"]`
14146 );
14147 const finish = () => {
14148 const factory = window.desktopModeCreateAiAssistant;
14149 if (!factory) {
14150 reject(
14151 new Error(
14152 "[desktop-mode] ai-assistant bundle loaded but did not register desktopModeCreateAiAssistant"
14153 )
14154 );
14155 return;
14156 }
14157 resolve2(factory);
14158 };
14159 if (existing) {
14160 if (window.desktopModeCreateAiAssistant) {
14161 finish();
14162 } else {
14163 existing.addEventListener("load", finish);
14164 existing.addEventListener(
14165 "error",
14166 () => reject(new Error("failed to load ai-assistant bundle"))
14167 );
14168 }
14169 return;
14170 }
14171 const s = document.createElement("script");
14172 s.src = scriptUrl;
14173 s.async = true;
14174 s.dataset.desktopModeAi = "1";
14175 s.addEventListener("load", finish);
14176 s.addEventListener(
14177 "error",
14178 () => reject(new Error("failed to load ai-assistant bundle"))
14179 );
14180 document.head.appendChild(s);
14181 });
14182 }
14183 class AiAssistantStub {
14184 constructor(config, scriptUrl) {
14185 this._real = null;
14186 this._loadPromise = null;
14187 this._pendingAsk = null;
14188 this._intendOpen = false;
14189 this.ask = (...args) => {
14190 return this._ensure().then((r) => r.ask(...args));
14191 };
14192 this._config = config;
14193 this._scriptUrl = scriptUrl;
14194 }
14195 _ensure() {
14196 if (this._loadPromise) {
14197 return this._loadPromise;
14198 }
14199 this._loadPromise = loadImpl(this._scriptUrl).then((factory) => {
14200 const real = factory(this._config);
14201 if (this._pendingAsk) {
14202 real.attachAsk(this._pendingAsk);
14203 }
14204 this._real = real;
14205 return real;
14206 });
14207 return this._loadPromise;
14208 }
14209 open() {
14210 this._intendOpen = true;
14211 void this._ensure().then((r) => r.open());
14212 }
14213 close() {
14214 this._intendOpen = false;
14215 if (this._real) {
14216 this._real.close();
14217 }
14218 }
14219 toggle() {
14220 if (this.isOpen) {
14221 this.close();
14222 } else {
14223 this.open();
14224 }
14225 }
14226 get isOpen() {
14227 return this._real ? this._real.isOpen : this._intendOpen;
14228 }
14229 /**
14230 * Late-bind the programmatic `ask` callback. Mirrors the real
14231 * class's `attachAsk` signature so `desktop.ts`'s call site is
14232 * identical whether it's wiring the stub or the impl.
14233 */
14234 attachAsk(fn) {
14235 this._pendingAsk = fn;
14236 if (this._real) {
14237 this._real.attachAsk(fn);
14238 }
14239 }
14240 }
14241 const isAbortError = (err) => {
14242 if (!err || typeof err !== "object") {
14243 return false;
14244 }
14245 return err.name === "AbortError";
14246 };
14247 const normaliseToolsOpt = (tools) => {
14248 if (!tools) {
14249 return [];
14250 }
14251 const all2 = listAiCallableCommands();
14252 if (tools === true || tools === "aiCallable") {
14253 return all2;
14254 }
14255 if (Array.isArray(tools)) {
14256 const allowed = new Set(tools.map((s) => s.toLowerCase()));
14257 return all2.filter((c) => allowed.has(c.slug));
14258 }
14259 if (typeof tools === "function") {
14260 return all2.filter((c) => {
14261 try {
14262 return tools(c.slug) === true;
14263 } catch {
14264 return false;
14265 }
14266 });
14267 }
14268 return [];
14269 };
14270 const normaliseSystemPrompt = (sp) => {
14271 if (!sp) {
14272 return null;
14273 }
14274 if (typeof sp === "string") {
14275 return { text: sp, mode: "append" };
14276 }
14277 if (typeof sp === "object" && typeof sp.text === "string" && sp.text !== "") {
14278 return {
14279 text: sp.text,
14280 mode: sp.mode === "replace" ? "replace" : "append"
14281 };
14282 }
14283 return null;
14284 };
14285 function liftMessage(payloadMessage, result) {
14286 const seed2 = payloadMessage ?? "";
14287 if (seed2 !== "") {
14288 return seed2;
14289 }
14290 if (typeof result === "string" && result !== "") {
14291 return result;
14292 }
14293 if (result && typeof result === "object" && "message" in result && typeof result.message === "string") {
14294 return result.message;
14295 }
14296 return "";
14297 }
14298 function serialiseOutcome(result) {
14299 if (result === void 0) {
14300 return { value: null };
14301 }
14302 if (typeof result === "object" && result !== null) {
14303 return result;
14304 }
14305 return { value: result };
14306 }
14307 function createAsk(deps2) {
14308 const postToSearch = async (body, signal) => {
14309 const config = deps2.config();
14310 const url = config.aiSearchUrl ?? "";
14311 const nonce = config.restNonce ?? "";
14312 if (!url || !nonce) {
14313 throw new Error(
14314 "[desktop-mode] wp.desktop.ai.ask: aiSearchUrl / restNonce missing from config. AI Copilot may not be enabled."
14315 );
14316 }
14317 try {
14318 return await trackedFetch$1(
14319 url,
14320 {
14321 method: "POST",
14322 credentials: "same-origin",
14323 headers: {
14324 "Content-Type": "application/json",
14325 "X-WP-Nonce": nonce
14326 },
14327 body: JSON.stringify(body),
14328 signal
14329 },
14330 { source: "desktop-mode/ai-ask" }
14331 );
14332 } catch (err) {
14333 if (isAbortError(err)) {
14334 throw err;
14335 }
14336 throw new Error(
14337 `[desktop-mode] wp.desktop.ai.ask: network error — ${String(
14338 err?.message ?? err
14339 )}`
14340 );
14341 }
14342 };
14343 const dispatchToolCall = async (payload, opts) => {
14344 const slug = payload.tool?.slug ?? "";
14345 const args = payload.tool?.args ?? "";
14346 const cmd = findCommand(slug);
14347 if (!cmd) {
14348 return {
14349 ok: false,
14350 response: {
14351 answer_type: "tool_call",
14352 message: `Command /${slug} was not registered on this page.`,
14353 entity: null,
14354 admin_links: null,
14355 toolCall: {
14356 slug,
14357 args,
14358 result: { error: "command_not_found" }
14359 },
14360 request_id: payload.request_id
14361 }
14362 };
14363 }
14364 const ctx = opts.commandContext ?? deps2.fallbackContext();
14365 let result;
14366 try {
14367 result = await Promise.resolve(cmd.run(args, ctx));
14368 } catch (err) {
14369 result = { error: String(err?.message ?? err) };
14370 }
14371 return { ok: true, slug, args, result };
14372 };
14373 const composeFollowUp = async (text, slug, args, result, sp, signal) => {
14374 const body = {
14375 query: text,
14376 follow_up: {
14377 tool: { slug, args },
14378 result: serialiseOutcome(result)
14379 }
14380 };
14381 if (sp) {
14382 body.system_prompt_text = sp.text;
14383 body.system_prompt_mode = sp.mode;
14384 }
14385 let res;
14386 try {
14387 res = await postToSearch(body, signal);
14388 } catch (err) {
14389 if (isAbortError(err)) {
14390 throw err;
14391 }
14392 return null;
14393 }
14394 if (!res.ok) {
14395 return null;
14396 }
14397 const payload = await res.json().catch(() => ({}));
14398 const message = typeof payload.message === "string" ? payload.message.trim() : "";
14399 return message !== "" ? payload.message ?? null : null;
14400 };
14401 return async function ask(query, opts = {}) {
14402 const text = (query ?? "").trim();
14403 if (text === "") {
14404 const hasMeaningfulOpts = opts.tools !== void 0 || opts.systemPrompt !== void 0 || opts.followUp === true || opts.resumeTool !== void 0 || opts.commandContext !== void 0;
14405 if (hasMeaningfulOpts) {
14406 throw new Error(
14407 "[desktop-mode] wp.desktop.ai.ask: empty query passed with non-default options — likely a caller bug. Provide a query or call without options."
14408 );
14409 }
14410 return {
14411 answer_type: "chat",
14412 message: "",
14413 entity: null,
14414 admin_links: null
14415 };
14416 }
14417 const commandTools = normaliseToolsOpt(opts.tools);
14418 const sp = normaliseSystemPrompt(opts.systemPrompt);
14419 const body = { query: text };
14420 if (opts.resumeTool) {
14421 body.resume_tool = opts.resumeTool;
14422 }
14423 if (typeof opts.startOffset === "number") {
14424 body.start_offset = opts.startOffset;
14425 }
14426 if (commandTools.length > 0) {
14427 body.command_tools = commandTools;
14428 }
14429 if (sp) {
14430 body.system_prompt_text = sp.text;
14431 body.system_prompt_mode = sp.mode;
14432 }
14433 const res = await postToSearch(body, opts.signal);
14434 if (!res.ok) {
14435 const detail = await res.json().catch(() => ({ message: res.statusText }));
14436 throw new Error(
14437 `[desktop-mode] wp.desktop.ai.ask: HTTP ${res.status} — ${detail.message ?? res.statusText}`
14438 );
14439 }
14440 const payload = await res.json();
14441 if (payload.answer_type !== "tool_call" || !payload.tool) {
14442 return {
14443 answer_type: payload.answer_type,
14444 message: payload.message ?? "",
14445 entity: payload.entity ?? null,
14446 admin_links: payload.admin_links ?? null,
14447 request_id: payload.request_id,
14448 continue: payload.continue ?? null
14449 };
14450 }
14451 const dispatch2 = await dispatchToolCall(payload, opts);
14452 if (!dispatch2.ok) {
14453 return dispatch2.response;
14454 }
14455 const { slug, args, result } = dispatch2;
14456 let message = liftMessage(payload.message, result);
14457 if (opts.followUp === true) {
14458 const composed = await composeFollowUp(
14459 text,
14460 slug,
14461 args,
14462 result,
14463 sp,
14464 opts.signal
14465 );
14466 if (composed !== null) {
14467 message = composed;
14468 }
14469 }
14470 return {
14471 answer_type: "tool_call",
14472 message,
14473 entity: null,
14474 admin_links: null,
14475 toolCall: { slug, args, result },
14476 request_id: payload.request_id
14477 };
14478 };
14479 }
14480 const EVENT_NAME = "desktop-mode-broadcast";
14481 const POSTMESSAGE_TYPE = "desktop-mode-broadcast";
14482 const ORIGIN = window.location.origin;
14483 let _manager = null;
14484 function attachBroadcastBus(manager) {
14485 _manager = manager;
14486 }
14487 function broadcast(topic, payload) {
14488 const filteredTopic = String(
14489 applyFilters("desktop-mode.broadcast.topic", topic, { payload }) ?? topic
14490 );
14491 const filteredPayload = applyFilters(
14492 "desktop-mode.broadcast.payload",
14493 payload,
14494 { topic: filteredTopic }
14495 );
14496 const detail = {
14497 topic: filteredTopic,
14498 payload: filteredPayload
14499 };
14500 document.dispatchEvent(new CustomEvent(EVENT_NAME, { detail }));
14501 doAction(HOOKS.BROADCAST, detail);
14502 activity.publish(
14503 filteredTopic,
14504 filteredPayload
14505 );
14506 if (!_manager) {
14507 return;
14508 }
14509 const message = {
14510 type: POSTMESSAGE_TYPE,
14511 topic: filteredTopic,
14512 payload: filteredPayload
14513 };
14514 for (const win of _manager._stack) {
14515 const target2 = win.iframe?.contentWindow;
14516 if (!target2) {
14517 continue;
14518 }
14519 try {
14520 target2.postMessage(message, ORIGIN);
14521 } catch (err) {
14522 }
14523 }
14524 }
14525 function subscribe$2(topic, cb) {
14526 const handler = (e) => {
14527 const detail = e.detail;
14528 if (!detail) {
14529 return;
14530 }
14531 if (topic !== "*" && detail.topic !== topic) {
14532 return;
14533 }
14534 try {
14535 cb(detail.payload, { topic: detail.topic });
14536 } catch (err) {
14537 doAction(HOOKS.SHELL_ERROR, {
14538 scope: "broadcast-subscriber",
14539 topic: detail.topic,
14540 error: err
14541 });
14542 }
14543 };
14544 document.addEventListener(EVENT_NAME, handler);
14545 return () => document.removeEventListener(EVENT_NAME, handler);
14546 }
14547 function installBroadcastReceiver() {
14548 window.addEventListener("message", (e) => {
14549 if (e.origin !== ORIGIN) {
14550 return;
14551 }
14552 const data = e.data;
14553 if (!data || data.type !== POSTMESSAGE_TYPE) {
14554 return;
14555 }
14556 if (data._fromParent) {
14557 return;
14558 }
14559 if (typeof data.topic !== "string") {
14560 return;
14561 }
14562 broadcast(data.topic, data.payload);
14563 });
14564 }
14565 const LOG_PREFIX = "[desktop-mode-bin badge]";
14566 function log(...args) {
14567 try {
14568 if (window.localStorage?.getItem("desktopModeBinDebug")) {
14569 console.info(LOG_PREFIX, ...args);
14570 }
14571 } catch {
14572 }
14573 }
14574 function warn(...args) {
14575 console.warn(LOG_PREFIX, ...args);
14576 }
14577 const TARGET_ID = "desktop-mode-recycle-bin";
14578 const HEARTBEAT_FIELD$1 = "desktop_mode_recycle_bin_seen_ts";
14579 function getDesktopApi() {
14580 return window.wp?.desktop;
14581 }
14582 const store$3 = createSharedStore(
14583 "desktop-mode/recycle-bin/badge",
14584 () => ({
14585 current: 0,
14586 seenTs: 0,
14587 started: false,
14588 countUrl: ""
14589 })
14590 );
14591 function setRecycleBinBadge(next) {
14592 const safe = Math.max(0, Math.floor(next));
14593 const prev = store$3.state.current;
14594 store$3.state.current = safe;
14595 log("setRecycleBinBadge", { prev, next: safe });
14596 paintBadge(safe);
14597 }
14598 function adjustRecycleBinBadge(delta) {
14599 setRecycleBinBadge(store$3.state.current + delta);
14600 }
14601 function _currentRecycleBinBadge() {
14602 return store$3.state.current;
14603 }
14604 function paintBadge(count) {
14605 const desktop = getDesktopApi();
14606 const active2 = isBinWindowActive();
14607 const visible = active2 ? 0 : count;
14608 log("paintBadge", { count, visible, active: active2 });
14609 desktop?.dock?.setBadge?.(TARGET_ID, visible);
14610 desktop?.taskbar?.setBadge?.(TARGET_ID, visible);
14611 desktop?.icons?.setBadge?.(TARGET_ID, visible);
14612 }
14613 function isBinWindowActive() {
14614 return !!getDesktopApi()?.windowManager?.isActive?.(TARGET_ID);
14615 }
14616 function startRecycleBinBadge(initialRaw, countUrl = "") {
14617 const initial = Number(initialRaw) || 0;
14618 const cfg = window.desktopModeConfig;
14619 const cfgCount = cfg?.recycleBinCount;
14620 const cfgUrl = cfg?.recycleBinCountUrl;
14621 const cfgDebug = cfg?.desktopModeBinDebug;
14622 log("startRecycleBinBadge entry", {
14623 initial,
14624 countUrl,
14625 alreadyStarted: store$3.state.started,
14626 cfgCount,
14627 cfgUrl,
14628 cfgDebug,
14629 readyState: document.readyState
14630 });
14631 const cfgCountNum = Number(cfgCount);
14632 const cfgCountIsHealthy = (typeof cfgCount === "number" || typeof cfgCount === "string") && Number.isFinite(cfgCountNum);
14633 if (!cfgCountIsHealthy) {
14634 warn(
14635 "desktopModeConfig.recycleBinCount is missing — PHP filter `desktop_mode_shell_config` did not deliver. Check your PHP error log for `[desktop-mode-bin debug]` lines.",
14636 { cfg }
14637 );
14638 }
14639 if (store$3.state.started) {
14640 setRecycleBinBadge(initial);
14641 return;
14642 }
14643 store$3.state.started = true;
14644 store$3.state.countUrl = countUrl;
14645 store$3.state.seenTs = Date.now();
14646 setRecycleBinBadge(initial);
14647 wireDockTileSignal();
14648 wireDesktopIconsSignal();
14649 wireBroadcastDeltas();
14650 wirePostMessageFastPath();
14651 wireHeartbeatProbe();
14652 wireWindowLifecycleSignals();
14653 }
14654 function wireWindowLifecycleSignals() {
14655 const ns = "desktop-mode/recycle-bin/badge-lifecycle";
14656 const repaint = (payload) => {
14657 const detail = payload;
14658 if (detail?.windowId !== TARGET_ID) {
14659 return;
14660 }
14661 paintBadge(store$3.state.current);
14662 };
14663 addAction(HOOKS.WINDOW_OPENED, ns, repaint);
14664 addAction(HOOKS.WINDOW_FOCUSED, ns, repaint);
14665 addAction(HOOKS.WINDOW_BLURRED, ns, repaint);
14666 addAction(HOOKS.WINDOW_MINIMIZED, ns, repaint);
14667 addAction(HOOKS.WINDOW_RESTORED, ns, repaint);
14668 addAction(HOOKS.WINDOW_CLOSED, ns, repaint);
14669 addAction(HOOKS.WINDOW_REOPENED, ns, repaint);
14670 }
14671 function wireDockTileSignal() {
14672 addAction(
14673 HOOKS.DOCK_ITEM_APPENDED,
14674 "desktop-mode/recycle-bin/badge",
14675 (payload) => {
14676 if (payload?.id === TARGET_ID) {
14677 paintBadge(store$3.state.current);
14678 }
14679 }
14680 );
14681 }
14682 function wireDesktopIconsSignal() {
14683 addAction(
14684 HOOKS.DESKTOP_ICONS_RENDERED,
14685 "desktop-mode/recycle-bin/badge",
14686 (payload) => {
14687 if (payload?.ids?.includes(TARGET_ID)) {
14688 paintBadge(store$3.state.current);
14689 }
14690 }
14691 );
14692 }
14693 function wireBroadcastDeltas() {
14694 const onDomain = (payload) => {
14695 const detail = payload;
14696 if (!detail) {
14697 return;
14698 }
14699 const ids = Array.isArray(detail.ids) ? detail.ids.length : 0;
14700 switch (detail.action) {
14701 case "trashed":
14702 adjustRecycleBinBadge(+ids);
14703 break;
14704 case "untrashed":
14705 case "deleted":
14706 adjustRecycleBinBadge(-ids);
14707 break;
14708 }
14709 };
14710 subscribe$2("desktop-mode.post.changed", onDomain);
14711 subscribe$2("desktop-mode.page.changed", onDomain);
14712 subscribe$2("desktop-mode.attachment.changed", onDomain);
14713 subscribe$2("desktop-mode.comment.changed", onDomain);
14714 subscribe$2("desktop-mode.placement.changed", onDomain);
14715 subscribe$2("desktop-mode.shortcut.changed", onDomain);
14716 subscribe$2("desktop-mode.folder.changed", onDomain);
14717 }
14718 function wirePostMessageFastPath() {
14719 const expectedOrigin = window.location.origin;
14720 window.addEventListener("message", (e) => {
14721 if (e.origin !== expectedOrigin) {
14722 return;
14723 }
14724 const data = e.data;
14725 if (!data || data.type !== "desktop-mode-recycle-bin-changed") {
14726 return;
14727 }
14728 const ts = typeof data.ts === "number" ? data.ts : Date.now();
14729 if (ts <= store$3.state.seenTs) {
14730 log("postMessage skipped (ts <= seenTs)", { ts, seenTs: store$3.state.seenTs });
14731 return;
14732 }
14733 log("postMessage triggers refetch", { ts, prevSeenTs: store$3.state.seenTs });
14734 store$3.state.seenTs = ts;
14735 void refetchCount();
14736 });
14737 }
14738 function wireHeartbeatProbe() {
14739 const $ = window.jQuery;
14740 if (!$) {
14741 warn("wireHeartbeatProbe: window.jQuery not available — heartbeat path disabled");
14742 return;
14743 }
14744 log("wireHeartbeatProbe: jQuery + heartbeat hooks attached");
14745 $(document).on("heartbeat-send", (...args) => {
14746 const data = args[1];
14747 if (data) {
14748 data[HEARTBEAT_FIELD$1] = store$3.state.seenTs;
14749 }
14750 });
14751 $(document).on("heartbeat-tick", (...args) => {
14752 const response = args[1];
14753 const block = response?.desktop_mode_recycle_bin;
14754 log("heartbeat-tick", { hasBlock: !!block, block });
14755 if (!block) {
14756 return;
14757 }
14758 if (typeof block.ts === "number" && block.ts > store$3.state.seenTs) {
14759 store$3.state.seenTs = block.ts;
14760 }
14761 if (typeof block.count === "number") {
14762 setRecycleBinBadge(block.count);
14763 }
14764 });
14765 }
14766 async function refetchCount() {
14767 if (!store$3.state.countUrl) {
14768 log("refetchCount: no countUrl, skip");
14769 return;
14770 }
14771 log("refetchCount: hitting", store$3.state.countUrl);
14772 try {
14773 const response = await fetch(store$3.state.countUrl, {
14774 credentials: "same-origin",
14775 headers: { Accept: "application/json" }
14776 });
14777 if (!response.ok) {
14778 warn("refetchCount: non-OK", response.status, response.statusText);
14779 return;
14780 }
14781 const json = await response.json();
14782 log("refetchCount: response", json);
14783 if (typeof json.count === "number") {
14784 setRecycleBinBadge(json.count);
14785 }
14786 } catch (err) {
14787 warn("refetchCount: fetch failed", err);
14788 }
14789 }
14790 const OS_SETTINGS_ID = "desktop-mode-os-settings";
14791 const RECYCLE_BIN_ID = "desktop-mode-recycle-bin";
14792 function registerBuiltInPeekRenderers(opts) {
14793 const wpHooks = getWpHooks();
14794 if (!wpHooks) {
14795 return;
14796 }
14797 wpHooks.addFilter(
14798 "desktop-mode.dock.peek-card-content",
14799 "desktop-mode/built-in-peek-renderers",
14800 (body, ctx) => {
14801 const context = ctx;
14802 const id = context.window.id;
14803 if (id === OS_SETTINGS_ID) {
14804 return renderOsSettings();
14805 }
14806 if (id === RECYCLE_BIN_ID) {
14807 return renderRecycleBin(context, opts.getRecycleBinCount);
14808 }
14809 return body;
14810 }
14811 );
14812 }
14813 function renderOsSettings(_ctx) {
14814 const root = document.createElement("span");
14815 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--os-settings";
14816 root.setAttribute("aria-hidden", "true");
14817 const hero = document.createElement("span");
14818 hero.className = "desktop-mode-dock-peek__os-hero dashicons dashicons-admin-generic";
14819 root.appendChild(hero);
14820 const subtitle = document.createElement("span");
14821 subtitle.className = "desktop-mode-dock-peek__os-subtitle";
14822 subtitle.textContent = __("System Preferences");
14823 root.appendChild(subtitle);
14824 const tabs = document.createElement("span");
14825 tabs.className = "desktop-mode-dock-peek__os-tabs";
14826 for (const cls of [
14827 "dashicons-art",
14828 "dashicons-admin-customizer",
14829 "dashicons-editor-help"
14830 ]) {
14831 const tab = document.createElement("span");
14832 tab.className = `desktop-mode-dock-peek__os-tab dashicons ${cls}`;
14833 tabs.appendChild(tab);
14834 }
14835 root.appendChild(tabs);
14836 return root;
14837 }
14838 function renderRecycleBin(_ctx, getCount) {
14839 const root = document.createElement("span");
14840 root.className = "desktop-mode-dock-peek__card-body desktop-mode-dock-peek__card-body--recycle-bin";
14841 root.setAttribute("aria-hidden", "true");
14842 const count = Math.max(0, Math.floor(getCount() || 0));
14843 root.dataset.empty = count === 0 ? "true" : "false";
14844 const stage = document.createElement("span");
14845 stage.className = "desktop-mode-dock-peek__bin-stage";
14846 const stack = document.createElement("span");
14847 stack.className = "desktop-mode-dock-peek__bin-stack";
14848 for (let i = 0; i < 3; i++) {
14849 const slip = document.createElement("span");
14850 slip.className = "desktop-mode-dock-peek__bin-slip";
14851 stack.appendChild(slip);
14852 }
14853 stage.appendChild(stack);
14854 const icon = document.createElement("span");
14855 icon.className = `desktop-mode-dock-peek__bin-icon dashicons ${count === 0 ? "dashicons-trash" : "dashicons-trash"}`;
14856 stage.appendChild(icon);
14857 root.appendChild(stage);
14858 const label = document.createElement("span");
14859 label.className = "desktop-mode-dock-peek__bin-label";
14860 if (count === 0) {
14861 label.textContent = __("Recycle Bin — empty");
14862 } else if (count === 1) {
14863 label.textContent = __("1 item");
14864 } else if (count > 99) {
14865 label.textContent = "99+ items";
14866 } else {
14867 label.textContent = `${count} items`;
14868 }
14869 root.appendChild(label);
14870 return root;
14871 }
14872 function getWpHooks() {
14873 const wp = window.wp;
14874 return wp?.hooks ?? null;
14875 }
14876 const BUG_REPORT_WINDOW_ID = "desktop-mode-bug-report";
14877 const REPO_OWNER = "WordPress";
14878 const REPO_NAME = "desktop-mode";
14879 const MAX_BODY_LENGTH = 6e3;
14880 function renderBugReport(body) {
14881 body.classList.add("desktop-mode-bug-report");
14882 body.replaceChildren();
14883 const form = document.createElement("form");
14884 form.className = "desktop-mode-bug-report__form";
14885 form.setAttribute("novalidate", "");
14886 const intro = document.createElement("p");
14887 intro.className = "desktop-mode-bug-report__intro";
14888 intro.textContent = __(
14889 "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."
14890 );
14891 form.appendChild(intro);
14892 form.appendChild(buildTypeField());
14893 form.appendChild(buildTextField("title", __("Title"), {
14894 placeholder: __("A short summary"),
14895 required: true
14896 }));
14897 form.appendChild(buildTextareaField("description", __("What happened? What did you expect?"), {
14898 placeholder: __("Describe the issue or the feature you have in mind."),
14899 rows: 5,
14900 required: true
14901 }));
14902 form.appendChild(buildTextareaField("steps", __("Steps to reproduce (bug only)"), {
14903 placeholder: __("One step per line"),
14904 rows: 4
14905 }));
14906 const meta = buildMetadataPreview();
14907 form.appendChild(meta);
14908 const actions = document.createElement("div");
14909 actions.className = "desktop-mode-bug-report__actions";
14910 const submit = document.createElement("button");
14911 submit.type = "submit";
14912 submit.className = "desktop-mode-bug-report__submit";
14913 submit.textContent = __("Open issue on GitHub");
14914 actions.appendChild(submit);
14915 const hint = document.createElement("span");
14916 hint.className = "desktop-mode-bug-report__hint";
14917 hint.textContent = __("You will review and submit on GitHub.");
14918 actions.appendChild(hint);
14919 form.appendChild(actions);
14920 form.addEventListener("submit", (e) => {
14921 e.preventDefault();
14922 const state2 = readFormState(form);
14923 if (!state2.title.trim() || !state2.description.trim()) {
14924 showInlineError(form, __("Title and description are both required."));
14925 return;
14926 }
14927 const url = buildGithubIssueUrl(state2);
14928 window.open(url, "_blank", "noopener");
14929 });
14930 body.appendChild(form);
14931 }
14932 function buildTypeField() {
14933 const wrap = document.createElement("div");
14934 wrap.className = "desktop-mode-bug-report__field desktop-mode-bug-report__field--type";
14935 const label = document.createElement("span");
14936 label.className = "desktop-mode-bug-report__label";
14937 label.textContent = __("Type");
14938 wrap.appendChild(label);
14939 const group = document.createElement("div");
14940 group.className = "desktop-mode-bug-report__radio-group";
14941 group.setAttribute("role", "radiogroup");
14942 const options = [
14943 { value: "bug", label: __("Bug"), checked: true },
14944 { value: "feature", label: __("Feature request") },
14945 { value: "question", label: __("Question") }
14946 ];
14947 for (const opt of options) {
14948 const radioLabel = document.createElement("label");
14949 radioLabel.className = "desktop-mode-bug-report__radio";
14950 const input = document.createElement("input");
14951 input.type = "radio";
14952 input.name = "type";
14953 input.value = opt.value;
14954 if (opt.checked) {
14955 input.checked = true;
14956 }
14957 radioLabel.appendChild(input);
14958 const text = document.createElement("span");
14959 text.textContent = opt.label;
14960 radioLabel.appendChild(text);
14961 group.appendChild(radioLabel);
14962 }
14963 wrap.appendChild(group);
14964 return wrap;
14965 }
14966 function buildTextField(name, labelText, opts = {}) {
14967 const wrap = document.createElement("div");
14968 wrap.className = "desktop-mode-bug-report__field";
14969 const label = document.createElement("label");
14970 label.className = "desktop-mode-bug-report__label";
14971 label.textContent = labelText;
14972 wrap.appendChild(label);
14973 const input = document.createElement("input");
14974 input.type = "text";
14975 input.name = name;
14976 input.className = "desktop-mode-bug-report__input";
14977 if (opts.placeholder) {
14978 input.placeholder = opts.placeholder;
14979 }
14980 if (opts.required) {
14981 input.setAttribute("aria-required", "true");
14982 }
14983 label.appendChild(input);
14984 return wrap;
14985 }
14986 function buildTextareaField(name, labelText, opts = {}) {
14987 const wrap = document.createElement("div");
14988 wrap.className = "desktop-mode-bug-report__field";
14989 const label = document.createElement("label");
14990 label.className = "desktop-mode-bug-report__label";
14991 label.textContent = labelText;
14992 wrap.appendChild(label);
14993 const textarea = document.createElement("textarea");
14994 textarea.name = name;
14995 textarea.className = "desktop-mode-bug-report__textarea";
14996 textarea.rows = opts.rows ?? 4;
14997 if (opts.placeholder) {
14998 textarea.placeholder = opts.placeholder;
14999 }
15000 if (opts.required) {
15001 textarea.setAttribute("aria-required", "true");
15002 }
15003 label.appendChild(textarea);
15004 return wrap;
15005 }
15006 function buildMetadataPreview() {
15007 const details = document.createElement("details");
15008 details.className = "desktop-mode-bug-report__metadata";
15009 const summary = document.createElement("summary");
15010 summary.textContent = __("Environment included with the report");
15011 details.appendChild(summary);
15012 const pre = document.createElement("pre");
15013 pre.className = "desktop-mode-bug-report__metadata-body";
15014 pre.textContent = formatMetadata(collectMetadata());
15015 details.appendChild(pre);
15016 return details;
15017 }
15018 function showInlineError(form, msg) {
15019 let banner = form.querySelector(".desktop-mode-bug-report__error");
15020 if (!banner) {
15021 banner = document.createElement("div");
15022 banner.className = "desktop-mode-bug-report__error";
15023 banner.setAttribute("role", "alert");
15024 form.prepend(banner);
15025 }
15026 banner.textContent = msg;
15027 }
15028 function readFormState(form) {
15029 const data = new FormData(form);
15030 return {
15031 type: data.get("type") ?? "bug",
15032 title: data.get("title") ?? "",
15033 description: data.get("description") ?? "",
15034 steps: data.get("steps") ?? ""
15035 };
15036 }
15037 function buildGithubIssueUrl(state2) {
15038 const labels = labelsForType(state2.type);
15039 const body = composeIssueBody(state2);
15040 const params = new URLSearchParams();
15041 params.set("title", state2.title.trim());
15042 params.set("body", body);
15043 if (labels.length) {
15044 params.set("labels", labels.join(","));
15045 }
15046 return `https://github.com/${REPO_OWNER}/${REPO_NAME}/issues/new?${params.toString()}`;
15047 }
15048 function labelsForType(type) {
15049 switch (type) {
15050 case "bug":
15051 return ["bug"];
15052 case "feature":
15053 return ["enhancement"];
15054 case "question":
15055 return ["question"];
15056 default:
15057 return [];
15058 }
15059 }
15060 function composeIssueBody(state2) {
15061 const parts = [];
15062 parts.push(state2.description.trim());
15063 if (state2.type === "bug" && state2.steps.trim()) {
15064 parts.push("");
15065 parts.push("## Steps to reproduce");
15066 parts.push("");
15067 parts.push(state2.steps.trim());
15068 }
15069 parts.push("");
15070 parts.push("<details><summary>Environment</summary>");
15071 parts.push("");
15072 parts.push("```");
15073 parts.push(formatMetadata(collectMetadata()));
15074 parts.push("```");
15075 parts.push("");
15076 parts.push("</details>");
15077 let out = parts.join("\n");
15078 if (out.length > MAX_BODY_LENGTH) {
15079 out = out.slice(0, MAX_BODY_LENGTH) + "\n\n…(truncated to fit GitHub URL length limit)";
15080 }
15081 return out;
15082 }
15083 function collectMetadata() {
15084 const cfg = window.wp?.desktop?.config;
15085 return {
15086 pluginVersion: cfg?.pluginVersion ?? "unknown",
15087 wordpressVersion: cfg?.wordpressVersion ?? "unknown",
15088 userAgent: navigator.userAgent,
15089 viewport: `${window.innerWidth}x${window.innerHeight}`,
15090 platform: navigator.platform || "unknown",
15091 currentUrl: window.location.href
15092 };
15093 }
15094 function formatMetadata(m) {
15095 return [
15096 `Plugin version: ${m.pluginVersion}`,
15097 `WordPress version: ${m.wordpressVersion}`,
15098 `User agent: ${m.userAgent}`,
15099 `Viewport: ${m.viewport}`,
15100 `Platform: ${m.platform}`,
15101 `Current URL: ${m.currentUrl}`
15102 ].join("\n");
15103 }
15104 let _config = null;
15105 let _state = {
15106 installHintDismissed: false,
15107 notificationsEnabled: false
15108 };
15109 const _listeners = /* @__PURE__ */ new Set();
15110 function initPwaState(config) {
15111 if (!config) {
15112 _config = null;
15113 return;
15114 }
15115 _config = config;
15116 _state = { ...config.state };
15117 notify$4();
15118 }
15119 function getPwaState() {
15120 return { ..._state };
15121 }
15122 function updatePwaState(patch) {
15123 _state = { ..._state, ...patch };
15124 notify$4();
15125 if (!_config) {
15126 return getPwaState();
15127 }
15128 const body = JSON.stringify(patch);
15129 const nonce = readRestNonce$2();
15130 void fetch(_config.stateUrl, {
15131 method: "POST",
15132 credentials: "same-origin",
15133 headers: {
15134 "Content-Type": "application/json",
15135 ...nonce ? { "X-WP-Nonce": nonce } : {}
15136 },
15137 body
15138 }).catch((err) => {
15139 if (typeof console !== "undefined") {
15140 console.warn("[desktop-mode] pwa-state write failed:", err);
15141 }
15142 });
15143 return getPwaState();
15144 }
15145 function subscribePwaState(cb) {
15146 _listeners.add(cb);
15147 return () => {
15148 _listeners.delete(cb);
15149 };
15150 }
15151 function notify$4() {
15152 const snapshot = getPwaState();
15153 for (const cb of Array.from(_listeners)) {
15154 try {
15155 cb(snapshot);
15156 } catch (err) {
15157 if (typeof console !== "undefined") {
15158 console.error(
15159 "[desktop-mode] pwa-state listener threw:",
15160 err
15161 );
15162 }
15163 }
15164 }
15165 }
15166 function readRestNonce$2() {
15167 const cfg = window.desktopModeConfig;
15168 return cfg?.restNonce ?? "";
15169 }
15170 const state = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
15171 __proto__: null,
15172 getPwaState,
15173 initPwaState,
15174 subscribePwaState,
15175 updatePwaState
15176 }, Symbol.toStringTag, { value: "Module" }));
15177 let _registration = null;
15178 let _registrationFailed = false;
15179 let _controllerChangeBound = false;
15180 let _reloadingForSwUpdate = false;
15181 let _status = "pending";
15182 function bindControllerChangeReload() {
15183 if (_controllerChangeBound) {
15184 return;
15185 }
15186 _controllerChangeBound = true;
15187 const hadInitialController = !!navigator.serviceWorker.controller;
15188 navigator.serviceWorker.addEventListener("controllerchange", () => {
15189 if (!hadInitialController) {
15190 return;
15191 }
15192 if (_reloadingForSwUpdate) {
15193 return;
15194 }
15195 if (wasRecentlyReloadedForSwUpdate()) {
15196 return;
15197 }
15198 markReloadedForSwUpdate();
15199 _reloadingForSwUpdate = true;
15200 setTimeout(() => window.location.reload(), 0);
15201 });
15202 }
15203 const SW_RELOAD_THROTTLE_KEY = "wpd-sw-reload-ts";
15204 const SW_RELOAD_THROTTLE_MS = 3e4;
15205 function wasRecentlyReloadedForSwUpdate() {
15206 try {
15207 const raw = sessionStorage.getItem(SW_RELOAD_THROTTLE_KEY);
15208 const last = raw ? Number.parseInt(raw, 10) : 0;
15209 if (!Number.isFinite(last) || last <= 0) {
15210 return false;
15211 }
15212 return Date.now() - last < SW_RELOAD_THROTTLE_MS;
15213 } catch {
15214 return false;
15215 }
15216 }
15217 function markReloadedForSwUpdate() {
15218 try {
15219 sessionStorage.setItem(SW_RELOAD_THROTTLE_KEY, String(Date.now()));
15220 } catch {
15221 }
15222 }
15223 async function registerServiceWorker(config, options = {}) {
15224 if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
15225 _status = "unsupported";
15226 return null;
15227 }
15228 if (!config?.swUrl) {
15229 _status = "unsupported";
15230 return null;
15231 }
15232 if (!window.isSecureContext) {
15233 _status = "unsupported";
15234 return null;
15235 }
15236 if (_registration || _registrationFailed) {
15237 return _registration;
15238 }
15239 if (!options.forceReplace) {
15240 const existing = await navigator.serviceWorker.getRegistrations().catch(() => []);
15241 const foreign = existing.find((reg) => {
15242 const url = reg.active?.scriptURL ?? reg.installing?.scriptURL ?? "";
15243 return url !== "" && url !== config.swUrl;
15244 });
15245 if (foreign) {
15246 _status = "foreign-sw";
15247 if (typeof console !== "undefined") {
15248 console.warn(
15249 "[desktop-mode] another service worker is already registered (" + foreign.scope + "); skipping desktop-mode SW. Set desktop_mode_pwa_force_replace_sw=true to override."
15250 );
15251 }
15252 return null;
15253 }
15254 }
15255 try {
15256 _registration = await navigator.serviceWorker.register(config.swUrl, {
15257 scope: "/",
15258 updateViaCache: "none"
15259 });
15260 _status = "registered";
15261 bindControllerChangeReload();
15262 return _registration;
15263 } catch (err) {
15264 _registrationFailed = true;
15265 _status = "failed";
15266 if (typeof console !== "undefined") {
15267 console.warn("[desktop-mode] SW registration failed:", err);
15268 }
15269 return null;
15270 }
15271 }
15272 function getSwRegistrationStatus() {
15273 return _status;
15274 }
15275 const PWA_INSTALL_TILE_ID = "desktop-mode-pwa-install";
15276 function isStandaloneDisplay() {
15277 if (typeof window === "undefined") {
15278 return false;
15279 }
15280 if (window.matchMedia?.("(display-mode: standalone)").matches) {
15281 return true;
15282 }
15283 const nav = window.navigator;
15284 return nav.standalone === true;
15285 }
15286 async function isLikelyInstalled() {
15287 if (isStandaloneDisplay()) {
15288 return true;
15289 }
15290 const nav = window.navigator;
15291 if (typeof nav.getInstalledRelatedApps !== "function") {
15292 return false;
15293 }
15294 try {
15295 const apps = await nav.getInstalledRelatedApps();
15296 return Array.isArray(apps) && apps.length > 0;
15297 } catch {
15298 return false;
15299 }
15300 }
15301 let _deferred = null;
15302 function installPwaInstallAffordance(siteName, showToast2) {
15303 if (typeof window === "undefined") {
15304 return;
15305 }
15306 window.removeEventListener(
15307 "beforeinstallprompt",
15308 _handleBeforeInstall
15309 );
15310 window.addEventListener(
15311 "beforeinstallprompt",
15312 _handleBeforeInstall
15313 );
15314 window.removeEventListener("appinstalled", _handleAppInstalled);
15315 window.addEventListener("appinstalled", _handleAppInstalled);
15316 function _handleBeforeInstall(ev) {
15317 ev.preventDefault();
15318 _deferred = ev;
15319 }
15320 function _handleAppInstalled() {
15321 _deferred = null;
15322 showToast2({
15323 message: sprintf(
15324 /* translators: %s: site name */
15325 __("Installed %s as an app."),
15326 siteName
15327 )
15328 });
15329 }
15330 }
15331 function getInstallTileDef(siteName, showToast2) {
15332 return {
15333 id: PWA_INSTALL_TILE_ID,
15334 title: sprintf(
15335 /* translators: %s: site name */
15336 __("Install %s as an app"),
15337 siteName
15338 ),
15339 // Dashicons class — the dock renderer prefers Dashicons
15340 // strings. `dashicons-download` is the closest match for
15341 // "install" in the WordPress glyph set without shipping
15342 // bespoke artwork.
15343 icon: "dashicons-download",
15344 onOpen: () => {
15345 void onTileClick(siteName, showToast2);
15346 }
15347 };
15348 }
15349 async function onTileClick(siteName, showToast2) {
15350 if (_deferred) {
15351 const event = _deferred;
15352 _deferred = null;
15353 try {
15354 await event.prompt();
15355 const choice = await event.userChoice;
15356 if (choice.outcome === "dismissed") {
15357 showToast2({
15358 message: __("Install cancelled.")
15359 });
15360 }
15361 } catch (err) {
15362 if (typeof console !== "undefined") {
15363 console.warn(
15364 "[desktop-mode] install prompt failed:",
15365 err
15366 );
15367 }
15368 }
15369 return;
15370 }
15371 if (await isLikelyInstalled()) {
15372 showToast2({
15373 message: sprintf(
15374 /* translators: %s: site name */
15375 __(
15376 "%s is already installed. Open it from your apps menu or home screen."
15377 ),
15378 siteName
15379 )
15380 });
15381 return;
15382 }
15383 if (getSwRegistrationStatus() === "foreign-sw") {
15384 showToast2({
15385 message: __(
15386 "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."
15387 )
15388 });
15389 return;
15390 }
15391 showToast2({
15392 message: __(
15393 "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."
15394 )
15395 });
15396 }
15397 async function promptInstall() {
15398 if (!_deferred) {
15399 return "unavailable";
15400 }
15401 const event = _deferred;
15402 _deferred = null;
15403 try {
15404 await event.prompt();
15405 const choice = await event.userChoice;
15406 return choice.outcome;
15407 } catch {
15408 return "unavailable";
15409 }
15410 }
15411 function undismissInstallHint() {
15412 Promise.resolve().then(() => state).then((m) => {
15413 m.updatePwaState({ installHintDismissed: false });
15414 });
15415 }
15416 function notify$3(options) {
15417 const intent = activity.filter(
15418 "desktop-mode/notification-requested",
15419 { ...options }
15420 );
15421 if (!intent || intent.cancel === true || !intent.title) {
15422 return () => void 0;
15423 }
15424 let dismissed = false;
15425 let dismissNative = null;
15426 let dismissToast = null;
15427 const dismiss = () => {
15428 if (dismissed) {
15429 return;
15430 }
15431 dismissed = true;
15432 if (dismissNative) {
15433 dismissNative();
15434 }
15435 if (dismissToast) {
15436 dismissToast();
15437 }
15438 };
15439 const fallback = () => {
15440 dismissToast = showToast({
15441 message: intent.body ? intent.title + " — " + intent.body : intent.title
15442 });
15443 activity.publish("desktop-mode/notification-shown", {
15444 ...intent,
15445 fallback: "toast"
15446 });
15447 };
15448 if (typeof window === "undefined" || typeof Notification === "undefined") {
15449 fallback();
15450 return dismiss;
15451 }
15452 const perm = Notification.permission;
15453 if (perm === "granted") {
15454 dismissNative = renderNative(intent);
15455 if (!dismissNative) {
15456 fallback();
15457 }
15458 return dismiss;
15459 }
15460 if (perm === "denied") {
15461 fallback();
15462 return dismiss;
15463 }
15464 void Notification.requestPermission().then((result) => {
15465 if (dismissed) {
15466 return;
15467 }
15468 if (result === "granted") {
15469 updatePwaState({ notificationsEnabled: true });
15470 dismissNative = renderNative(intent);
15471 if (!dismissNative) {
15472 fallback();
15473 }
15474 return;
15475 }
15476 fallback();
15477 });
15478 return dismiss;
15479 }
15480 function renderNative(intent) {
15481 let n = null;
15482 try {
15483 n = new Notification(intent.title, {
15484 body: intent.body,
15485 icon: intent.icon,
15486 tag: intent.tag,
15487 requireInteraction: intent.requireInteraction
15488 });
15489 } catch (err) {
15490 if (typeof console !== "undefined") {
15491 console.warn("[desktop-mode] Notification ctor threw:", err);
15492 }
15493 return null;
15494 }
15495 if (intent.onClick) {
15496 const handler = intent.onClick;
15497 n.onclick = () => {
15498 try {
15499 handler(n);
15500 } catch (hErr) {
15501 if (typeof console !== "undefined") {
15502 console.error(
15503 "[desktop-mode] notification onClick threw:",
15504 hErr
15505 );
15506 }
15507 }
15508 };
15509 }
15510 activity.publish("desktop-mode/notification-shown", {
15511 ...intent,
15512 fallback: null
15513 });
15514 return () => {
15515 if (n) {
15516 n.close();
15517 }
15518 };
15519 }
15520 async function requestNotificationPermission() {
15521 if (typeof Notification === "undefined") {
15522 return "unsupported";
15523 }
15524 if (Notification.permission !== "default") {
15525 return Notification.permission;
15526 }
15527 const result = await Notification.requestPermission();
15528 if (result === "granted") {
15529 updatePwaState({ notificationsEnabled: true });
15530 }
15531 return result;
15532 }
15533 function getNotificationPermission() {
15534 if (typeof Notification === "undefined") {
15535 return "unsupported";
15536 }
15537 return Notification.permission;
15538 }
15539 function bootstrapPwa(config, showToast2) {
15540 if (!config.pwa) {
15541 return;
15542 }
15543 initPwaState(config.pwa);
15544 installPwaInstallAffordance(
15545 config.pwa.appName || "WordPress",
15546 showToast2
15547 );
15548 void registerServiceWorker(config.pwa, {
15549 forceReplace: !!config.pwa.forceReplaceSw
15550 });
15551 }
15552 const DRAG_BRIDGE_EVENTS = {
15553 START: "desktop-mode-cross-frame-drag-start",
15554 END: "desktop-mode-cross-frame-drag-end"
15555 };
15556 function isStart(m) {
15557 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-start" && !!m.payload && typeof m.payload === "object";
15558 }
15559 function isEnd(m) {
15560 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-end";
15561 }
15562 function isPayloadRequest(m) {
15563 return !!m && typeof m === "object" && m.type === "desktop-mode-drag-payload-request";
15564 }
15565 function normalizeLegacyPayload(payload) {
15566 const obj = payload;
15567 if (obj.kind !== void 0 && obj.kind !== null) {
15568 return payload;
15569 }
15570 if (typeof obj.id === "number" && typeof obj.url === "string" && typeof obj.mime === "string") {
15571 return {
15572 kind: "attachment",
15573 id: obj.id,
15574 url: obj.url,
15575 title: typeof obj.title === "string" ? obj.title : "",
15576 alt: typeof obj.alt === "string" ? obj.alt : "",
15577 mime: obj.mime,
15578 thumbnailUrl: typeof obj.thumbnailUrl === "string" ? obj.thumbnailUrl : void 0,
15579 sizes: obj.sizes && typeof obj.sizes === "object" ? obj.sizes : void 0
15580 };
15581 }
15582 return payload;
15583 }
15584 class DragBridge {
15585 constructor() {
15586 this._payload = null;
15587 this._onMessage = (e) => {
15588 if (e.origin !== this._origin) {
15589 return;
15590 }
15591 const msg = e.data;
15592 if (isStart(msg)) {
15593 this._startDrag(msg.payload);
15594 return;
15595 }
15596 if (isEnd(msg)) {
15597 this._endDrag();
15598 return;
15599 }
15600 if (isPayloadRequest(msg) && this._payload && e.source) {
15601 try {
15602 e.source.postMessage(
15603 { type: "desktop-mode-drag-payload", payload: this._payload },
15604 this._origin
15605 );
15606 } catch {
15607 }
15608 }
15609 };
15610 this._origin = window.location.origin;
15611 window.addEventListener("message", this._onMessage);
15612 }
15613 getPayload() {
15614 return this._payload;
15615 }
15616 isDragging() {
15617 return this._payload !== null;
15618 }
15619 start(payload) {
15620 if (this._payload === payload) {
15621 return;
15622 }
15623 this._startDrag(payload);
15624 }
15625 end() {
15626 this._endDrag();
15627 }
15628 _startDrag(payload) {
15629 const normalized = normalizeLegacyPayload(payload);
15630 this._payload = normalized;
15631 document.dispatchEvent(
15632 new CustomEvent(DRAG_BRIDGE_EVENTS.START, {
15633 detail: { payload: normalized }
15634 })
15635 );
15636 }
15637 _endDrag() {
15638 if (this._payload === null) {
15639 return;
15640 }
15641 const payload = this._payload;
15642 this._payload = null;
15643 document.dispatchEvent(
15644 new CustomEvent(DRAG_BRIDGE_EVENTS.END, { detail: { payload } })
15645 );
15646 }
15647 }
15648 class DropTargetRegistry {
15649 constructor() {
15650 this._targets = /* @__PURE__ */ new Map();
15651 this._byElement = /* @__PURE__ */ new Map();
15652 }
15653 register(target2) {
15654 const prev = this._targets.get(target2.id);
15655 if (prev) {
15656 this._byElement.delete(prev.element);
15657 }
15658 this._targets.set(target2.id, target2);
15659 this._byElement.set(target2.element, target2);
15660 return () => {
15661 const cur = this._targets.get(target2.id);
15662 if (cur === target2) {
15663 this._targets.delete(target2.id);
15664 this._byElement.delete(target2.element);
15665 }
15666 };
15667 }
15668 list() {
15669 return Array.from(this._targets.values());
15670 }
15671 clear() {
15672 this._targets.clear();
15673 this._byElement.clear();
15674 }
15675 /**
15676 * Find the deepest registered target whose element is `el` or an
15677 * ancestor of `el`. Walks the DOM tree once (O(depth)).
15678 *
15679 * Window claim boundary: if the walk crosses a `.desktop-mode-window`
15680 * element BEFORE finding a registered target, hit-testing stops
15681 * there and returns null. This is the rule that makes "drag over
15682 * a Gutenberg admin window" produce reject feedback instead of
15683 * silently routing the drop to the wallpaper canvas underneath.
15684 *
15685 * A window can opt INTO accepting drops by registering a target
15686 * on its own body (e.g. Recycle Bin's `[data-desktop-mode-recycle-bin-root]`):
15687 * since that element sits inside the window, the walk hits it
15688 * before reaching the window boundary and the body's target wins.
15689 */
15690 hitTest(el) {
15691 let cur = el;
15692 while (cur) {
15693 if (cur instanceof HTMLElement) {
15694 const t = this._byElement.get(cur);
15695 if (t) {
15696 return t;
15697 }
15698 if (cur.classList.contains("desktop-mode-window")) {
15699 return null;
15700 }
15701 }
15702 cur = cur.parentElement;
15703 }
15704 return null;
15705 }
15706 /**
15707 * Convenience: pick the target at viewport `(clientX, clientY)`.
15708 * Caller is responsible for hiding any obscuring ghost element
15709 * before calling — see `GhostHandle.withHidden()`.
15710 */
15711 hitTestPoint(clientX, clientY) {
15712 const el = document.elementFromPoint(clientX, clientY);
15713 const target2 = this.hitTest(el);
15714 return { target: target2, element: el, accepted: false };
15715 }
15716 }
15717 const GHOST_CLASS = "desktop-mode-drag-ghost";
15718 const GHOST_ACCEPT_CLASS = "desktop-mode-drag-ghost--accept";
15719 const GHOST_REJECT_CLASS = "desktop-mode-drag-ghost--reject";
15720 const HINT_CLASS = "desktop-mode-drag-hint";
15721 const HINT_ACCEPT_CLASS = "desktop-mode-drag-hint--accept";
15722 const HINT_REJECT_CLASS = "desktop-mode-drag-hint--reject";
15723 const HINT_NEUTRAL_CLASS = "desktop-mode-drag-hint--neutral";
15724 const HINT_OFFSET_X = 16;
15725 const HINT_OFFSET_Y = 18;
15726 function mountGhost(payload, clientX, clientY) {
15727 const ghost = buildGhost(payload);
15728 const offsetX = payload.ghost?.offsetX ?? defaultOffsetX(payload.source);
15729 const offsetY = payload.ghost?.offsetY ?? defaultOffsetY(payload.source);
15730 ghost.classList.add(GHOST_CLASS);
15731 ghost.setAttribute("aria-hidden", "true");
15732 ghost.style.position = "fixed";
15733 ghost.style.left = "0";
15734 ghost.style.top = "0";
15735 ghost.style.margin = "0";
15736 ghost.style.pointerEvents = "none";
15737 ghost.style.zIndex = "2147483647";
15738 ghost.style.willChange = "transform";
15739 document.body.appendChild(ghost);
15740 const labels = resolveHintLabels(payload);
15741 const hint = labels ? buildHintChip() : null;
15742 if (hint) {
15743 document.body.appendChild(hint);
15744 }
15745 const handle = {
15746 get element() {
15747 return ghost;
15748 },
15749 moveTo(cx, cy) {
15750 ghost.style.transform = `translate3d(${cx - offsetX}px, ${cy - offsetY}px, 0)`;
15751 if (hint) {
15752 hint.style.transform = `translate3d(${cx + HINT_OFFSET_X}px, ${cy + HINT_OFFSET_Y}px, 0)`;
15753 }
15754 },
15755 setMode(mode, overrides) {
15756 ghost.classList.remove(GHOST_ACCEPT_CLASS, GHOST_REJECT_CLASS);
15757 if (mode === "accept") {
15758 ghost.classList.add(GHOST_ACCEPT_CLASS);
15759 } else if (mode === "reject") {
15760 ghost.classList.add(GHOST_REJECT_CLASS);
15761 }
15762 if (hint && labels) {
15763 hint.classList.remove(
15764 HINT_ACCEPT_CLASS,
15765 HINT_REJECT_CLASS,
15766 HINT_NEUTRAL_CLASS
15767 );
15768 if (mode === "accept") {
15769 hint.classList.add(HINT_ACCEPT_CLASS);
15770 hint.textContent = overrides?.acceptLabel ?? labels.accept;
15771 } else if (mode === "reject") {
15772 hint.classList.add(HINT_REJECT_CLASS);
15773 hint.textContent = labels.reject;
15774 } else {
15775 hint.classList.add(HINT_NEUTRAL_CLASS);
15776 hint.textContent = labels.neutral;
15777 }
15778 hint.hidden = !hint.textContent;
15779 }
15780 },
15781 withHidden(fn) {
15782 const prevG = ghost.style.visibility;
15783 const prevH = hint?.style.visibility ?? "";
15784 ghost.style.visibility = "hidden";
15785 if (hint) {
15786 hint.style.visibility = "hidden";
15787 }
15788 try {
15789 return fn();
15790 } finally {
15791 ghost.style.visibility = prevG;
15792 if (hint) {
15793 hint.style.visibility = prevH;
15794 }
15795 }
15796 },
15797 dispose() {
15798 if (ghost.isConnected) {
15799 ghost.remove();
15800 }
15801 if (hint?.isConnected) {
15802 hint.remove();
15803 }
15804 }
15805 };
15806 handle.moveTo(clientX, clientY);
15807 handle.setMode("neutral");
15808 return handle;
15809 }
15810 function buildHintChip() {
15811 const chip = document.createElement("div");
15812 chip.className = HINT_CLASS;
15813 chip.setAttribute("aria-hidden", "true");
15814 chip.setAttribute("role", "presentation");
15815 chip.style.position = "fixed";
15816 chip.style.left = "0";
15817 chip.style.top = "0";
15818 chip.style.margin = "0";
15819 chip.style.pointerEvents = "none";
15820 chip.style.zIndex = "2147483647";
15821 chip.style.willChange = "transform";
15822 return chip;
15823 }
15824 function resolveHintLabels(payload) {
15825 const cfg = payload.ghost?.hint;
15826 if (cfg?.hidden) {
15827 return null;
15828 }
15829 return {
15830 accept: cfg?.accept ?? defaultAcceptLabel(payload),
15831 reject: cfg?.reject ?? defaultRejectLabel(),
15832 neutral: cfg?.neutral ?? defaultNeutralLabel(payload)
15833 };
15834 }
15835 function defaultAcceptLabel(payload) {
15836 if (payload.type === "shortcut") {
15837 return __("Drop here to create shortcut", "desktop-mode");
15838 }
15839 if (payload.type === "desktop-file") {
15840 return __("Drop here to move", "desktop-mode");
15841 }
15842 return __("Drop here", "desktop-mode");
15843 }
15844 function defaultRejectLabel(_payload) {
15845 return __("Can’t drop here", "desktop-mode");
15846 }
15847 function defaultNeutralLabel(payload) {
15848 if (payload.type === "shortcut") {
15849 return __(
15850 "Drop on the desktop or a folder",
15851 "desktop-mode"
15852 );
15853 }
15854 if (payload.type === "desktop-file") {
15855 return __("Drop in a folder", "desktop-mode");
15856 }
15857 return "";
15858 }
15859 function buildGhost(payload) {
15860 if (payload.ghost?.element) {
15861 return payload.ghost.element;
15862 }
15863 const clone = payload.source.cloneNode(true);
15864 clone.removeAttribute("id");
15865 const rect = payload.source.getBoundingClientRect();
15866 clone.style.width = `${rect.width}px`;
15867 clone.style.height = `${rect.height}px`;
15868 return clone;
15869 }
15870 function defaultOffsetX(source) {
15871 return source.offsetWidth / 2;
15872 }
15873 function defaultOffsetY(source) {
15874 return source.offsetHeight / 2;
15875 }
15876 let _installed$2 = false;
15877 function installRecovery(cancelActive) {
15878 if (_installed$2) {
15879 return;
15880 }
15881 _installed$2 = true;
15882 document.addEventListener("keydown", (e) => {
15883 if (e.key === "Escape") {
15884 cancelActive("escape");
15885 }
15886 });
15887 window.addEventListener("blur", () => {
15888 cancelActive("blur");
15889 });
15890 document.addEventListener("visibilitychange", () => {
15891 if (document.hidden) {
15892 cancelActive("visibility");
15893 }
15894 });
15895 }
15896 const DRAG_THRESHOLD_PX = 4;
15897 const DRAG_EVENTS = {
15898 START: "desktop-mode.drag.start",
15899 MOVE: "desktop-mode.drag.move",
15900 ENTER: "desktop-mode.drag.enter",
15901 LEAVE: "desktop-mode.drag.leave",
15902 REJECTED: "desktop-mode.drag.rejected",
15903 COMMIT: "desktop-mode.drag.commit",
15904 CANCEL: "desktop-mode.drag.cancel",
15905 END: "desktop-mode.drag.end"
15906 };
15907 const SOURCE_DRAGGING_CLASS = "desktop-mode-file-tile--dragging";
15908 const TARGET_DROP_ACTIVE_CLASS = "desktop-mode-file-tile--drop-target";
15909 const TRASH_DROP_ACTIVE_ATTR$1 = "data-desktop-mode-trash-drop-active";
15910 const FILES_DROP_ACTIVE_ATTR = "data-files-drop-active";
15911 const BODY_DRAGGING_ATTR = "data-desktop-mode-dragging";
15912 const BODY_DRAG_TYPE_ATTR = "data-desktop-mode-drag-type";
15913 const BODY_DRAG_MODE_ATTR = "data-desktop-mode-drag-mode";
15914 class DragManager {
15915 constructor() {
15916 this._registry = new DropTargetRegistry();
15917 this._active = null;
15918 this._docListenersAttached = false;
15919 this._lastLiftedEndAt = 0;
15920 this._onPointerMove = (e) => {
15921 const session = this._active;
15922 if (!session || session._pointerId !== e.pointerId) {
15923 return;
15924 }
15925 const dx = e.clientX - session._origin.clientX;
15926 const dy = e.clientY - session._origin.clientY;
15927 if (!session._lifted) {
15928 if (Math.abs(dx) < DRAG_THRESHOLD_PX && Math.abs(dy) < DRAG_THRESHOLD_PX) {
15929 return;
15930 }
15931 this._lift(session, e);
15932 }
15933 if (!session._ghost) {
15934 return;
15935 }
15936 session._ghost.moveTo(e.clientX, e.clientY);
15937 this._updateHover(session, e.clientX, e.clientY);
15938 dispatchOnDocument(DRAG_EVENTS.MOVE, {
15939 payload: session.payload,
15940 clientX: e.clientX,
15941 clientY: e.clientY
15942 });
15943 };
15944 this._onPointerUp = (e) => {
15945 const session = this._active;
15946 if (!session || session._pointerId !== e.pointerId) {
15947 return;
15948 }
15949 if (!session._lifted) {
15950 session._finished = true;
15951 this._active = null;
15952 try {
15953 session._callbacks.onClickOnly?.();
15954 } catch (err) {
15955 console.error("[desktop-mode] drag onClickOnly threw:", err);
15956 }
15957 return;
15958 }
15959 const hit = this._hitTestNow(session, e.clientX, e.clientY);
15960 if (hit && hit.accepted && hit.target) {
15961 this._commit(session, hit.target, e.clientX, e.clientY);
15962 return;
15963 }
15964 this._cancel(session, hit && hit.target ? "rejected" : "no-target");
15965 };
15966 this._onPointerCancel = (e) => {
15967 const session = this._active;
15968 if (!session || session._pointerId !== e.pointerId) {
15969 return;
15970 }
15971 this._cancel(session, "pointercancel");
15972 };
15973 }
15974 start(opts) {
15975 if (this._active) {
15976 return null;
15977 }
15978 if (opts.origin.button !== 0) {
15979 return null;
15980 }
15981 const session = {
15982 payload: opts.payload,
15983 isFinished: () => session._finished,
15984 cancel: (reason) => this._cancel(session, reason ?? "caller"),
15985 _origin: opts.origin,
15986 _pointerId: opts.origin.pointerId,
15987 _lifted: false,
15988 _finished: false,
15989 _callbacks: {
15990 onClickOnly: opts.onClickOnly,
15991 onCancel: opts.onCancel,
15992 onCommit: opts.onCommit
15993 },
15994 _ghost: null,
15995 _currentTarget: null,
15996 _currentAccepted: false
15997 };
15998 this._active = session;
15999 this._ensureDocListeners();
16000 installRecovery((reason) => {
16001 if (this._active) {
16002 this._cancel(this._active, reason);
16003 }
16004 });
16005 return session;
16006 }
16007 registerDropTarget(target2) {
16008 return this._registry.register(target2);
16009 }
16010 isDragging() {
16011 return this._active !== null && this._active._lifted;
16012 }
16013 /**
16014 * Whether a real (lifted) drag ended within `withinMs` of now.
16015 * Surfaces that bind plain `click` listeners use this to ignore
16016 * the synthesized click that fires after a drop. 500 ms is a
16017 * generous default — browsers fire the click within 10–50 ms of
16018 * pointerup, but plugins may chain post-drag work into a
16019 * `requestAnimationFrame` and call back into a click-driven API.
16020 *
16021 * @public
16022 * @since 0.8.5
16023 */
16024 recentlyEndedDrag(withinMs = 500) {
16025 if (this._lastLiftedEndAt === 0) {
16026 return false;
16027 }
16028 return Date.now() - this._lastLiftedEndAt < withinMs;
16029 }
16030 getActive() {
16031 return this._active;
16032 }
16033 debug() {
16034 return {
16035 findOrphans: () => findOrphans(),
16036 listTargets: () => this._registry.list()
16037 };
16038 }
16039 // -----------------------------------------------------------------
16040 // Internals
16041 // -----------------------------------------------------------------
16042 _ensureDocListeners() {
16043 if (this._docListenersAttached) {
16044 return;
16045 }
16046 this._docListenersAttached = true;
16047 document.addEventListener("pointermove", this._onPointerMove, true);
16048 document.addEventListener("pointerup", this._onPointerUp, true);
16049 document.addEventListener("pointercancel", this._onPointerCancel, true);
16050 }
16051 _lift(session, e) {
16052 session._lifted = true;
16053 session.payload.source.classList.add(SOURCE_DRAGGING_CLASS);
16054 session._ghost = mountGhost(session.payload, e.clientX, e.clientY);
16055 if (typeof document !== "undefined" && document.body) {
16056 document.body.setAttribute(BODY_DRAGGING_ATTR, "");
16057 document.body.setAttribute(
16058 BODY_DRAG_TYPE_ATTR,
16059 String(session.payload.type)
16060 );
16061 document.body.setAttribute(BODY_DRAG_MODE_ATTR, "neutral");
16062 }
16063 dispatchOnDocument(DRAG_EVENTS.START, { payload: session.payload });
16064 }
16065 _hitTestNow(session, clientX, clientY) {
16066 const run = () => {
16067 const el = document.elementFromPoint(clientX, clientY);
16068 const target2 = this._registry.hitTest(el);
16069 if (!target2) {
16070 return { target: null, accepted: false };
16071 }
16072 let accepted = false;
16073 try {
16074 accepted = target2.accept(session.payload);
16075 } catch (err) {
16076 console.error("[desktop-mode] drop target accept() threw:", target2.id, err);
16077 }
16078 return { target: target2, accepted };
16079 };
16080 if (session._ghost) {
16081 return session._ghost.withHidden(run);
16082 }
16083 return run();
16084 }
16085 _updateHover(session, clientX, clientY) {
16086 const next = this._hitTestNow(session, clientX, clientY);
16087 const prevTarget = session._currentTarget;
16088 if (next.target === prevTarget && next.accepted === session._currentAccepted) {
16089 return;
16090 }
16091 if (prevTarget) {
16092 fireLeave(prevTarget, session);
16093 }
16094 session._currentTarget = next.target;
16095 session._currentAccepted = next.accepted;
16096 let mode;
16097 if (next.target) {
16098 if (next.accepted) {
16099 fireEnter(next.target, session);
16100 session._ghost?.setMode("accept", {
16101 acceptLabel: next.target.acceptLabel
16102 });
16103 mode = "accept";
16104 } else {
16105 session._ghost?.setMode("reject");
16106 dispatchOnDocument(DRAG_EVENTS.REJECTED, {
16107 payload: session.payload,
16108 targetId: next.target.id
16109 });
16110 mode = "reject";
16111 }
16112 } else {
16113 session._ghost?.setMode("reject");
16114 mode = "reject";
16115 }
16116 if (typeof document !== "undefined" && document.body) {
16117 document.body.setAttribute(BODY_DRAG_MODE_ATTR, mode);
16118 }
16119 }
16120 _commit(session, target2, clientX, clientY) {
16121 session._finished = true;
16122 this._lastLiftedEndAt = Date.now();
16123 fireLeave(target2, session);
16124 this._cleanupDom(session);
16125 const prevActive = this._active;
16126 this._active = null;
16127 try {
16128 void target2.onDrop(session, { clientX, clientY });
16129 } catch (err) {
16130 console.error("[desktop-mode] drop target onDrop threw:", target2.id, err);
16131 }
16132 try {
16133 session._callbacks.onCommit?.(target2);
16134 } catch (err) {
16135 console.error("[desktop-mode] drag onCommit threw:", err);
16136 }
16137 dispatchOnDocument(DRAG_EVENTS.COMMIT, {
16138 payload: session.payload,
16139 targetId: target2.id
16140 });
16141 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason: "commit" });
16142 if (this._active === prevActive) {
16143 this._active = null;
16144 }
16145 }
16146 _cancel(session, reason) {
16147 if (session._finished) {
16148 return;
16149 }
16150 session._finished = true;
16151 if (session._lifted) {
16152 this._lastLiftedEndAt = Date.now();
16153 }
16154 if (session._currentTarget) {
16155 fireLeave(session._currentTarget, session);
16156 }
16157 this._cleanupDom(session);
16158 this._active = null;
16159 try {
16160 session._callbacks.onCancel?.(reason);
16161 } catch (err) {
16162 console.error("[desktop-mode] drag onCancel threw:", err);
16163 }
16164 dispatchOnDocument(DRAG_EVENTS.CANCEL, { payload: session.payload, reason });
16165 dispatchOnDocument(DRAG_EVENTS.END, { payload: session.payload, reason });
16166 }
16167 _cleanupDom(session) {
16168 try {
16169 session.payload.source.classList.remove(SOURCE_DRAGGING_CLASS);
16170 } catch {
16171 }
16172 session._ghost?.dispose();
16173 session._ghost = null;
16174 session._currentTarget = null;
16175 session._currentAccepted = false;
16176 if (typeof document !== "undefined" && document.body) {
16177 document.body.removeAttribute(BODY_DRAGGING_ATTR);
16178 document.body.removeAttribute(BODY_DRAG_TYPE_ATTR);
16179 document.body.removeAttribute(BODY_DRAG_MODE_ATTR);
16180 }
16181 scrubOrphans();
16182 }
16183 }
16184 function dispatchOnDocument(type, detail) {
16185 if (typeof document === "undefined") {
16186 return;
16187 }
16188 document.dispatchEvent(new CustomEvent(type, { detail }));
16189 }
16190 function fireEnter(target2, session) {
16191 try {
16192 target2.onEnter?.(session);
16193 } catch (err) {
16194 console.error("[desktop-mode] drop target onEnter threw:", target2.id, err);
16195 }
16196 dispatchOnDocument(DRAG_EVENTS.ENTER, {
16197 payload: session.payload,
16198 targetId: target2.id
16199 });
16200 }
16201 function fireLeave(target2, session) {
16202 try {
16203 target2.onLeave?.(session);
16204 } catch (err) {
16205 console.error("[desktop-mode] drop target onLeave threw:", target2.id, err);
16206 }
16207 dispatchOnDocument(DRAG_EVENTS.LEAVE, {
16208 payload: session.payload,
16209 targetId: target2.id
16210 });
16211 }
16212 function findOrphans() {
16213 if (typeof document === "undefined") {
16214 return [];
16215 }
16216 const out = [];
16217 for (const sel of [
16218 `.${SOURCE_DRAGGING_CLASS}`,
16219 `.${TARGET_DROP_ACTIVE_CLASS}`,
16220 `[${TRASH_DROP_ACTIVE_ATTR$1}]`,
16221 `[${FILES_DROP_ACTIVE_ATTR}]`
16222 ]) {
16223 document.querySelectorAll(sel).forEach((el) => out.push(el));
16224 }
16225 return out;
16226 }
16227 function scrubOrphans() {
16228 for (const el of findOrphans()) {
16229 el.classList.remove(SOURCE_DRAGGING_CLASS, TARGET_DROP_ACTIVE_CLASS);
16230 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR$1);
16231 el.removeAttribute(FILES_DROP_ACTIVE_ATTR);
16232 }
16233 }
16234 const TARGET_ID_PREFIX = "desktop-mode-iframe-drop-";
16235 const IFRAME_SELECTOR = "iframe.desktop-mode-window__iframe";
16236 const DROP_ACTIVE_ATTR = "data-desktop-mode-iframe-drop-active";
16237 let _installed$1 = false;
16238 let _dragManager = null;
16239 const _suppressedIframes = /* @__PURE__ */ new Map();
16240 const _activeRegistrations = /* @__PURE__ */ new Map();
16241 let _bridgeInterceptPayload = null;
16242 let _lastHoveredBridgeIframe = null;
16243 function suppressIframePointerEventsBridge() {
16244 const iframes = document.querySelectorAll(
16245 IFRAME_SELECTOR
16246 );
16247 iframes.forEach((iframe) => {
16248 if (_suppressedIframes.has(iframe)) {
16249 return;
16250 }
16251 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
16252 iframe.style.pointerEvents = "none";
16253 });
16254 }
16255 function restoreIframePointerEvents() {
16256 _suppressedIframes.forEach((prev, iframe) => {
16257 iframe.style.pointerEvents = prev;
16258 });
16259 _suppressedIframes.clear();
16260 }
16261 function findIframeAtCursor(clientX, clientY) {
16262 const el = document.elementFromPoint(clientX, clientY);
16263 if (!el) {
16264 return null;
16265 }
16266 const win = el.closest(".desktop-mode-window");
16267 if (!(win instanceof HTMLElement)) {
16268 return null;
16269 }
16270 const iframe = win.querySelector(IFRAME_SELECTOR);
16271 return iframe instanceof HTMLIFrameElement ? iframe : null;
16272 }
16273 const onBridgeDragOver = (e) => {
16274 if (!_bridgeInterceptPayload) {
16275 return;
16276 }
16277 e.preventDefault();
16278 if (e.dataTransfer) {
16279 e.dataTransfer.dropEffect = "copy";
16280 }
16281 const iframe = findIframeAtCursor(e.clientX, e.clientY);
16282 if (iframe === _lastHoveredBridgeIframe) {
16283 return;
16284 }
16285 if (_lastHoveredBridgeIframe) {
16286 postIntoIframe(_lastHoveredBridgeIframe, {
16287 type: "desktop-mode-drag-leave"
16288 });
16289 }
16290 _lastHoveredBridgeIframe = iframe;
16291 if (iframe) {
16292 postIntoIframe(iframe, {
16293 type: "desktop-mode-drag-over",
16294 payload: _bridgeInterceptPayload
16295 });
16296 }
16297 };
16298 const onBridgeDrop = (e) => {
16299 if (!_bridgeInterceptPayload) {
16300 return;
16301 }
16302 e.preventDefault();
16303 e.stopPropagation();
16304 if (typeof e.stopImmediatePropagation === "function") {
16305 e.stopImmediatePropagation();
16306 }
16307 const iframe = findIframeAtCursor(e.clientX, e.clientY);
16308 const payload = _bridgeInterceptPayload;
16309 stopBridgeIntercept();
16310 if (!iframe) {
16311 return;
16312 }
16313 const rect = iframe.getBoundingClientRect();
16314 postIntoIframe(iframe, {
16315 type: "desktop-mode-drop",
16316 payload,
16317 position: {
16318 x: e.clientX - rect.left,
16319 y: e.clientY - rect.top
16320 }
16321 });
16322 };
16323 const onBridgeDragEnd = () => {
16324 stopBridgeIntercept();
16325 };
16326 function startBridgeIntercept(payload) {
16327 if (_bridgeInterceptPayload) {
16328 _bridgeInterceptPayload = payload;
16329 return;
16330 }
16331 _bridgeInterceptPayload = payload;
16332 suppressIframePointerEventsBridge();
16333 document.addEventListener("dragover", onBridgeDragOver, true);
16334 document.addEventListener("drop", onBridgeDrop, true);
16335 document.addEventListener("dragend", onBridgeDragEnd, true);
16336 }
16337 function stopBridgeIntercept() {
16338 if (!_bridgeInterceptPayload) {
16339 return;
16340 }
16341 _bridgeInterceptPayload = null;
16342 if (_lastHoveredBridgeIframe) {
16343 postIntoIframe(_lastHoveredBridgeIframe, {
16344 type: "desktop-mode-drag-leave"
16345 });
16346 _lastHoveredBridgeIframe = null;
16347 }
16348 document.removeEventListener("dragover", onBridgeDragOver, true);
16349 document.removeEventListener("drop", onBridgeDrop, true);
16350 document.removeEventListener("dragend", onBridgeDragEnd, true);
16351 restoreIframePointerEvents();
16352 }
16353 function extractBridgePayload(payload) {
16354 if (!payload || typeof payload !== "object") {
16355 return void 0;
16356 }
16357 const obj = payload;
16358 if (obj.type !== "shortcut" && obj.type !== "desktop-file") {
16359 return void 0;
16360 }
16361 const data = obj.data;
16362 return data?.bridgePayload;
16363 }
16364 function postIntoIframe(iframe, msg) {
16365 const w = iframe.contentWindow;
16366 if (!w) {
16367 return;
16368 }
16369 try {
16370 w.postMessage(msg, window.location.origin);
16371 } catch {
16372 }
16373 }
16374 function registerDropTargetFor(dragManager, iframe, target2, windowId) {
16375 return dragManager.registerDropTarget({
16376 id: `${TARGET_ID_PREFIX}${windowId}`,
16377 element: target2,
16378 accept: (payload) => !!extractBridgePayload(payload),
16379 onEnter: (session) => {
16380 const bridge = extractBridgePayload(session.payload);
16381 if (!bridge) {
16382 return;
16383 }
16384 target2.setAttribute(DROP_ACTIVE_ATTR, "");
16385 postIntoIframe(iframe, {
16386 type: "desktop-mode-drag-over",
16387 payload: bridge
16388 });
16389 },
16390 onLeave: () => {
16391 target2.removeAttribute(DROP_ACTIVE_ATTR);
16392 postIntoIframe(iframe, { type: "desktop-mode-drag-leave" });
16393 },
16394 onDrop: (session, ev) => {
16395 target2.removeAttribute(DROP_ACTIVE_ATTR);
16396 const bridge = extractBridgePayload(session.payload);
16397 if (!bridge) {
16398 return;
16399 }
16400 const rect = iframe.getBoundingClientRect();
16401 postIntoIframe(iframe, {
16402 type: "desktop-mode-drop",
16403 payload: bridge,
16404 position: {
16405 x: ev.clientX - rect.left,
16406 y: ev.clientY - rect.top
16407 }
16408 });
16409 }
16410 });
16411 }
16412 function deriveWindowIdFromIframe(iframe) {
16413 let cur = iframe.parentElement;
16414 while (cur) {
16415 if (cur.id.startsWith("wp-window-")) {
16416 return cur.id.slice("wp-window-".length);
16417 }
16418 cur = cur.parentElement;
16419 }
16420 return `unknown-${Math.random().toString(36).slice(2, 10)}`;
16421 }
16422 function onDragStart(payload) {
16423 const dragManager = _dragManager;
16424 if (!dragManager) {
16425 return;
16426 }
16427 const iframes = document.querySelectorAll(IFRAME_SELECTOR);
16428 const isBridgeable = !!extractBridgePayload(payload);
16429 console.info(
16430 "[desktop-mode] drag-start: suppressing %d iframe(s); bridgeable=%s",
16431 iframes.length,
16432 isBridgeable,
16433 payload
16434 );
16435 iframes.forEach((iframe) => {
16436 if (!_suppressedIframes.has(iframe)) {
16437 _suppressedIframes.set(iframe, iframe.style.pointerEvents);
16438 iframe.style.pointerEvents = "none";
16439 }
16440 if (!isBridgeable) {
16441 return;
16442 }
16443 if (_activeRegistrations.has(iframe)) {
16444 return;
16445 }
16446 const dropTargetEl = iframe.parentElement;
16447 if (!dropTargetEl) {
16448 return;
16449 }
16450 const windowId = deriveWindowIdFromIframe(iframe);
16451 const deregister = registerDropTargetFor(
16452 dragManager,
16453 iframe,
16454 dropTargetEl,
16455 windowId
16456 );
16457 _activeRegistrations.set(iframe, deregister);
16458 });
16459 }
16460 function onDragEnd() {
16461 _suppressedIframes.forEach((prev, iframe) => {
16462 iframe.style.pointerEvents = prev;
16463 });
16464 _suppressedIframes.clear();
16465 _activeRegistrations.forEach((deregister) => {
16466 try {
16467 deregister();
16468 } catch {
16469 }
16470 });
16471 _activeRegistrations.clear();
16472 }
16473 function installIframeDropTargets(dragManager) {
16474 if (_installed$1) {
16475 return;
16476 }
16477 _installed$1 = true;
16478 _dragManager = dragManager;
16479 document.addEventListener(DRAG_EVENTS.START, (e) => {
16480 const detail = e.detail;
16481 onDragStart(detail?.payload);
16482 });
16483 document.addEventListener(DRAG_EVENTS.END, () => {
16484 onDragEnd();
16485 });
16486 document.addEventListener(DRAG_BRIDGE_EVENTS.START, (e) => {
16487 const detail = e.detail;
16488 if (!detail?.payload) {
16489 return;
16490 }
16491 startBridgeIntercept(detail.payload);
16492 });
16493 document.addEventListener(DRAG_BRIDGE_EVENTS.END, () => {
16494 stopBridgeIntercept();
16495 });
16496 addAction(
16497 HOOKS.WINDOW_CLOSED,
16498 "desktop-mode/drag/iframe-drop-targets-window-close",
16499 () => {
16500 for (const [iframe] of Array.from(_suppressedIframes)) {
16501 if (!iframe.isConnected) {
16502 _suppressedIframes.delete(iframe);
16503 }
16504 }
16505 for (const [iframe, deregister] of Array.from(_activeRegistrations)) {
16506 if (!iframe.isConnected) {
16507 try {
16508 deregister();
16509 } catch {
16510 }
16511 _activeRegistrations.delete(iframe);
16512 }
16513 }
16514 }
16515 );
16516 window.__desktopModeIframeDropDebug = () => ({
16517 installed: _installed$1,
16518 iframesInDom: document.querySelectorAll(IFRAME_SELECTOR).length,
16519 suppressedCount: _suppressedIframes.size,
16520 registeredCount: _activeRegistrations.size,
16521 suppressedIframeIds: Array.from(_suppressedIframes.keys()).map(
16522 deriveWindowIdFromIframe
16523 )
16524 });
16525 }
16526 function collectOpenables() {
16527 const desktop = window.wp?.desktop;
16528 if (!desktop) {
16529 return [];
16530 }
16531 const wm = desktop.windowManager;
16532 const config = desktop.config;
16533 if (!wm || !config) {
16534 return [];
16535 }
16536 const items = [];
16537 const fromMenu = (item, group) => ({
16538 id: item.id,
16539 label: item.title,
16540 description: group,
16541 icon: item.icon,
16542 open: () => wm.open({
16543 id: item.id,
16544 baseId: item.id,
16545 url: item.url,
16546 title: item.title,
16547 icon: item.icon
16548 })
16549 });
16550 for (const item of config.dockItems ?? []) {
16551 items.push(fromMenu(item, "Admin menu"));
16552 }
16553 const filtered = applyFilters(
16554 "desktop-mode.open-command.items",
16555 items
16556 );
16557 return Array.isArray(filtered) ? filtered : items;
16558 }
16559 const openCommand = {
16560 slug: "open",
16561 label: "Open",
16562 description: "Open an admin page or registered window.",
16563 hint: "[window]",
16564 icon: "dashicons-external",
16565 /**
16566 * Suggest matching windows as the user types args. Simple
16567 * case-insensitive substring match against label AND id so
16568 * "add" finds "Add New Post" and "jorvy" finds Jorvy whether
16569 * the plugin listed it with a friendly label or the slug.
16570 */
16571 suggest(args) {
16572 const q = args.trim().toLowerCase();
16573 const list2 = collectOpenables();
16574 const hits = q === "" ? list2 : list2.filter(
16575 (w) => w.label.toLowerCase().includes(q) || w.id.toLowerCase().includes(q)
16576 );
16577 return hits.slice(0, 12).map((w) => ({
16578 value: w.label,
16579 label: w.label,
16580 description: w.description,
16581 icon: w.icon ?? "dashicons-external"
16582 }));
16583 },
16584 run(args, ctx) {
16585 const q = args.trim();
16586 if (!q) {
16587 return "Type the name of a window to open, for example `/open Posts`.";
16588 }
16589 const list2 = collectOpenables();
16590 const ql = q.toLowerCase();
16591 const match = list2.find((w) => w.label.toLowerCase() === ql || w.id.toLowerCase() === ql) ?? list2.find(
16592 (w) => w.label.toLowerCase().includes(ql) || w.id.toLowerCase().includes(ql)
16593 );
16594 if (!match) {
16595 return `No window matching **${q}** — try \`/open\` alone to see available options.`;
16596 }
16597 match.open();
16598 ctx.close();
16599 }
16600 };
16601 function registerBuiltInCommands() {
16602 registerCommand(openCommand);
16603 }
16604 const palettes = [];
16605 const listeners$2 = /* @__PURE__ */ new Set();
16606 function registerPalette(p) {
16607 if (!p || typeof p.id !== "string" || p.id === "") {
16608 return () => {
16609 };
16610 }
16611 if (typeof p.open !== "function" || typeof p.close !== "function" || typeof p.isOpen !== "function") {
16612 return () => {
16613 };
16614 }
16615 const idx = palettes.findIndex((x) => x.id === p.id);
16616 if (idx >= 0) {
16617 palettes[idx] = p;
16618 } else {
16619 palettes.push(p);
16620 }
16621 notify$2();
16622 return () => {
16623 const i = palettes.findIndex((x) => x.id === p.id);
16624 if (i >= 0) {
16625 palettes.splice(i, 1);
16626 notify$2();
16627 }
16628 };
16629 }
16630 function unregisterPalette(id) {
16631 const idx = palettes.findIndex((x) => x.id === id);
16632 if (idx >= 0) {
16633 palettes.splice(idx, 1);
16634 notify$2();
16635 }
16636 }
16637 function listPalettes() {
16638 return palettes.slice();
16639 }
16640 function notify$2() {
16641 for (const cb of Array.from(listeners$2)) {
16642 try {
16643 cb();
16644 } catch (err) {
16645 if (typeof console !== "undefined") {
16646 console.error("[desktop-mode] palette-registry listener threw:", err);
16647 }
16648 }
16649 }
16650 }
16651 function cyclePalettes() {
16652 if (palettes.length === 0) {
16653 return;
16654 }
16655 const cur = palettes.findIndex((p) => {
16656 try {
16657 return p.isOpen();
16658 } catch {
16659 return false;
16660 }
16661 });
16662 if (cur === -1) {
16663 try {
16664 palettes[0].open();
16665 } catch {
16666 }
16667 return;
16668 }
16669 try {
16670 palettes[cur].close();
16671 } catch {
16672 }
16673 const next = cur + 1;
16674 if (next < palettes.length) {
16675 try {
16676 palettes[next].open();
16677 } catch {
16678 }
16679 }
16680 }
16681 function openPaletteOnly(id) {
16682 const target2 = palettes.find((p) => p.id === id);
16683 if (!target2) {
16684 return;
16685 }
16686 for (const p of palettes) {
16687 if (p.id !== id) {
16688 try {
16689 if (p.isOpen()) {
16690 p.close();
16691 }
16692 } catch {
16693 }
16694 }
16695 }
16696 try {
16697 target2.open();
16698 } catch {
16699 }
16700 }
16701 let installed$1 = false;
16702 function installPaletteShortcut() {
16703 if (installed$1) {
16704 return;
16705 }
16706 installed$1 = true;
16707 document.addEventListener(
16708 "keydown",
16709 (e) => {
16710 if (!(e.metaKey || e.ctrlKey) || e.key !== "k") {
16711 return;
16712 }
16713 if (e.shiftKey || e.altKey) {
16714 return;
16715 }
16716 e.preventDefault();
16717 e.stopImmediatePropagation();
16718 cyclePalettes();
16719 },
16720 true
16721 );
16722 const origin = window.location.origin;
16723 window.addEventListener("message", (e) => {
16724 if (e.origin !== origin) {
16725 return;
16726 }
16727 const data = e.data;
16728 if (data && data.type === "desktop-mode-palette-cycle") {
16729 cyclePalettes();
16730 }
16731 });
16732 }
16733 const suppliers = /* @__PURE__ */ new Map();
16734 const subscribers = /* @__PURE__ */ new Map();
16735 let booted$2 = false;
16736 const heartbeat = {
16737 contribute(field, supplier) {
16738 suppliers.set(field, supplier);
16739 return () => {
16740 if (suppliers.get(field) === supplier) {
16741 suppliers.delete(field);
16742 }
16743 };
16744 },
16745 subscribe(field, cb) {
16746 let set = subscribers.get(field);
16747 if (!set) {
16748 set = /* @__PURE__ */ new Set();
16749 subscribers.set(field, set);
16750 }
16751 set.add(cb);
16752 return () => {
16753 set.delete(cb);
16754 };
16755 }
16756 };
16757 function bootHeartbeatBus() {
16758 if (booted$2) {
16759 return;
16760 }
16761 booted$2 = true;
16762 const $ = window.jQuery;
16763 if (!$) {
16764 console.warn(
16765 "[desktop-mode/heartbeat] jQuery missing — Heartbeat bus disabled."
16766 );
16767 return;
16768 }
16769 $(document).on("heartbeat-send", (...args) => {
16770 const data = args[1];
16771 if (!data) {
16772 return;
16773 }
16774 for (const [field, supplier] of suppliers) {
16775 try {
16776 data[field] = supplier();
16777 } catch (err) {
16778 console.error(
16779 `[desktop-mode/heartbeat] supplier for "${field}" threw:`,
16780 err
16781 );
16782 }
16783 }
16784 });
16785 $(document).on("heartbeat-tick", (...args) => {
16786 const response = args[1];
16787 if (!response) {
16788 return;
16789 }
16790 for (const [field, set] of subscribers) {
16791 const value = response[field];
16792 if (value === void 0) {
16793 continue;
16794 }
16795 for (const cb of set) {
16796 try {
16797 cb(value);
16798 } catch (err) {
16799 console.error(
16800 `[desktop-mode/heartbeat] subscriber for "${field}" threw:`,
16801 err
16802 );
16803 }
16804 }
16805 }
16806 });
16807 }
16808 const store$2 = createSharedStore(
16809 "desktop-mode/presence",
16810 () => ({ byUser: /* @__PURE__ */ new Map(), serverTimeMs: 0 })
16811 );
16812 const ACTIVE_THRESHOLD_MS = 5 * 60 * 1e3;
16813 let lastInputMs = Date.now();
16814 let booted$1 = false;
16815 function noteUserActivity() {
16816 lastInputMs = Date.now();
16817 }
16818 function applySnapshot(block) {
16819 if (!block || !block.snapshot) {
16820 return;
16821 }
16822 const previous = store$2.state.byUser;
16823 const next = new Map(previous);
16824 const transitions = [];
16825 for (const [rawId, raw] of Object.entries(block.snapshot)) {
16826 const userId = Number(rawId);
16827 if (!Number.isFinite(userId) || userId <= 0) {
16828 continue;
16829 }
16830 const status = raw?.status ?? "offline";
16831 const entry = {
16832 status,
16833 lastSeenMs: Number(raw?.lastSeenMs ?? 0) || 0,
16834 lastActiveMs: Number(raw?.lastActiveMs ?? 0) || 0
16835 };
16836 const old = previous.get(userId);
16837 next.set(userId, entry);
16838 if (!old || old.status !== entry.status) {
16839 transitions.push({
16840 userId,
16841 oldStatus: old ? old.status : null,
16842 newStatus: entry.status,
16843 entry
16844 });
16845 }
16846 }
16847 store$2.state.byUser = next;
16848 if (typeof block.serverTimeMs === "number") {
16849 store$2.state.serverTimeMs = block.serverTimeMs;
16850 }
16851 store$2.notify();
16852 for (const t of transitions) {
16853 const detail = {
16854 userId: t.userId,
16855 oldStatus: t.oldStatus,
16856 newStatus: t.newStatus,
16857 lastSeenMs: t.entry.lastSeenMs,
16858 lastActiveMs: t.entry.lastActiveMs
16859 };
16860 document.dispatchEvent(
16861 new CustomEvent("desktop-mode-presence-changed", { detail })
16862 );
16863 activity.publish("desktop-mode/presence-changed", detail);
16864 }
16865 activity.publish("desktop-mode/presence-snapshot-applied", {
16866 applied: Object.keys(block.snapshot).length,
16867 transitions: transitions.length
16868 });
16869 }
16870 function bootPresenceProbe() {
16871 if (booted$1) {
16872 return;
16873 }
16874 booted$1 = true;
16875 document.addEventListener("pointerdown", noteUserActivity, {
16876 capture: true,
16877 passive: true
16878 });
16879 document.addEventListener("keydown", noteUserActivity, {
16880 capture: true,
16881 passive: true
16882 });
16883 document.addEventListener("visibilitychange", () => {
16884 if (!document.hidden) {
16885 noteUserActivity();
16886 }
16887 });
16888 heartbeat.contribute("desktop_mode_presence_active", () => true);
16889 heartbeat.contribute(
16890 "desktop_mode_user_active",
16891 () => Date.now() - lastInputMs < ACTIVE_THRESHOLD_MS
16892 );
16893 heartbeat.subscribe("desktop_mode_presence", (block) => {
16894 applySnapshot(block);
16895 });
16896 }
16897 function getStatus(userId) {
16898 const entry = store$2.state.byUser.get(userId);
16899 return entry ? entry.status : "offline";
16900 }
16901 function getAll() {
16902 return new Map(store$2.state.byUser);
16903 }
16904 function getEntry(userId) {
16905 return store$2.state.byUser.get(userId) ?? null;
16906 }
16907 function subscribe$1(cb) {
16908 return store$2.subscribe((s) => cb(s));
16909 }
16910 function markActive() {
16911 noteUserActivity();
16912 }
16913 function applyPresenceBatch(updates) {
16914 if (!Array.isArray(updates) || updates.length === 0) {
16915 return;
16916 }
16917 const previous = store$2.state.byUser;
16918 const next = new Map(previous);
16919 const transitions = [];
16920 for (const u of updates) {
16921 const userId = Number(u.userId);
16922 if (!Number.isFinite(userId) || userId <= 0) {
16923 continue;
16924 }
16925 const old = previous.get(userId);
16926 const entry = {
16927 status: u.status,
16928 lastSeenMs: typeof u.lastSeenMs === "number" ? u.lastSeenMs : old?.lastSeenMs ?? 0,
16929 lastActiveMs: typeof u.lastActiveMs === "number" ? u.lastActiveMs : old?.lastActiveMs ?? 0
16930 };
16931 next.set(userId, entry);
16932 if (!old || old.status !== entry.status) {
16933 transitions.push({
16934 userId,
16935 oldStatus: old ? old.status : null,
16936 newStatus: entry.status,
16937 entry
16938 });
16939 }
16940 }
16941 if (transitions.length === 0 && next.size === previous.size) {
16942 return;
16943 }
16944 store$2.state.byUser = next;
16945 store$2.notify();
16946 for (const t of transitions) {
16947 const detail = {
16948 userId: t.userId,
16949 oldStatus: t.oldStatus,
16950 newStatus: t.newStatus,
16951 lastSeenMs: t.entry.lastSeenMs,
16952 lastActiveMs: t.entry.lastActiveMs
16953 };
16954 document.dispatchEvent(
16955 new CustomEvent("desktop-mode-presence-changed", { detail })
16956 );
16957 activity.publish("desktop-mode/presence-changed", detail);
16958 }
16959 activity.publish("desktop-mode/presence-snapshot-applied", {
16960 applied: updates.length,
16961 transitions: transitions.length
16962 });
16963 }
16964 const presenceApi = Object.freeze({
16965 getStatus,
16966 getAll,
16967 getEntry,
16968 subscribe: subscribe$1,
16969 markActive,
16970 applyBatch: applyPresenceBatch
16971 });
16972 const HEARTBEAT_FIELD = "desktop_mode_nonces";
16973 const targets = /* @__PURE__ */ new Map();
16974 let booted = false;
16975 function registerNonceTarget(action, updater) {
16976 if (typeof action !== "string" || action === "") {
16977 return () => {
16978 };
16979 }
16980 let set = targets.get(action);
16981 if (!set) {
16982 set = /* @__PURE__ */ new Set();
16983 targets.set(action, set);
16984 }
16985 set.add(updater);
16986 return () => {
16987 set.delete(updater);
16988 };
16989 }
16990 function bootNonceRefresh() {
16991 if (booted) {
16992 return;
16993 }
16994 booted = true;
16995 heartbeat.subscribe(HEARTBEAT_FIELD, (payload) => {
16996 if (!payload || typeof payload !== "object") {
16997 return;
16998 }
16999 for (const [action, value] of Object.entries(payload)) {
17000 if (typeof value !== "string" || value === "") {
17001 continue;
17002 }
17003 const set = targets.get(action);
17004 if (!set) {
17005 continue;
17006 }
17007 for (const updater of set) {
17008 try {
17009 updater(value);
17010 } catch (err) {
17011 console.error(
17012 `[desktop-mode/nonce-refresh] updater for "${action}" threw:`,
17013 err
17014 );
17015 }
17016 }
17017 }
17018 });
17019 registerShellAndPluginsWindowTargets();
17020 }
17021 function registerShellAndPluginsWindowTargets() {
17022 registerNonceTarget("wp_rest", updateAllRestNonces);
17023 registerNonceTarget("desktop-mode-plugins", (fresh) => {
17024 writeWindowConfigField("desktop-mode-plugins", "ajaxNonce", fresh);
17025 });
17026 registerNonceTarget("updates", (fresh) => {
17027 writeWindowConfigField("desktop-mode-plugins", "updatesNonce", fresh);
17028 });
17029 }
17030 function updateAllRestNonces(fresh) {
17031 const cfg = readShellConfig();
17032 if (cfg && typeof cfg.restNonce === "string") {
17033 cfg.restNonce = fresh;
17034 }
17035 const windowConfigs = readWindowConfigs();
17036 if (!windowConfigs) {
17037 return;
17038 }
17039 for (const blob of Object.values(windowConfigs)) {
17040 if (blob && typeof blob === "object" && typeof blob.restNonce === "string") {
17041 blob.restNonce = fresh;
17042 }
17043 }
17044 }
17045 function writeWindowConfigField(windowId, field, value) {
17046 const blobs = readWindowConfigs();
17047 const blob = blobs?.[windowId];
17048 if (blob && typeof blob === "object") {
17049 blob[field] = value;
17050 }
17051 }
17052 function readShellConfig() {
17053 if (typeof window === "undefined") {
17054 return void 0;
17055 }
17056 return window.desktopModeConfig;
17057 }
17058 function readWindowConfigs() {
17059 if (typeof window === "undefined") {
17060 return void 0;
17061 }
17062 return window.desktopModeWindowConfig;
17063 }
17064 const VIEWPORT_CLAMP_MARGIN = 12;
17065 function findDockEntryForUrl(url, config) {
17066 const windowId = deriveWindowId(url, config.adminUrl);
17067 return (config.dockItems || []).find(
17068 (i) => deriveWindowId(i.url, config.adminUrl) === windowId || (i.submenu || []).some(
17069 (s) => deriveWindowId(s.url, config.adminUrl) === windowId
17070 )
17071 );
17072 }
17073 function clampGeometryToViewport(win, rect) {
17074 const maxW = Math.max(200, rect.width - VIEWPORT_CLAMP_MARGIN * 2);
17075 const maxH = Math.max(200, rect.height - VIEWPORT_CLAMP_MARGIN * 2);
17076 const width = Math.min(win.width, maxW);
17077 const height = Math.min(win.height, maxH);
17078 const maxX = Math.max(0, rect.width - width - VIEWPORT_CLAMP_MARGIN);
17079 const maxY = Math.max(0, rect.height - height - VIEWPORT_CLAMP_MARGIN);
17080 const x = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.x, maxX));
17081 const y = Math.max(VIEWPORT_CLAMP_MARGIN, Math.min(win.y, maxY));
17082 return { x, y, width, height };
17083 }
17084 const INITIAL_ORIGIN$1 = window.location.origin;
17085 function bindTopWindowLinkInterceptor(manager, config) {
17086 document.addEventListener(
17087 "click",
17088 (e) => {
17089 if (e.defaultPrevented) {
17090 return;
17091 }
17092 if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
17093 return;
17094 }
17095 const target2 = e.target;
17096 const link = target2 && target2.closest ? target2.closest("a[href]") : null;
17097 if (!link) {
17098 return;
17099 }
17100 const anchor = link;
17101 const linkTarget = anchor.getAttribute("target");
17102 if (linkTarget && linkTarget !== "" && linkTarget !== "_self") {
17103 return;
17104 }
17105 if (anchor.hasAttribute("download")) {
17106 return;
17107 }
17108 const rawHref = anchor.getAttribute("href");
17109 if (!rawHref || rawHref.charAt(0) === "#") {
17110 return;
17111 }
17112 if (/^(mailto:|tel:|javascript:|data:)/i.test(rawHref)) {
17113 return;
17114 }
17115 let url;
17116 try {
17117 url = new URL(rawHref, window.location.href);
17118 } catch (err) {
17119 if (typeof console !== "undefined") {
17120 console.warn(
17121 "[desktop-mode] Couldn’t parse href; letting the browser handle the click:",
17122 rawHref,
17123 err
17124 );
17125 }
17126 return;
17127 }
17128 if (url.origin !== INITIAL_ORIGIN$1) {
17129 return;
17130 }
17131 let adminPath;
17132 try {
17133 adminPath = new URL(config.adminUrl).pathname;
17134 } catch (err) {
17135 if (typeof console !== "undefined") {
17136 console.error(
17137 "[desktop-mode] config.adminUrl is not a valid URL; falling back to /wp-admin/:",
17138 config.adminUrl,
17139 err
17140 );
17141 }
17142 adminPath = "/wp-admin/";
17143 }
17144 if (!url.pathname.startsWith(adminPath)) {
17145 return;
17146 }
17147 if (/\/(admin-post|admin-ajax)\.php$/.test(url.pathname)) {
17148 return;
17149 }
17150 if (url.searchParams.has("action") && url.searchParams.get("action") === "logout") {
17151 return;
17152 }
17153 if (url.searchParams.has("desktop_mode_classic")) {
17154 return;
17155 }
17156 e.preventDefault();
17157 e.stopPropagation();
17158 if (tryNativeUrlRemap(url.href)) {
17159 return;
17160 }
17161 const windowId = deriveWindowId(url.href, config.adminUrl);
17162 const dockEntry = findDockEntryForUrl(url.href, config);
17163 const fallbackTitle = (anchor.textContent || "").trim() || dockEntry?.title || "";
17164 const isAdminBarNew = !!anchor.closest("#wp-admin-bar-new-content");
17165 const openOpts = {
17166 id: windowId,
17167 baseId: windowId,
17168 multi: !!dockEntry?.multi || isAdminBarNew,
17169 url: url.href,
17170 parentUrl: dockEntry?.url ?? url.href,
17171 title: dockEntry?.title || fallbackTitle,
17172 icon: dockEntry?.icon || "dashicons-admin-generic",
17173 submenu: dockEntry?.submenu
17174 };
17175 if (isAdminBarNew) {
17176 void manager.openNew(openOpts);
17177 return;
17178 }
17179 void manager.open(openOpts);
17180 },
17181 true
17182 );
17183 }
17184 const REGISTRY_CHANGED_EVENT = "desktop-mode-registry-changed";
17185 function diffIds(prev, next) {
17186 const prevIds = /* @__PURE__ */ new Set();
17187 if (Array.isArray(prev)) {
17188 for (const item of prev) {
17189 if (item && typeof item.id === "string") {
17190 prevIds.add(item.id);
17191 }
17192 }
17193 }
17194 const nextIds = /* @__PURE__ */ new Set();
17195 for (const item of next) {
17196 if (item && typeof item.id === "string") {
17197 nextIds.add(item.id);
17198 }
17199 }
17200 const added = [];
17201 for (const id of nextIds) {
17202 if (!prevIds.has(id)) {
17203 added.push(id);
17204 }
17205 }
17206 const removed = [];
17207 for (const id of prevIds) {
17208 if (!nextIds.has(id)) {
17209 removed.push(id);
17210 }
17211 }
17212 return { added, removed };
17213 }
17214 function emitRegistryChanged(registry2, prev, next) {
17215 const { added, removed } = diffIds(prev, next);
17216 if (added.length === 0 && removed.length === 0) {
17217 return;
17218 }
17219 if (typeof document === "undefined") {
17220 return;
17221 }
17222 const detail = { registry: registry2, added, removed };
17223 document.dispatchEvent(
17224 new CustomEvent(REGISTRY_CHANGED_EVENT, { detail })
17225 );
17226 }
17227 function createApplyPayload(deps2) {
17228 const {
17229 applyDockItems,
17230 config,
17231 syncNativeWindows,
17232 syncServerWidgets,
17233 syncServerWallpapers,
17234 syncServerCommands,
17235 syncServerSettingsTabs,
17236 syncServerTitleBarButtons,
17237 syncServerUnfocusEffects,
17238 syncServerDockRailRenderers,
17239 renderIcons
17240 } = deps2;
17241 return function applyPayload(payload) {
17242 const dockItems = payload.dockItems;
17243 const nativeWindows = payload.nativeWindows;
17244 const serverWidgets = payload.serverWidgets;
17245 const serverWallpapers = payload.serverWallpapers;
17246 const serverCommandScripts = payload.serverCommandScripts;
17247 const serverCommands = payload.serverCommands;
17248 const serverSettingsTabScripts = payload.serverSettingsTabScripts;
17249 const serverSettingsTabs = payload.serverSettingsTabs;
17250 const serverDockRailRendererScripts = payload.serverDockRailRendererScripts;
17251 const serverTitleBarButtonScripts = payload.serverTitleBarButtonScripts;
17252 const serverUnfocusEffectScripts = payload.serverUnfocusEffectScripts;
17253 const serverWindowNotices = payload.serverWindowNotices;
17254 const desktopIcons = payload.desktopIcons;
17255 if (!Array.isArray(dockItems) || dockItems.length === 0) {
17256 return;
17257 }
17258 const prevDockItems = config.dockItems;
17259 applyDockItems(dockItems);
17260 config.dockItems = dockItems;
17261 emitRegistryChanged(
17262 "dock-items",
17263 prevDockItems,
17264 dockItems
17265 );
17266 if (Array.isArray(nativeWindows)) {
17267 const prevNativeWindows = config.nativeWindows;
17268 void syncNativeWindows(
17269 nativeWindows
17270 );
17271 config.nativeWindows = nativeWindows;
17272 emitRegistryChanged(
17273 "native-windows",
17274 prevNativeWindows,
17275 nativeWindows
17276 );
17277 }
17278 if (Array.isArray(serverWidgets)) {
17279 void syncServerWidgets(
17280 serverWidgets
17281 );
17282 config.serverWidgets = serverWidgets;
17283 }
17284 if (Array.isArray(serverWallpapers)) {
17285 void syncServerWallpapers(
17286 serverWallpapers
17287 );
17288 config.serverWallpapers = serverWallpapers;
17289 }
17290 if (Array.isArray(serverCommandScripts)) {
17291 void syncServerCommands(
17292 serverCommandScripts,
17293 Array.isArray(serverCommands) ? serverCommands : void 0
17294 );
17295 config.serverCommandScripts = serverCommandScripts;
17296 if (Array.isArray(serverCommands)) {
17297 config.serverCommands = serverCommands;
17298 }
17299 }
17300 if (Array.isArray(serverSettingsTabScripts)) {
17301 void syncServerSettingsTabs(
17302 serverSettingsTabScripts,
17303 Array.isArray(serverSettingsTabs) ? serverSettingsTabs : void 0
17304 );
17305 config.serverSettingsTabScripts = serverSettingsTabScripts;
17306 if (Array.isArray(serverSettingsTabs)) {
17307 config.serverSettingsTabs = serverSettingsTabs;
17308 }
17309 }
17310 if (Array.isArray(serverTitleBarButtonScripts)) {
17311 void syncServerTitleBarButtons(
17312 serverTitleBarButtonScripts
17313 );
17314 config.serverTitleBarButtonScripts = serverTitleBarButtonScripts;
17315 }
17316 if (Array.isArray(serverUnfocusEffectScripts)) {
17317 void syncServerUnfocusEffects(
17318 serverUnfocusEffectScripts
17319 );
17320 config.serverUnfocusEffectScripts = serverUnfocusEffectScripts;
17321 }
17322 if (Array.isArray(serverDockRailRendererScripts)) {
17323 void syncServerDockRailRenderers(
17324 serverDockRailRendererScripts
17325 );
17326 config.serverDockRailRendererScripts = serverDockRailRendererScripts;
17327 }
17328 if (Array.isArray(serverWindowNotices)) {
17329 applyServerWindowNotices(
17330 serverWindowNotices
17331 );
17332 config.serverWindowNotices = serverWindowNotices;
17333 }
17334 if (Array.isArray(desktopIcons)) {
17335 const prevDesktopIcons = config.desktopIcons;
17336 renderIcons(desktopIcons);
17337 config.desktopIcons = desktopIcons;
17338 emitRegistryChanged(
17339 "desktop-icons",
17340 prevDesktopIcons,
17341 desktopIcons
17342 );
17343 }
17344 };
17345 }
17346 const MENU_REFRESH_TIMEOUT_MS = 8e3;
17347 function bindMenuRefresh(deps2) {
17348 const {
17349 layoutDispatcher,
17350 desktopArea,
17351 config,
17352 syncNativeWindows,
17353 syncServerWidgets,
17354 syncServerWallpapers,
17355 syncServerCommands,
17356 syncServerSettingsTabs,
17357 syncServerTitleBarButtons,
17358 syncServerUnfocusEffects,
17359 syncServerDockRailRenderers,
17360 renderIcons
17361 } = deps2;
17362 const applyPayload = createApplyPayload({
17363 applyDockItems: (items) => layoutDispatcher?.applyDockItems(items),
17364 config,
17365 syncNativeWindows,
17366 syncServerWidgets,
17367 syncServerWallpapers,
17368 syncServerCommands,
17369 syncServerSettingsTabs,
17370 syncServerTitleBarButtons,
17371 syncServerUnfocusEffects,
17372 syncServerDockRailRenderers,
17373 renderIcons
17374 });
17375 window.addEventListener("message", (e) => {
17376 if (e.origin !== INITIAL_ORIGIN$1) {
17377 return;
17378 }
17379 const data = e.data;
17380 if (!data || data.type !== "desktop-mode-plugins-changed") {
17381 return;
17382 }
17383 if (data.payload) {
17384 applyPayload(data.payload);
17385 }
17386 });
17387 const refresh = () => {
17388 if (!config.adminUrl) {
17389 return Promise.resolve();
17390 }
17391 const probeUrl = (() => {
17392 try {
17393 const url = new URL("admin.php", config.adminUrl);
17394 url.searchParams.set("desktop_mode_chromeless", "1");
17395 url.searchParams.set("desktop_mode_menu_refresh", "1");
17396 return url.toString();
17397 } catch (_err) {
17398 return null;
17399 }
17400 })();
17401 if (!probeUrl) {
17402 return Promise.resolve();
17403 }
17404 return new Promise((resolve2) => {
17405 const iframe = document.createElement("iframe");
17406 iframe.setAttribute("aria-hidden", "true");
17407 iframe.tabIndex = -1;
17408 iframe.style.cssText = "position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;border:0;opacity:0;pointer-events:none;";
17409 iframe.src = probeUrl;
17410 let done = false;
17411 const cleanup = () => {
17412 if (done) {
17413 return;
17414 }
17415 done = true;
17416 window.clearTimeout(timeoutId);
17417 window.removeEventListener("message", onMessage);
17418 if (iframe.parentNode) {
17419 iframe.parentNode.removeChild(iframe);
17420 }
17421 resolve2();
17422 };
17423 const onMessage = (e) => {
17424 if (e.source !== iframe.contentWindow) {
17425 return;
17426 }
17427 const data = e.data;
17428 if (!data || data.type !== "desktop-mode-plugins-changed") {
17429 return;
17430 }
17431 cleanup();
17432 };
17433 const timeoutId = window.setTimeout(() => {
17434 doAction(HOOKS.SHELL_ERROR, {
17435 scope: "menu-refresh",
17436 error: new Error("menu refresh probe timed out")
17437 });
17438 cleanup();
17439 }, MENU_REFRESH_TIMEOUT_MS);
17440 window.addEventListener("message", onMessage);
17441 document.body.appendChild(iframe);
17442 });
17443 };
17444 return refresh;
17445 }
17446 function hasRestorableSession(session) {
17447 if (!session) {
17448 return false;
17449 }
17450 if (Array.isArray(session.windows) && session.windows.length > 0) {
17451 return true;
17452 }
17453 if (typeof session.updated !== "number" || session.updated <= 0 || !Array.isArray(session.desktops) || session.desktops.length === 0) {
17454 return false;
17455 }
17456 if (session.desktops.length > 1) {
17457 return true;
17458 }
17459 const onlyDesktop = session.desktops[0];
17460 if (onlyDesktop?.id && onlyDesktop.id !== "desktop-1") {
17461 return true;
17462 }
17463 return !!session.activeDesktop && session.activeDesktop !== "desktop-1";
17464 }
17465 async function restoreSession(manager, config, desktopArea) {
17466 const rect = desktopArea.getBoundingClientRect();
17467 if (Array.isArray(config.session.desktops) && config.session.desktops.length > 0) {
17468 manager.seedDesktops(
17469 config.session.desktops,
17470 config.session.activeDesktop || config.session.desktops[0].id
17471 );
17472 }
17473 for (const win of config.session.windows) {
17474 const clamped = clampGeometryToViewport(win, rect);
17475 const dockEntry = findDockEntryForUrl(win.url, config);
17476 const opened = await manager.open({
17477 id: win.id,
17478 baseId: win.baseId || win.id,
17479 desktopId: win.desktopId,
17480 multi: !!dockEntry?.multi,
17481 url: win.url,
17482 // `dockEntry?.url` is the parent menu's landing page —
17483 // recover it so the synthetic "back to parent" tab in
17484 // the in-window strip points at the dock URL even when
17485 // the saved `win.url` is a sub-page (e.g. theme-install.php
17486 // under Appearance, or a deep wc-admin route under
17487 // WooCommerce). Without this the dedup check in
17488 // `dom.ts` sees the iframe URL match a submenu entry
17489 // and suppresses the parent tab — losing the only
17490 // affordance to navigate back.
17491 parentUrl: dockEntry?.url ?? win.url,
17492 title: win.title,
17493 icon: win.icon || "dashicons-admin-generic",
17494 x: clamped.x,
17495 y: clamped.y,
17496 width: clamped.width,
17497 height: clamped.height,
17498 initialState: win.state,
17499 submenu: dockEntry?.submenu
17500 });
17501 if (Array.isArray(win.externalTabs)) {
17502 for (const ext of win.externalTabs) {
17503 if (ext && typeof ext.url === "string" && ext.url !== "") {
17504 opened.addExternalTab(
17505 ext.url,
17506 typeof ext.label === "string" && ext.label !== "" ? ext.label : ext.url
17507 );
17508 }
17509 }
17510 }
17511 }
17512 if (config.session.focused) {
17513 const focused = manager.getById(config.session.focused);
17514 if (focused) {
17515 manager.focus(focused);
17516 }
17517 }
17518 }
17519 async function openCurrentPage(manager, config) {
17520 if (tryNativeUrlRemap(config.currentPage)) {
17521 return;
17522 }
17523 const windowId = deriveWindowId(config.currentPage, config.adminUrl);
17524 const dockEntry = findDockEntryForUrl(config.currentPage, config);
17525 await manager.open({
17526 id: windowId,
17527 baseId: windowId,
17528 multi: !!dockEntry?.multi,
17529 url: config.currentPage,
17530 parentUrl: dockEntry?.url ?? config.currentPage,
17531 title: config.currentTitle,
17532 icon: config.currentIcon,
17533 submenu: dockEntry?.submenu
17534 });
17535 }
17536 function shouldAutoOpenCurrentPage(inputs) {
17537 const suppress = inputs.fromPortal && !inputs.fromPortalIntent && (inputs.hasSession || !inputs.defaultEnabled || inputs.isNativeDefault);
17538 return !suppress;
17539 }
17540 function trackedFetch(manager, input, requestInit, opts) {
17541 const finalInit = injectRestNonce(input, requestInit);
17542 const promise = window.fetch(input, finalInit);
17543 if (opts?.silent) {
17544 return promise;
17545 }
17546 let target2 = opts?.window;
17547 if (!target2 && opts?.windowId) {
17548 target2 = manager.getById(opts.windowId) ?? null;
17549 }
17550 if (!target2) {
17551 target2 = manager.getFocused();
17552 }
17553 if (target2 && typeof target2.trackActivity === "function") {
17554 void target2.trackActivity(promise).catch(() => {
17555 });
17556 }
17557 return promise;
17558 }
17559 const SESSION_SAVE_DEBOUNCE_MS = 500;
17560 function createSessionSaver(manager, config) {
17561 let debounceTimer = null;
17562 let inFlight = false;
17563 const doSave = async () => {
17564 if (inFlight) {
17565 return;
17566 }
17567 const payload = manager.snapshot();
17568 inFlight = true;
17569 try {
17570 await trackedFetch(
17571 manager,
17572 config.sessionUrl,
17573 {
17574 method: "POST",
17575 credentials: "same-origin",
17576 headers: {
17577 "Content-Type": "application/json",
17578 "X-WP-Nonce": config.restNonce
17579 },
17580 body: JSON.stringify({ session: payload }),
17581 // Best-effort: we don't block the UI on persistence.
17582 keepalive: true
17583 },
17584 { silent: true }
17585 );
17586 } catch (err) {
17587 doAction(HOOKS.SHELL_ERROR, { scope: "session-save", error: err });
17588 } finally {
17589 inFlight = false;
17590 }
17591 };
17592 const flushImmediately = () => {
17593 if (debounceTimer !== null) {
17594 clearTimeout(debounceTimer);
17595 debounceTimer = null;
17596 }
17597 const payload = manager.snapshot();
17598 const body = new Blob(
17599 [JSON.stringify({ session: payload })],
17600 { type: "application/json" }
17601 );
17602 const beaconUrl = config.sessionUrl + (config.sessionUrl.includes("?") ? "&" : "?") + "_wpnonce=" + encodeURIComponent(config.restNonce);
17603 if (navigator.sendBeacon && navigator.sendBeacon(beaconUrl, body)) {
17604 return;
17605 }
17606 void doSave();
17607 };
17608 const schedule = () => {
17609 if (debounceTimer !== null) {
17610 clearTimeout(debounceTimer);
17611 }
17612 debounceTimer = window.setTimeout(() => {
17613 debounceTimer = null;
17614 void doSave();
17615 }, SESSION_SAVE_DEBOUNCE_MS);
17616 };
17617 window.addEventListener("pagehide", flushImmediately);
17618 document.addEventListener("visibilitychange", () => {
17619 if (document.visibilityState === "hidden") {
17620 flushImmediately();
17621 }
17622 });
17623 return schedule;
17624 }
17625 const SHELL_RESIZE_DEBOUNCE_MS = 120;
17626 function wireSessionEvents(save) {
17627 document.addEventListener("desktop-mode-window-opened", save);
17628 document.addEventListener("desktop-mode-window-closed", save);
17629 document.addEventListener("desktop-mode-window-focused", save);
17630 document.addEventListener("desktop-mode-window-changed", save);
17631 addAction(HOOKS.DESKTOP_CREATED, "desktop-mode/session-save", save);
17632 addAction(HOOKS.DESKTOP_CLOSED, "desktop-mode/session-save", save);
17633 addAction(HOOKS.DESKTOP_SWITCHED, "desktop-mode/session-save", save);
17634 }
17635 function bindShellLifecycle() {
17636 const shellEl = document.getElementById("desktop-mode-shell");
17637 let resizeTimer = null;
17638 const fireShellResize = () => {
17639 resizeTimer = null;
17640 const rect = shellEl ? shellEl.getBoundingClientRect() : null;
17641 doAction(HOOKS.SHELL_RESIZED, {
17642 width: rect ? Math.round(rect.width) : window.innerWidth,
17643 height: rect ? Math.round(rect.height) : window.innerHeight
17644 });
17645 };
17646 window.addEventListener("resize", () => {
17647 if (resizeTimer !== null) {
17648 window.clearTimeout(resizeTimer);
17649 }
17650 resizeTimer = window.setTimeout(
17651 fireShellResize,
17652 SHELL_RESIZE_DEBOUNCE_MS
17653 );
17654 });
17655 document.addEventListener("visibilitychange", () => {
17656 doAction(HOOKS.SHELL_VISIBILITY, {
17657 state: document.hidden ? "hidden" : "visible"
17658 });
17659 });
17660 }
17661 function applyTileClasses(baseClasses, item, ctx) {
17662 const fullCtx = {
17663 rail: ctx.rail ?? "dock",
17664 orientation: ctx.orientation,
17665 dockId: ctx.dockId,
17666 container: ctx.container ?? document.body,
17667 item,
17668 isSystem: ctx.isSystem
17669 };
17670 return applyFilters(
17671 HOOKS.DOCK_TILE_CLASS,
17672 baseClasses,
17673 fullCtx
17674 );
17675 }
17676 function applyTileElement(tile2, item, ctx) {
17677 const fullCtx = {
17678 rail: ctx.rail ?? "dock",
17679 orientation: ctx.orientation,
17680 dockId: ctx.dockId,
17681 container: ctx.container ?? document.body,
17682 item,
17683 isSystem: ctx.isSystem
17684 };
17685 return applyFilters(
17686 HOOKS.DOCK_TILE_ELEMENT,
17687 tile2,
17688 fullCtx
17689 );
17690 }
17691 function applyTileTooltip(label, item, ctx) {
17692 const fullCtx = {
17693 rail: ctx.rail ?? "dock",
17694 orientation: ctx.orientation,
17695 dockId: ctx.dockId,
17696 container: ctx.container ?? document.body,
17697 item,
17698 isSystem: ctx.isSystem
17699 };
17700 return applyFilters(
17701 HOOKS.DOCK_TILE_TOOLTIP,
17702 label,
17703 fullCtx
17704 );
17705 }
17706 function dispatchTileRendered(el, item, ctx) {
17707 const fullCtx = {
17708 rail: ctx.rail ?? "dock",
17709 orientation: ctx.orientation,
17710 dockId: ctx.dockId,
17711 container: ctx.container ?? document.body,
17712 item,
17713 isSystem: ctx.isSystem
17714 };
17715 doAction(HOOKS.DOCK_TILE_RENDERED, { ...fullCtx, el });
17716 }
17717 const DEFAULT_DOCK_SELECTOR = [
17718 ".desktop-mode-dock",
17719 "#desktop-mode-dock",
17720 "#desktop-mode-side-dock",
17721 ".desktop-mode-dock__tooltip",
17722 ".desktop-mode-dock-submenu"
17723 ].join(",");
17724 const customSelectors = /* @__PURE__ */ new Set();
17725 function isDockElement(target2) {
17726 if (!target2 || typeof target2.closest !== "function") {
17727 return false;
17728 }
17729 const el = target2;
17730 if (el.closest(DEFAULT_DOCK_SELECTOR)) {
17731 return true;
17732 }
17733 for (const selector of customSelectors) {
17734 if (el.closest(selector)) {
17735 return true;
17736 }
17737 }
17738 return false;
17739 }
17740 function registerDockSelector(selector) {
17741 if (typeof selector !== "string" || selector.trim() === "") {
17742 return () => void 0;
17743 }
17744 customSelectors.add(selector);
17745 return () => {
17746 customSelectors.delete(selector);
17747 };
17748 }
17749 const states = /* @__PURE__ */ new Map();
17750 const INITIAL_ORIGIN = window.location.origin;
17751 function ensureState(windowId) {
17752 let s = states.get(windowId);
17753 if (!s) {
17754 s = {
17755 headers: /* @__PURE__ */ new Map(),
17756 observers: /* @__PURE__ */ new Set(),
17757 observeCount: 0,
17758 loadHandler: null,
17759 loadHandlerTarget: null
17760 };
17761 states.set(windowId, s);
17762 }
17763 ensureLoadHandler(windowId, s);
17764 return s;
17765 }
17766 function ensureLoadHandler(windowId, s) {
17767 const iframe = findIframe(windowId);
17768 if (!iframe) {
17769 return;
17770 }
17771 if (s.loadHandlerTarget === iframe && s.loadHandler) {
17772 return;
17773 }
17774 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
17775 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
17776 }
17777 if (typeof iframe.addEventListener !== "function") {
17778 return;
17779 }
17780 const handler = () => {
17781 queueMicrotask(() => pushInstrumentation(windowId));
17782 };
17783 iframe.addEventListener("load", handler);
17784 s.loadHandler = handler;
17785 s.loadHandlerTarget = iframe;
17786 }
17787 function detachLoadHandler(s) {
17788 if (s.loadHandlerTarget && s.loadHandler && typeof s.loadHandlerTarget.removeEventListener === "function") {
17789 s.loadHandlerTarget.removeEventListener("load", s.loadHandler);
17790 }
17791 s.loadHandler = null;
17792 s.loadHandlerTarget = null;
17793 }
17794 function findIframe(windowId) {
17795 const wpd = window.wp?.desktop?.windowManager;
17796 if (wpd && typeof wpd.getById === "function") {
17797 const win = wpd.getById(windowId);
17798 if (win?.iframe) {
17799 return win.iframe;
17800 }
17801 if (win?.element) {
17802 const synth = win.element.querySelector("iframe");
17803 if (synth) {
17804 return synth;
17805 }
17806 }
17807 }
17808 const fallback = document.getElementById(`wp-window-${windowId}`);
17809 return fallback?.querySelector("iframe") ?? null;
17810 }
17811 function snapshotHeaders(s) {
17812 const out = {};
17813 for (const [name, contributions] of s.headers) {
17814 const parts = [];
17815 for (const c of contributions) {
17816 let v;
17817 try {
17818 v = typeof c.value === "function" ? c.value() : c.value;
17819 } catch {
17820 continue;
17821 }
17822 if (typeof v === "string" && v !== "") {
17823 parts.push(v);
17824 }
17825 }
17826 if (parts.length > 0) {
17827 out[name] = parts.join(", ");
17828 }
17829 }
17830 return out;
17831 }
17832 function pushInstrumentation(windowId) {
17833 const iframe = findIframe(windowId);
17834 if (!iframe || !iframe.contentWindow) {
17835 return;
17836 }
17837 const s = states.get(windowId);
17838 const headers = s ? snapshotHeaders(s) : {};
17839 const observe = !!s && s.observeCount > 0;
17840 try {
17841 iframe.contentWindow.postMessage(
17842 {
17843 type: "desktop-mode-instrument-set",
17844 headers,
17845 observe
17846 },
17847 INITIAL_ORIGIN
17848 );
17849 } catch {
17850 }
17851 }
17852 addAction(HOOKS.IFRAME_READY, "desktop-mode/devtools/replay", (payload) => {
17853 const p = payload;
17854 if (p && typeof p.windowId === "string" && states.has(p.windowId)) {
17855 pushInstrumentation(p.windowId);
17856 }
17857 });
17858 addAction(
17859 HOOKS.IFRAME_NETWORK_COMPLETED,
17860 "desktop-mode/devtools/dispatch",
17861 (payload) => {
17862 const p = payload;
17863 if (!p || typeof p.windowId !== "string") {
17864 return;
17865 }
17866 const s = states.get(p.windowId);
17867 if (!s) {
17868 return;
17869 }
17870 for (const cb of s.observers) {
17871 try {
17872 cb(p);
17873 } catch {
17874 }
17875 }
17876 }
17877 );
17878 const sessions = /* @__PURE__ */ new Map();
17879 const POLL_INTERVAL_MS = 1e3;
17880 function pollOnce(sessionId, restUrl2, restNonce) {
17881 const sp = sessions.get(sessionId);
17882 if (!sp || sp.inflight) {
17883 return;
17884 }
17885 sp.inflight = true;
17886 const u = new URL(restUrl2 + "desktop-mode/v1/debug", window.location.origin);
17887 u.searchParams.set("sessionId", sessionId);
17888 u.searchParams.set("since", String(sp.cursor));
17889 for (const ch of sp.channels.keys()) {
17890 u.searchParams.append("channels[]", ch);
17891 }
17892 const url = u.toString();
17893 fetch(url, {
17894 credentials: "same-origin",
17895 headers: { "X-WP-Nonce": restNonce }
17896 }).then((r) => r.ok ? r.json() : { events: [], cursor: sp.cursor }).then((body) => {
17897 sp.inflight = false;
17898 if (!sessions.has(sessionId)) {
17899 return;
17900 }
17901 if (typeof body.cursor === "number") {
17902 sp.cursor = body.cursor;
17903 }
17904 for (const ev of body.events || []) {
17905 const bucket2 = sp.channels.get(ev.channel);
17906 if (!bucket2) {
17907 continue;
17908 }
17909 for (const cb of bucket2) {
17910 try {
17911 cb(ev);
17912 } catch {
17913 }
17914 }
17915 }
17916 }).catch(() => {
17917 sp.inflight = false;
17918 }).finally(() => {
17919 const stillThere = sessions.get(sessionId);
17920 if (stillThere && stillThere.channels.size > 0) {
17921 stillThere.timer = setTimeout(
17922 () => pollOnce(sessionId, restUrl2, restNonce),
17923 POLL_INTERVAL_MS
17924 );
17925 }
17926 });
17927 }
17928 function getRestEndpoint() {
17929 const cfg = window.desktopModeConfig;
17930 if (!cfg || !cfg.restUrl || !cfg.restNonce) {
17931 return null;
17932 }
17933 return { restUrl: cfg.restUrl, restNonce: cfg.restNonce };
17934 }
17935 function dispatchLocal(sessionId, ev) {
17936 const sp = sessions.get(sessionId);
17937 if (!sp) {
17938 return;
17939 }
17940 const bucket2 = sp.channels.get(ev.channel);
17941 if (!bucket2) {
17942 return;
17943 }
17944 for (const cb of bucket2) {
17945 try {
17946 cb(ev);
17947 } catch {
17948 }
17949 }
17950 }
17951 let _localEventCounter = 0;
17952 const debugBus = {
17953 startSession() {
17954 const cryptoApi = window.crypto;
17955 if (cryptoApi && typeof cryptoApi.randomUUID === "function") {
17956 return cryptoApi.randomUUID();
17957 }
17958 return "wpdbg-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
17959 },
17960 publish(sessionId, channel, payload) {
17961 dispatchLocal(sessionId, {
17962 id: ++_localEventCounter,
17963 t: Date.now(),
17964 channel,
17965 payload
17966 });
17967 },
17968 subscribe(sessionId, channel, cb) {
17969 let sp = sessions.get(sessionId);
17970 const startedFresh = !sp;
17971 if (!sp) {
17972 sp = {
17973 channels: /* @__PURE__ */ new Map(),
17974 cursor: 0,
17975 timer: null,
17976 inflight: false
17977 };
17978 sessions.set(sessionId, sp);
17979 }
17980 let bucket2 = sp.channels.get(channel);
17981 if (!bucket2) {
17982 bucket2 = /* @__PURE__ */ new Set();
17983 sp.channels.set(channel, bucket2);
17984 }
17985 bucket2.add(cb);
17986 if (startedFresh) {
17987 const ep = getRestEndpoint();
17988 if (ep) {
17989 pollOnce(sessionId, ep.restUrl, ep.restNonce);
17990 }
17991 }
17992 return () => {
17993 const cur = sessions.get(sessionId);
17994 if (!cur) {
17995 return;
17996 }
17997 const b = cur.channels.get(channel);
17998 if (b) {
17999 b.delete(cb);
18000 if (b.size === 0) {
18001 cur.channels.delete(channel);
18002 }
18003 }
18004 if (cur.channels.size === 0) {
18005 if (cur.timer) {
18006 clearTimeout(cur.timer);
18007 }
18008 sessions.delete(sessionId);
18009 }
18010 };
18011 }
18012 };
18013 const devtools = {
18014 addRequestHeader(windowId, name, value) {
18015 if (typeof windowId !== "string" || windowId === "") {
18016 return () => {
18017 };
18018 }
18019 if (typeof name !== "string" || name === "") {
18020 return () => {
18021 };
18022 }
18023 const s = ensureState(windowId);
18024 const contribution = { value };
18025 let bucket2 = s.headers.get(name);
18026 if (!bucket2) {
18027 bucket2 = [];
18028 s.headers.set(name, bucket2);
18029 }
18030 bucket2.push(contribution);
18031 pushInstrumentation(windowId);
18032 return () => {
18033 const cur = states.get(windowId);
18034 if (!cur) {
18035 return;
18036 }
18037 const b = cur.headers.get(name);
18038 if (!b) {
18039 return;
18040 }
18041 const i = b.indexOf(contribution);
18042 if (i >= 0) {
18043 b.splice(i, 1);
18044 }
18045 if (b.length === 0) {
18046 cur.headers.delete(name);
18047 }
18048 pushInstrumentation(windowId);
18049 gcWindowState(windowId);
18050 };
18051 },
18052 onRequest(windowId, cb, opts) {
18053 if (typeof windowId !== "string" || windowId === "") {
18054 return () => {
18055 };
18056 }
18057 if (typeof cb !== "function") {
18058 return () => {
18059 };
18060 }
18061 const s = ensureState(windowId);
18062 s.observers.add(cb);
18063 const wantsObserve = !!opts?.observe;
18064 if (wantsObserve) {
18065 s.observeCount++;
18066 pushInstrumentation(windowId);
18067 }
18068 return () => {
18069 const cur = states.get(windowId);
18070 if (!cur) {
18071 return;
18072 }
18073 cur.observers.delete(cb);
18074 if (wantsObserve) {
18075 cur.observeCount = Math.max(0, cur.observeCount - 1);
18076 pushInstrumentation(windowId);
18077 }
18078 gcWindowState(windowId);
18079 };
18080 },
18081 reloadWithDebugSession(windowId, sessionId, opts) {
18082 if (typeof windowId !== "string" || windowId === "" || typeof sessionId !== "string" || sessionId === "") {
18083 return null;
18084 }
18085 const iframe = findIframe(windowId);
18086 if (!iframe) {
18087 return null;
18088 }
18089 const headerName = opts?.headerName || "X-WP-Debug-Session";
18090 const queryArg = opts?.queryArg || "wp_debug_session";
18091 const stopHeader = devtools.addRequestHeader(windowId, headerName, sessionId);
18092 try {
18093 const currentSrc = iframe.getAttribute("src") || iframe.src || "";
18094 const u = new URL(currentSrc, window.location.origin);
18095 u.searchParams.set(queryArg, sessionId);
18096 iframe.src = u.toString();
18097 } catch {
18098 }
18099 return {
18100 dispose: () => {
18101 stopHeader();
18102 }
18103 };
18104 },
18105 debug: debugBus
18106 };
18107 function gcWindowState(windowId) {
18108 const s = states.get(windowId);
18109 if (!s) {
18110 return;
18111 }
18112 if (s.headers.size === 0 && s.observers.size === 0) {
18113 detachLoadHandler(s);
18114 states.delete(windowId);
18115 }
18116 }
18117 async function wpdConfirm(options) {
18118 await ensureShellOverlaysLoaded(shellOverlaysBundleUrl());
18119 return new Promise((resolve2) => {
18120 const dialog2 = document.createElement("wpd-confirm-dialog");
18121 dialog2.setAttribute("open", "");
18122 if (options.title) {
18123 dialog2.setAttribute("title", options.title);
18124 }
18125 dialog2.setAttribute("message", options.message);
18126 if (options.confirmLabel) {
18127 dialog2.setAttribute("confirm-label", options.confirmLabel);
18128 }
18129 if (options.cancelLabel) {
18130 dialog2.setAttribute("cancel-label", options.cancelLabel);
18131 }
18132 if (options.danger) {
18133 dialog2.setAttribute("danger", "");
18134 }
18135 if (options.hideCancel) {
18136 dialog2.setAttribute("hide-cancel", "");
18137 }
18138 if (options.dismissable) {
18139 dialog2.setAttribute("dismissable", "");
18140 }
18141 const cleanup = (ok) => {
18142 dialog2.remove();
18143 resolve2(ok);
18144 };
18145 dialog2.addEventListener("wpd-confirm", () => cleanup(true));
18146 dialog2.addEventListener("wpd-cancel", () => cleanup(false));
18147 document.body.appendChild(dialog2);
18148 const inner = dialog2.shadowRoot?.querySelector(".dialog");
18149 (inner ?? dialog2).focus?.();
18150 });
18151 }
18152 function collectWallpaperSurfaces(manager) {
18153 const seed2 = [];
18154 for (const w of manager.getVisibleRects()) {
18155 if (w.state === "minimized") {
18156 continue;
18157 }
18158 if (w.element.offsetParent === null) {
18159 continue;
18160 }
18161 const r = w.element.getBoundingClientRect();
18162 seed2.push({
18163 id: `window:${w.windowId}`,
18164 kind: "window",
18165 rect: rectFromDom(r),
18166 face: "top",
18167 element: w.element
18168 });
18169 }
18170 const shellEl = document.getElementById("desktop-mode-shell");
18171 if (shellEl) {
18172 const r = shellEl.getBoundingClientRect();
18173 seed2.push({
18174 id: "shell:floor",
18175 kind: "shell",
18176 rect: {
18177 x: r.left,
18178 y: r.bottom - 1,
18179 width: r.width,
18180 height: 1
18181 },
18182 face: "top",
18183 element: shellEl
18184 });
18185 }
18186 const dockEls = document.querySelectorAll(
18187 ".desktop-mode-dock"
18188 );
18189 let dockIndex = 0;
18190 for (const dockEl of Array.from(dockEls)) {
18191 const r = dockEl.getBoundingClientRect();
18192 if (r.width <= 0 || r.height <= 0) {
18193 continue;
18194 }
18195 const placement = dockEl.getAttribute("data-desktop-mode-dock-placement") ?? "bottom";
18196 const id = dockIndex === 0 ? "dock:edge" : `dock:edge:${dockIndex}`;
18197 dockIndex++;
18198 if (placement === "bottom") {
18199 seed2.push({
18200 id,
18201 kind: "dock",
18202 rect: { x: r.left, y: r.top, width: r.width, height: 1 },
18203 face: "top",
18204 element: dockEl
18205 });
18206 } else if (placement === "right") {
18207 seed2.push({
18208 id,
18209 kind: "dock",
18210 rect: { x: r.left, y: r.top, width: 1, height: r.height },
18211 face: "left",
18212 element: dockEl
18213 });
18214 } else {
18215 seed2.push({
18216 id,
18217 kind: "dock",
18218 rect: {
18219 x: r.right - 1,
18220 y: r.top,
18221 width: 1,
18222 height: r.height
18223 },
18224 face: "right",
18225 element: dockEl
18226 });
18227 }
18228 }
18229 const widgetCards = document.querySelectorAll(
18230 ".desktop-mode-widgets__card"
18231 );
18232 let widgetIndex = 0;
18233 widgetCards.forEach((card) => {
18234 const r = card.getBoundingClientRect();
18235 if (r.width === 0 && r.height === 0) {
18236 return;
18237 }
18238 const id = card.dataset.widgetId ?? String(widgetIndex++);
18239 seed2.push({
18240 id: `widget:${id}`,
18241 kind: "widget",
18242 rect: rectFromDom(r),
18243 face: "top",
18244 element: card
18245 });
18246 });
18247 const filtered = applyFilters(HOOKS.WALLPAPER_SURFACES, seed2);
18248 return Array.isArray(filtered) ? filtered : seed2;
18249 }
18250 function rectFromDom(r) {
18251 return {
18252 x: r.left,
18253 y: r.top,
18254 width: r.width,
18255 height: r.height
18256 };
18257 }
18258 const NODE_KEY_PROP = "__desktop_modeKeyedListKey";
18259 const NODE_DATA_PROP = "__desktop_modeKeyedListData";
18260 function getHostState(host) {
18261 const cached = host.__desktop_modeKeyedList;
18262 if (cached) {
18263 return cached;
18264 }
18265 const fresh = { byKey: /* @__PURE__ */ new Map() };
18266 host.__desktop_modeKeyedList = fresh;
18267 return fresh;
18268 }
18269 function renderKeyedList(host, items, opts) {
18270 const state2 = getHostState(host);
18271 const prev = state2.byKey;
18272 const next = /* @__PURE__ */ new Map();
18273 const ordered = [];
18274 const seenKeys = /* @__PURE__ */ new Set();
18275 for (const item of items) {
18276 const key = String(opts.keyOf(item));
18277 if (seenKeys.has(key)) {
18278 console.warn(
18279 "[desktop-mode/keyed-list] duplicate key — only the last item with this key will render:",
18280 key
18281 );
18282 }
18283 seenKeys.add(key);
18284 const reused = prev.get(key);
18285 if (reused) {
18286 const prevData = reused.data;
18287 opts.updateItem?.(reused.el, item, prevData);
18288 reused.data = item;
18289 next.set(key, reused);
18290 ordered.push(reused.el);
18291 continue;
18292 }
18293 const el = opts.buildItem(item);
18294 el[NODE_KEY_PROP] = key;
18295 el[NODE_DATA_PROP] = item;
18296 next.set(key, { el, data: item });
18297 ordered.push(el);
18298 }
18299 for (const [key, entry] of prev) {
18300 if (!next.has(key)) {
18301 entry.el.remove();
18302 }
18303 }
18304 for (let i = 0; i < ordered.length; i++) {
18305 const desired = ordered[i];
18306 const live = host.children[i];
18307 if (live === desired) {
18308 continue;
18309 }
18310 host.insertBefore(desired, live ?? null);
18311 }
18312 state2.byKey = next;
18313 }
18314 function clearKeyedList(host) {
18315 const cached = host.__desktop_modeKeyedList;
18316 if (!cached) {
18317 return;
18318 }
18319 for (const entry of cached.byKey.values()) {
18320 entry.el.remove();
18321 }
18322 cached.byKey.clear();
18323 delete host.__desktop_modeKeyedList;
18324 }
18325 function createInfiniteList(options) {
18326 const {
18327 root,
18328 fetchPage,
18329 getId,
18330 renderItem,
18331 rootMargin = "200px",
18332 initialCursor = null,
18333 onLoadingChange = () => void 0,
18334 onError = (err) => {
18335 if (typeof console !== "undefined") {
18336 console.error("[desktop-mode] createInfiniteList:", err);
18337 }
18338 }
18339 } = options;
18340 let sentinel = options.sentinel ?? null;
18341 if (!sentinel) {
18342 sentinel = document.createElement("div");
18343 sentinel.dataset.wpdInfiniteListSentinel = "";
18344 sentinel.style.height = "1px";
18345 root.appendChild(sentinel);
18346 }
18347 const seen = /* @__PURE__ */ new Set();
18348 let cursor = initialCursor;
18349 let hasMoreInternal = true;
18350 let loading = false;
18351 let controller = null;
18352 let renderedCount = 0;
18353 let destroyed = false;
18354 let observer = null;
18355 const setLoading = (next) => {
18356 if (loading === next) {
18357 return;
18358 }
18359 loading = next;
18360 try {
18361 onLoadingChange(next);
18362 } catch (err) {
18363 onError(err);
18364 }
18365 };
18366 const detachObserver = () => {
18367 if (observer) {
18368 observer.disconnect();
18369 observer = null;
18370 }
18371 };
18372 const ensureObserver = () => {
18373 if (observer || !sentinel || destroyed) {
18374 return;
18375 }
18376 observer = new IntersectionObserver(
18377 (entries) => {
18378 for (const entry of entries) {
18379 if (entry.isIntersecting) {
18380 void loadMore();
18381 }
18382 }
18383 },
18384 { rootMargin }
18385 );
18386 observer.observe(sentinel);
18387 };
18388 const loadMore = async () => {
18389 if (destroyed || loading || !hasMoreInternal) {
18390 return;
18391 }
18392 setLoading(true);
18393 controller = new AbortController();
18394 const localController = controller;
18395 try {
18396 const page = await fetchPage(cursor, localController.signal);
18397 if (destroyed || localController !== controller) {
18398 return;
18399 }
18400 let appended = 0;
18401 const frag = document.createDocumentFragment();
18402 for (const item of page.items ?? []) {
18403 const key = String(getId(item));
18404 if (seen.has(key)) {
18405 continue;
18406 }
18407 seen.add(key);
18408 const el = renderItem(item, renderedCount + appended);
18409 frag.appendChild(el);
18410 appended++;
18411 }
18412 if (appended > 0) {
18413 if (sentinel && sentinel.parentNode === root) {
18414 root.insertBefore(frag, sentinel);
18415 } else {
18416 root.appendChild(frag);
18417 }
18418 renderedCount += appended;
18419 }
18420 cursor = page.nextCursor ?? null;
18421 if (!cursor) {
18422 hasMoreInternal = false;
18423 detachObserver();
18424 }
18425 } catch (err) {
18426 if (err?.name === "AbortError") {
18427 return;
18428 }
18429 onError(err);
18430 } finally {
18431 if (localController === controller) {
18432 setLoading(false);
18433 controller = null;
18434 }
18435 }
18436 };
18437 const reset = () => {
18438 if (destroyed) {
18439 return;
18440 }
18441 controller?.abort();
18442 controller = null;
18443 seen.clear();
18444 cursor = initialCursor;
18445 hasMoreInternal = true;
18446 renderedCount = 0;
18447 const sentinelInRoot = sentinel && sentinel.parentNode === root;
18448 while (root.firstChild) {
18449 root.removeChild(root.firstChild);
18450 }
18451 if (sentinelInRoot && sentinel) {
18452 root.appendChild(sentinel);
18453 }
18454 setLoading(false);
18455 ensureObserver();
18456 void loadMore();
18457 };
18458 const destroy = () => {
18459 if (destroyed) {
18460 return;
18461 }
18462 destroyed = true;
18463 detachObserver();
18464 controller?.abort();
18465 controller = null;
18466 if (!options.sentinel && sentinel && sentinel.parentNode === root) {
18467 root.removeChild(sentinel);
18468 }
18469 sentinel = null;
18470 setLoading(false);
18471 };
18472 ensureObserver();
18473 void loadMore();
18474 return {
18475 reset,
18476 loadMore,
18477 hasMore: () => hasMoreInternal,
18478 isLoading: () => loading,
18479 destroy
18480 };
18481 }
18482 const POPUP_DEFAULT_WIDTH = 520;
18483 const POPUP_DEFAULT_HEIGHT = 720;
18484 const POPUP_CLOSE_POLL_MS = 500;
18485 function startOAuth(service, options = {}) {
18486 if (typeof service !== "string" || service === "") {
18487 return Promise.reject(
18488 new Error("[desktop-mode] startOAuth requires a non-empty service slug.")
18489 );
18490 }
18491 const restRoot2 = readRestRoot$1();
18492 const restNonce = readRestNonce$1();
18493 return trackedFetch$1(
18494 joinRestUrl(restRoot2, "desktop-mode/v1/oauth/start"),
18495 {
18496 method: "POST",
18497 headers: {
18498 "Content-Type": "application/json",
18499 "X-WP-Nonce": restNonce ?? ""
18500 },
18501 body: JSON.stringify({ service })
18502 },
18503 { source: "desktop-mode/oauth-start" }
18504 ).then(async (res) => {
18505 if (!res.ok) {
18506 const text = await res.text().catch(() => "");
18507 throw new Error(
18508 `[desktop-mode] OAuth start failed (${res.status}): ${text}`
18509 );
18510 }
18511 return await res.json();
18512 }).then((startBody) => openPopupAndWait(startBody, service, options));
18513 }
18514 function openPopupAndWait(body, service, options) {
18515 return new Promise((resolve2, reject) => {
18516 const width = options.width ?? POPUP_DEFAULT_WIDTH;
18517 const height = options.height ?? POPUP_DEFAULT_HEIGHT;
18518 const left = Math.max(0, Math.floor((window.screen.width - width) / 2));
18519 const top = Math.max(0, Math.floor((window.screen.height - height) / 2));
18520 const features = [
18521 `width=${width}`,
18522 `height=${height}`,
18523 `left=${left}`,
18524 `top=${top}`,
18525 "menubar=no",
18526 "toolbar=no",
18527 "location=yes",
18528 "status=no",
18529 "resizable=yes",
18530 "scrollbars=yes"
18531 ].join(",");
18532 const popup = window.open(
18533 body.authorize_url,
18534 `desktop-mode-oauth-${service}`,
18535 features
18536 );
18537 if (!popup) {
18538 reject(
18539 new Error(
18540 "[desktop-mode] OAuth popup blocked. Tell users to allow popups for this site."
18541 )
18542 );
18543 return;
18544 }
18545 const expectedOrigin = window.location.origin;
18546 let pollTimer = null;
18547 let detached = false;
18548 const cleanup = () => {
18549 if (detached) {
18550 return;
18551 }
18552 detached = true;
18553 window.removeEventListener("message", onMessage);
18554 if (pollTimer !== null) {
18555 window.clearInterval(pollTimer);
18556 pollTimer = null;
18557 }
18558 };
18559 const onMessage = (e) => {
18560 if (e.origin !== expectedOrigin) {
18561 return;
18562 }
18563 const data = e.data;
18564 if (!data || data.type !== "desktop-mode-oauth-callback") {
18565 return;
18566 }
18567 const payload = data.payload;
18568 cleanup();
18569 if (payload && payload.ok) {
18570 resolve2(payload);
18571 } else {
18572 const reason = payload?.reason ?? "unknown";
18573 const message = payload?.message ?? "OAuth flow failed";
18574 const err = new Error(
18575 `[desktop-mode] startOAuth(${service}) failed: ${reason} — ${message}`
18576 );
18577 err.cause = payload;
18578 reject(err);
18579 }
18580 };
18581 window.addEventListener("message", onMessage);
18582 pollTimer = window.setInterval(() => {
18583 if (popup.closed) {
18584 cleanup();
18585 reject(
18586 new Error(
18587 `[desktop-mode] startOAuth(${service}) cancelled — popup closed before completing.`
18588 )
18589 );
18590 }
18591 }, POPUP_CLOSE_POLL_MS);
18592 });
18593 }
18594 function readDesktopConfig() {
18595 return window.desktopModeConfig ?? {};
18596 }
18597 function readRestRoot$1() {
18598 const root = readDesktopConfig().restRoot;
18599 if (typeof root === "string" && root !== "") {
18600 return root;
18601 }
18602 return `${window.location.origin}/wp-json/`;
18603 }
18604 function readRestNonce$1() {
18605 const nonce = readDesktopConfig().restNonce;
18606 return typeof nonce === "string" && nonce !== "" ? nonce : null;
18607 }
18608 const RESERVED_NAMESPACE_KEYS = /* @__PURE__ */ new Set([
18609 "windowManager",
18610 "dock",
18611 "sideDock",
18612 "taskbar",
18613 "desktopLayout",
18614 "icons",
18615 "files",
18616 "confirm",
18617 "saveSession",
18618 "hooks",
18619 "HOOKS",
18620 "isActive",
18621 "registerWallpaper",
18622 "registerWidget",
18623 "widgetLayer",
18624 "widgets",
18625 "registerSystemTile",
18626 "registerWindow",
18627 "openWindow",
18628 "openNewWindow",
18629 "cloneTemplate",
18630 "onWindow",
18631 "createInfiniteList",
18632 "startOAuth",
18633 "repaintLoadingOverlays",
18634 "loadVendorScript",
18635 "getWallpaperSurfaces",
18636 "registerModule",
18637 "loadModules",
18638 "whenReady",
18639 "ready",
18640 "isReady",
18641 "setDefaultWindow",
18642 "refreshMenu",
18643 "config",
18644 "ai",
18645 "dragBridge",
18646 "dragManager",
18647 "registerCommand",
18648 "unregisterCommand",
18649 "listCommands",
18650 "registerDestructiveAdminAction",
18651 "unregisterDestructiveAdminAction",
18652 "listDestructiveAdminActions",
18653 "registerSettingsTab",
18654 "unregisterSettingsTab",
18655 "listSettingsTabs",
18656 "registerDockRailRenderer",
18657 "unregisterDockRailRenderer",
18658 "listDockRailRenderers",
18659 "openOsSettings",
18660 "getOsSettings",
18661 "subscribeOsSettings",
18662 "updateOsSettings",
18663 "deriveWindowId",
18664 "listSystemTiles",
18665 "getSystemTile",
18666 "getMenuItems",
18667 "renderIcon",
18668 "applyTileClasses",
18669 "applyTileElement",
18670 "applyTileTooltip",
18671 "dispatchTileRendered",
18672 "isDockElement",
18673 "registerDockSelector",
18674 "registerTitleBarButton",
18675 "unregisterTitleBarButton",
18676 "listTitleBarButtons",
18677 "registerUnfocusEffect",
18678 "unregisterUnfocusEffect",
18679 "listUnfocusEffects",
18680 "registerWindowTheme",
18681 "unregisterWindowTheme",
18682 "listWindowThemes",
18683 "applyWindowTheme",
18684 "registerWindowControl",
18685 "unregisterWindowControl",
18686 "listWindowControls",
18687 "applyWindowControls",
18688 "registerWindowSlot",
18689 "unregisterWindowSlot",
18690 "listWindowSlots",
18691 "applyWindowSlot",
18692 "registerWindowNotice",
18693 "unregisterWindowNotice",
18694 "listWindowNotices",
18695 "dismissWindowNotice",
18696 "undismissWindowNotice",
18697 "registerWindowChrome",
18698 "unregisterWindowChrome",
18699 "listWindowChromes",
18700 "applyWindowChrome",
18701 "connect",
18702 "getConnection",
18703 "broadcast",
18704 "subscribe",
18705 "registerPalette",
18706 "unregisterPalette",
18707 "listPalettes",
18708 "openPalette",
18709 "devtools",
18710 "createSharedStore",
18711 "presence",
18712 "activity",
18713 "heartbeat",
18714 "showToast",
18715 "renderKeyedList",
18716 "clearKeyedList",
18717 "registerNamespace",
18718 "notify",
18719 "pwa",
18720 "getWindowConfig",
18721 "debug",
18722 "fetch"
18723 ]);
18724 function buildPublicApi(deps2) {
18725 const {
18726 manager,
18727 dock,
18728 layoutDispatcher,
18729 osSettings,
18730 iconsApi: iconsApi2,
18731 filesApi: filesApi2,
18732 saveSession,
18733 widgetLayer,
18734 registerWindow,
18735 openWindowById,
18736 openNewWindowById,
18737 placeSystemTile,
18738 setDefaultWindow,
18739 refreshMenu,
18740 openOsSettings,
18741 aiAssistant,
18742 dragBridge,
18743 dragManager,
18744 connect,
18745 getConnection,
18746 config
18747 } = deps2;
18748 const desktopApi = {
18749 windowManager: manager,
18750 dock,
18751 sideDock: layoutDispatcher?.getSide() ?? null,
18752 desktopLayout: osSettings.getOsSettingsSnapshot().desktopLayout,
18753 icons: iconsApi2,
18754 files: filesApi2,
18755 confirm: wpdConfirm,
18756 saveSession,
18757 hooks: rawHooks(),
18758 HOOKS,
18759 isActive: () => !!document.getElementById("desktop-mode-shell"),
18760 registerWallpaper: (def) => {
18761 register$2(def);
18762 osSettings.apply();
18763 },
18764 registerWidget: (def) => {
18765 register(def);
18766 },
18767 widgetLayer,
18768 widgets: {
18769 redock: (id) => {
18770 widgetLayer?.redock(id);
18771 }
18772 },
18773 loadVendorScript,
18774 getWallpaperSurfaces: () => collectWallpaperSurfaces(manager),
18775 registerWindow,
18776 openWindow: openWindowById,
18777 openNewWindow: openNewWindowById,
18778 fetch: (input, requestInit, opts) => trackedFetch(manager, input, requestInit, opts),
18779 repaintLoadingOverlays,
18780 cloneTemplate,
18781 onWindow,
18782 createInfiniteList,
18783 startOAuth,
18784 registerSystemTile: (item) => {
18785 placeSystemTile(item);
18786 doAction(HOOKS.DOCK_ITEM_APPENDED, { id: item.id });
18787 },
18788 registerModule,
18789 loadModules,
18790 whenReady,
18791 ready: whenReady,
18792 isReady,
18793 setDefaultWindow,
18794 refreshMenu,
18795 config,
18796 ai: aiAssistant,
18797 dragBridge,
18798 dragManager,
18799 registerCommand,
18800 unregisterCommand,
18801 listCommands,
18802 registerDestructiveAdminAction,
18803 unregisterDestructiveAdminAction,
18804 listDestructiveAdminActions,
18805 registerSettingsTab,
18806 unregisterSettingsTab,
18807 listSettingsTabs,
18808 registerDockRailRenderer: register$1,
18809 unregisterDockRailRenderer: unregister$1,
18810 listDockRailRenderers: list,
18811 openOsSettings,
18812 getOsSettings: () => osSettings.getOsSettingsSnapshot(),
18813 subscribeOsSettings: (cb) => osSettings.subscribeOsSettings(cb),
18814 updateOsSettings: (patch, opts = {}) => {
18815 if (typeof patch.wallpaper === "string") {
18816 osSettings.state.wallpaper = patch.wallpaper;
18817 }
18818 if (typeof patch.accent === "string") {
18819 osSettings.state.accent = patch.accent;
18820 }
18821 if (typeof patch.dockSize === "string") {
18822 osSettings.state.dockSize = patch.dockSize;
18823 }
18824 if (typeof patch.desktopLayout === "string") {
18825 osSettings.state.desktopLayout = patch.desktopLayout;
18826 }
18827 if (typeof patch.dockRailRenderer === "string") {
18828 osSettings.state.dockRailRenderer = patch.dockRailRenderer;
18829 }
18830 if (patch.ai && typeof patch.ai === "object") {
18831 osSettings.state.ai = { ...osSettings.state.ai, ...patch.ai };
18832 }
18833 if (typeof patch.nativePostsEnabled === "boolean") {
18834 osSettings.state.nativePostsEnabled = patch.nativePostsEnabled;
18835 }
18836 if (typeof patch.nativePagesEnabled === "boolean") {
18837 osSettings.state.nativePagesEnabled = patch.nativePagesEnabled;
18838 }
18839 if (typeof patch.nativeUsersEnabled === "boolean") {
18840 osSettings.state.nativeUsersEnabled = patch.nativeUsersEnabled;
18841 }
18842 if (typeof patch.nativePluginsEnabled === "boolean") {
18843 osSettings.state.nativePluginsEnabled = patch.nativePluginsEnabled;
18844 }
18845 if (typeof patch.nativeCommentsEnabled === "boolean") {
18846 osSettings.state.nativeCommentsEnabled = patch.nativeCommentsEnabled;
18847 }
18848 if (typeof patch.foldersSharingEnabled === "boolean") {
18849 osSettings.state.foldersSharingEnabled = patch.foldersSharingEnabled;
18850 }
18851 if (Array.isArray(patch.nativePostsHiddenColumns)) {
18852 osSettings.state.nativePostsHiddenColumns = patch.nativePostsHiddenColumns.filter(
18853 (v) => typeof v === "string" && v !== ""
18854 ).slice(0, 32);
18855 }
18856 if (patch.itemVisibility && typeof patch.itemVisibility === "object") {
18857 const allowed = ["both", "dock", "desktop", "hidden"];
18858 const next = {};
18859 for (const [k, v] of Object.entries(
18860 patch.itemVisibility
18861 )) {
18862 if (typeof k !== "string" || k === "") {
18863 continue;
18864 }
18865 if (typeof v !== "string" || !allowed.includes(v)) {
18866 continue;
18867 }
18868 next[k] = v;
18869 }
18870 osSettings.state.itemVisibility = next;
18871 }
18872 if (Array.isArray(patch.dockOrder)) {
18873 osSettings.state.dockOrder = patch.dockOrder.filter(
18874 (v) => typeof v === "string" && v !== ""
18875 ).slice(0, 256);
18876 }
18877 if (patch.dockPromotedPositions && typeof patch.dockPromotedPositions === "object") {
18878 const MAX_COORD = 1e5;
18879 const next = {};
18880 for (const [k, v] of Object.entries(
18881 patch.dockPromotedPositions
18882 )) {
18883 if (typeof k !== "string" || k === "") {
18884 continue;
18885 }
18886 if (!v || typeof v !== "object") {
18887 continue;
18888 }
18889 const pos = v;
18890 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) {
18891 continue;
18892 }
18893 next[k] = { x: pos.x, y: pos.y };
18894 if (Object.keys(next).length >= 256) {
18895 break;
18896 }
18897 }
18898 osSettings.state.dockPromotedPositions = next;
18899 }
18900 osSettings.save(opts);
18901 if (patch.itemVisibility || patch.dockOrder) {
18902 layoutDispatcher?.refresh();
18903 }
18904 },
18905 deriveWindowId: (url, overrideAdminUrl) => deriveWindowId(url, overrideAdminUrl ?? config.adminUrl),
18906 listSystemTiles: () => layoutDispatcher?.listSystemTiles() ?? [],
18907 getSystemTile: (id) => layoutDispatcher?.getSystemTile(id) ?? null,
18908 getMenuItems: () => layoutDispatcher?.getMenuItems() ?? [],
18909 renderIcon,
18910 applyTileClasses,
18911 applyTileElement,
18912 applyTileTooltip,
18913 dispatchTileRendered,
18914 isDockElement,
18915 registerDockSelector,
18916 registerTitleBarButton,
18917 unregisterTitleBarButton,
18918 listTitleBarButtons,
18919 registerUnfocusEffect,
18920 unregisterUnfocusEffect,
18921 listUnfocusEffects,
18922 registerWindowTheme,
18923 unregisterWindowTheme,
18924 listWindowThemes,
18925 applyWindowTheme: (windowId, override) => {
18926 const win = manager.getById(windowId);
18927 if (!win) {
18928 return;
18929 }
18930 win.setAppearanceTheme(override);
18931 },
18932 registerWindowControl,
18933 unregisterWindowControl,
18934 listWindowControls,
18935 applyWindowControls: (windowId, override) => {
18936 const win = manager.getById(windowId);
18937 if (!win) {
18938 return;
18939 }
18940 win.setAppearanceControls(override);
18941 },
18942 registerWindowSlot,
18943 unregisterWindowSlot,
18944 listWindowSlots,
18945 applyWindowSlot: (windowId, slot, slotConfig) => {
18946 const win = manager.getById(windowId);
18947 if (!win) {
18948 return;
18949 }
18950 win.setAppearanceSlot(slot, slotConfig);
18951 },
18952 registerWindowNotice,
18953 unregisterWindowNotice,
18954 listWindowNotices,
18955 dismissWindowNotice,
18956 undismissWindowNotice,
18957 registerWindowChrome,
18958 unregisterWindowChrome,
18959 listWindowChromes,
18960 applyWindowChrome: (windowId, chromeId) => {
18961 const win = manager.getById(windowId);
18962 if (!win) {
18963 return;
18964 }
18965 win.setAppearanceChrome(chromeId);
18966 },
18967 connect,
18968 getConnection,
18969 broadcast,
18970 subscribe: subscribe$2,
18971 registerPalette,
18972 unregisterPalette,
18973 listPalettes,
18974 openPalette: openPaletteOnly,
18975 devtools,
18976 createSharedStore,
18977 presence: presenceApi,
18978 activity,
18979 heartbeat,
18980 showToast,
18981 notify: notify$3,
18982 pwa: {
18983 promptInstall,
18984 undismissInstallHint,
18985 getState: getPwaState,
18986 subscribe: subscribePwaState,
18987 requestNotificationPermission,
18988 getNotificationPermission
18989 },
18990 renderKeyedList,
18991 clearKeyedList,
18992 registerNamespace: (name, api) => {
18993 if (typeof name !== "string" || name === "") {
18994 console.warn(
18995 "[desktop-mode] registerNamespace: name must be a non-empty string"
18996 );
18997 return;
18998 }
18999 if (!api || typeof api !== "object") {
19000 console.warn(
19001 `[desktop-mode] registerNamespace("${name}"): api must be an object`
19002 );
19003 return;
19004 }
19005 if (RESERVED_NAMESPACE_KEYS.has(name)) {
19006 console.warn(
19007 `[desktop-mode] registerNamespace("${name}"): name is reserved by the shell — pick a plugin-specific key`
19008 );
19009 return;
19010 }
19011 desktopApi[name] = api;
19012 },
19013 getWindowConfig: (id) => {
19014 const store2 = window.desktopModeWindowConfig;
19015 if (!store2 || typeof store2 !== "object") {
19016 return void 0;
19017 }
19018 const value = store2[id];
19019 return value === void 0 ? void 0 : value;
19020 },
19021 debug: {
19022 window: (id) => {
19023 const entry = (config.nativeWindows ?? []).find(
19024 (e) => e.id === id
19025 );
19026 if (!entry) {
19027 return null;
19028 }
19029 const url = entry.scriptUrl || "";
19030 let loadPath = "unknown";
19031 let tagInDom = false;
19032 if (url) {
19033 const lazyTag = document.querySelector(
19034 `script[data-desktop-mode-vendor="${url.replace(/"/g, '\\"')}"]`
19035 );
19036 if (lazyTag) {
19037 loadPath = "lazy";
19038 tagInDom = true;
19039 } else {
19040 const eagerTag = Array.from(
19041 document.querySelectorAll(
19042 "script[src]"
19043 )
19044 ).find((s) => s.src === url);
19045 if (eagerTag) {
19046 loadPath = "eager";
19047 tagInDom = true;
19048 }
19049 }
19050 }
19051 const cfgStore = window.desktopModeWindowConfig;
19052 const configPresent = !!(cfgStore && typeof cfgStore === "object" && Object.prototype.hasOwnProperty.call(cfgStore, id));
19053 return {
19054 id,
19055 scriptHandle: entry.scriptHandle || "",
19056 scriptUrl: url,
19057 loadPath,
19058 tagInDom,
19059 configPresent,
19060 extras: {
19061 hasTranslations: !!entry.scriptTranslations,
19062 l10nCount: (entry.scriptL10n ?? []).length,
19063 beforeCount: (entry.scriptBefore ?? []).length,
19064 afterCount: (entry.scriptAfter ?? []).length
19065 }
19066 };
19067 }
19068 }
19069 };
19070 return desktopApi;
19071 }
19072 function installPublicApi(api) {
19073 if (!window.wp) {
19074 window.wp = {};
19075 }
19076 if (!window.wp.desktop) {
19077 window.wp.desktop = api;
19078 return;
19079 }
19080 Object.assign(
19081 window.wp.desktop,
19082 api
19083 );
19084 }
19085 const store$1 = createSharedStore("desktop-mode/layout", () => ({
19086 // Default mirrors the OsSettingsSnapshot default; the shell
19087 // re-publishes the persisted value as soon as it boots.
19088 layout: "classic"
19089 }));
19090 function setCurrentLayout(layout) {
19091 if (store$1.state.layout === layout) {
19092 return;
19093 }
19094 store$1.state.layout = layout;
19095 store$1.notify();
19096 }
19097 class DesktopFile {
19098 constructor(shape) {
19099 this.shape = shape;
19100 }
19101 /** Title shown under the tile. Defaults to `shape.title`. */
19102 title() {
19103 return this.shape.title;
19104 }
19105 /** Dashicon class or data URI. Defaults to `shape.icon`. */
19106 icon() {
19107 return this.shape.icon;
19108 }
19109 /** Optional preview-image URL. Defaults to `shape.previewUrl`. */
19110 previewUrl() {
19111 return this.shape.previewUrl;
19112 }
19113 /** Reference (id, URL, …). */
19114 ref() {
19115 return this.shape.ref;
19116 }
19117 /** Whether the underlying entity still exists. */
19118 exists() {
19119 return this.shape.exists;
19120 }
19121 }
19122 class DefaultDesktopFile extends DesktopFile {
19123 constructor(shape, typeSlug) {
19124 super(shape);
19125 this.typeSlug = typeSlug;
19126 }
19127 type() {
19128 return this.typeSlug;
19129 }
19130 }
19131 const seed$1 = /* @__PURE__ */ new Map();
19132 const listeners$1 = /* @__PURE__ */ new Set();
19133 function registerType(def) {
19134 if (!def.type) {
19135 throw new Error("[desktop-mode] registerType: `type` is required.");
19136 }
19137 if (!def.label) {
19138 throw new Error("[desktop-mode] registerType: `label` is required.");
19139 }
19140 seed$1.set(def.type, {
19141 type: def.type,
19142 label: def.label,
19143 sort: typeof def.sort === "number" ? def.sort : 100,
19144 DesktopFile: def.DesktopFile
19145 });
19146 doAction("desktop-mode.files.type-registered", def.type, def);
19147 notify$1();
19148 }
19149 function unregisterType(typeSlug) {
19150 if (seed$1.delete(typeSlug)) {
19151 doAction("desktop-mode.files.type-unregistered", typeSlug);
19152 notify$1();
19153 }
19154 }
19155 function getType(typeSlug) {
19156 const entry = seed$1.get(typeSlug);
19157 return entry ? entry : null;
19158 }
19159 function getTypes() {
19160 const list2 = Array.from(seed$1.values()).slice();
19161 const filtered = applyFilters(
19162 "desktop-mode.files.types",
19163 list2
19164 );
19165 const arr = Array.isArray(filtered) ? filtered : list2;
19166 arr.sort((a, b) => {
19167 if (a.sort !== b.sort) {
19168 return a.sort - b.sort;
19169 }
19170 return a.label.localeCompare(b.label);
19171 });
19172 return arr;
19173 }
19174 function resolve(shape) {
19175 const entry = seed$1.get(shape.type);
19176 if (entry?.DesktopFile) {
19177 return new entry.DesktopFile(shape);
19178 }
19179 return new DefaultDesktopFile(shape, shape.type);
19180 }
19181 function subscribe(cb) {
19182 listeners$1.add(cb);
19183 return () => listeners$1.delete(cb);
19184 }
19185 function notify$1() {
19186 for (const cb of listeners$1) {
19187 try {
19188 cb();
19189 } catch (err) {
19190 console.error("[desktop-mode] files registry subscriber threw:", err);
19191 }
19192 }
19193 }
19194 const seed = /* @__PURE__ */ new Map();
19195 const listeners = /* @__PURE__ */ new Set();
19196 let userAssociations = {};
19197 function setUserAssociations(map) {
19198 userAssociations = { ...map };
19199 notify();
19200 }
19201 function getUserAssociations() {
19202 return { ...userAssociations };
19203 }
19204 function registerOpener(def) {
19205 if (!def.id) {
19206 throw new Error("[desktop-mode] registerOpener: `id` is required.");
19207 }
19208 if (!def.label) {
19209 throw new Error("[desktop-mode] registerOpener: `label` is required.");
19210 }
19211 if (!Array.isArray(def.types) || def.types.length === 0) {
19212 throw new Error("[desktop-mode] registerOpener: `types` must be a non-empty array.");
19213 }
19214 if (!def.handler || typeof def.handler !== "object") {
19215 throw new Error("[desktop-mode] registerOpener: `handler` is required.");
19216 }
19217 seed.set(def.id, {
19218 id: def.id,
19219 label: def.label,
19220 types: def.types.slice(),
19221 isDefault: !!def.isDefault,
19222 sort: typeof def.sort === "number" ? def.sort : 100,
19223 handler: def.handler
19224 });
19225 doAction("desktop-mode.files.opener-registered", def.id, def);
19226 notify();
19227 }
19228 function unregisterOpener(id) {
19229 if (seed.delete(id)) {
19230 doAction("desktop-mode.files.opener-unregistered", id);
19231 notify();
19232 }
19233 }
19234 function getOpener(id) {
19235 return seed.get(id) ?? null;
19236 }
19237 function getOpeners() {
19238 const list2 = Array.from(seed.values()).slice();
19239 const filtered = applyFilters(
19240 "desktop-mode.files.openers",
19241 list2
19242 );
19243 const arr = Array.isArray(filtered) ? filtered : list2;
19244 arr.sort((a, b) => {
19245 const sa = typeof a.sort === "number" ? a.sort : 100;
19246 const sb = typeof b.sort === "number" ? b.sort : 100;
19247 if (sa !== sb) {
19248 return sa - sb;
19249 }
19250 return a.label.localeCompare(b.label);
19251 });
19252 return arr;
19253 }
19254 function getOpenersForType(type) {
19255 return getOpeners().filter((e) => e.types.includes(type));
19256 }
19257 function resolveOpener(type) {
19258 const candidates = getOpenersForType(type);
19259 if (candidates.length === 0) {
19260 return null;
19261 }
19262 const override = userAssociations[type];
19263 let resolved = null;
19264 if (override) {
19265 resolved = candidates.find((e) => e.id === override) ?? null;
19266 }
19267 if (!resolved) {
19268 resolved = candidates.find((e) => e.isDefault) ?? null;
19269 }
19270 if (!resolved) {
19271 resolved = candidates[0];
19272 }
19273 const filtered = applyFilters(
19274 "desktop-mode.files.resolve-opener",
19275 resolved,
19276 type
19277 );
19278 return filtered ?? null;
19279 }
19280 function subscribeOpeners(cb) {
19281 listeners.add(cb);
19282 return () => listeners.delete(cb);
19283 }
19284 function notify() {
19285 for (const cb of listeners) {
19286 try {
19287 cb();
19288 } catch (err) {
19289 console.error("[desktop-mode] openers subscriber threw:", err);
19290 }
19291 }
19292 }
19293 let deps$1 = null;
19294 function installOpenDeps(next) {
19295 deps$1 = next;
19296 }
19297 async function openFile(file, ctx) {
19298 if (!deps$1) {
19299 console.warn(
19300 "[desktop-mode] wp.desktop.files.open() called before the shell installed open deps. The file will not open."
19301 );
19302 return false;
19303 }
19304 const opener = resolveOpener(file.type());
19305 if (!opener) {
19306 doAction("desktop-mode.files.open-failed", {
19307 reason: "no-opener",
19308 type: file.type(),
19309 ref: file.ref()
19310 });
19311 return false;
19312 }
19313 doAction("desktop-mode.files.opening", { file, openerId: opener.id });
19314 try {
19315 const handler = opener.handler;
19316 if (handler.kind === "url") {
19317 const url = await handler.url(file);
19318 if (!url) {
19319 return false;
19320 }
19321 const id = handler.windowId ? handler.windowId(file) : deps$1.deriveWindowId(url);
19322 const title = handler.title ? handler.title(file) : file.title();
19323 const icon = file.icon();
19324 const opened = deps$1.openUrl({ id, url, title, icon });
19325 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "url" });
19326 return opened;
19327 }
19328 if (handler.kind === "window") {
19329 const config = handler.config ? handler.config(file) : void 0;
19330 const opened = deps$1.openNativeWindow(handler.windowId, config);
19331 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "window" });
19332 return opened;
19333 }
19334 await handler.open(file, ctx);
19335 doAction("desktop-mode.files.opened", { file, openerId: opener.id, kind: "js" });
19336 return true;
19337 } catch (err) {
19338 doAction("desktop-mode.files.open-failed", {
19339 reason: "handler-threw",
19340 type: file.type(),
19341 ref: file.ref(),
19342 openerId: opener.id,
19343 error: err
19344 });
19345 console.error("[desktop-mode] file opener threw:", err);
19346 return false;
19347 }
19348 }
19349 function registerBuiltInFileTypes() {
19350 registerType({ type: "shortcut", label: "Plugin shortcut", sort: 1 });
19351 registerType({ type: "folder", label: "Folder", sort: 5 });
19352 registerType({ type: "post", label: "Post", sort: 10 });
19353 registerType({ type: "attachment", label: "Media", sort: 20 });
19354 registerType({ type: "user", label: "User", sort: 30 });
19355 registerType({ type: "term", label: "Taxonomy term", sort: 40 });
19356 registerType({ type: "comment", label: "Comment", sort: 50 });
19357 registerType({ type: "bookmark", label: "Bookmark", sort: 60 });
19358 registerType({ type: "link", label: "Web link", sort: 70 });
19359 registerType({ type: "embed", label: "Embedded web window", sort: 80 });
19360 }
19361 let deps = null;
19362 function installRestDeps(next) {
19363 deps = next;
19364 }
19365 function ensureDeps() {
19366 if (!deps) {
19367 throw new Error("[desktop-mode] files REST client called before installRestDeps().");
19368 }
19369 return deps;
19370 }
19371 class FilesConflictError extends Error {
19372 constructor(detail) {
19373 super(
19374 `Row was changed by ${detail.actor.name || "another session"} (parent="${detail.current.parentName}")`
19375 );
19376 this.name = "FilesConflictError";
19377 this.status = 409;
19378 this.detail = detail;
19379 }
19380 }
19381 async function call(path, init2) {
19382 const { baseUrl, nonce } = ensureDeps();
19383 const url = joinRestUrl(baseUrl, path);
19384 const headers = new Headers(init2.headers ?? {});
19385 headers.set("X-WP-Nonce", nonce);
19386 if (init2.body && !headers.has("Content-Type")) {
19387 headers.set("Content-Type", "application/json");
19388 }
19389 const res = await trackedFetch$1(
19390 url,
19391 { ...init2, headers, credentials: "same-origin" },
19392 { source: "desktop-mode/files" }
19393 );
19394 const text = await res.text();
19395 let body = null;
19396 let parseError = null;
19397 if (text) {
19398 try {
19399 body = JSON.parse(text);
19400 } catch (e) {
19401 body = null;
19402 parseError = e;
19403 }
19404 }
19405 if (!res.ok) {
19406 if (res.status === 409) {
19407 const data = body?.data?.data ?? body?.data;
19408 if (data && typeof data === "object") {
19409 throw new FilesConflictError(data);
19410 }
19411 }
19412 const err = body;
19413 throw new Error(
19414 `[desktop-mode] files REST ${res.status}: ${err?.code ?? ""} ${err?.message ?? ""}`.trim()
19415 );
19416 }
19417 if (null === body) {
19418 if (parseError && text) {
19419 const head = text.slice(0, 120).replace(/\s+/g, " ");
19420 throw new Error(
19421 `[desktop-mode] files REST ${res.status} returned non-JSON body — ${parseError.message}. First 120 chars: ${head}`
19422 );
19423 }
19424 throw new Error(
19425 `[desktop-mode] files REST ${res.status}: empty or unparseable body.`
19426 );
19427 }
19428 return body;
19429 }
19430 function listPlacements(folderId = 0) {
19431 return call(
19432 `/placements?folder=${encodeURIComponent(String(folderId))}`,
19433 { method: "GET" }
19434 );
19435 }
19436 function createPlacement(body) {
19437 return call("/placements", {
19438 method: "POST",
19439 body: JSON.stringify(body)
19440 });
19441 }
19442 function updatePlacement(id, body, ifMatchMs) {
19443 const headers = {};
19444 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
19445 headers["If-Match"] = String(ifMatchMs);
19446 }
19447 return call(`/placements/${id}`, {
19448 method: "PATCH",
19449 body: JSON.stringify(body),
19450 headers
19451 });
19452 }
19453 function deletePlacement(id) {
19454 return call(`/placements/${id}`, { method: "DELETE" });
19455 }
19456 async function restoreTrashedItem(id, type) {
19457 const { baseUrl, nonce } = ensureDeps();
19458 const root = baseUrl.replace(/\/files\/?$/, "");
19459 const url = `${root}/recycle-bin/restore`;
19460 const res = await trackedFetch$1(
19461 url,
19462 {
19463 method: "POST",
19464 headers: {
19465 "Content-Type": "application/json",
19466 "X-WP-Nonce": nonce
19467 },
19468 credentials: "same-origin",
19469 body: JSON.stringify({ items: [{ id, type }] })
19470 },
19471 { source: "desktop-mode/files" }
19472 );
19473 if (!res.ok) {
19474 throw new Error(`[desktop-mode] restore ${res.status}`);
19475 }
19476 return await res.json();
19477 }
19478 function listFolders() {
19479 return call("/folders", { method: "GET" });
19480 }
19481 function createFolder(body) {
19482 return call("/folders", {
19483 method: "POST",
19484 body: JSON.stringify(body)
19485 });
19486 }
19487 function updateFolder(id, body, ifMatchMs) {
19488 const headers = {};
19489 if (typeof ifMatchMs === "number" && ifMatchMs > 0) {
19490 headers["If-Match"] = String(ifMatchMs);
19491 }
19492 return call(`/folders/${id}`, {
19493 method: "PATCH",
19494 body: JSON.stringify(body),
19495 headers
19496 });
19497 }
19498 function deleteFolder(id) {
19499 return call(`/folders/${id}`, { method: "DELETE" });
19500 }
19501 function saveAssociations(associations) {
19502 return call("/associations", {
19503 method: "PUT",
19504 body: JSON.stringify({ associations })
19505 });
19506 }
19507 function listShares(folderId) {
19508 return call(`/folders/${folderId}/shares`, { method: "GET" });
19509 }
19510 function inviteShare(folderId, body) {
19511 return call(`/folders/${folderId}/shares`, {
19512 method: "POST",
19513 body: JSON.stringify(body)
19514 });
19515 }
19516 function updateShareCapability(folderId, shareId, capability) {
19517 return call(`/folders/${folderId}/shares/${shareId}`, {
19518 method: "PATCH",
19519 body: JSON.stringify({ capability })
19520 });
19521 }
19522 function revokeShare(folderId, shareId) {
19523 return call(`/folders/${folderId}/shares/${shareId}`, {
19524 method: "DELETE"
19525 });
19526 }
19527 function acceptShare(folderId, shareId) {
19528 return call(`/folders/${folderId}/shares/${shareId}/accept`, {
19529 method: "POST"
19530 });
19531 }
19532 function denyShare(folderId, shareId) {
19533 return call(`/folders/${folderId}/shares/${shareId}/deny`, {
19534 method: "POST"
19535 });
19536 }
19537 function leaveShare(folderId) {
19538 return call(`/folders/${folderId}/leave`, {
19539 method: "POST"
19540 });
19541 }
19542 function purgeFolderSharingTables() {
19543 return call(
19544 "/folder-sharing-tables/purge",
19545 { method: "POST" }
19546 );
19547 }
19548 const filesRest = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
19549 __proto__: null,
19550 FilesConflictError,
19551 acceptShare,
19552 createFolder,
19553 createPlacement,
19554 deleteFolder,
19555 deletePlacement,
19556 denyShare,
19557 installRestDeps,
19558 inviteShare,
19559 leaveShare,
19560 listFolders,
19561 listPlacements,
19562 listShares,
19563 purgeFolderSharingTables,
19564 restoreTrashedItem,
19565 revokeShare,
19566 saveAssociations,
19567 updateFolder,
19568 updatePlacement,
19569 updateShareCapability
19570 }, Symbol.toStringTag, { value: "Module" }));
19571 const STORE_KEY = "desktop-mode/files";
19572 function getFilesStore() {
19573 return createSharedStore(STORE_KEY, () => ({
19574 placementsByFolder: /* @__PURE__ */ new Map(),
19575 folders: /* @__PURE__ */ new Map(),
19576 hydratedFolders: /* @__PURE__ */ new Set()
19577 }));
19578 }
19579 function fireChanged(detail) {
19580 if (typeof document === "undefined") {
19581 return;
19582 }
19583 document.dispatchEvent(
19584 new CustomEvent("desktop-mode-files-changed", {
19585 detail: { source: "local", ...detail }
19586 })
19587 );
19588 }
19589 function setFolderPlacements(folderId, placements) {
19590 const store2 = getFilesStore();
19591 const next = new Map(store2.state.placementsByFolder);
19592 next.set(folderId, placements.slice());
19593 const hydrated = new Set(store2.state.hydratedFolders);
19594 hydrated.add(folderId);
19595 store2.state = { ...store2.state, placementsByFolder: next, hydratedFolders: hydrated };
19596 store2.notify();
19597 fireChanged({ kind: "placements-set", folderId });
19598 }
19599 function upsertPlacement(placement, source = "local") {
19600 if (!placement || typeof placement.id !== "number") {
19601 console.warn(
19602 "[desktop-mode] upsertPlacement called with a non-placement value; ignoring.",
19603 placement
19604 );
19605 return;
19606 }
19607 const store2 = getFilesStore();
19608 const next = new Map(store2.state.placementsByFolder);
19609 for (const [folderId, list2] of next) {
19610 const idx2 = list2.findIndex((p) => p && p.id === placement.id);
19611 if (idx2 >= 0 && folderId !== placement.parentId) {
19612 const copy = list2.filter(Boolean);
19613 const removeAt = copy.findIndex((p) => p.id === placement.id);
19614 if (removeAt >= 0) {
19615 copy.splice(removeAt, 1);
19616 }
19617 next.set(folderId, copy);
19618 }
19619 }
19620 const rawTarget = next.get(placement.parentId)?.slice() ?? [];
19621 const target2 = rawTarget.filter(Boolean);
19622 const idx = target2.findIndex((p) => p.id === placement.id);
19623 if (idx >= 0) {
19624 target2[idx] = placement;
19625 } else {
19626 target2.push(placement);
19627 }
19628 next.set(placement.parentId, target2);
19629 store2.state = { ...store2.state, placementsByFolder: next };
19630 store2.notify();
19631 fireChanged({ kind: "placement-upserted", placementId: placement.id, folderId: placement.parentId, source });
19632 }
19633 function removePlacement(placementId, source = "local") {
19634 const store2 = getFilesStore();
19635 const next = new Map(store2.state.placementsByFolder);
19636 let touchedFolder;
19637 for (const [folderId, list2] of next) {
19638 const idx = list2.findIndex((p) => p && p.id === placementId);
19639 if (idx >= 0) {
19640 const copy = list2.filter(Boolean).filter(
19641 (p) => p.id !== placementId
19642 );
19643 next.set(folderId, copy);
19644 touchedFolder = folderId;
19645 }
19646 }
19647 if (touchedFolder === void 0) {
19648 return;
19649 }
19650 store2.state = { ...store2.state, placementsByFolder: next };
19651 store2.notify();
19652 fireChanged({ kind: "placement-removed", placementId, folderId: touchedFolder, source });
19653 }
19654 function setFolders(folders) {
19655 const store2 = getFilesStore();
19656 const next = /* @__PURE__ */ new Map();
19657 for (const f of folders) {
19658 next.set(f.id, f);
19659 }
19660 store2.state = { ...store2.state, folders: next };
19661 store2.notify();
19662 fireChanged({ kind: "folders-set" });
19663 }
19664 function upsertFolder(folder, source = "local") {
19665 const store2 = getFilesStore();
19666 const next = new Map(store2.state.folders);
19667 next.set(folder.id, folder);
19668 store2.state = { ...store2.state, folders: next };
19669 store2.notify();
19670 fireChanged({ kind: "folder-upserted", folderRowId: folder.id, source });
19671 }
19672 function removeFolder(folderId, source = "local") {
19673 const store2 = getFilesStore();
19674 const folders = new Map(store2.state.folders);
19675 folders.delete(folderId);
19676 const placements = new Map(store2.state.placementsByFolder);
19677 placements.delete(folderId);
19678 store2.state = { ...store2.state, folders, placementsByFolder: placements };
19679 store2.notify();
19680 fireChanged({ kind: "folder-removed", folderRowId: folderId, source });
19681 }
19682 function subscribeFilesStore(cb) {
19683 const store2 = getFilesStore();
19684 const off = store2.subscribe(cb);
19685 return off;
19686 }
19687 function getFilesState() {
19688 return getFilesStore().getState();
19689 }
19690 const store = {
19691 getState: getFilesState,
19692 subscribe: subscribeFilesStore,
19693 setFolderPlacements,
19694 upsertPlacement,
19695 upsertFolder,
19696 removePlacement,
19697 removeFolder
19698 };
19699 const styles$5 = css`:host{display:inline-block}`;
19700 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 )}`;
19701 const _WpdRibbon = class _WpdRibbon extends Component {
19702 render() {
19703 return html`<span class="banner" part="banner"><slot></slot></span>`;
19704 }
19705 };
19706 _WpdRibbon.props = ["placement", "tone"];
19707 _WpdRibbon.styles = [styles$4];
19708 _WpdRibbon.help = {
19709 title: "Ribbon",
19710 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.",
19711 status: "experimental",
19712 since: "0.8.6",
19713 props: [
19714 {
19715 name: "placement",
19716 type: '"top-end" | "top-start" | "bottom-end" | "bottom-start"',
19717 description: "Which corner of the parent the ribbon hugs. Defaults to `top-end` (logical right in LTR, left in RTL)."
19718 },
19719 {
19720 name: "tone",
19721 type: '"primary" | "success" | "warning" | "danger" | "info" | "neutral"',
19722 description: "Background color tone. Defaults to `primary` (the admin theme accent)."
19723 }
19724 ],
19725 slots: [{ name: "(default)", description: "Ribbon label text. Keep short." }],
19726 cssProps: [
19727 { name: "--wpd-ribbon-size", default: "90px", description: "Square clipping window edge." },
19728 { name: "--wpd-ribbon-banner-width", default: "140px", description: "Width of the rotated strip." },
19729 { name: "--wpd-ribbon-banner-offset", default: "20px", description: "Distance from corner to strip center." },
19730 { name: "--wpd-ribbon-banner-pull", default: "-36px", description: "How far the strip overhangs the clip edge." },
19731 { name: "--wpd-ribbon-bg", default: "var(--wp-admin-theme-color, #2271b1)" },
19732 { name: "--wpd-ribbon-fg", default: "#fff" },
19733 { name: "--wpd-ribbon-shadow", default: "0 2px 4px rgba(0,0,0,0.2)" },
19734 { name: "--wpd-ribbon-padding", default: "4px 0" },
19735 { name: "--wpd-ribbon-font", default: "700 10px/1.4 system-ui" },
19736 { name: "--wpd-ribbon-tracking", default: "0.06em" },
19737 { name: "--wpd-ribbon-z", default: "2" }
19738 ],
19739 example: html`
19740 <div
19741 style="position: relative; width: 240px; height: 120px;
19742 border: 1px solid #ccc; border-radius: 8px;
19743 padding: 16px; box-sizing: border-box;"
19744 >
19745 <wpd-ribbon>Featured</wpd-ribbon>
19746 Card body…
19747 </div>
19748 `
19749 };
19750 let WpdRibbon = _WpdRibbon;
19751 defineComponent("wpd-ribbon", WpdRibbon);
19752 const TILE_CLASS = "desktop-mode-file-tile";
19753 const STATUS_LABEL = {
19754 draft: "Draft",
19755 pending: "Pending",
19756 private: "Private",
19757 future: "Scheduled"
19758 };
19759 function statusRibbonsEnabled() {
19760 const get2 = window.wp?.desktop?.getOsSettings;
19761 if (typeof get2 !== "function") {
19762 return true;
19763 }
19764 try {
19765 return get2()?.showPostStatusRibbons !== false;
19766 } catch {
19767 return true;
19768 }
19769 }
19770 function getDragManager$1() {
19771 const api = window.wp?.desktop?.dragManager;
19772 return api ?? null;
19773 }
19774 const REACTIVE_PROPS = [
19775 "type",
19776 "ref",
19777 "label",
19778 "icon",
19779 "thumbnail",
19780 "kind",
19781 "status",
19782 "selected",
19783 "missing",
19784 "access-gated",
19785 "drag-kind",
19786 "drag-title",
19787 "drag-icon"
19788 ];
19789 const _WpdTile = class _WpdTile extends Component {
19790 constructor() {
19791 super(...arguments);
19792 this._pointerdownHandler = null;
19793 this._keydownHandler = null;
19794 }
19795 connectedCallback() {
19796 super.connectedCallback();
19797 if (!this._keydownHandler) {
19798 this._keydownHandler = (e) => {
19799 if (e.key === "Enter" || e.key === " ") {
19800 e.preventDefault();
19801 this.click();
19802 }
19803 };
19804 this.addEventListener("keydown", this._keydownHandler);
19805 }
19806 this._paint();
19807 }
19808 disconnectedCallback() {
19809 if (this._pointerdownHandler) {
19810 this.removeEventListener(
19811 "pointerdown",
19812 this._pointerdownHandler
19813 );
19814 this._pointerdownHandler = null;
19815 }
19816 if (this._keydownHandler) {
19817 this.removeEventListener(
19818 "keydown",
19819 this._keydownHandler
19820 );
19821 this._keydownHandler = null;
19822 }
19823 }
19824 /**
19825 * Bypass the templated render loop. Lit-html's `render(template,
19826 * root)` would wipe the host's light-DOM children every tick —
19827 * including the visual / label / ribbon `_paint()` just
19828 * inserted. We override `requestUpdate` directly so attribute
19829 * changes call `_paint` (idempotent) without lit-html getting
19830 * involved.
19831 */
19832 requestUpdate() {
19833 if (!this.isConnected) {
19834 return;
19835 }
19836 this._paint();
19837 }
19838 render() {
19839 return html``;
19840 }
19841 _paint() {
19842 const type = this.getAttribute("type") ?? "";
19843 const ref = this.getAttribute("ref") ?? "";
19844 const label = this.getAttribute("label") ?? "";
19845 const icon = this.getAttribute("icon") ?? "";
19846 const thumbnail = this.getAttribute("thumbnail") ?? "";
19847 const kind = this.getAttribute("kind") ?? "entry";
19848 const status = this.getAttribute("status") ?? "";
19849 const selected = this.hasAttribute("selected");
19850 const missing = this.hasAttribute("missing");
19851 const accessGated = this.hasAttribute("access-gated");
19852 const ownedClasses = [
19853 TILE_CLASS,
19854 `${TILE_CLASS}--folder`,
19855 `${TILE_CLASS}--missing`,
19856 `${TILE_CLASS}--access-gated`,
19857 `${TILE_CLASS}--selected`
19858 ];
19859 for (const c of ownedClasses) {
19860 this.classList.remove(c);
19861 }
19862 this.classList.add(TILE_CLASS);
19863 if (kind === "folder") {
19864 this.classList.add(`${TILE_CLASS}--folder`);
19865 }
19866 if (missing) {
19867 this.classList.add(`${TILE_CLASS}--missing`);
19868 }
19869 if (accessGated) {
19870 this.classList.add(`${TILE_CLASS}--access-gated`);
19871 }
19872 if (selected) {
19873 this.classList.add(`${TILE_CLASS}--selected`);
19874 }
19875 this.dataset.fileType = type;
19876 this.dataset.fileRef = ref;
19877 if (kind) {
19878 this.dataset.role = kind;
19879 }
19880 this.setAttribute("role", "listitem");
19881 this.setAttribute("aria-label", label);
19882 if (!this.hasAttribute("tabindex")) {
19883 this.setAttribute("tabindex", "0");
19884 }
19885 const accessGatedTitle = "You don’t have permission to open this — ask the folder owner for access.";
19886 if (accessGated) {
19887 this.title = accessGatedTitle;
19888 this.setAttribute("aria-disabled", "true");
19889 } else {
19890 this.removeAttribute("aria-disabled");
19891 if (this.title === accessGatedTitle) {
19892 this.removeAttribute("title");
19893 }
19894 }
19895 const SLOTS = [
19896 `${TILE_CLASS}__visual`,
19897 `${TILE_CLASS}__label`,
19898 `${TILE_CLASS}__lock`
19899 ];
19900 for (const cls of SLOTS) {
19901 this.querySelectorAll(`:scope > .${cls}`).forEach(
19902 (n) => n.remove()
19903 );
19904 }
19905 this.querySelectorAll(":scope > wpd-ribbon").forEach(
19906 (n) => n.remove()
19907 );
19908 const visual = document.createElement("span");
19909 visual.className = `${TILE_CLASS}__visual`;
19910 if (thumbnail) {
19911 const img = document.createElement("img");
19912 img.src = thumbnail;
19913 img.alt = "";
19914 img.loading = "lazy";
19915 img.decoding = "async";
19916 img.className = `${TILE_CLASS}__preview`;
19917 img.draggable = false;
19918 visual.appendChild(img);
19919 } else if (icon) {
19920 const iconNode = renderIcon(icon, {
19921 title: label,
19922 className: `${TILE_CLASS}__icon`
19923 });
19924 visual.appendChild(iconNode);
19925 }
19926 this.appendChild(visual);
19927 const labelNode = document.createElement("span");
19928 labelNode.className = `${TILE_CLASS}__label`;
19929 labelNode.textContent = label;
19930 this.appendChild(labelNode);
19931 if (accessGated) {
19932 const lock = document.createElement("span");
19933 lock.className = `${TILE_CLASS}__lock dashicons dashicons-lock`;
19934 lock.setAttribute("aria-hidden", "true");
19935 this.appendChild(lock);
19936 }
19937 if (status && status !== "publish" && STATUS_LABEL[status] && statusRibbonsEnabled()) {
19938 const ribbon = document.createElement("wpd-ribbon");
19939 ribbon.setAttribute("placement", "top-end");
19940 ribbon.setAttribute("tone", ribbonToneFor(status));
19941 ribbon.textContent = STATUS_LABEL[status];
19942 this.appendChild(ribbon);
19943 }
19944 applyTileEntryStagger(this);
19945 doAction("desktop-mode.tile.rendered", { tile: this });
19946 this._wireDragOut();
19947 }
19948 _wireDragOut() {
19949 if (this._pointerdownHandler) {
19950 this.removeEventListener(
19951 "pointerdown",
19952 this._pointerdownHandler
19953 );
19954 this._pointerdownHandler = null;
19955 }
19956 const dragKind = this.getAttribute("drag-kind");
19957 if (!dragKind) {
19958 return;
19959 }
19960 const handler = (e) => {
19961 if (e.button !== 0) {
19962 return;
19963 }
19964 const dragManager = getDragManager$1();
19965 if (!dragManager) {
19966 return;
19967 }
19968 const ref = this.getAttribute("ref") ?? "";
19969 const title = this.getAttribute("drag-title") ?? this.getAttribute("label") ?? void 0;
19970 const icon = this.getAttribute("drag-icon") ?? this.getAttribute("icon") ?? void 0;
19971 const rect = this.getBoundingClientRect();
19972 dragManager.start({
19973 payload: {
19974 type: "shortcut",
19975 source: this,
19976 data: {
19977 kind: dragKind,
19978 ref,
19979 title,
19980 icon
19981 },
19982 ghost: {
19983 offsetX: e.clientX - rect.left,
19984 offsetY: e.clientY - rect.top
19985 }
19986 },
19987 origin: e
19988 });
19989 };
19990 this._pointerdownHandler = handler;
19991 this.addEventListener("pointerdown", handler);
19992 }
19993 };
19994 _WpdTile.shadow = false;
19995 _WpdTile.props = REACTIVE_PROPS;
19996 _WpdTile.styles = [styles$5];
19997 _WpdTile.help = {
19998 title: "Tile",
19999 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.",
20000 status: "experimental",
20001 since: "0.8.6",
20002 props: [
20003 { name: "type", type: "string" },
20004 { name: "ref", type: "string" },
20005 { name: "label", type: "string" },
20006 { name: "icon", type: "string", description: "Dashicon class / URL / data URI. Ignored when `thumbnail` is set." },
20007 { name: "thumbnail", type: "string", description: "Preview image URL. Renders as `<img>` and wins over `icon`." },
20008 { name: "kind", type: "`entry` | `folder`" },
20009 { name: "status", type: "`draft` | `pending` | `private` | `future` | `publish`" },
20010 { name: "selected", type: "boolean" },
20011 { name: "missing", type: "boolean" },
20012 { name: "access-gated", type: "boolean" },
20013 { name: "drag-kind", type: "string", description: "When set, the component wires pointerdown → DragManager." },
20014 { name: "drag-title", type: "string" },
20015 { name: "drag-icon", type: "string" }
20016 ]
20017 };
20018 let WpdTile = _WpdTile;
20019 function ribbonToneFor(status) {
20020 switch (status) {
20021 case "draft":
20022 return "warning";
20023 case "pending":
20024 return "info";
20025 case "private":
20026 return "danger";
20027 case "future":
20028 return "primary";
20029 default:
20030 return "primary";
20031 }
20032 }
20033 defineComponent("wpd-tile", WpdTile);
20034 function buildTileFromSpec(spec) {
20035 const tile2 = document.createElement("wpd-tile");
20036 tile2.setAttribute("type", spec.type);
20037 tile2.setAttribute("ref", spec.ref);
20038 tile2.setAttribute("label", spec.label);
20039 if (spec.icon) {
20040 tile2.setAttribute("icon", spec.icon);
20041 }
20042 if (spec.thumbnail) {
20043 tile2.setAttribute("thumbnail", spec.thumbnail);
20044 }
20045 if (spec.role) {
20046 tile2.setAttribute("kind", spec.role);
20047 }
20048 if (spec.status) {
20049 tile2.setAttribute("status", spec.status);
20050 }
20051 if (spec.missing) {
20052 tile2.setAttribute("missing", "");
20053 }
20054 if (spec.accessGated) {
20055 tile2.setAttribute("access-gated", "");
20056 }
20057 if (spec.dataset) {
20058 for (const [key, raw] of Object.entries(spec.dataset)) {
20059 if (raw === void 0 || raw === null) {
20060 continue;
20061 }
20062 tile2.dataset[key] = String(raw);
20063 }
20064 }
20065 if (Array.isArray(spec.extraClasses)) {
20066 for (const c of spec.extraClasses) {
20067 if (c) {
20068 tile2.classList.add(c);
20069 }
20070 }
20071 }
20072 const classFiltered = applyFilters(
20073 "desktop-mode.tile.class",
20074 tile2.className,
20075 spec
20076 );
20077 if (classFiltered && classFiltered !== tile2.className) {
20078 tile2.className = classFiltered;
20079 }
20080 if (typeof spec.x === "number" && typeof spec.y === "number") {
20081 tile2.style.position = "absolute";
20082 tile2.style.left = `${spec.x}px`;
20083 tile2.style.top = `${spec.y}px`;
20084 }
20085 return tile2;
20086 }
20087 function placementToSpec(placement, folderId) {
20088 const file = resolve(placement.file);
20089 const previewUrl = file.previewUrl();
20090 const metaName = placement.meta && typeof placement.meta.name === "string" ? placement.meta.name.trim() : "";
20091 const label = metaName !== "" ? metaName : file.title();
20092 const metaIconUrl = placement.meta && typeof placement.meta.iconUrl === "string" ? placement.meta.iconUrl.trim() : "";
20093 return {
20094 type: placement.file.type,
20095 ref: placement.file.ref,
20096 label,
20097 // Preview wins over icon (matches the previous behavior).
20098 thumbnail: previewUrl || void 0,
20099 icon: previewUrl ? void 0 : metaIconUrl || file.icon(),
20100 x: placement.x,
20101 y: placement.y,
20102 dataset: {
20103 placementId: placement.id,
20104 folderId
20105 },
20106 meta: placement.meta,
20107 missing: !placement.file.exists,
20108 accessGated: Boolean(placement.accessGated),
20109 ariaLabel: label
20110 };
20111 }
20112 function buildTile(placement, folderId) {
20113 const file = resolve(placement.file);
20114 const tile2 = buildTileFromSpec(placementToSpec(placement, folderId));
20115 const classFiltered = applyFilters(
20116 "desktop-mode.files.tile-class",
20117 TILE_CLASS,
20118 placement
20119 );
20120 if (classFiltered && classFiltered !== TILE_CLASS) {
20121 tile2.className = classFiltered;
20122 }
20123 const extra = applyFilters(
20124 "desktop-mode.files.tile-element",
20125 null,
20126 placement
20127 );
20128 if (extra instanceof Element) {
20129 tile2.appendChild(extra);
20130 }
20131 tile2.addEventListener("dblclick", (e) => {
20132 e.preventDefault();
20133 e.stopPropagation();
20134 if (placement.accessGated) {
20135 showToast({
20136 message: `You don’t have permission to open "${placement.file.title || file.title()}". Ask the folder owner if you need access to this item.`,
20137 duration: 6e3
20138 });
20139 return;
20140 }
20141 void openFile(file, {
20142 placement: {
20143 id: placement.id,
20144 x: placement.x,
20145 y: placement.y,
20146 meta: placement.meta
20147 }
20148 });
20149 });
20150 doAction("desktop-mode.files.tile-rendered", { tile: tile2, placement });
20151 return tile2;
20152 }
20153 function setTilePosition(tile2, x, y) {
20154 tile2.style.left = `${x}px`;
20155 tile2.style.top = `${y}px`;
20156 }
20157 function attachDismissable(host, options) {
20158 const onAway = (e) => {
20159 if (e.target instanceof Node && host.contains(e.target)) {
20160 return;
20161 }
20162 if (e.target instanceof Node) {
20163 for (const sel of options.siblingSelectors ?? []) {
20164 const matches = Array.from(
20165 document.querySelectorAll(sel)
20166 );
20167 for (const m of matches) {
20168 if (m.contains(e.target)) {
20169 return;
20170 }
20171 }
20172 }
20173 }
20174 if (options.excludeOutsideTarget && e.target instanceof Node && options.excludeOutsideTarget.contains(e.target)) {
20175 return;
20176 }
20177 options.close();
20178 };
20179 const onKey = (e) => {
20180 if (e.key === "Escape") {
20181 options.close();
20182 }
20183 };
20184 document.addEventListener("mousedown", onAway, { capture: true });
20185 document.addEventListener("keydown", onKey);
20186 return () => {
20187 document.removeEventListener("mousedown", onAway, { capture: true });
20188 document.removeEventListener("keydown", onKey);
20189 };
20190 }
20191 const MENU_CLASS$2 = "desktop-mode-wallpaper-menu";
20192 let activeMenu$2 = null;
20193 function closeTileMenu() {
20194 if (!activeMenu$2) {
20195 return;
20196 }
20197 activeMenu$2.dispatchEvent(new CustomEvent("tile-menu-closed"));
20198 activeMenu$2.remove();
20199 activeMenu$2 = null;
20200 doAction("desktop-mode.files.tile-menu.closed", {});
20201 }
20202 let openGeneration$1 = 0;
20203 function openTileMenu(pos, opts) {
20204 closeTileMenu();
20205 const myGen = ++openGeneration$1;
20206 openWithShellOverlays(
20207 () => myGen === openGeneration$1,
20208 () => openTileMenuImmediate(pos, opts)
20209 );
20210 }
20211 function openTileMenuImmediate(pos, { placement, items }) {
20212 const list2 = applyFilters(
20213 "desktop-mode.files.tile-menu",
20214 items.slice(),
20215 placement
20216 );
20217 const sorted = (Array.isArray(list2) ? list2 : items).slice().sort((a, b) => {
20218 const sa = typeof a.sort === "number" ? a.sort : 100;
20219 const sb = typeof b.sort === "number" ? b.sort : 100;
20220 if (sa !== sb) {
20221 return sa - sb;
20222 }
20223 return a.label.localeCompare(b.label);
20224 });
20225 if (sorted.length === 0) {
20226 return;
20227 }
20228 const menu = document.createElement("wpd-context-menu");
20229 menu.setAttribute("open", "");
20230 menu.classList.add(MENU_CLASS$2);
20231 menu.dataset.placementId = String(placement.id);
20232 menu.style.left = `${pos.x}px`;
20233 menu.style.top = `${pos.y}px`;
20234 const itemById = /* @__PURE__ */ new Map();
20235 for (const item of sorted) {
20236 itemById.set(item.id, item);
20237 const opt = document.createElement("wpd-context-menu-option");
20238 opt.dataset.menuItemId = item.id;
20239 opt.setAttribute("value", item.id);
20240 if (item.danger) {
20241 opt.setAttribute("danger", "");
20242 }
20243 if (item.disabled) {
20244 opt.setAttribute("disabled", "");
20245 }
20246 if (item.icon) {
20247 opt.setAttribute("icon", sanitizeClass$2(item.icon));
20248 }
20249 opt.textContent = item.label;
20250 menu.appendChild(opt);
20251 }
20252 menu.addEventListener("wpd-context-menu-pick", (e) => {
20253 const detail = e.detail;
20254 const item = itemById.get(detail.id);
20255 if (!item) {
20256 return;
20257 }
20258 closeTileMenu();
20259 void item.onClick(new MouseEvent("click"));
20260 });
20261 document.body.appendChild(menu);
20262 activeMenu$2 = menu;
20263 const rect = menu.getBoundingClientRect();
20264 if (rect.right > window.innerWidth) {
20265 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
20266 }
20267 if (rect.bottom > window.innerHeight) {
20268 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
20269 }
20270 const detach = attachDismissable(menu, {
20271 close: () => closeTileMenu()
20272 });
20273 menu.addEventListener("tile-menu-closed", detach);
20274 doAction("desktop-mode.files.tile-menu.opened", {
20275 placementId: placement.id,
20276 items: sorted.map((i) => i.id)
20277 });
20278 }
20279 function sanitizeClass$2(raw) {
20280 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
20281 }
20282 const ROOT_CLASS$3 = "desktop-mode-create-folder-dialog";
20283 let active$1 = null;
20284 function closeCreateFolderDialog() {
20285 if (!active$1) {
20286 return;
20287 }
20288 active$1.dispatchEvent(new CustomEvent("create-folder-dialog-closed"));
20289 active$1.remove();
20290 active$1 = null;
20291 doAction("desktop-mode.files.create-folder.closed", {});
20292 }
20293 function openCreateFolderDialog(options) {
20294 closeCreateFolderDialog();
20295 const decision = applyFilters(
20296 "desktop-mode.files.create-folder.dialog",
20297 null,
20298 options
20299 );
20300 if (decision === false) {
20301 return;
20302 }
20303 const initial = (options.initialName ?? "Untitled folder").trim();
20304 const overlay = document.createElement("div");
20305 overlay.className = `${ROOT_CLASS$3}__overlay`;
20306 overlay.setAttribute("role", "presentation");
20307 const dialog2 = document.createElement("div");
20308 dialog2.className = ROOT_CLASS$3;
20309 dialog2.setAttribute("role", "dialog");
20310 dialog2.setAttribute("aria-modal", "true");
20311 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS$3}-title`);
20312 const title = document.createElement("h2");
20313 title.id = `${ROOT_CLASS$3}-title`;
20314 title.className = `${ROOT_CLASS$3}__title`;
20315 title.textContent = options.title ?? "New folder";
20316 dialog2.appendChild(title);
20317 const label = document.createElement("label");
20318 label.className = `${ROOT_CLASS$3}__label`;
20319 label.htmlFor = `${ROOT_CLASS$3}-input`;
20320 label.textContent = options.label ?? "Folder name";
20321 dialog2.appendChild(label);
20322 const input = document.createElement("input");
20323 input.type = "text";
20324 input.id = `${ROOT_CLASS$3}-input`;
20325 input.className = `${ROOT_CLASS$3}__input`;
20326 input.value = initial;
20327 input.setAttribute("autocomplete", "off");
20328 input.setAttribute("spellcheck", "false");
20329 dialog2.appendChild(input);
20330 const error = document.createElement("p");
20331 error.className = `${ROOT_CLASS$3}__error`;
20332 error.hidden = true;
20333 error.setAttribute("role", "alert");
20334 dialog2.appendChild(error);
20335 const actions = document.createElement("div");
20336 actions.className = `${ROOT_CLASS$3}__actions`;
20337 const cancel = document.createElement("button");
20338 cancel.type = "button";
20339 cancel.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--secondary`;
20340 cancel.textContent = "Cancel";
20341 const submit = document.createElement("button");
20342 submit.type = "button";
20343 submit.className = `${ROOT_CLASS$3}__btn ${ROOT_CLASS$3}__btn--primary`;
20344 submit.textContent = options.submitLabel ?? "Create";
20345 actions.appendChild(cancel);
20346 actions.appendChild(submit);
20347 dialog2.appendChild(actions);
20348 overlay.appendChild(dialog2);
20349 document.body.appendChild(overlay);
20350 active$1 = overlay;
20351 input.focus();
20352 input.select();
20353 doAction("desktop-mode.files.create-folder.opened", {});
20354 const setBusy = (busy) => {
20355 input.disabled = busy;
20356 cancel.disabled = busy;
20357 submit.disabled = busy;
20358 dialog2.classList.toggle(`${ROOT_CLASS$3}--busy`, busy);
20359 };
20360 const showError = (msg) => {
20361 error.textContent = msg;
20362 error.hidden = false;
20363 };
20364 const doCancel = () => {
20365 closeCreateFolderDialog();
20366 options.onCancel?.();
20367 };
20368 const doSubmit = async () => {
20369 const name = input.value.trim();
20370 if (!name) {
20371 showError("Please enter a name.");
20372 input.focus();
20373 return;
20374 }
20375 error.hidden = true;
20376 setBusy(true);
20377 try {
20378 await options.onSubmit(name);
20379 closeCreateFolderDialog();
20380 } catch (err) {
20381 setBusy(false);
20382 showError(
20383 err instanceof Error ? err.message : "Could not create the folder."
20384 );
20385 input.focus();
20386 input.select();
20387 }
20388 };
20389 cancel.addEventListener("click", () => doCancel());
20390 submit.addEventListener("click", () => void doSubmit());
20391 overlay.addEventListener("click", (e) => {
20392 if (e.target === overlay) {
20393 doCancel();
20394 }
20395 });
20396 const onKey = (e) => {
20397 if (e.key === "Escape") {
20398 e.preventDefault();
20399 doCancel();
20400 } else if (e.key === "Enter" && !e.isComposing) {
20401 e.preventDefault();
20402 void doSubmit();
20403 }
20404 };
20405 dialog2.addEventListener("keydown", onKey);
20406 overlay.addEventListener("create-folder-dialog-closed", () => {
20407 dialog2.removeEventListener("keydown", onKey);
20408 });
20409 }
20410 const GRID_PADDING = 16;
20411 const GRID_CELL_W = 96;
20412 const GRID_CELL_H = 110;
20413 function pointToCell(x, y) {
20414 const col = Math.max(0, Math.round((x - GRID_PADDING) / GRID_CELL_W));
20415 const row = Math.max(0, Math.round((y - GRID_PADDING) / GRID_CELL_H));
20416 return cellToPos(col, row);
20417 }
20418 function cellToPos(col, row) {
20419 return {
20420 col,
20421 row,
20422 x: GRID_PADDING + col * GRID_CELL_W,
20423 y: GRID_PADDING + row * GRID_CELL_H
20424 };
20425 }
20426 function snapToEmptyCell(x, y, occupied, host) {
20427 const target2 = pointToCell(x, y);
20428 if (!occupied.has(cellKey(target2.col, target2.row))) {
20429 return target2;
20430 }
20431 const maxRows = host ? Math.max(1, Math.floor((host.clientHeight - GRID_PADDING) / GRID_CELL_H)) : 999;
20432 for (let col = 0; col < 999; col++) {
20433 for (let row = 0; row < maxRows; row++) {
20434 if (!occupied.has(cellKey(col, row))) {
20435 return cellToPos(col, row);
20436 }
20437 }
20438 }
20439 return target2;
20440 }
20441 function nextRowMajorCell(occupied, host) {
20442 const cols = host ? Math.max(
20443 1,
20444 Math.floor((host.clientWidth - GRID_PADDING) / GRID_CELL_W)
20445 ) : 4;
20446 const maxCols = Math.max(1, cols);
20447 for (let row = 0; row < 999; row++) {
20448 for (let col = 0; col < maxCols; col++) {
20449 if (!occupied.has(cellKey(col, row))) {
20450 return cellToPos(col, row);
20451 }
20452 }
20453 }
20454 return cellToPos(0, 0);
20455 }
20456 function buildOccupiedSet(placements, excludeId) {
20457 const out = /* @__PURE__ */ new Set();
20458 for (const p of placements) {
20459 const cell = pointToCell(p.x, p.y);
20460 out.add(cellKey(cell.col, cell.row));
20461 }
20462 return out;
20463 }
20464 function cellKey(col, row) {
20465 return `${col},${row}`;
20466 }
20467 function isConflict(err) {
20468 return err instanceof FilesConflictError;
20469 }
20470 function buildReason(err) {
20471 const actor = err.detail.actor.name || "Someone else";
20472 const where = err.detail.current.parentName || "another folder";
20473 if (err.detail.reason === "trashed") {
20474 return "This item is in the recycle bin.";
20475 }
20476 if (err.detail.reason === "forbidden") {
20477 return "You no longer have access.";
20478 }
20479 if (err.detail.reason === "gone") {
20480 return "This item was deleted.";
20481 }
20482 return `${actor} moved this to "${where}".`;
20483 }
20484 function showConflictToast(err) {
20485 const reason = buildReason(err);
20486 const targetParentId = err.detail.current.parentId;
20487 let action;
20488 if (targetParentId > 0) {
20489 action = {
20490 label: "View folder",
20491 onClick: () => {
20492 const winId = `desktop-mode-folder-${targetParentId}`;
20493 const mgr = window.desktopMode?.windowManager;
20494 if (mgr?.focus) {
20495 const w = mgr.focus(winId);
20496 if (w) {
20497 return;
20498 }
20499 }
20500 if (mgr?.open) {
20501 void mgr.open(winId);
20502 }
20503 }
20504 };
20505 }
20506 showToast({
20507 message: reason,
20508 action,
20509 duration: 7e3
20510 });
20511 }
20512 function broadcastFilesChange(kind, action, ids) {
20513 const api = window.wp?.desktop;
20514 api?.broadcast?.(`desktop-mode.${kind}.changed`, {
20515 source: "desktop-files",
20516 action,
20517 ids
20518 });
20519 }
20520 function showTrashErrorToast(err) {
20521 const api = window.wp?.desktop;
20522 if (!api?.showToast) {
20523 return;
20524 }
20525 const raw = err instanceof Error ? err.message : String(err);
20526 const friendly = raw.replace(/^\[desktop-mode\][^:]*:\s*/, "").replace(/^desktop_mode_files_[a-z_]+\s*/, "");
20527 api.showToast({
20528 message: friendly || "Could not move this item to the recycle bin.",
20529 duration: 5e3
20530 });
20531 }
20532 function showTrashedToast(message, onUndo) {
20533 const api = window.wp?.desktop;
20534 if (!api?.showToast) {
20535 return;
20536 }
20537 api.showToast({
20538 message,
20539 duration: 6e3,
20540 action: {
20541 label: "Undo",
20542 onClick: onUndo
20543 }
20544 });
20545 }
20546 async function trashPlacementWithUndo(placement) {
20547 const placementId = placement.id;
20548 const parentId = placement.parentId;
20549 const title = placement.file?.title ?? "Item";
20550 const kind = placement.file?.type === "shortcut" ? "shortcut" : "placement";
20551 store.removePlacement(placementId);
20552 try {
20553 await deletePlacement(placementId);
20554 broadcastFilesChange(kind, "trashed", [placementId]);
20555 showTrashedToast(`"${title}" moved to Trash`, async () => {
20556 try {
20557 await restoreTrashedItem(placementId, "placement");
20558 const res = await listPlacements(parentId);
20559 store.setFolderPlacements(parentId, res.placements);
20560 broadcastFilesChange(kind, "untrashed", [placementId]);
20561 } catch (err) {
20562 console.error("[desktop-mode] restore failed:", err);
20563 }
20564 });
20565 } catch (err) {
20566 console.error("[desktop-mode] deletePlacement failed:", err);
20567 showTrashErrorToast(err);
20568 void listPlacements(parentId).then((res) => {
20569 store.setFolderPlacements(parentId, res.placements);
20570 });
20571 }
20572 }
20573 async function trashFolderWithUndo(placement) {
20574 const folderId = parseInt(placement.file.ref, 10);
20575 if (!folderId) {
20576 return;
20577 }
20578 const placementId = placement.id;
20579 const parentId = placement.parentId;
20580 const title = placement.file?.title ?? "Folder";
20581 store.removePlacement(placementId);
20582 store.removeFolder(folderId);
20583 try {
20584 await deleteFolder(folderId);
20585 broadcastFilesChange("folder", "trashed", [folderId]);
20586 showTrashedToast(`"${title}" moved to Trash`, async () => {
20587 try {
20588 await restoreTrashedItem(folderId, "folder");
20589 const res = await listPlacements(parentId);
20590 store.setFolderPlacements(parentId, res.placements);
20591 broadcastFilesChange("folder", "untrashed", [folderId]);
20592 } catch (err) {
20593 console.error("[desktop-mode] restore folder failed:", err);
20594 }
20595 });
20596 } catch (err) {
20597 console.error("[desktop-mode] deleteFolder failed:", err);
20598 showTrashErrorToast(err);
20599 void listPlacements(parentId).then((res) => {
20600 store.setFolderPlacements(parentId, res.placements);
20601 });
20602 }
20603 }
20604 function trashByFileType(placement) {
20605 if (placement.file?.type === "folder") {
20606 return trashFolderWithUndo(placement);
20607 }
20608 return trashPlacementWithUndo(placement);
20609 }
20610 function buildBridgePayloadFromPlacement(placement) {
20611 const file = placement.file;
20612 if (!file) {
20613 return void 0;
20614 }
20615 const id = parseInt(String(file.ref ?? ""), 10);
20616 if (!Number.isFinite(id) || id <= 0) {
20617 return void 0;
20618 }
20619 const title = String(file.title ?? "");
20620 if (file.type === "attachment") {
20621 const url = String(file.sourceUrl ?? file.previewUrl ?? "");
20622 return {
20623 kind: "attachment",
20624 id,
20625 url,
20626 title,
20627 alt: String(file.alt ?? ""),
20628 mime: String(file.mime ?? ""),
20629 thumbnailUrl: file.previewUrl ? String(file.previewUrl) : void 0
20630 };
20631 }
20632 if (file.type === "post") {
20633 return {
20634 kind: "post",
20635 id,
20636 postType: String(file.postType ?? "post"),
20637 url: String(file.link ?? ""),
20638 title
20639 };
20640 }
20641 if (file.type === "user") {
20642 return {
20643 kind: "user",
20644 id,
20645 url: String(file.link ?? ""),
20646 title
20647 };
20648 }
20649 return void 0;
20650 }
20651 function getDragManager() {
20652 const api = window.wp?.desktop?.dragManager;
20653 return api ?? null;
20654 }
20655 const LAYER_CLASS = "desktop-mode-files-layer";
20656 function mountFilesLayer(host, folderId = 0) {
20657 const container = document.createElement("div");
20658 container.className = LAYER_CLASS;
20659 container.setAttribute("role", "list");
20660 container.dataset.folderId = String(folderId);
20661 host.appendChild(container);
20662 let lastFingerprint = "";
20663 let selectedId = null;
20664 const selectionListeners = /* @__PURE__ */ new Set();
20665 const notifySelection = (placement) => {
20666 for (const cb of selectionListeners) {
20667 try {
20668 cb(placement);
20669 } catch (err) {
20670 console.error(
20671 "[desktop-mode] files: selection listener threw:",
20672 err
20673 );
20674 }
20675 }
20676 };
20677 const setSelected = (placement) => {
20678 const newId = placement ? placement.id : null;
20679 if (newId === selectedId) {
20680 return;
20681 }
20682 container.querySelectorAll(`.${TILE_CLASS}--selected`).forEach((n) => n.removeAttribute("selected"));
20683 if (placement) {
20684 const tile2 = container.querySelector(
20685 `[data-placement-id="${placement.id}"]`
20686 );
20687 tile2?.setAttribute("selected", "");
20688 }
20689 selectedId = newId;
20690 notifySelection(placement);
20691 };
20692 const computeLayout = (list2) => {
20693 const pinnedSlots = /* @__PURE__ */ new Map();
20694 const occupiedCells = /* @__PURE__ */ new Set();
20695 let pinnedIdx = 0;
20696 for (const placement of list2) {
20697 if (!isPinned(placement)) {
20698 continue;
20699 }
20700 const slot = cellToPos(0, pinnedIdx);
20701 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
20702 occupiedCells.add(cellKey(slot.col, slot.row));
20703 pinnedIdx += 1;
20704 }
20705 const displaced = /* @__PURE__ */ new Map();
20706 for (const placement of list2) {
20707 if (pinnedSlots.has(placement.id)) {
20708 continue;
20709 }
20710 const target2 = pointToCell(placement.x, placement.y);
20711 const key = cellKey(target2.col, target2.row);
20712 if (!occupiedCells.has(key)) {
20713 occupiedCells.add(key);
20714 continue;
20715 }
20716 const free = snapToEmptyCell(
20717 placement.x,
20718 placement.y,
20719 occupiedCells,
20720 host
20721 );
20722 occupiedCells.add(cellKey(free.col, free.row));
20723 displaced.set(placement.id, { x: free.x, y: free.y });
20724 }
20725 return { pinnedSlots, displaced };
20726 };
20727 const applyTilePosition = (tile2, placement, pinnedSlots, displaced) => {
20728 const pinned = pinnedSlots.get(placement.id);
20729 const moved = displaced.get(placement.id);
20730 if (pinned) {
20731 setTilePosition(tile2, pinned.x, pinned.y);
20732 } else if (moved) {
20733 setTilePosition(tile2, moved.x, moved.y);
20734 } else {
20735 setTilePosition(tile2, placement.x, placement.y);
20736 }
20737 };
20738 const wireTile = (placement, pinnedSlots, displaced) => {
20739 const tile2 = buildTile(placement, folderId);
20740 const pinnedSlot = pinnedSlots.get(placement.id);
20741 if (pinnedSlot) {
20742 setTilePosition(tile2, pinnedSlot.x, pinnedSlot.y);
20743 tile2.classList.add(`${TILE_CLASS}--pinned`);
20744 attachContextMenu(tile2, placement);
20745 attachSelectOnClick(tile2, placement);
20746 if (shouldRejectTileDrops(placement)) {
20747 const dragManager = getDragManager();
20748 if (dragManager) {
20749 const deregister = dragManager.registerDropTarget({
20750 id: `desktop-mode-files-tile-${placement.id}-reject`,
20751 element: tile2,
20752 accept: () => false,
20753 onDrop: () => {
20754 }
20755 });
20756 tileRejectDeregisters.set(placement.id, deregister);
20757 }
20758 }
20759 return tile2;
20760 }
20761 const moved = displaced.get(placement.id);
20762 if (moved) {
20763 setTilePosition(tile2, moved.x, moved.y);
20764 }
20765 attachTileDrag(tile2, placement, folderId);
20766 attachContextMenu(tile2, placement);
20767 attachSelectOnClick(tile2, placement);
20768 if (placement.file.type === "folder") {
20769 const targetFolderId = parseInt(placement.file.ref, 10);
20770 if (targetFolderId > 0) {
20771 const dragManager = getDragManager();
20772 if (dragManager) {
20773 const deregister = registerFolderDropTarget(
20774 dragManager,
20775 tile2,
20776 targetFolderId
20777 );
20778 folderDropDeregisters.set(placement.id, deregister);
20779 }
20780 }
20781 } else if (shouldRejectTileDrops(placement)) {
20782 const dragManager = getDragManager();
20783 if (dragManager) {
20784 const deregister = dragManager.registerDropTarget({
20785 id: `desktop-mode-files-tile-${placement.id}-reject`,
20786 element: tile2,
20787 accept: () => false,
20788 onDrop: () => {
20789 }
20790 });
20791 tileRejectDeregisters.set(placement.id, deregister);
20792 }
20793 }
20794 return tile2;
20795 };
20796 const tryPatchIncremental = (list2) => {
20797 const existing = /* @__PURE__ */ new Map();
20798 for (const tile2 of container.querySelectorAll(
20799 "[data-placement-id]"
20800 )) {
20801 const raw = tile2.dataset.placementId ?? "";
20802 const id = parseInt(raw, 10);
20803 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
20804 return false;
20805 }
20806 existing.set(id, tile2);
20807 }
20808 const wantIds = /* @__PURE__ */ new Set();
20809 for (const placement of list2) {
20810 wantIds.add(placement.id);
20811 }
20812 for (const placement of list2) {
20813 const tile2 = existing.get(placement.id);
20814 if (!tile2) {
20815 continue;
20816 }
20817 if (tile2.dataset.fileType !== placement.file.type) {
20818 return false;
20819 }
20820 if (tile2.dataset.fileRef !== placement.file.ref) {
20821 return false;
20822 }
20823 const wasPinned = tile2.classList.contains(
20824 `${TILE_CLASS}--pinned`
20825 );
20826 if (wasPinned !== isPinned(placement)) {
20827 return false;
20828 }
20829 }
20830 for (const [id, tile2] of existing) {
20831 if (wantIds.has(id)) {
20832 continue;
20833 }
20834 const folderDereg = folderDropDeregisters.get(id);
20835 if (folderDereg) {
20836 try {
20837 folderDereg();
20838 } catch {
20839 }
20840 folderDropDeregisters.delete(id);
20841 }
20842 const rejectDereg = tileRejectDeregisters.get(id);
20843 if (rejectDereg) {
20844 try {
20845 rejectDereg();
20846 } catch {
20847 }
20848 tileRejectDeregisters.delete(id);
20849 }
20850 tile2.remove();
20851 }
20852 const { pinnedSlots, displaced } = computeLayout(list2);
20853 for (const placement of list2) {
20854 const tile2 = existing.get(placement.id);
20855 if (tile2) {
20856 applyTilePosition(tile2, placement, pinnedSlots, displaced);
20857 continue;
20858 }
20859 container.appendChild(
20860 wireTile(placement, pinnedSlots, displaced)
20861 );
20862 }
20863 if (selectedId !== null && !container.querySelector(
20864 `[data-placement-id="${selectedId}"]`
20865 )) {
20866 selectedId = null;
20867 notifySelection(null);
20868 }
20869 doAction("desktop-mode.files.grid-rendered", {
20870 folderId,
20871 count: list2.length
20872 });
20873 return true;
20874 };
20875 const repaint = (state2) => {
20876 const raw = state2.placementsByFolder.get(folderId) ?? [];
20877 const list2 = raw.slice().sort((a, b) => {
20878 const ap = isPinned(a) ? 0 : 1;
20879 const bp = isPinned(b) ? 0 : 1;
20880 return ap - bp;
20881 });
20882 const fp = fingerprint(list2);
20883 if (fp === lastFingerprint) {
20884 return;
20885 }
20886 lastFingerprint = fp;
20887 if (tryPatchPositions(list2, container, host)) {
20888 return;
20889 }
20890 if (tryPatchIncremental(list2)) {
20891 return;
20892 }
20893 container.replaceChildren();
20894 for (const [, deregister] of folderDropDeregisters) {
20895 try {
20896 deregister();
20897 } catch {
20898 }
20899 }
20900 folderDropDeregisters.clear();
20901 for (const [, deregister] of tileRejectDeregisters) {
20902 try {
20903 deregister();
20904 } catch {
20905 }
20906 }
20907 tileRejectDeregisters.clear();
20908 const { pinnedSlots, displaced } = computeLayout(list2);
20909 for (const placement of list2) {
20910 container.appendChild(
20911 wireTile(placement, pinnedSlots, displaced)
20912 );
20913 }
20914 if (selectedId !== null && !container.querySelector(`[data-placement-id="${selectedId}"]`)) {
20915 selectedId = null;
20916 notifySelection(null);
20917 } else if (selectedId !== null) {
20918 const tile2 = container.querySelector(
20919 `[data-placement-id="${selectedId}"]`
20920 );
20921 tile2?.setAttribute("selected", "");
20922 }
20923 doAction("desktop-mode.files.grid-rendered", {
20924 folderId,
20925 count: list2.length
20926 });
20927 };
20928 const dropTargetDeregisters = [];
20929 const folderDropDeregisters = /* @__PURE__ */ new Map();
20930 const tileRejectDeregisters = /* @__PURE__ */ new Map();
20931 let dropPreviewEl = null;
20932 let dropPreviewMoveHandler = null;
20933 const installCanvasDropPreview = (session) => {
20934 if (dropPreviewEl) {
20935 return;
20936 }
20937 if (session.payload.type !== "desktop-file") {
20938 return;
20939 }
20940 const previewEl = document.createElement("div");
20941 previewEl.className = "desktop-mode-files-drop-preview";
20942 previewEl.setAttribute("aria-hidden", "true");
20943 container.appendChild(previewEl);
20944 dropPreviewEl = previewEl;
20945 const ghost = session.payload.ghost;
20946 const offsetX = ghost?.offsetX ?? 0;
20947 const offsetY = ghost?.offsetY ?? 0;
20948 const data = session.payload.data;
20949 const movingId = data?.placement?.id;
20950 const updatePreview = (clientX, clientY) => {
20951 const rect = container.getBoundingClientRect();
20952 const rawX = Math.max(0, clientX - rect.left - offsetX);
20953 const rawY = Math.max(0, clientY - rect.top - offsetY);
20954 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
20955 const occupied = buildVisualOccupiedSet(peers, movingId);
20956 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
20957 previewEl.style.transform = `translate3d(${cell.x}px, ${cell.y}px, 0)`;
20958 };
20959 const sourceRect = session.payload.source.getBoundingClientRect();
20960 updatePreview(
20961 sourceRect.left + offsetX,
20962 sourceRect.top + offsetY
20963 );
20964 const moveHandler = (ev) => {
20965 updatePreview(ev.clientX, ev.clientY);
20966 };
20967 document.addEventListener("pointermove", moveHandler);
20968 dropPreviewMoveHandler = moveHandler;
20969 };
20970 const teardownCanvasDropPreview = () => {
20971 if (dropPreviewMoveHandler) {
20972 document.removeEventListener("pointermove", dropPreviewMoveHandler);
20973 dropPreviewMoveHandler = null;
20974 }
20975 if (dropPreviewEl) {
20976 dropPreviewEl.remove();
20977 dropPreviewEl = null;
20978 }
20979 };
20980 const canvasDropTarget = {
20981 id: `desktop-mode-files-canvas-${folderId}`,
20982 element: host,
20983 accept: (payload) => {
20984 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
20985 return false;
20986 }
20987 if (folderId > 0 && payload.type === "desktop-file") {
20988 const data = payload.data;
20989 if (data.placement.file?.type === "folder") {
20990 const movingFolderId = parseInt(data.placement.file.ref, 10);
20991 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, folderId)) {
20992 return false;
20993 }
20994 }
20995 }
20996 return true;
20997 },
20998 onEnter: (session) => {
20999 host.setAttribute("data-files-drop-active", "");
21000 installCanvasDropPreview(session);
21001 },
21002 onLeave: () => {
21003 host.removeAttribute("data-files-drop-active");
21004 teardownCanvasDropPreview();
21005 },
21006 onDrop: (session, ev) => {
21007 host.removeAttribute("data-files-drop-active");
21008 teardownCanvasDropPreview();
21009 const rect = container.getBoundingClientRect();
21010 const ghost = session.payload.ghost;
21011 const offsetX = ghost?.offsetX ?? 0;
21012 const offsetY = ghost?.offsetY ?? 0;
21013 const rawX = Math.max(0, ev.clientX - rect.left - offsetX);
21014 const rawY = Math.max(0, ev.clientY - rect.top - offsetY);
21015 const peers = store.getState().placementsByFolder.get(folderId) ?? [];
21016 if (session.payload.type === "desktop-file") {
21017 const data = session.payload.data;
21018 const occupied = buildVisualOccupiedSet(peers, data.placement.id);
21019 const cell = snapToEmptyCell(rawX, rawY, occupied, host);
21020 const next = {
21021 ...data.placement,
21022 x: cell.x,
21023 y: cell.y,
21024 parentId: folderId
21025 };
21026 store.upsertPlacement(next);
21027 doAction("desktop-mode.files.tile-manually-placed", {
21028 folderId,
21029 placementId: data.placement.id
21030 });
21031 if (isSyntheticPlacement(data.placement)) {
21032 const dockItemId = readSynthSource(data.placement);
21033 if (dockItemId) {
21034 persistDockPromotedPosition(
21035 dockItemId,
21036 cell.x,
21037 cell.y
21038 );
21039 }
21040 return;
21041 }
21042 void updatePlacement(
21043 data.placement.id,
21044 {
21045 x: cell.x,
21046 y: cell.y,
21047 parentId: folderId
21048 },
21049 data.placement.updatedAtMs
21050 ).then((server) => {
21051 store.upsertPlacement(server, "remote");
21052 }).catch((err) => {
21053 if (isConflict(err)) {
21054 showConflictToast(err);
21055 } else {
21056 console.error(
21057 "[desktop-mode] files: drag persist failed",
21058 err
21059 );
21060 }
21061 store.upsertPlacement(data.placement);
21062 });
21063 return;
21064 }
21065 if (session.payload.type === "shortcut") {
21066 const data = session.payload.data;
21067 const occupied = buildVisualOccupiedSet(peers);
21068 const cell = nextRowMajorCell(occupied, host);
21069 void createPlacement({
21070 parentId: folderId,
21071 type: data.kind,
21072 ref: data.ref,
21073 x: cell.x,
21074 y: cell.y
21075 }).then((placement) => {
21076 store.upsertPlacement(placement);
21077 doAction("desktop-mode.files.shortcut-dropped", {
21078 folderId,
21079 placement
21080 });
21081 }).catch((err) => {
21082 console.error(
21083 "[desktop-mode] shortcut drop failed:",
21084 err
21085 );
21086 });
21087 }
21088 }
21089 };
21090 const dragManagerForLayer = getDragManager();
21091 if (dragManagerForLayer) {
21092 dropTargetDeregisters.push(
21093 dragManagerForLayer.registerDropTarget(canvasDropTarget)
21094 );
21095 }
21096 const onCanvasClick = (e) => {
21097 if (e.target instanceof Element && e.target.closest(`.${TILE_CLASS}`)) {
21098 return;
21099 }
21100 setSelected(null);
21101 };
21102 host.addEventListener("click", onCanvasClick);
21103 function attachSelectOnClick(tile2, placement) {
21104 tile2.addEventListener("click", (e) => {
21105 e.stopPropagation();
21106 setSelected(placement);
21107 });
21108 }
21109 repaint(store.getState());
21110 const off = store.subscribe(repaint);
21111 let resolveHydrated = () => void 0;
21112 const hydrated = new Promise((resolve2) => {
21113 resolveHydrated = resolve2;
21114 });
21115 if (!store.getState().hydratedFolders.has(folderId)) {
21116 void listPlacements(folderId).then((res) => {
21117 store.setFolderPlacements(folderId, res.placements);
21118 }).catch((err) => {
21119 console.error("[desktop-mode] files: failed to hydrate folder", folderId, err);
21120 }).finally(() => {
21121 resolveHydrated();
21122 });
21123 } else {
21124 queueMicrotask(resolveHydrated);
21125 }
21126 const colsForWidth = () => {
21127 const w = host.clientWidth > 0 ? host.clientWidth : 4 * GRID_CELL_W;
21128 return Math.max(1, Math.floor((w - GRID_PADDING) / GRID_CELL_W));
21129 };
21130 const sortPlacements = (list2, mode) => {
21131 const sorted = list2.slice();
21132 switch (mode) {
21133 case "name-asc":
21134 sorted.sort(
21135 (a, b) => a.file.title.localeCompare(b.file.title)
21136 );
21137 break;
21138 case "name-desc":
21139 sorted.sort(
21140 (a, b) => b.file.title.localeCompare(a.file.title)
21141 );
21142 break;
21143 case "date-asc":
21144 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
21145 break;
21146 case "date-desc":
21147 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
21148 break;
21149 }
21150 return sorted;
21151 };
21152 const sort = (mode) => {
21153 const live = store.getState().placementsByFolder.get(folderId);
21154 if (!live || live.length === 0) {
21155 return;
21156 }
21157 const pinned = live.filter((p) => isPinned(p));
21158 const draggable = live.filter((p) => !isPinned(p));
21159 const sorted = sortPlacements(draggable, mode);
21160 const cols = colsForWidth();
21161 const occupied = /* @__PURE__ */ new Set();
21162 for (let i = 0; i < pinned.length; i += 1) {
21163 occupied.add(cellKey(0, i));
21164 }
21165 let idx = 0;
21166 const nextCell = () => {
21167 while (true) {
21168 const row = Math.floor(idx / cols);
21169 const col = idx % cols;
21170 idx += 1;
21171 if (!occupied.has(cellKey(col, row))) {
21172 return { col, row };
21173 }
21174 }
21175 };
21176 sorted.forEach((p, i) => {
21177 const cell = nextCell();
21178 const x = GRID_PADDING + cell.col * GRID_CELL_W;
21179 const y = GRID_PADDING + cell.row * GRID_CELL_H;
21180 const next = {
21181 ...p,
21182 x,
21183 y,
21184 sortOrder: i
21185 };
21186 store.upsertPlacement(next);
21187 if (isSyntheticPlacement(p)) {
21188 return;
21189 }
21190 void updatePlacement(p.id, { x, y, sortOrder: i }).catch((err) => {
21191 console.error(
21192 "[desktop-mode] files: sort persist failed",
21193 err
21194 );
21195 });
21196 });
21197 };
21198 const reflow = () => {
21199 const live = store.getState().placementsByFolder.get(folderId);
21200 if (!live || live.length === 0) {
21201 return;
21202 }
21203 const w = host.clientWidth > 0 ? host.clientWidth : Infinity;
21204 const overflowing = live.some((p) => {
21205 const right = p.x + GRID_CELL_W;
21206 return right > w;
21207 });
21208 if (!overflowing) {
21209 return;
21210 }
21211 const cols = colsForWidth();
21212 const pinned = live.filter((p) => isPinned(p));
21213 const draggable = live.filter((p) => !isPinned(p));
21214 const occupied = /* @__PURE__ */ new Set();
21215 for (let i = 0; i < pinned.length; i += 1) {
21216 occupied.add(cellKey(0, i));
21217 }
21218 let idx = 0;
21219 const nextCell = () => {
21220 while (true) {
21221 const row = Math.floor(idx / cols);
21222 const col = idx % cols;
21223 idx += 1;
21224 if (!occupied.has(cellKey(col, row))) {
21225 return { col, row };
21226 }
21227 }
21228 };
21229 for (const p of draggable) {
21230 const cell = nextCell();
21231 const x = GRID_PADDING + cell.col * GRID_CELL_W;
21232 const y = GRID_PADDING + cell.row * GRID_CELL_H;
21233 const tile2 = container.querySelector(
21234 `[data-placement-id="${p.id}"]`
21235 );
21236 if (tile2) {
21237 setTilePosition(tile2, x, y);
21238 }
21239 }
21240 };
21241 let lastWidth = host.clientWidth;
21242 let resizeObserver = null;
21243 if (typeof ResizeObserver !== "undefined") {
21244 resizeObserver = new ResizeObserver(() => {
21245 const w = host.clientWidth;
21246 if (w === lastWidth) {
21247 return;
21248 }
21249 lastWidth = w;
21250 reflow();
21251 });
21252 resizeObserver.observe(host);
21253 }
21254 return {
21255 host,
21256 folderId,
21257 onSelectionChange(cb) {
21258 selectionListeners.add(cb);
21259 return () => {
21260 selectionListeners.delete(cb);
21261 };
21262 },
21263 sort,
21264 reflow,
21265 hydrated,
21266 dispose() {
21267 off();
21268 resizeObserver?.disconnect();
21269 resizeObserver = null;
21270 for (const deregister of dropTargetDeregisters) {
21271 try {
21272 deregister();
21273 } catch {
21274 }
21275 }
21276 dropTargetDeregisters.length = 0;
21277 for (const deregister of folderDropDeregisters.values()) {
21278 try {
21279 deregister();
21280 } catch {
21281 }
21282 }
21283 folderDropDeregisters.clear();
21284 for (const deregister of tileRejectDeregisters.values()) {
21285 try {
21286 deregister();
21287 } catch {
21288 }
21289 }
21290 tileRejectDeregisters.clear();
21291 host.removeEventListener("click", onCanvasClick);
21292 selectionListeners.clear();
21293 container.remove();
21294 }
21295 };
21296 }
21297 function fingerprint(list2) {
21298 if (list2.length === 0) {
21299 return "0";
21300 }
21301 const parts = [];
21302 for (const p of list2) {
21303 parts.push(
21304 `${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}`
21305 );
21306 }
21307 return parts.join("|");
21308 }
21309 function isPinned(placement) {
21310 return Boolean(placement.file.pinned);
21311 }
21312 function readSynthSource(placement) {
21313 const meta = placement.meta;
21314 if (!meta || typeof meta !== "object") {
21315 return null;
21316 }
21317 const v = meta.__synthFromDockItem;
21318 return typeof v === "string" && v !== "" ? v : null;
21319 }
21320 function isSyntheticPlacement(placement) {
21321 return placement.id <= 0 || readSynthSource(placement) !== null;
21322 }
21323 const RECYCLE_BIN_REF = "desktop-mode-recycle-bin";
21324 function shouldRejectTileDrops(placement) {
21325 if (placement.file?.type === "folder") {
21326 return false;
21327 }
21328 if (placement.file?.ref === RECYCLE_BIN_REF) {
21329 return false;
21330 }
21331 return true;
21332 }
21333 function buildVisualOccupiedSet(placements, excludeId) {
21334 const sorted = placements.slice().sort((a, b) => {
21335 const ap = isPinned(a) ? 0 : 1;
21336 const bp = isPinned(b) ? 0 : 1;
21337 return ap - bp;
21338 });
21339 const set = /* @__PURE__ */ new Set();
21340 let pinnedIdx = 0;
21341 for (const p of sorted) {
21342 if (excludeId !== void 0 && p.id === excludeId) {
21343 continue;
21344 }
21345 if (isPinned(p)) {
21346 set.add(cellKey(0, pinnedIdx));
21347 pinnedIdx += 1;
21348 } else {
21349 const cell = pointToCell(p.x, p.y);
21350 set.add(cellKey(cell.col, cell.row));
21351 }
21352 }
21353 return set;
21354 }
21355 function wouldCreateFolderCycle(movingFolderId, targetParentId) {
21356 if (targetParentId <= 0 || movingFolderId <= 0) {
21357 return false;
21358 }
21359 if (movingFolderId === targetParentId) {
21360 return true;
21361 }
21362 const parentByFolderId = /* @__PURE__ */ new Map();
21363 const state2 = store.getState();
21364 for (const bucket2 of state2.placementsByFolder.values()) {
21365 for (const p of bucket2) {
21366 if (p.file?.type !== "folder") {
21367 continue;
21368 }
21369 const fid = parseInt(p.file.ref, 10);
21370 if (Number.isNaN(fid) || fid <= 0) {
21371 continue;
21372 }
21373 if (!parentByFolderId.has(fid)) {
21374 parentByFolderId.set(fid, p.parentId);
21375 }
21376 }
21377 }
21378 const visited = /* @__PURE__ */ new Set();
21379 let cursor = targetParentId;
21380 let maxDepth = 256;
21381 while (cursor > 0 && maxDepth-- > 0) {
21382 if (cursor === movingFolderId) {
21383 return true;
21384 }
21385 if (visited.has(cursor)) {
21386 return true;
21387 }
21388 visited.add(cursor);
21389 const next = parentByFolderId.get(cursor);
21390 if (next === void 0) {
21391 return false;
21392 }
21393 cursor = next;
21394 }
21395 return false;
21396 }
21397 function persistDockPromotedPosition(dockItemId, x, y) {
21398 const api = window.wp?.desktop;
21399 if (!api?.getOsSettings || !api?.updateOsSettings) {
21400 return;
21401 }
21402 const current = api.getOsSettings().dockPromotedPositions ?? {};
21403 api.updateOsSettings({
21404 dockPromotedPositions: {
21405 ...current,
21406 [dockItemId]: { x, y }
21407 }
21408 });
21409 }
21410 function tryPatchPositions(list2, container, host) {
21411 const tiles = Array.from(
21412 container.querySelectorAll("[data-placement-id]")
21413 );
21414 if (tiles.length !== list2.length) {
21415 return false;
21416 }
21417 const byId = /* @__PURE__ */ new Map();
21418 for (const tile2 of tiles) {
21419 const raw = tile2.dataset.placementId ?? "";
21420 const id = parseInt(raw, 10);
21421 if (raw === "" || Number.isNaN(id) && raw !== "-0") {
21422 return false;
21423 }
21424 byId.set(id, tile2);
21425 }
21426 for (const placement of list2) {
21427 const tile2 = byId.get(placement.id);
21428 if (!tile2) {
21429 return false;
21430 }
21431 if (tile2.dataset.fileType !== placement.file.type) {
21432 return false;
21433 }
21434 if (tile2.dataset.fileRef !== placement.file.ref) {
21435 return false;
21436 }
21437 const wasPinned = tile2.classList.contains(`${TILE_CLASS}--pinned`);
21438 if (wasPinned !== isPinned(placement)) {
21439 return false;
21440 }
21441 }
21442 const pinnedSlots = /* @__PURE__ */ new Map();
21443 const occupiedCells = /* @__PURE__ */ new Set();
21444 let pinnedIdx = 0;
21445 for (const placement of list2) {
21446 if (!isPinned(placement)) {
21447 continue;
21448 }
21449 const slot = cellToPos(0, pinnedIdx);
21450 pinnedSlots.set(placement.id, { x: slot.x, y: slot.y });
21451 occupiedCells.add(cellKey(slot.col, slot.row));
21452 pinnedIdx += 1;
21453 }
21454 const displaced = /* @__PURE__ */ new Map();
21455 for (const placement of list2) {
21456 if (pinnedSlots.has(placement.id)) {
21457 continue;
21458 }
21459 const target2 = pointToCell(placement.x, placement.y);
21460 const key = cellKey(target2.col, target2.row);
21461 if (!occupiedCells.has(key)) {
21462 occupiedCells.add(key);
21463 continue;
21464 }
21465 const free = snapToEmptyCell(
21466 placement.x,
21467 placement.y,
21468 occupiedCells,
21469 host
21470 );
21471 occupiedCells.add(cellKey(free.col, free.row));
21472 displaced.set(placement.id, { x: free.x, y: free.y });
21473 }
21474 for (const placement of list2) {
21475 const tile2 = byId.get(placement.id);
21476 if (!tile2) {
21477 continue;
21478 }
21479 const pinned = pinnedSlots.get(placement.id);
21480 const disp = displaced.get(placement.id);
21481 if (pinned) {
21482 setTilePosition(tile2, pinned.x, pinned.y);
21483 } else if (disp) {
21484 setTilePosition(tile2, disp.x, disp.y);
21485 } else {
21486 setTilePosition(tile2, placement.x, placement.y);
21487 }
21488 }
21489 return true;
21490 }
21491 function hidePromotedDockItem(dockItemId) {
21492 const api = window.wp?.desktop;
21493 if (!api?.getOsSettings || !api?.updateOsSettings) {
21494 return;
21495 }
21496 const current = api.getOsSettings().itemVisibility ?? {};
21497 const next = { ...current, [dockItemId]: "dock" };
21498 api.updateOsSettings({ itemVisibility: next });
21499 }
21500 function registerFolderDropTarget(dragManager, tile2, targetFolderId, currentFolderId) {
21501 const target2 = {
21502 id: `desktop-mode-files-folder-${targetFolderId}-tile-${tile2.dataset.placementId ?? "?"}`,
21503 element: tile2,
21504 accept: (payload) => {
21505 if (payload.type !== "desktop-file" && payload.type !== "shortcut") {
21506 return false;
21507 }
21508 if (payload.type === "desktop-file") {
21509 const data = payload.data;
21510 if (data.placement.file.type === "folder" && parseInt(data.placement.file.ref, 10) === targetFolderId) {
21511 return false;
21512 }
21513 if (data.placement.parentId === targetFolderId) {
21514 return false;
21515 }
21516 if (isSyntheticPlacement(data.placement)) {
21517 return false;
21518 }
21519 if (data.placement.file.type === "folder") {
21520 const movingFolderId = parseInt(data.placement.file.ref, 10);
21521 if (!Number.isNaN(movingFolderId) && wouldCreateFolderCycle(movingFolderId, targetFolderId)) {
21522 return false;
21523 }
21524 }
21525 }
21526 return true;
21527 },
21528 onEnter: () => {
21529 tile2.classList.add(`${TILE_CLASS}--drop-target`);
21530 },
21531 onLeave: () => {
21532 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
21533 },
21534 onDrop: (session) => {
21535 tile2.classList.remove(`${TILE_CLASS}--drop-target`);
21536 if (session.payload.type === "desktop-file") {
21537 const data = session.payload.data;
21538 const next = {
21539 ...data.placement,
21540 parentId: targetFolderId
21541 };
21542 store.upsertPlacement(next);
21543 void updatePlacement(
21544 data.placement.id,
21545 { parentId: targetFolderId },
21546 data.placement.updatedAtMs
21547 ).then((server) => {
21548 store.upsertPlacement(server, "remote");
21549 }).catch((err) => {
21550 if (isConflict(err)) {
21551 showConflictToast(err);
21552 } else {
21553 console.error(
21554 "[desktop-mode] files: move-into-folder persist failed",
21555 err
21556 );
21557 }
21558 store.upsertPlacement(data.placement);
21559 });
21560 return;
21561 }
21562 if (session.payload.type === "shortcut") {
21563 const data = session.payload.data;
21564 const peers = store.getState().placementsByFolder.get(targetFolderId) ?? [];
21565 const cell = nextRowMajorCell(buildVisualOccupiedSet(peers));
21566 void createPlacement({
21567 parentId: targetFolderId,
21568 type: data.kind,
21569 ref: data.ref,
21570 x: cell.x,
21571 y: cell.y
21572 }).then((placement) => {
21573 store.upsertPlacement(placement);
21574 doAction("desktop-mode.files.shortcut-dropped", {
21575 folderId: targetFolderId,
21576 placement
21577 });
21578 }).catch((err) => {
21579 console.error(
21580 "[desktop-mode] shortcut drop into folder failed:",
21581 err
21582 );
21583 });
21584 }
21585 }
21586 };
21587 return dragManager.registerDropTarget(target2);
21588 }
21589 function attachTileDrag(tile2, placement, folderId) {
21590 tile2.addEventListener("pointerdown", (e) => {
21591 if (e.button !== 0) {
21592 return;
21593 }
21594 const dragManager = getDragManager();
21595 if (!dragManager) {
21596 return;
21597 }
21598 const liveBucket = store.getState().placementsByFolder.get(folderId);
21599 const livePlacement = liveBucket?.find((p) => p.id === placement.id) ?? placement;
21600 parseFloat(tile2.style.left) || livePlacement.x;
21601 parseFloat(tile2.style.top) || livePlacement.y;
21602 dragManager.start({
21603 payload: {
21604 type: "desktop-file",
21605 source: tile2,
21606 data: {
21607 placement: livePlacement,
21608 sourceFolderId: folderId,
21609 // Synthesize a cross-frame bridge payload from the
21610 // placement's file shape so a wallpaper-placed
21611 // shortcut can be dropped into an open Gutenberg
21612 // iframe and inserted as the matching block. The
21613 // PHP serialize() methods (`Desktop_Mode_Post_File`,
21614 // `Desktop_Mode_User_File`, `Desktop_Mode_Attachment_File`)
21615 // surface the URL fields this needs.
21616 bridgePayload: buildBridgePayloadFromPlacement(livePlacement)
21617 },
21618 ghost: {
21619 offsetX: e.clientX - tile2.getBoundingClientRect().left,
21620 offsetY: e.clientY - tile2.getBoundingClientRect().top
21621 }
21622 },
21623 origin: e
21624 // `onClickOnly` intentionally empty — a tile click is
21625 // handled by the dedicated `attachSelectOnClick` listener
21626 // below, which fires from the regular `click` event after
21627 // a sub-threshold pointerup. The manager won't fire a
21628 // `click` itself; the browser does.
21629 });
21630 });
21631 }
21632 function attachContextMenu(tile2, placement) {
21633 tile2.addEventListener("contextmenu", (e) => {
21634 e.preventDefault();
21635 e.stopPropagation();
21636 const items = [
21637 {
21638 id: "open",
21639 label: "Open",
21640 icon: "dashicons-external",
21641 sort: 10,
21642 onClick: () => {
21643 const file = resolve(placement.file);
21644 void openFile(file);
21645 }
21646 }
21647 ];
21648 if (placement.file.type === "post") {
21649 items.push({
21650 id: "navigate-into",
21651 label: "Navigate into",
21652 icon: "dashicons-category",
21653 sort: 20,
21654 onClick: () => {
21655 const postId = parseInt(placement.file.ref, 10);
21656 if (!postId) {
21657 return;
21658 }
21659 const api = window.wp?.desktop?.myWordpress;
21660 const postType = typeof placement.file.postType === "string" ? placement.file.postType : "post";
21661 const entityId = postType === "page" ? "pages" : "posts";
21662 api?.openDetail({
21663 entityId,
21664 postId,
21665 postTitle: placement.file.title || `#${postId}`
21666 });
21667 }
21668 });
21669 }
21670 const isFolder = placement.file.type === "folder";
21671 if (isFolder) {
21672 items.push({
21673 id: "rename-folder",
21674 label: "Rename…",
21675 icon: "dashicons-edit",
21676 sort: 30,
21677 onClick: () => {
21678 const folderId = parseInt(placement.file.ref, 10);
21679 if (!folderId) {
21680 return;
21681 }
21682 openCreateFolderDialog({
21683 title: "Rename folder",
21684 label: "New name",
21685 submitLabel: "Rename",
21686 initialName: placement.file.title,
21687 onSubmit: async (name) => {
21688 const trimmed = name.trim();
21689 if (!trimmed || trimmed === placement.file.title) {
21690 return;
21691 }
21692 const previousTitle = placement.file.title;
21693 const optimistic = {
21694 ...placement,
21695 file: { ...placement.file, title: trimmed }
21696 };
21697 store.upsertPlacement(optimistic);
21698 try {
21699 const folderUpdatedAtMs = store.getState().folders.get(folderId)?.updatedAtMs ?? 0;
21700 const updated = await updateFolder(
21701 folderId,
21702 { name: trimmed },
21703 folderUpdatedAtMs
21704 );
21705 store.upsertFolder(updated);
21706 const refreshed = await listPlacements(
21707 placement.parentId
21708 );
21709 store.setFolderPlacements(
21710 placement.parentId,
21711 refreshed.placements
21712 );
21713 } catch (err) {
21714 console.error(
21715 "[desktop-mode] rename folder failed:",
21716 err
21717 );
21718 store.upsertPlacement({
21719 ...placement,
21720 file: {
21721 ...placement.file,
21722 title: previousTitle
21723 }
21724 });
21725 }
21726 }
21727 });
21728 }
21729 });
21730 if (placement.canTrash !== false) {
21731 items.push({
21732 id: "delete-folder",
21733 label: "Move folder to Trash",
21734 icon: "dashicons-trash",
21735 sort: 90,
21736 danger: true,
21737 onClick: () => trashFolderWithUndo(placement)
21738 });
21739 }
21740 } else {
21741 const synthFromDockItem = readSynthSource(placement);
21742 const isRegisteredIcon = placement.file.type === "shortcut";
21743 if (synthFromDockItem || isRegisteredIcon) {
21744 const hideId = synthFromDockItem ?? placement.file.ref;
21745 items.push({
21746 id: "hide-from-desktop",
21747 label: "Hide from desktop",
21748 icon: "dashicons-hidden",
21749 sort: 90,
21750 onClick: () => hidePromotedDockItem(hideId)
21751 });
21752 } else if (placement.canTrash !== false) {
21753 items.push({
21754 id: "remove",
21755 label: "Move to Trash",
21756 icon: "dashicons-trash",
21757 sort: 90,
21758 danger: true,
21759 onClick: () => trashPlacementWithUndo(placement)
21760 });
21761 }
21762 }
21763 openTileMenu({ x: e.clientX, y: e.clientY }, { placement, items });
21764 });
21765 }
21766 const STATUS_BAR_CLASS = "desktop-mode-folder-status-bar";
21767 const ROOT_CLASS$2 = STATUS_BAR_CLASS;
21768 function mountFolderStatusBar(host, folderId) {
21769 const bar = document.createElement("div");
21770 bar.className = ROOT_CLASS$2;
21771 bar.setAttribute("role", "status");
21772 bar.dataset.folderId = String(folderId);
21773 host.appendChild(bar);
21774 const repaint = () => {
21775 const list2 = getFilesState().placementsByFolder.get(folderId) ?? [];
21776 const folders = list2.filter((p) => p.file.type === "folder").length;
21777 const files = list2.length - folders;
21778 const ctx = {
21779 folderId,
21780 totals: { files, folders, total: list2.length }
21781 };
21782 const segments = computeSegments(ctx);
21783 render(bar, segments);
21784 };
21785 repaint();
21786 const off = subscribeFilesStore(() => repaint());
21787 return {
21788 dispose() {
21789 off();
21790 bar.remove();
21791 }
21792 };
21793 }
21794 function computeSegments(ctx) {
21795 const { folders, files } = ctx.totals;
21796 const builtIns = [
21797 {
21798 id: "count",
21799 label: pluralize(files, "file", "files") + (folders > 0 ? `, ${pluralize(folders, "folder", "folders")}` : ""),
21800 align: "start",
21801 sort: 10
21802 }
21803 ];
21804 const filtered = applyFilters(
21805 "desktop-mode.files.folder-window.status-bar",
21806 builtIns,
21807 ctx
21808 );
21809 return Array.isArray(filtered) ? filtered : builtIns;
21810 }
21811 function render(bar, segments) {
21812 const sort = (a, b) => {
21813 const sa = typeof a.sort === "number" ? a.sort : 100;
21814 const sb = typeof b.sort === "number" ? b.sort : 100;
21815 if (sa !== sb) {
21816 return sa - sb;
21817 }
21818 return a.label.localeCompare(b.label);
21819 };
21820 const start = segments.filter((s) => (s.align ?? "start") === "start").sort(sort);
21821 const end = segments.filter((s) => s.align === "end").sort(sort);
21822 bar.replaceChildren();
21823 bar.appendChild(buildCluster("start", start));
21824 bar.appendChild(buildCluster("end", end));
21825 }
21826 function buildCluster(align, segs) {
21827 const cluster = document.createElement("div");
21828 cluster.className = `${ROOT_CLASS$2}__cluster ${ROOT_CLASS$2}__cluster--${align}`;
21829 for (const seg of segs) {
21830 cluster.appendChild(buildSegment(seg));
21831 }
21832 return cluster;
21833 }
21834 function buildSegment(seg) {
21835 const interactive = typeof seg.onClick === "function";
21836 const el = document.createElement(interactive ? "button" : "span");
21837 el.className = `${ROOT_CLASS$2}__segment`;
21838 el.dataset.segmentId = seg.id;
21839 if (interactive) {
21840 el.type = "button";
21841 el.addEventListener("click", (e) => seg.onClick(e));
21842 }
21843 if (seg.icon) {
21844 const icon = document.createElement("span");
21845 icon.className = `${ROOT_CLASS$2}__icon dashicons ${seg.icon.replace(/[^a-zA-Z0-9_-]/g, "")}`;
21846 icon.setAttribute("aria-hidden", "true");
21847 el.appendChild(icon);
21848 }
21849 const label = document.createElement("span");
21850 label.className = `${ROOT_CLASS$2}__label`;
21851 label.textContent = seg.label;
21852 el.appendChild(label);
21853 return el;
21854 }
21855 function pluralize(n, singular, plural) {
21856 return `${n} ${n === 1 ? singular : plural}`;
21857 }
21858 const MENU_CLASS$1 = "desktop-mode-icon-canvas-menu";
21859 let activeMenu$1 = null;
21860 let activeFlyout = null;
21861 let activeCanvas = null;
21862 let outsideHandler = null;
21863 let escHandler = null;
21864 function attachIconCanvasMenu(canvas, deps2) {
21865 deps2.openOnBackgroundClick !== false;
21866 const onContextMenu = (e) => {
21867 if (isInsideTile(e.target) || isInsideMenu(e.target)) {
21868 return;
21869 }
21870 e.preventDefault();
21871 toggle(e.clientX, e.clientY);
21872 };
21873 let toggleGen = 0;
21874 const toggle = (x, y) => {
21875 if (activeCanvas === canvas && activeMenu$1) {
21876 closeMenu();
21877 return;
21878 }
21879 const items = buildItems(deps2);
21880 const filtered = applyFilters(
21881 "desktop-mode.icon-canvas.menu",
21882 items,
21883 deps2.scope
21884 );
21885 const finalItems = Array.isArray(filtered) ? filtered : items;
21886 const myGen = ++toggleGen;
21887 openWithShellOverlays(
21888 () => myGen === toggleGen,
21889 () => openMenu(finalItems, { x, y }, canvas)
21890 );
21891 };
21892 canvas.addEventListener("contextmenu", onContextMenu);
21893 return {
21894 dispose: () => {
21895 canvas.removeEventListener("contextmenu", onContextMenu);
21896 closeMenu();
21897 }
21898 };
21899 }
21900 function isInsideTile(target2) {
21901 if (!(target2 instanceof Element)) {
21902 return false;
21903 }
21904 return target2.closest(".desktop-mode-file-tile") !== null;
21905 }
21906 function isInsideMenu(target2) {
21907 if (!(target2 instanceof Element)) {
21908 return false;
21909 }
21910 return target2.closest(`.${MENU_CLASS$1}`) !== null;
21911 }
21912 function buildItems(deps2) {
21913 const sortItem = {
21914 id: "sort-by",
21915 label: __("Sort by", "desktop-mode"),
21916 icon: "dashicons-sort",
21917 sort: 10,
21918 children: [
21919 {
21920 id: "sort-name-asc",
21921 label: __("Name (A → Z)", "desktop-mode"),
21922 sort: 10,
21923 onClick: () => deps2.onSort("name-asc")
21924 },
21925 {
21926 id: "sort-name-desc",
21927 label: __("Name (Z → A)", "desktop-mode"),
21928 sort: 20,
21929 onClick: () => deps2.onSort("name-desc")
21930 },
21931 {
21932 id: "sort-date-desc",
21933 label: __("Newest first", "desktop-mode"),
21934 sort: 30,
21935 onClick: () => deps2.onSort("date-desc")
21936 },
21937 {
21938 id: "sort-date-asc",
21939 label: __("Oldest first", "desktop-mode"),
21940 sort: 40,
21941 onClick: () => deps2.onSort("date-asc")
21942 }
21943 ]
21944 };
21945 const items = [sortItem];
21946 if (Array.isArray(deps2.extraItems)) {
21947 items.push(...deps2.extraItems);
21948 }
21949 return items;
21950 }
21951 function sortItems(items) {
21952 return items.slice().sort((a, b) => {
21953 const sa = typeof a.sort === "number" ? a.sort : 100;
21954 const sb = typeof b.sort === "number" ? b.sort : 100;
21955 if (sa !== sb) {
21956 return sa - sb;
21957 }
21958 return a.label.localeCompare(b.label);
21959 });
21960 }
21961 function openMenu(items, pos, canvas) {
21962 closeMenu();
21963 if (items.length === 0) {
21964 return;
21965 }
21966 activeCanvas = canvas;
21967 const sorted = sortItems(items);
21968 const menu = document.createElement("wpd-context-menu");
21969 menu.setAttribute("open", "");
21970 menu.classList.add(MENU_CLASS$1);
21971 menu.style.left = `${pos.x}px`;
21972 menu.style.top = `${pos.y}px`;
21973 const itemById = /* @__PURE__ */ new Map();
21974 for (const item of sorted) {
21975 itemById.set(item.id, item);
21976 const opt = appendOption(menu, item);
21977 if (hasChildren(item)) {
21978 opt.addEventListener("mouseenter", () => {
21979 openFlyout(item, opt);
21980 });
21981 }
21982 }
21983 menu.addEventListener("wpd-context-menu-pick", (e) => {
21984 const detail = e.detail;
21985 const item = itemById.get(detail.id);
21986 if (!item) {
21987 return;
21988 }
21989 if (hasChildren(item)) {
21990 e.stopPropagation();
21991 const anchor = menu.querySelector(
21992 `[data-menu-item-id="${item.id}"]`
21993 );
21994 if (anchor) {
21995 openFlyout(item, anchor);
21996 }
21997 return;
21998 }
21999 closeMenu();
22000 item.onClick?.();
22001 });
22002 document.body.appendChild(menu);
22003 activeMenu$1 = menu;
22004 clampToViewport(menu);
22005 queueMicrotask(() => {
22006 outsideHandler = (e) => {
22007 if (isInsideMenu(e.target)) {
22008 return;
22009 }
22010 closeMenu();
22011 };
22012 escHandler = (e) => {
22013 if (e.key === "Escape") {
22014 closeMenu();
22015 }
22016 };
22017 document.addEventListener("mousedown", outsideHandler);
22018 document.addEventListener("keydown", escHandler);
22019 });
22020 }
22021 function appendOption(host, item) {
22022 const opt = document.createElement("wpd-context-menu-option");
22023 opt.dataset.menuItemId = item.id;
22024 opt.setAttribute("value", item.id);
22025 if (item.heading) {
22026 opt.setAttribute("heading", "");
22027 }
22028 if (item.disabled) {
22029 opt.setAttribute("disabled", "");
22030 }
22031 if (item.icon) {
22032 opt.setAttribute("icon", sanitizeClass$1(item.icon));
22033 }
22034 if (hasChildren(item)) {
22035 opt.setAttribute("has-children", "");
22036 }
22037 opt.textContent = item.label;
22038 host.appendChild(opt);
22039 return opt;
22040 }
22041 function openFlyout(parent, anchor) {
22042 closeFlyout();
22043 if (!hasChildren(parent)) {
22044 return;
22045 }
22046 const fly = document.createElement("wpd-context-menu");
22047 fly.setAttribute("open", "");
22048 fly.classList.add(MENU_CLASS$1, `${MENU_CLASS$1}--flyout`);
22049 const childById = /* @__PURE__ */ new Map();
22050 for (const child of sortItems(parent.children ?? [])) {
22051 childById.set(child.id, child);
22052 appendOption(fly, child);
22053 }
22054 fly.addEventListener("wpd-context-menu-pick", (e) => {
22055 const detail = e.detail;
22056 const child = childById.get(detail.id);
22057 if (!child) {
22058 return;
22059 }
22060 e.stopPropagation();
22061 closeMenu();
22062 child.onClick?.();
22063 });
22064 document.body.appendChild(fly);
22065 activeFlyout = fly;
22066 positionFlyout(fly, anchor);
22067 }
22068 function positionFlyout(fly, anchor) {
22069 const ar = anchor.getBoundingClientRect();
22070 fly.style.position = "fixed";
22071 fly.style.left = `${ar.right}px`;
22072 fly.style.top = `${ar.top}px`;
22073 const fr = fly.getBoundingClientRect();
22074 if (fr.right > window.innerWidth) {
22075 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
22076 }
22077 if (fr.bottom > window.innerHeight) {
22078 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
22079 }
22080 }
22081 function clampToViewport(menu) {
22082 const rect = menu.getBoundingClientRect();
22083 if (rect.right > window.innerWidth) {
22084 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
22085 }
22086 if (rect.bottom > window.innerHeight) {
22087 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
22088 }
22089 }
22090 function hasChildren(item) {
22091 return Array.isArray(item.children) && item.children.length > 0;
22092 }
22093 function closeFlyout() {
22094 if (activeFlyout) {
22095 activeFlyout.remove();
22096 activeFlyout = null;
22097 }
22098 }
22099 function closeMenu() {
22100 closeFlyout();
22101 if (activeMenu$1) {
22102 activeMenu$1.remove();
22103 activeMenu$1 = null;
22104 }
22105 activeCanvas = null;
22106 if (outsideHandler) {
22107 document.removeEventListener("mousedown", outsideHandler);
22108 outsideHandler = null;
22109 }
22110 if (escHandler) {
22111 document.removeEventListener("keydown", escHandler);
22112 escHandler = null;
22113 }
22114 }
22115 function sanitizeClass$1(raw) {
22116 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
22117 }
22118 const ROOT_CLASS$1 = "desktop-mode-breadcrumbs";
22119 function renderBreadcrumbs(host, segments, opts = {}) {
22120 host.replaceChildren();
22121 host.classList.add(ROOT_CLASS$1);
22122 if (opts.onBack) {
22123 const back = document.createElement("button");
22124 back.type = "button";
22125 back.className = `${ROOT_CLASS$1}__back`;
22126 back.setAttribute("aria-label", __("Back", "desktop-mode"));
22127 back.title = __("Back", "desktop-mode");
22128 const arrow = document.createElement("span");
22129 arrow.className = "dashicons dashicons-arrow-left-alt2";
22130 arrow.setAttribute("aria-hidden", "true");
22131 back.appendChild(arrow);
22132 if (opts.backDisabled) {
22133 back.disabled = true;
22134 }
22135 const onBack = opts.onBack;
22136 back.addEventListener("click", () => {
22137 if (back.disabled) {
22138 return;
22139 }
22140 onBack();
22141 });
22142 host.appendChild(back);
22143 }
22144 const nav = document.createElement("nav");
22145 nav.className = `${ROOT_CLASS$1}__crumbs`;
22146 nav.setAttribute("aria-label", __("Breadcrumb", "desktop-mode"));
22147 segments.forEach((seg, idx) => {
22148 if (idx > 0) {
22149 const sep = document.createElement("span");
22150 sep.className = `${ROOT_CLASS$1}__sep`;
22151 sep.setAttribute("aria-hidden", "true");
22152 sep.textContent = "›";
22153 nav.appendChild(sep);
22154 }
22155 if (!seg.onClick) {
22156 const here = document.createElement("span");
22157 here.className = `${ROOT_CLASS$1}__crumb ${ROOT_CLASS$1}__crumb--current`;
22158 here.setAttribute("aria-current", "page");
22159 here.textContent = seg.label;
22160 nav.appendChild(here);
22161 return;
22162 }
22163 const btn = document.createElement("button");
22164 btn.type = "button";
22165 btn.className = `${ROOT_CLASS$1}__crumb`;
22166 btn.textContent = seg.label;
22167 const onClick = seg.onClick;
22168 btn.addEventListener("click", () => {
22169 onClick();
22170 });
22171 nav.appendChild(btn);
22172 });
22173 host.appendChild(nav);
22174 }
22175 async function getJson(url, init2 = {}) {
22176 const response = await trackedFetch$1(url, {
22177 credentials: "same-origin",
22178 headers: {
22179 Accept: "application/json",
22180 "X-WP-Nonce": readRestNonce(),
22181 ...init2.headers ?? {}
22182 },
22183 ...init2
22184 });
22185 if (!response.ok) {
22186 throw new Error(`${response.status} ${response.statusText}`);
22187 }
22188 return await response.json();
22189 }
22190 function readRestNonce() {
22191 const cfg = window.wp?.desktop?.config;
22192 return cfg?.restNonce ?? "";
22193 }
22194 function readRestRoot() {
22195 const cfg = window.wp?.desktop?.config;
22196 if (cfg?.restUrl) {
22197 return cfg.restUrl.endsWith("/") ? cfg.restUrl : cfg.restUrl + "/";
22198 }
22199 return `${window.location.origin}/wp-json/`;
22200 }
22201 function restUrl(path) {
22202 return joinRestUrl(readRestRoot(), path);
22203 }
22204 function renderPlacementPreview(placement, host) {
22205 const filtered = applyFilters(
22206 "desktop-mode.files.preview",
22207 null,
22208 placement
22209 );
22210 if (filtered instanceof HTMLElement) {
22211 host.replaceChildren(filtered);
22212 return;
22213 }
22214 if (placement.accessGated) {
22215 host.replaceChildren(renderAccessGated(placement));
22216 return;
22217 }
22218 host.replaceChildren(renderLoading());
22219 void renderByType(placement).then((node) => {
22220 host.replaceChildren(node);
22221 }).catch((err) => {
22222 host.replaceChildren(renderError(err));
22223 });
22224 }
22225 function renderAccessGated(placement) {
22226 const wrap = document.createElement("div");
22227 wrap.className = "desktop-mode-files__access-gated";
22228 const ring = document.createElement("div");
22229 ring.className = "desktop-mode-files__access-gated-ring";
22230 const glyph = document.createElement("span");
22231 glyph.className = "dashicons dashicons-lock desktop-mode-files__access-gated-glyph";
22232 glyph.setAttribute("aria-hidden", "true");
22233 ring.appendChild(glyph);
22234 wrap.appendChild(ring);
22235 const title = document.createElement("h2");
22236 title.className = "desktop-mode-files__access-gated-title";
22237 title.textContent = "No permission to view";
22238 wrap.appendChild(title);
22239 const sub = document.createElement("p");
22240 sub.className = "desktop-mode-files__access-gated-sub";
22241 const target2 = placement.file.title || placement.file.type;
22242 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.`;
22243 wrap.appendChild(sub);
22244 const hint = document.createElement("p");
22245 hint.className = "desktop-mode-files__access-gated-hint";
22246 hint.textContent = "Ask the owner to grant access on the underlying item, or to remove it from the shared folder.";
22247 wrap.appendChild(hint);
22248 return wrap;
22249 }
22250 async function renderByType(placement) {
22251 const file = placement.file;
22252 switch (file.type) {
22253 case "post":
22254 return renderPostPreview(file.ref, file);
22255 case "folder":
22256 return renderFolderPreview(file);
22257 case "shortcut":
22258 return renderShortcutPreview(file);
22259 case "attachment":
22260 return renderAttachmentPreview(file.ref, file);
22261 case "user":
22262 return renderUserSummary(file.ref, file);
22263 case "term":
22264 return renderTermSummary(file);
22265 case "comment":
22266 return renderCommentSummary(file.ref, file);
22267 case "bookmark":
22268 return renderBookmarkPreview(file);
22269 default:
22270 return renderGenericPreview(file);
22271 }
22272 }
22273 async function renderPostPreview(ref, file) {
22274 const id = parseInt(ref, 10);
22275 if (!id) {
22276 return renderGenericPreview(file);
22277 }
22278 let data = null;
22279 for (const path of ["wp/v2/posts", "wp/v2/pages"]) {
22280 try {
22281 data = await getJson(
22282 restUrl(
22283 `${path}/${id}?_fields=id,title,content,date,link,status`
22284 )
22285 );
22286 break;
22287 } catch {
22288 }
22289 }
22290 if (!data) {
22291 return renderGenericPreview(file);
22292 }
22293 const wrap = articleShell();
22294 const h = document.createElement("h2");
22295 h.className = "desktop-mode-my-wordpress__article-title";
22296 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
22297 wrap.appendChild(h);
22298 const meta = document.createElement("p");
22299 meta.className = "desktop-mode-my-wordpress__article-meta";
22300 const parts = [];
22301 parts.push(formatDate(data.date));
22302 if (data.status && data.status !== "publish") {
22303 parts.push(data.status);
22304 }
22305 meta.textContent = parts.join(" · ");
22306 wrap.appendChild(meta);
22307 if (data.content?.rendered) {
22308 const body = document.createElement("div");
22309 body.className = "desktop-mode-my-wordpress__article-content";
22310 body.innerHTML = data.content.rendered;
22311 wrap.appendChild(body);
22312 }
22313 const footer = document.createElement("footer");
22314 footer.className = "desktop-mode-my-wordpress__article-footer";
22315 const myWordpressApi = window.wp?.desktop?.myWordpress;
22316 if (myWordpressApi) {
22317 const exploreBtn = document.createElement("wpd-button");
22318 exploreBtn.setAttribute("variant", "secondary");
22319 exploreBtn.textContent = __("Explore details", "desktop-mode");
22320 exploreBtn.title = __(
22321 "See author, comments, categories, tags, attached media, and revisions for this entry.",
22322 "desktop-mode"
22323 );
22324 exploreBtn.addEventListener("click", () => {
22325 const postType = typeof file.postType === "string" ? file.postType : "post";
22326 myWordpressApi.openDetail({
22327 entityId: postType === "page" ? "pages" : "posts",
22328 postId: id,
22329 postTitle: stripTags(data.title.rendered) || `#${id}`
22330 });
22331 });
22332 footer.appendChild(exploreBtn);
22333 }
22334 const editBtn = document.createElement("wpd-button");
22335 editBtn.setAttribute("variant", "primary");
22336 editBtn.textContent = __("Open in editor", "desktop-mode");
22337 editBtn.addEventListener("click", () => {
22338 const adminUrl = window.wp?.desktop?.config?.adminUrl;
22339 if (!adminUrl) {
22340 return;
22341 }
22342 const editUrl = `${adminUrl}post.php?post=${id}&action=edit`;
22343 const wm = window.wp?.desktop?.windowManager;
22344 const postType = typeof file.postType === "string" ? file.postType : "post";
22345 const entityId = postType === "page" ? "pages" : "posts";
22346 wm?.open({
22347 id: `${entityId}-edit-${id}`,
22348 url: editUrl,
22349 title: stripTags(data.title.rendered),
22350 icon: file.icon
22351 });
22352 });
22353 footer.appendChild(editBtn);
22354 wrap.appendChild(footer);
22355 return wrap;
22356 }
22357 async function renderUserSummary(ref, file) {
22358 const id = parseInt(ref, 10);
22359 if (!id) {
22360 return renderGenericPreview(file);
22361 }
22362 let data = null;
22363 try {
22364 data = await getJson(
22365 restUrl(`desktop-mode/v1/user-stats/${id}`)
22366 );
22367 } catch {
22368 return renderGenericPreview(file);
22369 }
22370 const wrap = articleShell("desktop-mode-my-wordpress__user");
22371 const header = document.createElement("header");
22372 header.className = "desktop-mode-my-wordpress__user-header";
22373 if (data.profile.avatarUrl) {
22374 const img = document.createElement("img");
22375 img.className = "desktop-mode-my-wordpress__user-avatar";
22376 img.src = data.profile.avatarUrl;
22377 img.alt = "";
22378 header.appendChild(img);
22379 }
22380 const head = document.createElement("div");
22381 head.className = "desktop-mode-my-wordpress__user-headline";
22382 const h = document.createElement("h2");
22383 h.className = "desktop-mode-my-wordpress__article-title";
22384 h.textContent = data.profile.name || file.title || `#${id}`;
22385 head.appendChild(h);
22386 if (data.profile.roleLabels && data.profile.roleLabels.length > 0) {
22387 const roles = document.createElement("div");
22388 roles.className = "desktop-mode-my-wordpress__user-roles";
22389 for (const r of data.profile.roleLabels) {
22390 const badge = document.createElement("span");
22391 badge.className = "desktop-mode-my-wordpress__user-role";
22392 badge.textContent = r;
22393 roles.appendChild(badge);
22394 }
22395 head.appendChild(roles);
22396 }
22397 header.appendChild(head);
22398 wrap.appendChild(header);
22399 if (data.profile.description) {
22400 const bio = document.createElement("div");
22401 bio.className = "desktop-mode-my-wordpress__user-bio";
22402 bio.textContent = data.profile.description;
22403 wrap.appendChild(bio);
22404 }
22405 const cards = document.createElement("div");
22406 cards.className = "desktop-mode-my-wordpress__user-stats";
22407 cards.appendChild(
22408 statCard(
22409 data.counts.posts.total.toLocaleString(),
22410 __("Posts", "desktop-mode")
22411 )
22412 );
22413 cards.appendChild(
22414 statCard(
22415 data.counts.pages.total.toLocaleString(),
22416 __("Pages", "desktop-mode")
22417 )
22418 );
22419 cards.appendChild(
22420 statCard(
22421 data.counts.commentsReceived.toLocaleString(),
22422 __("Comments received", "desktop-mode")
22423 )
22424 );
22425 wrap.appendChild(cards);
22426 return wrap;
22427 }
22428 async function renderTermSummary(file) {
22429 const id = parseInt(file.ref, 10);
22430 const taxonomy = typeof file.taxonomy === "string" && file.taxonomy ? file.taxonomy : "category";
22431 if (!id) {
22432 return renderGenericPreview(file);
22433 }
22434 let data = null;
22435 try {
22436 data = await getJson(
22437 restUrl(`desktop-mode/v1/term-stats/${taxonomy}/${id}`)
22438 );
22439 } catch {
22440 return renderGenericPreview(file);
22441 }
22442 const wrap = articleShell();
22443 const h = document.createElement("h2");
22444 h.className = "desktop-mode-my-wordpress__article-title";
22445 h.textContent = data.profile.name || file.title || `#${id}`;
22446 wrap.appendChild(h);
22447 const meta = document.createElement("p");
22448 meta.className = "desktop-mode-my-wordpress__article-meta";
22449 meta.textContent = data.profile.taxonomyLabel || data.profile.taxonomy;
22450 wrap.appendChild(meta);
22451 if (data.profile.description) {
22452 const desc = document.createElement("div");
22453 desc.className = "desktop-mode-my-wordpress__article-content";
22454 desc.innerHTML = data.profile.description;
22455 wrap.appendChild(desc);
22456 }
22457 const cards = document.createElement("div");
22458 cards.className = "desktop-mode-my-wordpress__user-stats";
22459 cards.appendChild(
22460 statCard(
22461 data.counts.posts.total.toLocaleString(),
22462 __("Posts", "desktop-mode")
22463 )
22464 );
22465 cards.appendChild(
22466 statCard(
22467 data.counts.commentsReceived.toLocaleString(),
22468 __("Comments", "desktop-mode")
22469 )
22470 );
22471 cards.appendChild(
22472 statCard(
22473 data.counts.distinctAuthors.toLocaleString(),
22474 __("Authors", "desktop-mode")
22475 )
22476 );
22477 wrap.appendChild(cards);
22478 return wrap;
22479 }
22480 async function renderCommentSummary(ref, file) {
22481 const id = parseInt(ref, 10);
22482 if (!id) {
22483 return renderGenericPreview(file);
22484 }
22485 let data = null;
22486 try {
22487 data = await getJson(
22488 restUrl(`desktop-mode/v1/comment-stats/${id}`)
22489 );
22490 } catch {
22491 return renderGenericPreview(file);
22492 }
22493 const wrap = articleShell();
22494 const header = document.createElement("header");
22495 header.className = "desktop-mode-my-wordpress__user-header";
22496 if (data.author.avatarUrl) {
22497 const img = document.createElement("img");
22498 img.className = "desktop-mode-my-wordpress__user-avatar";
22499 img.src = data.author.avatarUrl;
22500 img.alt = "";
22501 header.appendChild(img);
22502 }
22503 const head = document.createElement("div");
22504 head.className = "desktop-mode-my-wordpress__user-headline";
22505 const h = document.createElement("h2");
22506 h.className = "desktop-mode-my-wordpress__article-title";
22507 h.textContent = data.author.name;
22508 head.appendChild(h);
22509 const sub = document.createElement("p");
22510 sub.className = "desktop-mode-my-wordpress__article-meta";
22511 sub.textContent = `${formatDate(data.comment.date)} · ${data.comment.status}`;
22512 head.appendChild(sub);
22513 header.appendChild(head);
22514 wrap.appendChild(header);
22515 const body = document.createElement("div");
22516 body.className = "desktop-mode-my-wordpress__article-content";
22517 body.innerHTML = data.comment.rendered;
22518 wrap.appendChild(body);
22519 if (data.post) {
22520 const card = document.createElement("div");
22521 card.className = "desktop-mode-my-wordpress__comment-post";
22522 const link = document.createElement("a");
22523 link.className = "desktop-mode-my-wordpress__comment-post-title";
22524 link.href = data.post.link;
22525 link.target = "_blank";
22526 link.rel = "noopener noreferrer";
22527 link.textContent = data.post.title;
22528 card.appendChild(link);
22529 wrap.appendChild(card);
22530 }
22531 return wrap;
22532 }
22533 async function renderAttachmentPreview(ref, file) {
22534 const id = parseInt(ref, 10);
22535 if (!id) {
22536 return renderGenericPreview(file);
22537 }
22538 let data = null;
22539 try {
22540 data = await getJson(
22541 restUrl(
22542 `wp/v2/media/${id}?_fields=id,title,source_url,mime_type,alt_text,media_details`
22543 )
22544 );
22545 } catch {
22546 return renderGenericPreview(file);
22547 }
22548 const wrap = articleShell();
22549 const h = document.createElement("h2");
22550 h.className = "desktop-mode-my-wordpress__article-title";
22551 h.textContent = stripTags(data.title.rendered) || file.title || `#${id}`;
22552 wrap.appendChild(h);
22553 const meta = document.createElement("p");
22554 meta.className = "desktop-mode-my-wordpress__article-meta";
22555 meta.textContent = data.mime_type;
22556 wrap.appendChild(meta);
22557 if (data.mime_type.startsWith("image/")) {
22558 const img = document.createElement("img");
22559 img.className = "desktop-mode-my-wordpress__article-hero";
22560 const sizes = data.media_details?.sizes;
22561 img.src = sizes?.large?.source_url ?? sizes?.medium?.source_url ?? data.source_url;
22562 img.alt = data.alt_text ?? "";
22563 wrap.appendChild(img);
22564 } else {
22565 const p = document.createElement("p");
22566 const a = document.createElement("a");
22567 a.href = data.source_url;
22568 a.textContent = data.source_url;
22569 a.target = "_blank";
22570 a.rel = "noopener noreferrer";
22571 p.appendChild(a);
22572 wrap.appendChild(p);
22573 }
22574 return wrap;
22575 }
22576 function renderFolderPreview(file) {
22577 const wrap = articleShell();
22578 const h = document.createElement("h2");
22579 h.className = "desktop-mode-my-wordpress__article-title";
22580 h.textContent = file.title || __("(folder)", "desktop-mode");
22581 wrap.appendChild(h);
22582 const meta = document.createElement("p");
22583 meta.className = "desktop-mode-my-wordpress__article-meta";
22584 meta.textContent = __("Double-click to open.", "desktop-mode");
22585 wrap.appendChild(meta);
22586 return wrap;
22587 }
22588 function renderShortcutPreview(file) {
22589 const wrap = articleShell();
22590 const h = document.createElement("h2");
22591 h.className = "desktop-mode-my-wordpress__article-title";
22592 h.textContent = file.title || __("Shortcut", "desktop-mode");
22593 wrap.appendChild(h);
22594 const meta = document.createElement("p");
22595 meta.className = "desktop-mode-my-wordpress__article-meta";
22596 meta.textContent = __("Plugin shortcut. Double-click to open.", "desktop-mode");
22597 wrap.appendChild(meta);
22598 return wrap;
22599 }
22600 function renderBookmarkPreview(file) {
22601 const wrap = articleShell();
22602 const h = document.createElement("h2");
22603 h.className = "desktop-mode-my-wordpress__article-title";
22604 h.textContent = file.title || __("Bookmark", "desktop-mode");
22605 wrap.appendChild(h);
22606 const url = typeof file.url === "string" ? file.url : "";
22607 if (url) {
22608 const a = document.createElement("a");
22609 a.href = url;
22610 a.textContent = url;
22611 a.target = "_blank";
22612 a.rel = "noopener noreferrer";
22613 wrap.appendChild(a);
22614 }
22615 return wrap;
22616 }
22617 function renderGenericPreview(file) {
22618 const wrap = articleShell();
22619 const h = document.createElement("h2");
22620 h.className = "desktop-mode-my-wordpress__article-title";
22621 h.textContent = file.title || file.type;
22622 wrap.appendChild(h);
22623 const meta = document.createElement("p");
22624 meta.className = "desktop-mode-my-wordpress__article-meta";
22625 meta.textContent = sprintf(
22626 // translators: %s is a file-type slug.
22627 __("Type: %s", "desktop-mode"),
22628 file.type
22629 );
22630 wrap.appendChild(meta);
22631 if (!file.exists) {
22632 const warn2 = document.createElement("p");
22633 warn2.className = "desktop-mode-my-wordpress__article-meta";
22634 warn2.textContent = __(
22635 "The underlying entity is no longer available.",
22636 "desktop-mode"
22637 );
22638 wrap.appendChild(warn2);
22639 }
22640 return wrap;
22641 }
22642 function articleShell(extraClass = "") {
22643 const article = document.createElement("article");
22644 article.className = "desktop-mode-my-wordpress__article" + (extraClass ? " " + extraClass : "");
22645 return article;
22646 }
22647 function statCard(value, label) {
22648 const card = document.createElement("div");
22649 card.className = "desktop-mode-my-wordpress__user-stat";
22650 const v = document.createElement("span");
22651 v.className = "desktop-mode-my-wordpress__user-stat-value";
22652 v.textContent = value;
22653 card.appendChild(v);
22654 const l = document.createElement("span");
22655 l.className = "desktop-mode-my-wordpress__user-stat-label";
22656 l.textContent = label;
22657 card.appendChild(l);
22658 return card;
22659 }
22660 function renderLoading() {
22661 const wrap = document.createElement("div");
22662 wrap.className = "desktop-mode-my-wordpress__preview-loading";
22663 const spinner = document.createElement("wpd-spinner");
22664 wrap.appendChild(spinner);
22665 return wrap;
22666 }
22667 function renderError(err) {
22668 const wrap = document.createElement("div");
22669 wrap.className = "desktop-mode-my-wordpress__error";
22670 wrap.textContent = err instanceof Error ? err.message : __("Unknown error.", "desktop-mode");
22671 return wrap;
22672 }
22673 function stripTags(html2) {
22674 const div = document.createElement("div");
22675 div.innerHTML = html2;
22676 return (div.textContent ?? "").trim();
22677 }
22678 function formatDate(iso) {
22679 if (!iso) {
22680 return "";
22681 }
22682 try {
22683 return new Date(iso).toLocaleString();
22684 } catch {
22685 return iso;
22686 }
22687 }
22688 function renderPreviewEmpty() {
22689 const wrap = document.createElement("div");
22690 wrap.className = "desktop-mode-my-wordpress__preview-empty";
22691 wrap.textContent = __(
22692 "Select an item to preview it here.",
22693 "desktop-mode"
22694 );
22695 return wrap;
22696 }
22697 const ID_PREFIX = "desktop-mode-embed-";
22698 const DEFAULT_W = 800;
22699 const DEFAULT_H = 600;
22700 const MIN_W = 360;
22701 const MIN_H = 240;
22702 const PADDING = 16;
22703 const lastPersisted = /* @__PURE__ */ new Map();
22704 function openEmbedWindow(file, ctx) {
22705 const url = file.ref();
22706 if (!url) {
22707 return;
22708 }
22709 const wm = window.wp?.desktop?.windowManager;
22710 if (!wm) {
22711 return;
22712 }
22713 const placement = ctx?.placement;
22714 const meta = placement?.meta ?? null;
22715 const windowId = placement ? `${ID_PREFIX}${placement.id}` : `${ID_PREFIX}anon-${hash(url)}`;
22716 const customName = meta?.name?.trim() ?? "";
22717 const title = customName !== "" ? customName : file.title();
22718 const cfg = {
22719 id: windowId,
22720 baseId: windowId,
22721 url,
22722 title,
22723 icon: file.icon(),
22724 minWidth: MIN_W,
22725 minHeight: MIN_H
22726 };
22727 const saved = meta?.window;
22728 const area = document.getElementById("desktop-mode-area");
22729 const aw = area?.clientWidth ?? window.innerWidth;
22730 const ah = area?.clientHeight ?? window.innerHeight;
22731 if (saved && Number.isFinite(saved.width) && Number.isFinite(saved.height)) {
22732 const { x, y, width, height } = clampGeometry(saved, aw, ah);
22733 cfg.x = x;
22734 cfg.y = y;
22735 cfg.width = width;
22736 cfg.height = height;
22737 } else {
22738 cfg.width = Math.min(DEFAULT_W, Math.max(MIN_W, aw - PADDING * 2));
22739 cfg.height = Math.min(DEFAULT_H, Math.max(MIN_H, ah - PADDING * 2));
22740 }
22741 if (placement) {
22742 if (saved) {
22743 lastPersisted.set(windowId, { ...saved });
22744 }
22745 }
22746 wm.open(cfg);
22747 }
22748 let installed = false;
22749 function installEmbedPersistence() {
22750 if (installed) {
22751 return;
22752 }
22753 installed = true;
22754 const onChange = (payload) => {
22755 const p = payload;
22756 const id = p?.windowId;
22757 if (!id || !id.startsWith(ID_PREFIX)) {
22758 return;
22759 }
22760 const placementIdStr = id.slice(ID_PREFIX.length);
22761 const placementId = parseInt(placementIdStr, 10);
22762 if (!placementId) {
22763 return;
22764 }
22765 const wm = window.wp?.desktop?.windowManager;
22766 const win = wm?.getById?.(id);
22767 const el = win?.element;
22768 if (!el) {
22769 return;
22770 }
22771 const next = {
22772 x: el.offsetLeft,
22773 y: el.offsetTop,
22774 width: el.offsetWidth,
22775 height: el.offsetHeight
22776 };
22777 const prev = lastPersisted.get(id);
22778 if (prev && prev.x === next.x && prev.y === next.y && prev.width === next.width && prev.height === next.height) {
22779 return;
22780 }
22781 lastPersisted.set(id, next);
22782 void persist(placementId, next);
22783 };
22784 addAction(HOOKS.WINDOW_DRAG_END, "desktop-mode-embed-persist", onChange);
22785 addAction(HOOKS.WINDOW_RESIZE_END, "desktop-mode-embed-persist", onChange);
22786 }
22787 async function persist(placementId, geo) {
22788 try {
22789 const list2 = await listPlacements(0);
22790 const row = list2.placements.find((p) => p.id === placementId);
22791 const prevMeta = row?.meta ?? {};
22792 const nextMeta = {
22793 ...prevMeta,
22794 window: geo
22795 };
22796 await updatePlacement(placementId, { meta: nextMeta });
22797 } catch (err) {
22798 console.warn("[desktop-mode] embed window persist failed:", err);
22799 }
22800 }
22801 function clampGeometry(g, areaW, areaH) {
22802 const width = Math.max(MIN_W, Math.min(g.width, areaW - PADDING));
22803 const height = Math.max(MIN_H, Math.min(g.height, areaH - PADDING));
22804 const x = Math.max(0, Math.min(g.x, Math.max(0, areaW - width)));
22805 const y = Math.max(0, Math.min(g.y, Math.max(0, areaH - height)));
22806 return { x, y, width, height };
22807 }
22808 function hash(s) {
22809 let h = 0;
22810 for (let i = 0; i < s.length; i++) {
22811 h = (Math.imul(h, 31) + s.charCodeAt(i)) % 2147483647;
22812 }
22813 return Math.abs(h).toString(36);
22814 }
22815 function adminBase() {
22816 const cfg = window.wp?.desktop?.config;
22817 const url = cfg?.adminUrl ?? "/wp-admin/";
22818 return url.endsWith("/") ? url : `${url}/`;
22819 }
22820 function sanitizedWebUrl(file) {
22821 const url = typeof file.shape.url === "string" ? file.shape.url : "";
22822 if (!url) {
22823 return "";
22824 }
22825 try {
22826 const parsed = new URL(url, window.location.href);
22827 if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
22828 return "";
22829 }
22830 } catch {
22831 return "";
22832 }
22833 return url;
22834 }
22835 function registerBuiltInFileOpeners() {
22836 registerOpener({
22837 id: "wp-post-editor",
22838 label: "Block Editor",
22839 types: ["post"],
22840 isDefault: true,
22841 sort: 10,
22842 handler: {
22843 kind: "url",
22844 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
22845 }
22846 });
22847 registerOpener({
22848 id: "wp-media-editor",
22849 label: "Media editor",
22850 types: ["attachment"],
22851 isDefault: true,
22852 sort: 10,
22853 handler: {
22854 kind: "url",
22855 url: (file) => `${adminBase()}post.php?post=${encodeURIComponent(file.ref())}&action=edit`
22856 }
22857 });
22858 registerOpener({
22859 id: "wp-user-profile",
22860 label: "User profile",
22861 types: ["user"],
22862 isDefault: true,
22863 sort: 10,
22864 handler: {
22865 kind: "url",
22866 url: (file) => `${adminBase()}user-edit.php?user_id=${encodeURIComponent(file.ref())}`
22867 }
22868 });
22869 registerOpener({
22870 id: "wp-term-editor",
22871 label: "Term editor",
22872 types: ["term"],
22873 isDefault: true,
22874 sort: 10,
22875 handler: {
22876 kind: "url",
22877 url: (file) => {
22878 const [taxonomy, termId] = file.ref().split(":");
22879 return `${adminBase()}term.php?taxonomy=${encodeURIComponent(taxonomy ?? "")}&tag_ID=${encodeURIComponent(termId ?? "")}`;
22880 }
22881 }
22882 });
22883 registerOpener({
22884 id: "wp-comment-editor",
22885 label: "Comment editor",
22886 types: ["comment"],
22887 isDefault: true,
22888 sort: 10,
22889 handler: {
22890 kind: "url",
22891 url: (file) => `${adminBase()}comment.php?action=editcomment&c=${encodeURIComponent(file.ref())}`
22892 }
22893 });
22894 registerOpener({
22895 id: "desktop-mode-folder-window",
22896 label: "Open folder",
22897 types: ["folder"],
22898 isDefault: true,
22899 sort: 10,
22900 handler: {
22901 kind: "js",
22902 open: (file) => {
22903 const folderId = parseInt(file.ref(), 10);
22904 if (!folderId) {
22905 return;
22906 }
22907 const wm = window.wp?.desktop?.windowManager;
22908 if (!wm) {
22909 return;
22910 }
22911 const id = `desktop-mode-folder-${folderId}`;
22912 const folderRow = store.getState().folders.get(folderId);
22913 const viewerId2 = Number(window.desktopModeConfig?.currentUserId ?? 0);
22914 const isRecipient = !!folderRow && folderRow.ownerId > 0 && folderRow.ownerId !== viewerId2;
22915 const baseTitle = file.title();
22916 const titleWithCue = isRecipient ? `${baseTitle} · Shared` : baseTitle;
22917 wm.open({
22918 id,
22919 baseId: id,
22920 url: `#folder-${folderId}`,
22921 title: titleWithCue,
22922 icon: file.icon(),
22923 native: true,
22924 render: (body) => {
22925 body.replaceChildren();
22926 body.classList.add("desktop-mode-folder-window");
22927 const routes = [
22928 { folderId, title: file.title() }
22929 ];
22930 let currentDispose = null;
22931 const breadcrumbsHost = document.createElement("header");
22932 body.appendChild(breadcrumbsHost);
22933 const bodyHost = document.createElement("div");
22934 bodyHost.style.cssText = "flex:1 1 auto;min-height:0;display:flex;flex-direction:column;";
22935 body.appendChild(bodyHost);
22936 const paintBreadcrumbs = () => {
22937 const segments = routes.map(
22938 (route, idx) => {
22939 const isCurrent = idx === routes.length - 1;
22940 if (isCurrent) {
22941 return { label: route.title };
22942 }
22943 return {
22944 label: route.title,
22945 onClick: () => {
22946 routes.length = idx + 1;
22947 mountCurrent();
22948 }
22949 };
22950 }
22951 );
22952 renderBreadcrumbs(breadcrumbsHost, segments, {
22953 onBack: () => {
22954 if (routes.length <= 1) {
22955 return;
22956 }
22957 routes.pop();
22958 mountCurrent();
22959 },
22960 backDisabled: routes.length <= 1
22961 });
22962 };
22963 const mountCurrent = () => {
22964 currentDispose?.();
22965 currentDispose = null;
22966 bodyHost.replaceChildren();
22967 const split = document.createElement("div");
22968 split.className = "desktop-mode-folder-window__split";
22969 bodyHost.appendChild(split);
22970 const layerHost = document.createElement("div");
22971 layerHost.className = "desktop-mode-folder-window__layer";
22972 split.appendChild(layerHost);
22973 const previewPane = document.createElement("div");
22974 previewPane.className = "desktop-mode-folder-window__preview";
22975 previewPane.appendChild(renderPreviewEmpty());
22976 split.appendChild(previewPane);
22977 const route = routes[routes.length - 1];
22978 const layer = mountFilesLayer(
22979 layerHost,
22980 route.folderId
22981 );
22982 const offSelection = layer.onSelectionChange(
22983 (placement) => {
22984 if (!placement) {
22985 previewPane.replaceChildren(
22986 renderPreviewEmpty()
22987 );
22988 return;
22989 }
22990 renderPlacementPreview(
22991 placement,
22992 previewPane
22993 );
22994 }
22995 );
22996 const dblClickHandler = (e) => {
22997 if (!(e.target instanceof Element)) {
22998 return;
22999 }
23000 const tile2 = e.target.closest(
23001 ".desktop-mode-file-tile"
23002 );
23003 if (!tile2) {
23004 return;
23005 }
23006 if (tile2.dataset.fileType !== "folder") {
23007 return;
23008 }
23009 const subId = parseInt(
23010 tile2.dataset.fileRef ?? "",
23011 10
23012 );
23013 if (!subId) {
23014 return;
23015 }
23016 e.preventDefault();
23017 e.stopPropagation();
23018 const subTitle = tile2.querySelector(
23019 ".desktop-mode-file-tile__label"
23020 )?.textContent ?? `#${subId}`;
23021 routes.push({
23022 folderId: subId,
23023 title: subTitle
23024 });
23025 mountCurrent();
23026 };
23027 layerHost.addEventListener(
23028 "dblclick",
23029 dblClickHandler,
23030 true
23031 );
23032 const menu = attachIconCanvasMenu(layerHost, {
23033 scope: `desktop-mode-folder:${route.folderId}`,
23034 onSort: (mode) => layer.sort(mode),
23035 extraItems: [
23036 {
23037 id: "new-folder",
23038 label: "New folder",
23039 icon: "dashicons-portfolio",
23040 sort: 5,
23041 onClick: () => {
23042 openCreateFolderDialog({
23043 onSubmit: async (name) => {
23044 const folder = await createFolder({
23045 name
23046 });
23047 const peers = store.getState().placementsByFolder.get(
23048 route.folderId
23049 ) ?? [];
23050 const occupied = buildOccupiedSet(peers);
23051 const cell = snapToEmptyCell(
23052 GRID_PADDING,
23053 GRID_PADDING,
23054 occupied,
23055 layerHost
23056 );
23057 const placement = await createPlacement({
23058 type: "folder",
23059 ref: String(folder.id),
23060 parentId: route.folderId,
23061 x: cell.x,
23062 y: cell.y
23063 });
23064 store.upsertFolder(folder);
23065 store.upsertPlacement(
23066 placement
23067 );
23068 }
23069 });
23070 }
23071 }
23072 ]
23073 });
23074 const status = mountFolderStatusBar(
23075 bodyHost,
23076 route.folderId
23077 );
23078 currentDispose = () => {
23079 offSelection();
23080 menu.dispose();
23081 status.dispose();
23082 layerHost.removeEventListener(
23083 "dblclick",
23084 dblClickHandler,
23085 true
23086 );
23087 layer.dispose();
23088 };
23089 paintBreadcrumbs();
23090 };
23091 mountCurrent();
23092 },
23093 width: 720,
23094 height: 480,
23095 minWidth: 360,
23096 minHeight: 240
23097 });
23098 }
23099 }
23100 });
23101 registerOpener({
23102 id: "desktop-mode-shortcut-opener",
23103 label: "Open shortcut",
23104 types: ["shortcut"],
23105 isDefault: true,
23106 sort: 10,
23107 handler: {
23108 kind: "js",
23109 open: (file) => {
23110 const extras = file.shape;
23111 const wp = window.wp?.desktop;
23112 if (!wp) {
23113 return;
23114 }
23115 if (extras.shortcutWindow && wp.openWindow) {
23116 wp.openWindow(extras.shortcutWindow);
23117 return;
23118 }
23119 if (extras.shortcutUrl && wp.windowManager) {
23120 try {
23121 const u = new URL(extras.shortcutUrl, window.location.origin);
23122 if (u.origin !== window.location.origin) {
23123 window.open(u.toString(), "_blank", "noopener,noreferrer");
23124 return;
23125 }
23126 const adminUrl = wp.config?.adminUrl;
23127 const id = adminUrl ? deriveWindowId(u.toString(), adminUrl) : `desktop-icon-${file.ref()}`;
23128 wp.windowManager.open({
23129 id,
23130 baseId: id,
23131 url: u.toString(),
23132 title: file.title(),
23133 icon: file.icon()
23134 });
23135 } catch {
23136 }
23137 }
23138 }
23139 }
23140 });
23141 registerOpener({
23142 id: "browser-navigate",
23143 label: "Open in browser",
23144 types: ["bookmark"],
23145 isDefault: true,
23146 sort: 10,
23147 handler: {
23148 kind: "js",
23149 open: (file) => {
23150 const url = sanitizedWebUrl(file);
23151 if (!url) {
23152 return;
23153 }
23154 window.open(url, "_blank", "noopener,noreferrer");
23155 }
23156 }
23157 });
23158 registerOpener({
23159 id: "desktop-mode-link-opener",
23160 label: "Open in browser",
23161 types: ["link"],
23162 isDefault: true,
23163 sort: 10,
23164 handler: {
23165 kind: "js",
23166 open: (file) => {
23167 const url = sanitizedWebUrl(file);
23168 if (!url) {
23169 return;
23170 }
23171 window.open(url, "_blank", "noopener,noreferrer");
23172 }
23173 }
23174 });
23175 registerOpener({
23176 id: "desktop-mode-embed-opener",
23177 label: "Open as window",
23178 types: ["embed"],
23179 isDefault: true,
23180 sort: 10,
23181 handler: {
23182 kind: "js",
23183 open: (file, ctx) => {
23184 openEmbedWindow(file, ctx);
23185 }
23186 }
23187 });
23188 }
23189 const TAB_ID = "desktop-mode-file-associations";
23190 function registerFileAssociationsTab() {
23191 registerSettingsTab({
23192 id: TAB_ID,
23193 label: "File Associations",
23194 order: 50,
23195 render(body) {
23196 renderTab(body);
23197 }
23198 });
23199 }
23200 function renderTab(body) {
23201 body.replaceChildren();
23202 const types = getTypes();
23203 if (types.length === 0) {
23204 const empty = document.createElement("p");
23205 empty.className = "desktop-mode-file-associations__empty";
23206 empty.textContent = "No file types are registered.";
23207 body.appendChild(empty);
23208 return;
23209 }
23210 const intro = document.createElement("p");
23211 intro.className = "desktop-mode-file-associations__intro";
23212 intro.textContent = "Pick which app opens each kind of file when you double-click it on the desktop.";
23213 body.appendChild(intro);
23214 const associations = getUserAssociations();
23215 const list2 = document.createElement("div");
23216 list2.className = "desktop-mode-file-associations__list";
23217 list2.setAttribute("role", "list");
23218 for (const type of types) {
23219 list2.appendChild(buildRow(type.type, type.label, associations));
23220 }
23221 body.appendChild(list2);
23222 }
23223 function buildRow(typeSlug, typeLabel, associations) {
23224 const row = document.createElement("div");
23225 row.className = "desktop-mode-file-associations__row";
23226 row.setAttribute("role", "listitem");
23227 row.dataset.fileType = typeSlug;
23228 const label = document.createElement("label");
23229 label.className = "desktop-mode-file-associations__label";
23230 label.textContent = typeLabel;
23231 row.appendChild(label);
23232 const candidates = getOpenersForType(typeSlug);
23233 if (candidates.length === 0) {
23234 const empty = document.createElement("span");
23235 empty.className = "desktop-mode-file-associations__none";
23236 empty.textContent = "No app available";
23237 row.appendChild(empty);
23238 return row;
23239 }
23240 const resolved = resolveOpener(typeSlug);
23241 const currentId = associations[typeSlug] ?? resolved?.id ?? "";
23242 const select = document.createElement("wpd-select");
23243 select.setAttribute("value", currentId);
23244 select.setAttribute("aria-label", `Default app for ${typeLabel}`);
23245 select.className = "desktop-mode-file-associations__select";
23246 label.htmlFor = `assoc-${typeSlug}`;
23247 select.id = `assoc-${typeSlug}`;
23248 for (const o of candidates) {
23249 const opt = document.createElement("wpd-option");
23250 opt.setAttribute("value", o.id);
23251 opt.textContent = o.isDefault ? `${o.label} (default)` : o.label;
23252 select.appendChild(opt);
23253 }
23254 select.addEventListener("wpd-pick", (e) => {
23255 const next = e.detail?.value;
23256 if (!next) {
23257 return;
23258 }
23259 const merged = { ...getUserAssociations(), [typeSlug]: next };
23260 setUserAssociations(merged);
23261 void saveAssociations(merged).catch((err) => {
23262 console.error("[desktop-mode] saveAssociations failed:", err);
23263 });
23264 });
23265 row.appendChild(select);
23266 return row;
23267 }
23268 let _store$1 = null;
23269 function sharesStore() {
23270 if (!_store$1) {
23271 _store$1 = createSharedStore("desktop-files/shares", () => ({
23272 byFolder: /* @__PURE__ */ new Map(),
23273 pending: [],
23274 sharesVersion: 0,
23275 deniedFolders: /* @__PURE__ */ new Set()
23276 }));
23277 }
23278 return _store$1;
23279 }
23280 function setSharesForFolder(folderId, shares) {
23281 const s = sharesStore();
23282 s.state.byFolder.set(folderId, shares);
23283 s.notify();
23284 }
23285 function upsertShare(share) {
23286 if (!share || typeof share.folderId !== "number") {
23287 return;
23288 }
23289 const s = sharesStore();
23290 const existing = s.state.byFolder.get(share.folderId) ?? [];
23291 const next = existing.filter((r) => r.id !== share.id);
23292 next.push(share);
23293 s.state.byFolder.set(share.folderId, next);
23294 s.notify();
23295 }
23296 function removeShare(folderId, shareId) {
23297 const s = sharesStore();
23298 const existing = s.state.byFolder.get(folderId) ?? [];
23299 s.state.byFolder.set(
23300 folderId,
23301 existing.filter((r) => r.id !== shareId)
23302 );
23303 s.notify();
23304 }
23305 function inviteEquals(a, b) {
23306 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;
23307 }
23308 function ingestPendingInvites(invites) {
23309 const s = sharesStore();
23310 const existingById = new Map(s.state.pending.map((p) => [p.id, p]));
23311 let mutated = false;
23312 for (const inv of invites) {
23313 if (s.state.deniedFolders.has(inv.folderId)) {
23314 continue;
23315 }
23316 const existing = existingById.get(inv.id);
23317 if (existing) {
23318 if (inviteEquals(existing, inv)) {
23319 continue;
23320 }
23321 s.state.pending = s.state.pending.map((p) => p.id === inv.id ? inv : p);
23322 } else {
23323 s.state.pending.push(inv);
23324 }
23325 if (inv.invitedAtMs > s.state.sharesVersion) {
23326 s.state.sharesVersion = inv.invitedAtMs;
23327 }
23328 mutated = true;
23329 }
23330 if (mutated) {
23331 s.notify();
23332 }
23333 }
23334 function dropPending(shareId, opts = {}) {
23335 const s = sharesStore();
23336 s.state.pending = s.state.pending.filter((p) => p.id !== shareId);
23337 if (opts.denied && typeof opts.folderId === "number") {
23338 s.state.deniedFolders.add(opts.folderId);
23339 }
23340 s.notify();
23341 }
23342 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}`;
23343 const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
23344 const _WpdModal = class _WpdModal extends Component {
23345 constructor() {
23346 super(...arguments);
23347 this._prevFocus = null;
23348 this._onKey = (e) => {
23349 if (e.key === "Escape" && !this.hasAttribute("mandatory")) {
23350 e.preventDefault();
23351 this._cancel();
23352 return;
23353 }
23354 if (e.key === "Tab") {
23355 const f = this._focusables();
23356 if (f.length === 0) {
23357 return;
23358 }
23359 const first = f[0];
23360 const last = f[f.length - 1];
23361 const doc = this.ownerDocument;
23362 const fallback = doc ? doc.activeElement : null;
23363 const active2 = e.composedPath()[0] || fallback;
23364 if (e.shiftKey && active2 === first) {
23365 e.preventDefault();
23366 last.focus();
23367 } else if (!e.shiftKey && active2 === last) {
23368 e.preventDefault();
23369 first.focus();
23370 }
23371 }
23372 };
23373 this._onBackdrop = (e) => {
23374 if (this.hasAttribute("mandatory")) {
23375 return;
23376 }
23377 const path = e.composedPath();
23378 const original = path.length > 0 ? path[0] : e.target;
23379 if (original === this) {
23380 this._cancel();
23381 }
23382 };
23383 }
23384 connectedCallback() {
23385 super.connectedCallback();
23386 this.setAttribute("role", "dialog");
23387 this.setAttribute("aria-modal", "true");
23388 this.addEventListener("keydown", this._onKey);
23389 this.addEventListener("click", this._onBackdrop);
23390 }
23391 disconnectedCallback() {
23392 this.removeEventListener("keydown", this._onKey);
23393 this.removeEventListener("click", this._onBackdrop);
23394 }
23395 attributeChangedCallback(name, oldValue, newValue) {
23396 super.attributeChangedCallback?.(name, oldValue, newValue);
23397 if (name === "open") {
23398 if (newValue !== null) {
23399 const doc = this.ownerDocument;
23400 this._prevFocus = doc ? doc.activeElement : null;
23401 queueMicrotask(() => this._focusFirst());
23402 } else if (this._prevFocus) {
23403 try {
23404 this._prevFocus.focus();
23405 } catch (e) {
23406 }
23407 this._prevFocus = null;
23408 }
23409 }
23410 }
23411 showModal() {
23412 this.setAttribute("open", "");
23413 }
23414 hideModal() {
23415 this.removeAttribute("open");
23416 }
23417 _focusables() {
23418 const root = this.shadowRoot;
23419 if (!root) {
23420 return [];
23421 }
23422 const slotted = Array.from(this.querySelectorAll(FOCUSABLE));
23423 const inShadow = Array.from(root.querySelectorAll(FOCUSABLE));
23424 return [...slotted, ...inShadow].filter((el) => el.offsetParent !== null || el.tagName === "BUTTON");
23425 }
23426 _focusFirst() {
23427 const f = this._focusables();
23428 if (f.length > 0) {
23429 f[0].focus();
23430 } else {
23431 const inner = this.shadowRoot?.querySelector(".dialog");
23432 inner?.focus?.();
23433 }
23434 }
23435 _cancel() {
23436 const ev = new CustomEvent("wpd-modal-cancel", {
23437 bubbles: true,
23438 cancelable: true,
23439 composed: true
23440 });
23441 const allowed = this.dispatchEvent(ev);
23442 if (allowed) {
23443 this.hideModal();
23444 }
23445 }
23446 render() {
23447 const title = this.getAttribute("title") ?? "";
23448 const mandatory = this.hasAttribute("mandatory");
23449 return html`
23450 <div class="dialog" tabindex="-1">
23451 ${title ? html`
23452 <div class="header">
23453 <h2 class="title">${title}</h2>
23454 <div class="header-actions">
23455 <slot name="header-actions"></slot>
23456 ${mandatory ? html`` : html`<button
23457 type="button"
23458 class="close"
23459 aria-label="Close"
23460 @click=${() => this._cancel()}
23461 >×</button>`}
23462 </div>
23463 </div>
23464 ` : html``}
23465 <div class="body">
23466 <slot></slot>
23467 </div>
23468 <div class="footer">
23469 <slot name="footer"></slot>
23470 </div>
23471 </div>
23472 `;
23473 }
23474 };
23475 _WpdModal.props = ["open", "title", "size", "mandatory"];
23476 _WpdModal.styles = [modalStyles];
23477 _WpdModal.help = {
23478 title: "Modal overlay",
23479 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.",
23480 status: "experimental",
23481 since: "0.8.5",
23482 props: [
23483 { name: "open", type: "boolean attribute", description: "Mounts the dialog visible." },
23484 { name: "title", type: "string", description: "Heading shown at the top of the dialog." },
23485 { name: "size", type: "'sm' | 'md' | 'lg'", default: "md", description: "Width preset." },
23486 {
23487 name: "mandatory",
23488 type: "boolean attribute",
23489 description: "Disables ESC, click-outside and the close button."
23490 }
23491 ],
23492 slots: [
23493 { name: "(default)", description: "Body content." },
23494 { name: "footer", description: "Footer button row, right-aligned." },
23495 { name: "header-actions", description: "Extra actions next to the close button." }
23496 ],
23497 events: [
23498 {
23499 name: "wpd-modal-cancel",
23500 description: "Fires when the user dismisses the modal (ESC, click-outside, close button). Cancelable; calling `preventDefault()` keeps the modal open."
23501 }
23502 ]
23503 };
23504 let WpdModal = _WpdModal;
23505 defineComponent("wpd-modal", WpdModal);
23506 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}`;
23507 const _WpdUserSearch = class _WpdUserSearch extends Component {
23508 constructor() {
23509 super(...arguments);
23510 this._timer = null;
23511 this._abort = null;
23512 this._results = [];
23513 this._query = "";
23514 this._open = false;
23515 this._phase = "idle";
23516 this._error = "";
23517 this._dropdownStyle = "";
23518 this._onScrollOrResize = () => void 0;
23519 this._onInput = (e) => {
23520 const value = e.target.value;
23521 this._query = value;
23522 this._scheduleSearch(value);
23523 };
23524 this._onFocus = () => {
23525 if (this._results.length === 0 && this._phase === "idle") {
23526 this._scheduleSearch(this._query);
23527 return;
23528 }
23529 this._open = true;
23530 this._positionDropdown();
23531 this.requestUpdate();
23532 };
23533 this._onBlur = () => {
23534 setTimeout(() => {
23535 this._open = false;
23536 this.requestUpdate();
23537 }, 150);
23538 };
23539 this._pick = (user) => {
23540 this.emit("wpd-user-pick", { user });
23541 this._results = [];
23542 this._open = false;
23543 this._phase = "idle";
23544 this._query = "";
23545 const input = this.shadowRoot?.querySelector(".input");
23546 if (input) {
23547 input.value = "";
23548 }
23549 this.requestUpdate();
23550 };
23551 }
23552 connectedCallback() {
23553 super.connectedCallback();
23554 this._onScrollOrResize = () => {
23555 if (this._open) {
23556 this._positionDropdown();
23557 this.requestUpdate();
23558 }
23559 };
23560 window.addEventListener("resize", this._onScrollOrResize);
23561 window.addEventListener("scroll", this._onScrollOrResize, true);
23562 }
23563 disconnectedCallback() {
23564 if (this._timer) {
23565 clearTimeout(this._timer);
23566 }
23567 if (this._abort) {
23568 this._abort.abort();
23569 }
23570 window.removeEventListener("resize", this._onScrollOrResize);
23571 window.removeEventListener("scroll", this._onScrollOrResize, true);
23572 }
23573 _endpoint() {
23574 const attr = this.getAttribute("endpoint");
23575 if (attr) {
23576 return attr;
23577 }
23578 return window.desktopModeConfig?.filesUsersSearchUrl || "";
23579 }
23580 _scheduleSearch(q) {
23581 if (this._timer) {
23582 clearTimeout(this._timer);
23583 }
23584 this._phase = "loading";
23585 this._open = true;
23586 this._positionDropdown();
23587 this.requestUpdate();
23588 this._timer = setTimeout(() => this._runSearch(q), 200);
23589 }
23590 async _runSearch(q) {
23591 const url = this._endpoint();
23592 if (!url) {
23593 this._phase = "error";
23594 this._error = "Search endpoint is not configured.";
23595 this._results = [];
23596 this._open = true;
23597 this.requestUpdate();
23598 return;
23599 }
23600 if (this._abort) {
23601 this._abort.abort();
23602 }
23603 const ctrl = new AbortController();
23604 this._abort = ctrl;
23605 const exclude = this.getAttribute("exclude") || "";
23606 const full = url + "?q=" + encodeURIComponent(q) + "&exclude=" + encodeURIComponent(exclude);
23607 try {
23608 const init2 = {
23609 signal: ctrl.signal,
23610 credentials: "same-origin"
23611 };
23612 const res = await trackedFetch$1(full, init2, {
23613 source: "desktop-mode/files-user-search",
23614 silent: true
23615 });
23616 if (!res.ok) {
23617 throw new Error(`HTTP ${res.status}`);
23618 }
23619 const json = await res.json();
23620 this._results = json && Array.isArray(json.users) ? json.users : [];
23621 this._phase = "ready";
23622 this._error = "";
23623 this._open = true;
23624 } catch (e) {
23625 if (e.name === "AbortError") {
23626 return;
23627 }
23628 this._results = [];
23629 this._phase = "error";
23630 this._error = e.message || "Search failed.";
23631 this._open = true;
23632 }
23633 this._positionDropdown();
23634 this.requestUpdate();
23635 }
23636 _positionDropdown() {
23637 const input = this.shadowRoot?.querySelector(".input");
23638 if (!input) {
23639 return;
23640 }
23641 const rect = input.getBoundingClientRect();
23642 const top = rect.bottom + 4;
23643 const left = rect.left;
23644 const width = rect.width;
23645 const viewportH = window.innerHeight;
23646 const spaceBelow = viewportH - rect.bottom;
23647 const spaceAbove = rect.top;
23648 const maxHeight = Math.max(120, Math.min(280, Math.max(spaceBelow, spaceAbove) - 16));
23649 if (spaceBelow < 200 && spaceAbove > spaceBelow) {
23650 this._dropdownStyle = [
23651 "position:fixed",
23652 `left:${left}px`,
23653 `top:${rect.top - 4 - maxHeight}px`,
23654 `width:${width}px`,
23655 `max-height:${maxHeight}px`
23656 ].join(";");
23657 } else {
23658 this._dropdownStyle = [
23659 "position:fixed",
23660 `left:${left}px`,
23661 `top:${top}px`,
23662 `width:${width}px`,
23663 `max-height:${maxHeight}px`
23664 ].join(";");
23665 }
23666 }
23667 _dropdownContent() {
23668 if (this._phase === "loading") {
23669 return html`<div class="empty">Searching…</div>`;
23670 }
23671 if (this._phase === "error") {
23672 return html`<div class="empty error">${this._error}</div>`;
23673 }
23674 if (this._results.length === 0) {
23675 const message = this._query ? "No matches." : "No users available.";
23676 return html`<div class="empty">${message}</div>`;
23677 }
23678 return this._results.map(
23679 (u) => html`
23680 <button
23681 type="button"
23682 class="item"
23683 role="option"
23684 @mousedown=${(e) => e.preventDefault()}
23685 @click=${() => this._pick(u)}
23686 >
23687 <img class="avatar" src=${u.avatarUrl} alt="" />
23688 <div>
23689 <div class="name">${u.name}</div>
23690 <div class="slug">${u.slug}</div>
23691 </div>
23692 </button>
23693 `
23694 );
23695 }
23696 render() {
23697 const placeholder = this.getAttribute("placeholder") || "Search users…";
23698 return html`
23699 <input
23700 class="input"
23701 type="search"
23702 placeholder=${placeholder}
23703 autocomplete="off"
23704 @input=${this._onInput}
23705 @focus=${this._onFocus}
23706 @blur=${this._onBlur}
23707 .value=${this._query}
23708 />
23709 ${this._open ? html`
23710 <div class="dropdown" role="listbox" style=${this._dropdownStyle}>
23711 ${this._dropdownContent()}
23712 </div>
23713 ` : html``}
23714 `;
23715 }
23716 };
23717 _WpdUserSearch.props = ["placeholder", "exclude", "endpoint"];
23718 _WpdUserSearch.styles = [userSearchStyles];
23719 _WpdUserSearch.help = {
23720 title: "User autocomplete",
23721 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.",
23722 status: "experimental",
23723 since: "0.8.5",
23724 props: [
23725 { name: "placeholder", type: "string", description: "Input placeholder text." },
23726 {
23727 name: "exclude",
23728 type: "csv user ids",
23729 description: "Already-picked user ids to suppress in results."
23730 },
23731 {
23732 name: "endpoint",
23733 type: "URL",
23734 description: "Override the search URL (defaults to desktopModeConfig.filesUsersSearchUrl)."
23735 }
23736 ],
23737 events: [
23738 { name: "wpd-user-pick", description: "Emitted on pick. Detail: `{ user: SearchUser }`." }
23739 ]
23740 };
23741 let WpdUserSearch = _WpdUserSearch;
23742 defineComponent("wpd-user-search", WpdUserSearch);
23743 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}`;
23744 const _WpdRolePicker = class _WpdRolePicker extends Component {
23745 constructor() {
23746 super(...arguments);
23747 this._onToggle = (slug) => {
23748 const selected = !this._selectedSet().has(slug);
23749 this.emit("wpd-role-toggle", { slug, selected });
23750 };
23751 }
23752 _selectedSet() {
23753 const raw = this.getAttribute("selected") || "";
23754 return new Set(
23755 raw.split(",").map((s) => s.trim()).filter((s) => s !== "")
23756 );
23757 }
23758 _roles() {
23759 const attr = this.getAttribute("roles");
23760 if (attr) {
23761 try {
23762 const parsed = JSON.parse(attr);
23763 if (Array.isArray(parsed)) {
23764 return parsed;
23765 }
23766 } catch (e) {
23767 }
23768 }
23769 return window.desktopModeConfig?.shareEligibleRoles || [];
23770 }
23771 render() {
23772 const roles = this._roles();
23773 if (roles.length === 0) {
23774 return html`<span class="empty">No eligible roles.</span>`;
23775 }
23776 const set = this._selectedSet();
23777 return html`
23778 ${roles.map((r) => {
23779 const isSelected = set.has(r.slug);
23780 return html`
23781 <button
23782 type="button"
23783 class="chip"
23784 aria-pressed=${isSelected ? "true" : "false"}
23785 @click=${() => this._onToggle(r.slug)}
23786 >${r.name}</button>
23787 `;
23788 })}
23789 `;
23790 }
23791 };
23792 _WpdRolePicker.props = ["selected", "roles"];
23793 _WpdRolePicker.styles = [rolePickerStyles];
23794 _WpdRolePicker.help = {
23795 title: "Role picker",
23796 summary: "Chip multi-select for WordPress roles. Reads eligible roles from desktopModeConfig.shareEligibleRoles; emits wpd-role-toggle { slug, selected } on every change.",
23797 status: "experimental",
23798 since: "0.8.5",
23799 props: [
23800 {
23801 name: "selected",
23802 type: "csv role slugs",
23803 description: "Comma-separated role slugs that are currently selected."
23804 },
23805 {
23806 name: "roles",
23807 type: "JSON",
23808 description: "Override the source of eligible roles (defaults to the global config)."
23809 }
23810 ],
23811 events: [
23812 {
23813 name: "wpd-role-toggle",
23814 description: "Emitted on every click. Detail: `{ slug, selected }`."
23815 }
23816 ]
23817 };
23818 let WpdRolePicker = _WpdRolePicker;
23819 defineComponent("wpd-role-picker", WpdRolePicker);
23820 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}`;
23821 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}`;
23822 const _WpdSegment = class _WpdSegment extends Component {
23823 render() {
23824 this.setAttribute("role", "radio");
23825 return html`
23826 <button type="button" @click=${() => this._onPick()}>
23827 <slot></slot>
23828 </button>
23829 `;
23830 }
23831 _onPick() {
23832 this.emit("wpd-segment-pick", {
23833 value: this.value
23834 });
23835 }
23836 };
23837 _WpdSegment.props = ["value"];
23838 _WpdSegment.styles = [segmentStyles];
23839 _WpdSegment.help = {
23840 title: "Segment",
23841 summary: "Single pill inside a <wpd-segmented> group. Value identifies it for selection; aria-checked is mirrored by the parent.",
23842 status: "stable",
23843 since: "0.9.0",
23844 props: [
23845 {
23846 name: "value",
23847 type: "string",
23848 description: "Identifier this segment contributes to the parent group selection."
23849 }
23850 ],
23851 slots: [
23852 { name: "(default)", description: "Visible segment label." }
23853 ],
23854 events: [
23855 {
23856 name: "wpd-segment-pick",
23857 description: "Internal event bubbled to the parent <wpd-segmented>. Consumers should listen for wpd-pick on the group instead.",
23858 detail: "{ value: string }"
23859 }
23860 ]
23861 };
23862 let WpdSegment = _WpdSegment;
23863 defineComponent("wpd-segment", WpdSegment);
23864 const _WpdSegmented = class _WpdSegmented extends Component {
23865 connectedCallback() {
23866 super.connectedCallback();
23867 this.addEventListener("wpd-segment-pick", (e) => {
23868 const detail = e.detail;
23869 e.stopPropagation();
23870 this.value = detail.value;
23871 this.emit("wpd-pick", { value: detail.value });
23872 });
23873 }
23874 /**
23875 * Declarative item-list setter. Replaces the existing
23876 * `<wpd-segment>` children with a fresh set built from a
23877 * `{ value, label }` array; preserves the current selection
23878 * when the value still matches an entry, otherwise falls back
23879 * to the first item.
23880 *
23881 * Collapses the pre-0.11 imperative dance (clear children,
23882 * `createElement`, set `textContent`, `appendChild`, then
23883 * `setAttribute('value', …)` on the group — order matters) to
23884 * a single assignment:
23885 *
23886 * ```js
23887 * segmented.items = [
23888 * { value: 'm', label: 'm' },
23889 * { value: 'km', label: 'km' },
23890 * ];
23891 * ```
23892 *
23893 * @since 0.5.0
23894 */
23895 set items(list2) {
23896 const existing = this.querySelectorAll(":scope > wpd-segment");
23897 for (const el of Array.from(existing)) {
23898 el.remove();
23899 }
23900 for (const item of list2) {
23901 const seg = document.createElement("wpd-segment");
23902 seg.setAttribute("value", item.value);
23903 seg.textContent = item.label;
23904 this.appendChild(seg);
23905 }
23906 const current = this.value;
23907 const stillValid = current !== null && list2.some((i) => i.value === current);
23908 if (!stillValid && list2.length > 0) {
23909 this.value = list2[0].value;
23910 } else {
23911 this.requestUpdate();
23912 }
23913 }
23914 render() {
23915 const label = this.label || "";
23916 if (label) {
23917 this.setAttribute("aria-label", label);
23918 }
23919 this.setAttribute("role", "radiogroup");
23920 const current = this.value;
23921 queueMicrotask(() => {
23922 const segs = this.querySelectorAll("wpd-segment");
23923 for (const seg of Array.from(segs)) {
23924 const v = seg.getAttribute("value");
23925 seg.setAttribute(
23926 "aria-checked",
23927 v === current ? "true" : "false"
23928 );
23929 }
23930 });
23931 return html`<slot></slot>`;
23932 }
23933 };
23934 _WpdSegmented.props = ["value", "label"];
23935 _WpdSegmented.styles = [segmentedStyles];
23936 _WpdSegmented.help = {
23937 title: "Segmented",
23938 summary: "iOS-style segmented radio group. Pill-shaped bar of equal-width <wpd-segment> children where exactly one is active.",
23939 status: "stable",
23940 since: "0.9.0",
23941 props: [
23942 {
23943 name: "value",
23944 type: "string",
23945 description: "Currently selected segment value. Mirrored onto child aria-checked."
23946 },
23947 {
23948 name: "label",
23949 type: "string",
23950 description: "aria-label for the radiogroup."
23951 }
23952 ],
23953 slots: [
23954 { name: "(default)", description: '<wpd-segment value="…"> children.' }
23955 ],
23956 events: [
23957 {
23958 name: "wpd-pick",
23959 description: "Fires when the selected segment changes.",
23960 detail: "{ value: string }"
23961 }
23962 ],
23963 cssProps: [
23964 { name: "--desktop-mode-window-bg", description: "Pill background." },
23965 { name: "--desktop-mode-text", description: "Active label colour." },
23966 { name: "--desktop-mode-muted", description: "Inactive label colour." }
23967 ],
23968 example: html`
23969 <wpd-segmented value="md" label="Dock size">
23970 <wpd-segment value="sm">Small</wpd-segment>
23971 <wpd-segment value="md">Medium</wpd-segment>
23972 <wpd-segment value="lg">Large</wpd-segment>
23973 </wpd-segmented>
23974 `
23975 };
23976 let WpdSegmented = _WpdSegmented;
23977 defineComponent("wpd-segmented", WpdSegmented);
23978 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}`;
23979 const _WpdButton = class _WpdButton extends Component {
23980 render() {
23981 const disabled = this.disabled !== null;
23982 const type = this.type || "button";
23983 return html`
23984 <button part="button" type=${type} ?disabled=${disabled}>
23985 <slot></slot>
23986 </button>
23987 `;
23988 }
23989 };
23990 _WpdButton.props = ["variant", "disabled", "type", "busy", "fill-cell"];
23991 _WpdButton.styles = [styles$3];
23992 _WpdButton.help = {
23993 title: "Button",
23994 summary: "Thin wrapper around <button> with consistent variant styling and a slot for the label.",
23995 status: "stable",
23996 since: "0.9.0",
23997 props: [
23998 {
23999 name: "variant",
24000 type: "'primary' | 'secondary' | 'ghost' | 'danger' | 'link'",
24001 default: "ghost",
24002 description: "Visual weight of the button. Use primary for the single attention-grabbing action per surface."
24003 },
24004 {
24005 name: "disabled",
24006 type: "boolean attribute",
24007 description: "Disable pointer + keyboard interaction and dim the chrome."
24008 },
24009 {
24010 name: "type",
24011 type: "'button' | 'submit' | 'reset'",
24012 default: "button",
24013 description: "Forwarded to the underlying native <button>."
24014 },
24015 {
24016 name: "busy",
24017 type: "boolean attribute",
24018 description: "Marks the button as in-progress (e.g., awaiting a fetch)."
24019 },
24020 {
24021 name: "fill-cell",
24022 type: "boolean attribute",
24023 description: "Grow to fill the parent flex/grid cell. Useful for tiled keypads."
24024 }
24025 ],
24026 slots: [{ name: "(default)", description: "Button label." }],
24027 parts: [{ name: "button", description: "Underlying <button> element." }],
24028 cssProps: [
24029 { name: "--wpd-button-bg", description: "Background color." },
24030 { name: "--wpd-button-fg", description: "Text color." },
24031 { name: "--wpd-button-border", description: "Border shorthand." },
24032 { name: "--wpd-button-border-radius", default: "6px" },
24033 { name: "--wpd-button-padding", default: "6px 12px" },
24034 {
24035 name: "--wpd-button-min-height",
24036 description: "Minimum height when fill-cell is set."
24037 }
24038 ],
24039 example: html`
24040 <wpd-cluster gap="8">
24041 <wpd-button variant="primary">Primary</wpd-button>
24042 <wpd-button variant="secondary">Secondary</wpd-button>
24043 <wpd-button variant="ghost">Ghost</wpd-button>
24044 <wpd-button variant="danger">Danger</wpd-button>
24045 <wpd-button variant="link">Link</wpd-button>
24046 </wpd-cluster>
24047 `
24048 };
24049 let WpdButton = _WpdButton;
24050 defineComponent("wpd-button", WpdButton);
24051 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}`;
24052 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}}`;
24053 const _WpdToastContainer = class _WpdToastContainer extends Component {
24054 connectedCallback() {
24055 super.connectedCallback();
24056 this.setAttribute("aria-live", "polite");
24057 }
24058 render() {
24059 return html`<slot></slot>`;
24060 }
24061 };
24062 _WpdToastContainer.styles = [containerStyles];
24063 _WpdToastContainer.help = {
24064 title: "Toast container",
24065 summary: "Singleton stack beneath <body> that hosts transient <wpd-toast> notifications in the top-right. Created lazily by showToast(); authors rarely place one themselves.",
24066 status: "stable",
24067 since: "0.9.0",
24068 slots: [
24069 { name: "(default)", description: "<wpd-toast> children, stacked vertically." }
24070 ],
24071 cssProps: [
24072 { name: "--desktop-mode-z-fullscreen", description: "z-index base — toasts sit above fullscreen windows." }
24073 ],
24074 example: html`
24075 <wpd-toast-container>
24076 <wpd-toast state="in">Settings saved.</wpd-toast>
24077 <wpd-toast state="in" action="Undo">Theme changed.</wpd-toast>
24078 </wpd-toast-container>
24079 `
24080 };
24081 let WpdToastContainer = _WpdToastContainer;
24082 defineComponent("wpd-toast-container", WpdToastContainer);
24083 const _WpdToast = class _WpdToast extends Component {
24084 connectedCallback() {
24085 super.connectedCallback();
24086 if (!this.hasAttribute("role")) {
24087 this.setAttribute("role", "status");
24088 }
24089 }
24090 render() {
24091 const action = this.action || "";
24092 return html`
24093 <span class="wpd-toast__label"><slot></slot></span>
24094 <button
24095 type="button"
24096 ?hidden=${!action}
24097 @click=${(e) => this._onAction(e)}
24098 >
24099 ${action}
24100 </button>
24101 `;
24102 }
24103 _onAction(e) {
24104 e.preventDefault();
24105 e.stopPropagation();
24106 this.emit("wpd-toast-action", {});
24107 }
24108 };
24109 _WpdToast.props = ["action", "state"];
24110 _WpdToast.styles = [toastStyles];
24111 _WpdToast.help = {
24112 title: "Toast",
24113 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.',
24114 status: "stable",
24115 since: "0.9.0",
24116 props: [
24117 {
24118 name: "action",
24119 type: "string",
24120 description: "Optional action button label. When set, a button renders on the right and emits wpd-toast-action on click."
24121 },
24122 {
24123 name: "state",
24124 type: "'in' | 'out'",
24125 description: 'Drives the CSS fade transition. Set to "in" when rendered, flip to "out" before removal.'
24126 }
24127 ],
24128 slots: [
24129 { name: "(default)", description: "Message text." }
24130 ],
24131 events: [
24132 {
24133 name: "wpd-toast-action",
24134 description: "Fires when the action button is clicked.",
24135 detail: "{}"
24136 }
24137 ],
24138 example: html`
24139 <wpd-toast state="in" action="Undo">Post moved to trash.</wpd-toast>
24140 `
24141 };
24142 let WpdToast = _WpdToast;
24143 defineComponent("wpd-toast", WpdToast);
24144 function buildCapSegmented(initial, onChange) {
24145 const segmented = document.createElement("wpd-segmented");
24146 segmented.setAttribute("value", initial);
24147 segmented.setAttribute("label", "Capability");
24148 segmented.style.setProperty("--wpd-segmented-bg", "rgba(255,255,255,0.06)");
24149 segmented.style.setProperty(
24150 "--desktop-mode-window-bg",
24151 "var(--wp-admin-theme-color, #2271b1)"
24152 );
24153 segmented.style.setProperty("--desktop-mode-text", "#fff");
24154 segmented.style.setProperty("--desktop-mode-muted", "rgba(255,255,255,0.65)");
24155 const segRead = document.createElement("wpd-segment");
24156 segRead.setAttribute("value", "read");
24157 segRead.textContent = "Read";
24158 segmented.appendChild(segRead);
24159 const segWrite = document.createElement("wpd-segment");
24160 segWrite.setAttribute("value", "write");
24161 segWrite.textContent = "Read + Write";
24162 segmented.appendChild(segWrite);
24163 segmented.addEventListener("wpd-pick", (e) => {
24164 const detail = e.detail;
24165 onChange(detail.value);
24166 });
24167 return segmented;
24168 }
24169 function buildIconButton(label, onClick, opts = {}) {
24170 const btn = document.createElement("wpd-button");
24171 btn.setAttribute("variant", "ghost");
24172 btn.setAttribute("aria-label", opts.danger ? "Remove" : "Dismiss");
24173 btn.textContent = label;
24174 const fg = opts.danger ? "#ff8080" : "rgba(255,255,255,0.75)";
24175 const border = opts.danger ? "1px solid rgba(255,128,128,0.45)" : "1px solid rgba(255,255,255,0.18)";
24176 btn.style.setProperty("--wpd-button-fg", fg);
24177 btn.style.setProperty("--wpd-button-border", border);
24178 btn.style.setProperty("--wpd-button-padding", "6px 12px");
24179 btn.style.setProperty("--wpd-button-border-radius", "7px");
24180 btn.style.setProperty("--wpd-button-min-height", "34px");
24181 btn.style.minWidth = "34px";
24182 btn.style.fontSize = "18px";
24183 btn.style.lineHeight = "1";
24184 btn.addEventListener("click", onClick);
24185 return btn;
24186 }
24187 async function openShareSettingsModal(opts) {
24188 const modal = document.createElement("wpd-modal");
24189 modal.setAttribute("open", "");
24190 modal.setAttribute("size", "lg");
24191 modal.setAttribute("title", `Share "${opts.folderName}"`);
24192 document.body.appendChild(modal);
24193 let shares = [];
24194 let pendingPicks = [];
24195 const renderBody = () => {
24196 modal.innerHTML = "";
24197 const owner = document.createElement("div");
24198 owner.style.cssText = "opacity:0.7;margin-bottom:14px;font-size:12px;";
24199 owner.textContent = opts.ownerName ? `Owner: ${opts.ownerName} — cannot be changed` : "Owner cannot be changed";
24200 modal.appendChild(owner);
24201 const addPeople = document.createElement("div");
24202 addPeople.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
24203 const addPeopleLabel = document.createElement("div");
24204 addPeopleLabel.textContent = "Add people";
24205 addPeopleLabel.style.cssText = "font-weight:600;";
24206 addPeople.appendChild(addPeopleLabel);
24207 const userSearch = document.createElement("wpd-user-search");
24208 const excludedUserIds = shares.filter((s) => s.principalType === "user").map((s) => s.principalRef).concat(pendingPicks.filter((p) => p.kind === "user").map((p) => p.ref));
24209 userSearch.setAttribute("exclude", excludedUserIds.join(","));
24210 userSearch.setAttribute("placeholder", "Search users…");
24211 userSearch.addEventListener("wpd-user-pick", (e) => {
24212 const detail = e.detail;
24213 pendingPicks.push({
24214 kind: "user",
24215 ref: String(detail.user.id),
24216 label: detail.user.name,
24217 cap: "read"
24218 });
24219 renderBody();
24220 });
24221 addPeople.appendChild(userSearch);
24222 modal.appendChild(addPeople);
24223 const addRoles = document.createElement("div");
24224 addRoles.style.cssText = "display:flex;flex-direction:column;gap:6px;margin-bottom:14px;";
24225 const addRolesLabel = document.createElement("div");
24226 addRolesLabel.textContent = "Add roles";
24227 addRolesLabel.style.cssText = "font-weight:600;";
24228 addRoles.appendChild(addRolesLabel);
24229 const rolePicker = document.createElement("wpd-role-picker");
24230 const grantedRoles = shares.filter((s) => s.principalType === "role").map((s) => s.principalRef);
24231 const pickedRoles = pendingPicks.filter((p) => p.kind === "role").map((p) => p.ref);
24232 rolePicker.setAttribute("selected", [...grantedRoles, ...pickedRoles].join(","));
24233 rolePicker.addEventListener("wpd-role-toggle", (e) => {
24234 const detail = e.detail;
24235 const existing = shares.find(
24236 (s) => s.principalType === "role" && s.principalRef === detail.slug
24237 );
24238 if (existing) {
24239 if (!detail.selected) {
24240 void revoke(existing);
24241 }
24242 return;
24243 }
24244 if (detail.selected) {
24245 const eligible = (window.desktopModeConfig?.shareEligibleRoles ?? []).find(
24246 (r) => r.slug === detail.slug
24247 );
24248 pendingPicks.push({
24249 kind: "role",
24250 ref: detail.slug,
24251 label: eligible ? eligible.name : detail.slug,
24252 cap: "read"
24253 });
24254 } else {
24255 pendingPicks = pendingPicks.filter(
24256 (p) => !(p.kind === "role" && p.ref === detail.slug)
24257 );
24258 }
24259 renderBody();
24260 });
24261 addRoles.appendChild(rolePicker);
24262 modal.appendChild(addRoles);
24263 if (pendingPicks.length > 0) {
24264 const pendingBlock = document.createElement("div");
24265 pendingBlock.style.cssText = "border:1px dashed rgba(255,255,255,0.18);border-radius:8px;padding:10px;margin-bottom:14px;";
24266 const pendingTitle = document.createElement("div");
24267 pendingTitle.textContent = "New invites (not sent yet)";
24268 pendingTitle.style.cssText = "font-weight:600;margin-bottom:6px;font-size:12px;";
24269 pendingBlock.appendChild(pendingTitle);
24270 for (const pick of pendingPicks) {
24271 const row = document.createElement("div");
24272 row.style.cssText = "display:flex;align-items:center;gap:8px;padding:4px 0;font-size:13px;";
24273 const tag = document.createElement("span");
24274 tag.textContent = pick.kind === "role" ? `Role: ${pick.label}` : pick.label;
24275 tag.style.flex = "1";
24276 row.appendChild(tag);
24277 const capSeg = buildCapSegmented(pick.cap, (next) => {
24278 pick.cap = next;
24279 });
24280 row.appendChild(capSeg);
24281 const removeBtn = buildIconButton("×", () => {
24282 pendingPicks = pendingPicks.filter(
24283 (p) => !(p.kind === pick.kind && p.ref === pick.ref)
24284 );
24285 renderBody();
24286 });
24287 row.appendChild(removeBtn);
24288 pendingBlock.appendChild(row);
24289 }
24290 const sendBtn = document.createElement("wpd-button");
24291 sendBtn.setAttribute("variant", "primary");
24292 sendBtn.textContent = `Send ${pendingPicks.length} invite${pendingPicks.length === 1 ? "" : "s"}`;
24293 sendBtn.style.marginTop = "8px";
24294 sendBtn.addEventListener("click", async () => {
24295 if (pendingPicks.length === 0) {
24296 return;
24297 }
24298 sendBtn.setAttribute("busy", "");
24299 sendBtn.setAttribute("disabled", "");
24300 const snapshot = pendingPicks.slice();
24301 let succeeded = 0;
24302 let firstError = null;
24303 for (const pick of snapshot) {
24304 try {
24305 await inviteShare(opts.folderId, {
24306 principalType: pick.kind,
24307 principalRef: pick.ref,
24308 capability: pick.cap
24309 });
24310 succeeded++;
24311 } catch (err) {
24312 firstError = err;
24313 break;
24314 }
24315 }
24316 if (succeeded > 0) {
24317 pendingPicks = pendingPicks.slice(succeeded);
24318 }
24319 try {
24320 await refresh();
24321 } catch (_e) {
24322 }
24323 if (firstError) {
24324 showToast({
24325 message: `Could not send invites: ${firstError.message}`
24326 });
24327 } else {
24328 showToast({
24329 message: 1 === succeeded ? "Invite sent." : `${succeeded} invites sent.`
24330 });
24331 }
24332 sendBtn.removeAttribute("busy");
24333 sendBtn.removeAttribute("disabled");
24334 renderBody();
24335 });
24336 pendingBlock.appendChild(sendBtn);
24337 modal.appendChild(pendingBlock);
24338 }
24339 const listTitle = document.createElement("div");
24340 listTitle.textContent = "Who has access";
24341 listTitle.style.cssText = "font-weight:600;margin:8px 0 6px;";
24342 modal.appendChild(listTitle);
24343 if (shares.length === 0) {
24344 const empty = document.createElement("div");
24345 empty.textContent = "Only you can see this folder.";
24346 empty.style.cssText = "opacity:0.6;font-size:12px;";
24347 modal.appendChild(empty);
24348 } else {
24349 for (const s of shares) {
24350 const row = document.createElement("div");
24351 row.style.cssText = "display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.04);";
24352 const label = document.createElement("div");
24353 label.style.flex = "1";
24354 label.textContent = s.principalType === "role" ? `Role: ${s.displayName}` : s.displayName;
24355 if (s.state === "pending") {
24356 const tag = document.createElement("span");
24357 tag.textContent = " · pending";
24358 tag.style.cssText = "opacity:0.6;font-size:12px;";
24359 label.appendChild(tag);
24360 } else if (s.state === "denied") {
24361 const tag = document.createElement("span");
24362 tag.textContent = " · denied";
24363 tag.style.cssText = "color:#d63638;font-size:12px;";
24364 label.appendChild(tag);
24365 }
24366 row.appendChild(label);
24367 const cap = s.capability === "write" ? "write" : "read";
24368 const capSeg = buildCapSegmented(cap, (next) => {
24369 void changeCap(s, next);
24370 });
24371 row.appendChild(capSeg);
24372 const removeBtn = buildIconButton(
24373 "×",
24374 () => {
24375 void revoke(s);
24376 },
24377 { danger: true }
24378 );
24379 row.appendChild(removeBtn);
24380 modal.appendChild(row);
24381 }
24382 }
24383 const footer = document.createElement("div");
24384 footer.setAttribute("slot", "footer");
24385 footer.style.display = "flex";
24386 footer.style.justifyContent = "flex-end";
24387 footer.style.gap = "10px";
24388 footer.style.flexWrap = "wrap";
24389 const doneBtn = document.createElement("wpd-button");
24390 doneBtn.setAttribute("variant", "secondary");
24391 doneBtn.textContent = "Done";
24392 doneBtn.addEventListener("click", () => modal.remove());
24393 footer.appendChild(doneBtn);
24394 modal.appendChild(footer);
24395 };
24396 const refresh = async () => {
24397 try {
24398 const res = await listShares(opts.folderId);
24399 shares = res.shares;
24400 setSharesForFolder(opts.folderId, shares);
24401 } catch (err) {
24402 showToast({
24403 message: `Could not load shares: ${err.message}`
24404 });
24405 }
24406 renderBody();
24407 };
24408 const revoke = async (s) => {
24409 try {
24410 await revokeShare(opts.folderId, s.id);
24411 removeShare(opts.folderId, s.id);
24412 await refresh();
24413 showToast({ message: "Access revoked." });
24414 } catch (err) {
24415 showToast({
24416 message: `Could not revoke: ${err.message}`
24417 });
24418 }
24419 };
24420 const changeCap = async (s, cap) => {
24421 try {
24422 const next = await updateShareCapability(opts.folderId, s.id, cap);
24423 upsertShare(next);
24424 await refresh();
24425 } catch (err) {
24426 showToast({
24427 message: `Could not update capability: ${err.message}`
24428 });
24429 }
24430 };
24431 modal.addEventListener("wpd-modal-cancel", () => modal.remove());
24432 renderBody();
24433 await refresh();
24434 }
24435 function openPendingInviteModal(invite) {
24436 return new Promise((resolve2) => {
24437 const modal = document.createElement("wpd-modal");
24438 modal.setAttribute("open", "");
24439 modal.setAttribute("title", invite.folderName ? `${invite.ownerName ?? "Someone"} shared "${invite.folderName}" with you` : "Folder shared with you");
24440 const body = document.createElement("div");
24441 const capLabel = invite.capability === "write" ? "Read + Write" : "Read";
24442 body.innerHTML = `
24443 <p style="margin: 0 0 12px;">Accept the invite to add this folder to your desktop.</p>
24444 <p style="margin: 0; opacity: 0.75;">Access level: <strong>${capLabel}</strong></p>
24445 `;
24446 modal.appendChild(body);
24447 const footer = document.createElement("div");
24448 footer.setAttribute("slot", "footer");
24449 footer.style.display = "flex";
24450 footer.style.justifyContent = "flex-end";
24451 footer.style.gap = "10px";
24452 footer.style.flexWrap = "wrap";
24453 const laterBtn = document.createElement("wpd-button");
24454 laterBtn.setAttribute("variant", "secondary");
24455 laterBtn.textContent = "Decide later";
24456 laterBtn.addEventListener("click", () => {
24457 modal.remove();
24458 resolve2("dismissed");
24459 });
24460 const denyBtn = document.createElement("wpd-button");
24461 denyBtn.setAttribute("variant", "danger");
24462 denyBtn.textContent = "Deny";
24463 denyBtn.addEventListener("click", async () => {
24464 denyBtn.setAttribute("busy", "");
24465 denyBtn.setAttribute("disabled", "");
24466 try {
24467 await denyShare(invite.folderId, invite.id);
24468 sharesStore().state.deniedFolders.add(invite.folderId);
24469 sharesStore().notify();
24470 modal.remove();
24471 resolve2("denied");
24472 } catch (err) {
24473 showToast({
24474 message: `Could not deny: ${err.message}`
24475 });
24476 denyBtn.removeAttribute("busy");
24477 denyBtn.removeAttribute("disabled");
24478 }
24479 });
24480 const acceptBtn = document.createElement("wpd-button");
24481 acceptBtn.setAttribute("variant", "primary");
24482 acceptBtn.textContent = "Accept";
24483 acceptBtn.addEventListener("click", async () => {
24484 acceptBtn.setAttribute("busy", "");
24485 acceptBtn.setAttribute("disabled", "");
24486 try {
24487 await acceptShare(invite.folderId, invite.id);
24488 try {
24489 const res = await listPlacements(0);
24490 setFolderPlacements(0, res.placements);
24491 } catch (_e) {
24492 }
24493 modal.remove();
24494 resolve2("accepted");
24495 } catch (err) {
24496 showToast({
24497 message: `Could not accept: ${err.message}`
24498 });
24499 acceptBtn.removeAttribute("busy");
24500 acceptBtn.removeAttribute("disabled");
24501 }
24502 });
24503 footer.appendChild(laterBtn);
24504 footer.appendChild(denyBtn);
24505 footer.appendChild(acceptBtn);
24506 modal.appendChild(footer);
24507 modal.addEventListener("wpd-modal-cancel", () => {
24508 modal.remove();
24509 resolve2("dismissed");
24510 });
24511 document.body.appendChild(modal);
24512 });
24513 }
24514 function viewerId() {
24515 return Number(window.desktopModeConfig?.currentUserId ?? 0);
24516 }
24517 function sharingEnabled$1() {
24518 const settings = window.wp?.desktop?.getOsSettings?.();
24519 if (!settings) {
24520 return true;
24521 }
24522 return settings.foldersSharingEnabled !== false;
24523 }
24524 function folderOwnerId(folderId) {
24525 const folder = getFilesState().folders.get(folderId);
24526 return folder ? Number(folder.ownerId) : 0;
24527 }
24528 function folderIdFromBaseId(baseId) {
24529 if (typeof baseId !== "string") {
24530 return null;
24531 }
24532 const m = /^desktop-mode-folder-(\d+)$/.exec(baseId);
24533 return m ? Number(m[1]) : null;
24534 }
24535 function placementFolderId(placement) {
24536 if (placement.file.type !== "folder") {
24537 return null;
24538 }
24539 const ref = Number(placement.file.ref);
24540 if (!Number.isFinite(ref) || ref <= 0) {
24541 return null;
24542 }
24543 return ref;
24544 }
24545 function placementOwnerId(placement) {
24546 return Number(placement.file.ownerId ?? 0);
24547 }
24548 function installShareMenuItems() {
24549 addFilter(
24550 "desktop-mode.files.tile-menu",
24551 "desktop-mode/folder-share",
24552 (items, placement) => {
24553 if (!sharingEnabled$1()) {
24554 return items;
24555 }
24556 const folderId = placementFolderId(placement);
24557 if (folderId === null) {
24558 return items;
24559 }
24560 const ownerId = folderOwnerId(folderId) || placementOwnerId(placement);
24561 const viewer = viewerId();
24562 if (ownerId === viewer) {
24563 const shared = !!placement.file.shareSummary?.shared;
24564 const label = shared ? "Manage sharing…" : "Share folder…";
24565 items.push({
24566 id: "desktop-mode/folder-share",
24567 label,
24568 icon: "dashicons-share",
24569 sort: 30,
24570 onClick: () => {
24571 void openShareSettingsModal({
24572 folderId,
24573 folderName: placement.file.title || `Folder ${folderId}`
24574 });
24575 }
24576 });
24577 } else if (ownerId > 0) {
24578 items.push({
24579 id: "desktop-mode/folder-leave",
24580 label: "Leave shared folder",
24581 icon: "dashicons-exit",
24582 sort: 80,
24583 danger: true,
24584 onClick: async () => {
24585 const ok = await wpdConfirm$1({
24586 title: "Leave this folder?",
24587 message: "The folder will be removed from your desktop. The original and its contents are not deleted; the owner keeps them.",
24588 confirmLabel: "Leave",
24589 danger: true
24590 });
24591 if (!ok) {
24592 return;
24593 }
24594 try {
24595 await leaveShare(folderId);
24596 removePlacement(placement.id);
24597 try {
24598 const res = await listPlacements(0);
24599 setFolderPlacements(0, res.placements);
24600 } catch (_e) {
24601 }
24602 const winId = `desktop-mode-folder-${folderId}`;
24603 const mgr = window.desktopMode?.windowManager;
24604 mgr?.close?.(winId);
24605 showToast({ message: "You left the shared folder." });
24606 } catch (err) {
24607 showToast({
24608 message: `Could not leave: ${err.message}`
24609 });
24610 }
24611 }
24612 });
24613 }
24614 return items;
24615 }
24616 );
24617 registerTitleBarButton({
24618 id: "desktop-mode/folder-share",
24619 label: "Share folder",
24620 icon: "dashicons-share",
24621 placement: "right",
24622 order: 50,
24623 match: (w) => {
24624 if (!sharingEnabled$1()) {
24625 return false;
24626 }
24627 const base = w.config.baseId ?? w.id;
24628 const folderId = folderIdFromBaseId(base);
24629 if (folderId === null) {
24630 return false;
24631 }
24632 return folderOwnerId(folderId) === viewerId();
24633 },
24634 onClick: (w) => {
24635 const base = w.config.baseId ?? w.id;
24636 const folderId = folderIdFromBaseId(base);
24637 if (folderId === null) {
24638 return;
24639 }
24640 void openShareSettingsModal({
24641 folderId,
24642 folderName: w.config.title || `Folder ${folderId}`
24643 });
24644 }
24645 });
24646 addAction(
24647 "desktop-mode.files.tile-rendered",
24648 "desktop-mode/folder-share",
24649 (payload) => {
24650 const { tile: tile2, placement } = payload;
24651 if (placement.file.type !== "folder") {
24652 return;
24653 }
24654 const summary = placement.file.shareSummary;
24655 if (!summary?.shared) {
24656 return;
24657 }
24658 if (tile2.querySelector(".desktop-mode-file-tile__share-badge")) {
24659 return;
24660 }
24661 const badge = document.createElement("span");
24662 badge.className = "desktop-mode-file-tile__share-badge dashicons dashicons-share";
24663 badge.setAttribute("aria-label", "Shared folder");
24664 badge.title = "Shared folder";
24665 badge.style.cssText = [
24666 "position:absolute",
24667 "top:6px",
24668 "inset-inline-end:6px",
24669 "background:rgba(0,0,0,0.55)",
24670 "color:#fff",
24671 "border-radius:50%",
24672 "width:18px",
24673 "height:18px",
24674 "font-size:12px",
24675 "line-height:18px",
24676 "text-align:center",
24677 "pointer-events:none"
24678 ].join(";");
24679 tile2.appendChild(badge);
24680 }
24681 );
24682 }
24683 const prompted = /* @__PURE__ */ new Set();
24684 function sharingEnabled() {
24685 const settings = window.wp?.desktop?.getOsSettings?.();
24686 if (!settings) {
24687 return true;
24688 }
24689 return settings.foldersSharingEnabled !== false;
24690 }
24691 function installShareInviteBanner() {
24692 const store2 = sharesStore();
24693 const handle = (state2) => {
24694 if (!sharingEnabled()) {
24695 return;
24696 }
24697 for (const invite of state2.pending) {
24698 if (prompted.has(invite.id)) {
24699 continue;
24700 }
24701 prompted.add(invite.id);
24702 void openPendingInviteModal({
24703 id: invite.id,
24704 folderId: invite.folderId,
24705 folderName: invite.folderName,
24706 ownerName: invite.ownerName,
24707 capability: invite.capability
24708 }).then((decision) => {
24709 if (decision === "accepted") {
24710 dropPending(invite.id);
24711 } else if (decision === "denied") {
24712 dropPending(invite.id, { denied: true, folderId: invite.folderId });
24713 }
24714 });
24715 }
24716 };
24717 store2.subscribe(handle);
24718 handle(store2.state);
24719 }
24720 registerBuiltInFileTypes();
24721 registerBuiltInFileOpeners();
24722 installEmbedPersistence();
24723 registerFileAssociationsTab();
24724 installShareMenuItems();
24725 const seededPending = window.desktopModeConfig?.serverPendingShares;
24726 if (Array.isArray(seededPending) && seededPending.length > 0) {
24727 ingestPendingInvites(seededPending);
24728 }
24729 installShareInviteBanner();
24730 const filesApi = {
24731 DesktopFile,
24732 registerType,
24733 unregisterType,
24734 getType,
24735 getTypes,
24736 resolve,
24737 subscribe,
24738 registerOpener,
24739 unregisterOpener,
24740 getOpener,
24741 getOpeners,
24742 getOpenersForType,
24743 resolveOpener,
24744 subscribeOpeners,
24745 getUserAssociations,
24746 open: openFile,
24747 rest: filesRest,
24748 store: {
24749 get: getFilesStore,
24750 getState: getFilesState,
24751 subscribe: subscribeFilesStore,
24752 setFolderPlacements,
24753 upsertPlacement,
24754 removePlacement,
24755 setFolders,
24756 upsertFolder,
24757 removeFolder
24758 }
24759 };
24760 const SYNTH_META_KEY = "__synthFromDockItem";
24761 function hashToNegativeId(s) {
24762 let h = 0;
24763 for (let i = 0; i < s.length; i++) {
24764 h = (h * 31 + s.charCodeAt(i)) % 2147483647;
24765 }
24766 return -(h + 1);
24767 }
24768 function buildSyntheticPlacement(item, persistedPositions) {
24769 const saved = persistedPositions[item.id];
24770 return {
24771 id: hashToNegativeId(item.id),
24772 parentId: 0,
24773 x: saved ? saved.x : 0,
24774 y: saved ? saved.y : 0,
24775 sortOrder: 9999,
24776 updatedAtMs: Date.now(),
24777 meta: { [SYNTH_META_KEY]: item.id },
24778 file: {
24779 type: "shortcut",
24780 ref: `dock-promoted:${item.id}`,
24781 title: item.title,
24782 icon: item.icon,
24783 previewUrl: "",
24784 exists: true,
24785 // The shortcut opener (built-in-openers.ts) reads these
24786 // off the file shape — `shortcutUrl` is what a dock-item
24787 // promotion naturally has.
24788 shortcutUrl: item.url
24789 }
24790 };
24791 }
24792 function readDockItems() {
24793 const api = window.wp?.desktop;
24794 if (api?.getMenuItems) {
24795 const items = api.getMenuItems();
24796 return items.map((i) => ({
24797 id: i.id,
24798 title: i.title,
24799 icon: i.icon,
24800 url: i.url,
24801 badge: i.badge ?? 0,
24802 submenu: i.submenu ?? []
24803 }));
24804 }
24805 const cfg = window.desktopModeConfig;
24806 return cfg?.dockItems ?? [];
24807 }
24808 function readServerIcons() {
24809 const cfg = window.desktopModeConfig;
24810 return cfg?.desktopIcons ?? [];
24811 }
24812 let reentrant = false;
24813 const removedServerPlacementsByRef = /* @__PURE__ */ new Map();
24814 function prunePromotedPositions(ids) {
24815 const api = window.wp?.desktop;
24816 if (!api?.getOsSettings || !api?.updateOsSettings) {
24817 return;
24818 }
24819 const current = api.getOsSettings().dockPromotedPositions ?? {};
24820 const next = { ...current };
24821 let changed = false;
24822 for (const id of ids) {
24823 if (id in next) {
24824 delete next[id];
24825 changed = true;
24826 }
24827 }
24828 if (changed) {
24829 api.updateOsSettings({ dockPromotedPositions: next });
24830 }
24831 }
24832 function syncShortcutsWithVisibility(visibility, positions = {}) {
24833 if (reentrant) {
24834 return;
24835 }
24836 reentrant = true;
24837 try {
24838 const dockItems = readDockItems();
24839 const serverIcons = readServerIcons();
24840 const state2 = filesApi.store.getState();
24841 const root = state2.placementsByFolder.get(0) ?? [];
24842 const currentSynth = /* @__PURE__ */ new Map();
24843 for (const p of root) {
24844 const sourceId = (p.meta ?? null) && typeof p.meta === "object" ? p.meta[SYNTH_META_KEY] : null;
24845 if (typeof sourceId === "string") {
24846 currentSynth.set(sourceId, p);
24847 }
24848 }
24849 const realByRef = /* @__PURE__ */ new Map();
24850 const registeredIconIds = new Set(
24851 serverIcons.map((i) => i.id)
24852 );
24853 for (const p of root) {
24854 const ref = p?.file?.ref;
24855 if (typeof ref === "string" && registeredIconIds.has(ref)) {
24856 realByRef.set(ref, p);
24857 }
24858 }
24859 const desiredSynth = /* @__PURE__ */ new Set();
24860 for (const item of dockItems) {
24861 const placement = visibility[item.id];
24862 if (placement === "desktop" || placement === "both") {
24863 desiredSynth.add(item.id);
24864 if (!currentSynth.has(item.id)) {
24865 filesApi.store.upsertPlacement(
24866 buildSyntheticPlacement(item, positions)
24867 );
24868 }
24869 }
24870 }
24871 const positionsToPrune = [];
24872 for (const [sourceId, p] of currentSynth) {
24873 if (!desiredSynth.has(sourceId)) {
24874 filesApi.store.removePlacement(p.id);
24875 if (positions[sourceId]) {
24876 positionsToPrune.push(sourceId);
24877 }
24878 }
24879 }
24880 if (positionsToPrune.length > 0) {
24881 prunePromotedPositions(positionsToPrune);
24882 }
24883 for (const icon of serverIcons) {
24884 const placement = visibility[icon.id];
24885 const inStore = realByRef.get(icon.id);
24886 if (placement === "dock" || placement === "hidden") {
24887 if (inStore) {
24888 removedServerPlacementsByRef.set(icon.id, inStore);
24889 filesApi.store.removePlacement(inStore.id);
24890 }
24891 continue;
24892 }
24893 if (!inStore) {
24894 const cached = removedServerPlacementsByRef.get(icon.id);
24895 if (cached) {
24896 filesApi.store.upsertPlacement(cached);
24897 removedServerPlacementsByRef.delete(icon.id);
24898 }
24899 }
24900 }
24901 } finally {
24902 reentrant = false;
24903 }
24904 }
24905 function installShortcutsSync(getVisibility, getPositions = () => ({})) {
24906 queueMicrotask(
24907 () => syncShortcutsWithVisibility(getVisibility(), getPositions())
24908 );
24909 const off = filesApi.store.subscribe(() => {
24910 syncShortcutsWithVisibility(getVisibility(), getPositions());
24911 });
24912 return off;
24913 }
24914 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%}`;
24915 const DEFAULT_EVENT = "desktop-mode-os-settings-save-lifecycle";
24916 const DEFAULT_AUTO_CLEAR_SAVED_MS = 2200;
24917 const DEFAULT_AUTO_CLEAR_FAILED_MS = 6e3;
24918 const _WpdSaveStatus = class _WpdSaveStatus extends Component {
24919 constructor() {
24920 super(...arguments);
24921 this._autoTimer = null;
24922 this._docListener = null;
24923 }
24924 connectedCallback() {
24925 super.connectedCallback();
24926 if (this.auto !== null) {
24927 this._installAutoListener();
24928 }
24929 }
24930 disconnectedCallback() {
24931 this._removeAutoListener();
24932 if (this._autoTimer !== null) {
24933 window.clearTimeout(this._autoTimer);
24934 this._autoTimer = null;
24935 }
24936 }
24937 attributeChangedCallback(name, oldValue, newValue) {
24938 super.attributeChangedCallback(name, oldValue, newValue);
24939 if (name === "auto" || name === "event") {
24940 this._removeAutoListener();
24941 if (this.auto !== null) {
24942 this._installAutoListener();
24943 }
24944 }
24945 if (name === "phase") {
24946 this._scheduleAutoClear();
24947 const detail = {
24948 phase: this.phase ?? "idle",
24949 error: this.error ?? void 0
24950 };
24951 this.emit("wpd-save-status-change", detail);
24952 }
24953 }
24954 render() {
24955 const phase = this.phase ?? "idle";
24956 const mode = this.mode ?? "dot";
24957 const error = this.error ?? "";
24958 const title = error || this._labelForPhase(phase);
24959 if (title) {
24960 this.setAttribute("title", title);
24961 } else {
24962 this.removeAttribute("title");
24963 }
24964 this.setAttribute("aria-live", phase === "failed" ? "assertive" : "polite");
24965 this.setAttribute("role", phase === "failed" ? "alert" : "status");
24966 return html`
24967 <span class="wpd-save-status">
24968 <span class="wpd-save-status__indicator" aria-hidden="true">
24969 <span class="wpd-save-status__glyph">${this._renderGlyph(phase)}</span>
24970 </span>
24971 ${mode === "pill" ? html`<span class="wpd-save-status__label"
24972 >${this._labelForPhase(phase)}</span
24973 >` : html``}
24974 </span>
24975 `;
24976 }
24977 _renderGlyph(phase) {
24978 if (phase === "saved") {
24979 return _iconCheck();
24980 }
24981 if (phase === "failed") {
24982 return _iconBang();
24983 }
24984 return "";
24985 }
24986 _labelForPhase(phase) {
24987 switch (phase) {
24988 case "pending":
24989 case "saving":
24990 return this["saving-label"] ?? "Saving…";
24991 case "saved":
24992 return this["saved-label"] ?? "Saved";
24993 case "failed": {
24994 const err = this.error ?? "";
24995 return err || "Couldn’t save";
24996 }
24997 default:
24998 return this["idle-label"] ?? "";
24999 }
25000 }
25001 _installAutoListener() {
25002 const eventName = this.event || DEFAULT_EVENT;
25003 this._docListener = (e) => {
25004 const detail = e.detail;
25005 if (!detail || typeof detail.phase !== "string") {
25006 return;
25007 }
25008 this.phase = detail.phase;
25009 if (detail.error) {
25010 this.error = detail.error;
25011 } else if (detail.phase !== "failed" && this.error) {
25012 this.removeAttribute("error");
25013 }
25014 };
25015 document.addEventListener(eventName, this._docListener);
25016 }
25017 _removeAutoListener() {
25018 if (!this._docListener) {
25019 return;
25020 }
25021 const eventName = this.event || DEFAULT_EVENT;
25022 document.removeEventListener(eventName, this._docListener);
25023 this._docListener = null;
25024 }
25025 _scheduleAutoClear() {
25026 if (this._autoTimer !== null) {
25027 window.clearTimeout(this._autoTimer);
25028 this._autoTimer = null;
25029 }
25030 const phase = this.phase ?? "idle";
25031 const ms = this._autoClearMsFor(phase);
25032 if (ms <= 0) {
25033 return;
25034 }
25035 this._autoTimer = window.setTimeout(() => {
25036 this._autoTimer = null;
25037 this.phase = "idle";
25038 }, ms);
25039 }
25040 _autoClearMsFor(phase) {
25041 if (phase === "saved") {
25042 const raw = this["auto-clear-saved-ms"];
25043 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_SAVED_MS;
25044 }
25045 if (phase === "failed") {
25046 const raw = this["auto-clear-failed-ms"];
25047 return parseInt(raw || "", 10) || DEFAULT_AUTO_CLEAR_FAILED_MS;
25048 }
25049 return 0;
25050 }
25051 };
25052 _WpdSaveStatus.props = [
25053 "phase",
25054 "mode",
25055 "animation",
25056 "auto",
25057 "event",
25058 "error",
25059 "saving-label",
25060 "saved-label",
25061 "idle-label",
25062 "auto-clear-saved-ms",
25063 "auto-clear-failed-ms"
25064 ];
25065 _WpdSaveStatus.styles = [styles$2];
25066 _WpdSaveStatus.help = {
25067 title: "Save status",
25068 summary: 'Tiny status indicator for "is this change saved yet?" affordances. Three layouts (dot / icon / pill), five phases, optional auto-listen to a save-lifecycle CustomEvent so every input in the panel inherits feedback for free.',
25069 status: "experimental",
25070 since: "0.8.0",
25071 props: [
25072 {
25073 name: "phase",
25074 type: "'idle' | 'pending' | 'saving' | 'saved' | 'failed'",
25075 default: "idle",
25076 description: "Current lifecycle phase. Set manually for one-off integrations, or rely on `auto` to populate it from a CustomEvent."
25077 },
25078 {
25079 name: "mode",
25080 type: "'dot' | 'icon' | 'pill'",
25081 default: "dot",
25082 description: "Layout. `dot` is the smallest (10×10 colored dot); `icon` adds a glyph inside on saved/failed; `pill` adds an inline label."
25083 },
25084 {
25085 name: "animation",
25086 type: "'pulse' | 'modem'",
25087 default: "pulse",
25088 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."
25089 },
25090 {
25091 name: "auto",
25092 type: "boolean attribute",
25093 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="…"`.'
25094 },
25095 {
25096 name: "event",
25097 type: "string",
25098 default: "desktop-mode-os-settings-save-lifecycle",
25099 description: "CustomEvent name to listen on when `auto` is set."
25100 },
25101 {
25102 name: "error",
25103 type: "string",
25104 description: "Error message shown in `pill` mode and exposed as the host title attribute (so dot/icon modes still surface the message via tooltip)."
25105 },
25106 {
25107 name: "saving-label",
25108 type: "string",
25109 default: "Saving…",
25110 description: "Pill-mode label shown during `pending` / `saving`."
25111 },
25112 {
25113 name: "saved-label",
25114 type: "string",
25115 default: "Saved",
25116 description: "Pill-mode label shown during `saved`."
25117 },
25118 {
25119 name: "idle-label",
25120 type: "string",
25121 description: 'Optional pill-mode label shown during `idle` (e.g. "All changes saved"). When unset, the pill collapses to invisible while idle.'
25122 },
25123 {
25124 name: "auto-clear-saved-ms",
25125 type: "integer",
25126 default: "2200",
25127 description: "How long the `saved` phase stays visible before auto-fading back to `idle`."
25128 },
25129 {
25130 name: "auto-clear-failed-ms",
25131 type: "integer",
25132 default: "6000",
25133 description: "How long the `failed` phase stays visible before auto-fading back to `idle`."
25134 }
25135 ],
25136 events: [
25137 {
25138 name: "wpd-save-status-change",
25139 description: "Fires when the phase changes (manually or via auto-listen).",
25140 detail: "{ phase, error }"
25141 }
25142 ],
25143 cssProps: [
25144 {
25145 name: "--wpd-save-status-bg",
25146 description: "Indicator background color (saving/pending phase)."
25147 },
25148 {
25149 name: "--wpd-save-status-saved-bg",
25150 description: "Indicator background on saved."
25151 },
25152 {
25153 name: "--wpd-save-status-failed-bg",
25154 description: "Indicator background on failed."
25155 },
25156 {
25157 name: "--wpd-save-status-pill-bg",
25158 description: "Pill background (mode=pill)."
25159 },
25160 {
25161 name: "--wpd-save-status-pill-fg",
25162 description: "Pill foreground (mode=pill)."
25163 }
25164 ],
25165 example: html`
25166 <wpd-cluster gap="12">
25167 <wpd-save-status phase="pending"></wpd-save-status>
25168 <wpd-save-status phase="saving"></wpd-save-status>
25169 <wpd-save-status phase="saved"></wpd-save-status>
25170 <wpd-save-status phase="failed"></wpd-save-status>
25171 <wpd-save-status mode="pill" phase="saving"></wpd-save-status>
25172 <wpd-save-status mode="pill" phase="saved"></wpd-save-status>
25173 <wpd-save-status mode="pill" phase="failed" error="Network error."></wpd-save-status>
25174 </wpd-cluster>
25175 `
25176 };
25177 let WpdSaveStatus = _WpdSaveStatus;
25178 defineComponent("wpd-save-status", WpdSaveStatus);
25179 function _iconCheck() {
25180 return html`
25181 <svg
25182 viewBox="0 0 12 12"
25183 aria-hidden="true"
25184 focusable="false"
25185 fill="none"
25186 stroke="currentColor"
25187 stroke-width="2"
25188 stroke-linecap="round"
25189 stroke-linejoin="round"
25190 >
25191 <path d="M2.5 6 L5 8.5 L9.5 4" />
25192 </svg>
25193 `;
25194 }
25195 function _iconBang() {
25196 return html`
25197 <svg
25198 viewBox="0 0 12 12"
25199 aria-hidden="true"
25200 focusable="false"
25201 fill="currentColor"
25202 >
25203 <path
25204 d="M5 2 H7 V7 H5 z M5 8.5 H7 V10.5 H5 z"
25205 />
25206 </svg>
25207 `;
25208 }
25209 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}`;
25210 const _WpdTextarea = class _WpdTextarea extends Component {
25211 constructor() {
25212 super(...arguments);
25213 this._textareaEl = null;
25214 }
25215 connectedCallback() {
25216 super.connectedCallback();
25217 ensureAutoId(this);
25218 }
25219 render() {
25220 const label = this._attr("label") || "";
25221 const value = this._attr("value") ?? "";
25222 const placeholder = this._attr("placeholder") || "";
25223 const disabled = this._boolAttr("disabled");
25224 const readonly = this._boolAttr("readonly");
25225 const ariaLabel = this._attr("aria-label") || label;
25226 const name = this._attr("name") || "";
25227 const rows = Number(this._attr("rows")) || 3;
25228 const maxLength = this._attr("maxlength");
25229 const minLength = this._attr("minlength");
25230 const invalid = this._boolAttr("invalid");
25231 const hostId = this.id || "wpd-unnamed";
25232 const fieldId = `${hostId}__field`;
25233 return html`
25234 ${label ? html`<label class="wpd-textarea__label" for=${fieldId}>${label}</label>` : html``}
25235 <textarea
25236 id=${fieldId}
25237 part="textarea"
25238 .value=${value}
25239 placeholder=${placeholder}
25240 ?disabled=${disabled}
25241 ?readonly=${readonly}
25242 rows=${rows}
25243 maxlength=${maxLength ?? ""}
25244 minlength=${minLength ?? ""}
25245 name=${name}
25246 aria-invalid=${invalid ? "true" : "false"}
25247 aria-label=${ariaLabel || ""}
25248 @input=${(e) => this._onInput(e)}
25249 @change=${(e) => this._onChange(e)}
25250 @keydown=${(e) => this._onKeyDown(e)}
25251 ></textarea>
25252 `;
25253 }
25254 _attr(name) {
25255 return this.getAttribute(name);
25256 }
25257 _boolAttr(name) {
25258 return this.getAttribute(name) !== null;
25259 }
25260 _onInput(e) {
25261 const ta = e.target;
25262 this._textareaEl = ta;
25263 this.setAttribute("value", ta.value);
25264 this.emit("wpd-input-change", { value: ta.value });
25265 if (this._boolAttr("auto-grow")) {
25266 this._autosize(ta);
25267 }
25268 }
25269 _onChange(e) {
25270 const ta = e.target;
25271 this.emit("wpd-input-commit", { value: ta.value });
25272 }
25273 _onKeyDown(e) {
25274 if (!this._boolAttr("submit-on-enter")) {
25275 return;
25276 }
25277 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey) {
25278 e.preventDefault();
25279 const ta = e.target;
25280 this.emit("wpd-submit", { value: ta.value });
25281 }
25282 }
25283 /**
25284 * Grow the textarea height to fit content, capped at `max-rows`.
25285 * Resets to scroll-height each input then clamps; cheap because
25286 * the browser caches layout.
25287 */
25288 _autosize(ta) {
25289 const maxRows = Number(this._attr("max-rows")) || 8;
25290 const cs = window.getComputedStyle(ta);
25291 const fontSize = parseFloat(cs.fontSize) || 13;
25292 const lineHeightRaw = cs.lineHeight;
25293 const lineHeight = lineHeightRaw === "normal" ? fontSize * 1.45 : parseFloat(lineHeightRaw) || fontSize * 1.45;
25294 const paddingTop = parseFloat(cs.paddingTop) || 0;
25295 const paddingBottom = parseFloat(cs.paddingBottom) || 0;
25296 const max = lineHeight * maxRows + paddingTop + paddingBottom;
25297 ta.style.height = "auto";
25298 const next = Math.min(ta.scrollHeight, max);
25299 ta.style.height = `${next}px`;
25300 }
25301 /** Public helper for callers that programmatically set `.value` and want autosize to re-run. */
25302 refreshAutosize() {
25303 if (this._textareaEl && this._boolAttr("auto-grow")) {
25304 this._autosize(this._textareaEl);
25305 }
25306 }
25307 /** Imperatively focus the underlying textarea. */
25308 focusInput() {
25309 const root = this.shadowRoot ?? this;
25310 const ta = root.querySelector("textarea");
25311 ta?.focus();
25312 }
25313 /** Imperatively clear the value. */
25314 clear() {
25315 this.setAttribute("value", "");
25316 const root = this.shadowRoot ?? this;
25317 const ta = root.querySelector("textarea");
25318 if (ta) {
25319 ta.value = "";
25320 if (this._boolAttr("auto-grow")) {
25321 this._autosize(ta);
25322 }
25323 }
25324 }
25325 };
25326 _WpdTextarea.props = [
25327 "label",
25328 "value",
25329 "placeholder",
25330 "disabled",
25331 "readonly",
25332 "ariaLabel",
25333 "name",
25334 "rows",
25335 "maxlength",
25336 "minlength",
25337 "invalid",
25338 "autoGrow",
25339 "maxRows",
25340 "submitOnEnter"
25341 ];
25342 _WpdTextarea.styles = [textareaStyles];
25343 _WpdTextarea.help = {
25344 title: "Textarea",
25345 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).",
25346 status: "stable",
25347 since: "0.6.0",
25348 props: [
25349 { name: "label", type: "string", description: "Visible label above the textarea." },
25350 { name: "value", type: "string", description: "Current value; reflected two-way." },
25351 { name: "placeholder", type: "string", description: "Native placeholder." },
25352 { name: "disabled", type: "boolean attribute" },
25353 { name: "readonly", type: "boolean attribute" },
25354 { name: "aria-label", type: "string", description: "Accessible label when no visible label is rendered." },
25355 { name: "name", type: "string", description: "Forwarded to native textarea for form submission." },
25356 { name: "rows", type: "integer (string)", default: "3", description: "Initial visible row count." },
25357 { name: "maxlength", type: "integer (string)" },
25358 { name: "minlength", type: "integer (string)" },
25359 { name: "invalid", type: "boolean attribute", description: "Sets aria-invalid + error styling." },
25360 { name: "auto-grow", type: "boolean attribute", description: "Grows up to max-rows as the user types." },
25361 { name: "max-rows", type: "integer (string)", default: "8" },
25362 {
25363 name: "submit-on-enter",
25364 type: "boolean attribute",
25365 description: "Enter fires wpd-submit; Shift+Enter inserts a newline."
25366 }
25367 ],
25368 events: [
25369 { name: "wpd-input-change", description: "Fires on every keystroke.", detail: "{ value: string }" },
25370 { name: "wpd-input-commit", description: "Fires on blur / native change.", detail: "{ value: string }" },
25371 {
25372 name: "wpd-submit",
25373 description: "Fires on Enter (without Shift) when submit-on-enter is set.",
25374 detail: "{ value: string }"
25375 }
25376 ],
25377 example: html`
25378 <wpd-textarea label="Message" rows="3" auto-grow max-rows="8" submit-on-enter></wpd-textarea>
25379 `
25380 };
25381 let WpdTextarea = _WpdTextarea;
25382 defineComponent("wpd-textarea", WpdTextarea);
25383 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}`;
25384 const ICONS = {
25385 minimize: '<path d="M3 6h6" stroke="currentColor" stroke-width="1.25" stroke-linecap="round"/>',
25386 maximize: '<rect x="3" y="3" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.25" fill="none"/>',
25387 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"/>',
25388 "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"/>',
25389 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"/>',
25390 reload: (
25391 // Filled icon scaled from a 512×512 source into the 12×12 viewBox
25392 // shared with the other title-bar glyphs. The wrapping `<g>` does
25393 // the math; the inner path is dropped in unmodified so its
25394 // authoring tool can be re-edited and copy-pasted again.
25395 // `scale(0.021)` ≈ 90% of full fit, with `translate(0.6)` to
25396 // keep the result centered inside the 12×12 viewBox so the
25397 // glyph reads slightly smaller than min/max/close — closer to
25398 // the visual weight of the other title-bar buttons.
25399 '<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>'
25400 ),
25401 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"/>',
25402 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"/>'
25403 };
25404 const _WpdWindowButton = class _WpdWindowButton extends Component {
25405 constructor() {
25406 super(...arguments);
25407 this._activateWired = false;
25408 }
25409 render() {
25410 const iconKey = this.icon || "";
25411 const svgInner = ICONS[iconKey] || "";
25412 return html`
25413 <button type="button">
25414 <svg
25415 width="14"
25416 height="14"
25417 viewBox="0 0 12 12"
25418 aria-hidden="true"
25419 focusable="false"
25420 ></svg>
25421 <slot></slot>
25422 </button>
25423 <span data-svg-buffer style="display:none">${svgInner}</span>
25424 `;
25425 }
25426 /**
25427 * After each render, copy the raw SVG markup into the actual
25428 * `<svg>` element. The templater only writes text into slots,
25429 * so we stash the intended markup in a hidden buffer and
25430 * `innerHTML = ` the svg once here — a one-shot post-render
25431 * hook that keeps the declarative template honest.
25432 *
25433 * Also wires up the `wpd-button-activate` CustomEvent that
25434 * fires exactly once per gesture — the canonical contract
25435 * for plugin-registered title-bar buttons. Plugin authors who
25436 * use `addEventListener( 'click', cb )` directly still get
25437 * what they expect (the title bar's drag-handler now excludes
25438 * chrome buttons by class so static clicks land normally),
25439 * but `wpd-button-activate` is the documented surface that
25440 * documents the once-per-gesture contract explicitly. See
25441 * the class-level docblock for rationale.
25442 */
25443 connectedCallback() {
25444 super.connectedCallback();
25445 queueMicrotask(() => this._paintSvg());
25446 queueMicrotask(() => this._wireActivateEvent());
25447 }
25448 attributeChangedCallback(name, oldValue, newValue) {
25449 super.attributeChangedCallback(name, oldValue, newValue);
25450 queueMicrotask(() => this._paintSvg());
25451 }
25452 _paintSvg() {
25453 const root = this.shadowRoot;
25454 if (!root) {
25455 return;
25456 }
25457 const svg = root.querySelector("svg");
25458 const buffer = root.querySelector("[data-svg-buffer]");
25459 if (svg && buffer) {
25460 const markup = buffer.textContent || "";
25461 if (svg.innerHTML !== markup) {
25462 svg.innerHTML = markup;
25463 }
25464 }
25465 }
25466 _wireActivateEvent() {
25467 if (this._activateWired) {
25468 return;
25469 }
25470 const root = this.shadowRoot;
25471 if (!root) {
25472 return;
25473 }
25474 const button = root.querySelector("button");
25475 if (!button) {
25476 return;
25477 }
25478 this._activateWired = true;
25479 button.addEventListener("click", () => {
25480 this.dispatchEvent(
25481 new CustomEvent("wpd-button-activate", {
25482 bubbles: true,
25483 composed: true,
25484 cancelable: true
25485 })
25486 );
25487 });
25488 }
25489 };
25490 _WpdWindowButton.props = ["icon", "active", "danger"];
25491 _WpdWindowButton.styles = [styles$1];
25492 _WpdWindowButton.help = {
25493 title: "Window button",
25494 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.",
25495 status: "stable",
25496 since: "0.9.0",
25497 props: [
25498 {
25499 name: "icon",
25500 type: "'minimize' | 'maximize' | 'fullscreen' | 'fullscreen-exit' | 'detach' | 'reload' | 'close' | 'menu'",
25501 description: "Which built-in inline SVG to paint. Omit to supply your own via the slot."
25502 },
25503 {
25504 name: "active",
25505 type: "boolean attribute",
25506 description: "Applies the pressed-down look (used e.g. while a menu it triggers is open)."
25507 },
25508 {
25509 name: "danger",
25510 type: "boolean attribute",
25511 description: "Swaps the hover wash to red — used by the close button."
25512 }
25513 ],
25514 slots: [
25515 { name: "(default)", description: "Optional custom icon markup (inline SVG) when `icon` is omitted." }
25516 ],
25517 cssProps: [
25518 { name: "--wpd-btn-color", description: "Resting foreground." },
25519 { name: "--wpd-btn-color-hover", description: "Hover foreground." },
25520 { name: "--wpd-btn-bg-hover", description: "Hover background wash." },
25521 { name: "--wpd-btn-bg-active", description: "Pressed background." },
25522 { name: "--wpd-btn-danger-hover", description: "Hover background for danger variant." },
25523 { name: "--wpd-btn-outline", description: "Focus outline colour." }
25524 ],
25525 example: html`
25526 <wpd-cluster gap="2">
25527 <wpd-window-button icon="minimize"></wpd-window-button>
25528 <wpd-window-button icon="maximize"></wpd-window-button>
25529 <wpd-window-button icon="menu"></wpd-window-button>
25530 <wpd-window-button icon="close" danger></wpd-window-button>
25531 </wpd-cluster>
25532 `
25533 };
25534 let WpdWindowButton = _WpdWindowButton;
25535 defineComponent("wpd-window-button", WpdWindowButton);
25536 const DEFAULT_STICKY_TITLE = "Sticky Note";
25537 const LEGACY_METADATA_PREFIX = "<!-- wpworkspace-sticky:";
25538 const LEGACY_METADATA_SUFFIX = "-->";
25539 const TITLE_MAX = 64;
25540 const GENERATED_TITLE_MAX = 48;
25541 const EXCERPT_MAX = 180;
25542 function noteFromGuideline(guideline) {
25543 const title = titleField(guideline.title);
25544 const content = removeLegacyMetadataComment(
25545 textFieldValue(guideline.content, { stripHtmlForRendered: true })
25546 );
25547 const modifiedMs = modifiedTimeMs(guideline);
25548 return {
25549 localId: `guideline:${guideline.id}`,
25550 guidelineId: guideline.id,
25551 title,
25552 body: editorBody(title, content),
25553 modified: guideline.modified,
25554 ...modifiedMs > 0 ? { modifiedMs } : {},
25555 link: guideline.link,
25556 termIds: Array.isArray(guideline.wp_guideline_type) ? guideline.wp_guideline_type.filter(isFiniteNumber) : []
25557 };
25558 }
25559 function titleField(field) {
25560 const candidates = [];
25561 if (typeof field === "string") {
25562 candidates.push(field);
25563 } else if (field && typeof field === "object") {
25564 if (typeof field.raw === "string") {
25565 candidates.push(field.raw);
25566 }
25567 if (typeof field.rendered === "string") {
25568 candidates.push(stripHtml(field.rendered));
25569 }
25570 }
25571 for (const candidate of candidates) {
25572 const trimmed = stripHtml(candidate).trim();
25573 if (trimmed) {
25574 return trimmed;
25575 }
25576 }
25577 return DEFAULT_STICKY_TITLE;
25578 }
25579 function textFieldValue(field, options = {}) {
25580 if (typeof field === "string") {
25581 return field;
25582 }
25583 if (!field || typeof field !== "object") {
25584 return "";
25585 }
25586 if (typeof field.raw === "string" && field.raw.length > 0) {
25587 return field.raw;
25588 }
25589 if (typeof field.rendered === "string") {
25590 return options.stripHtmlForRendered ? stripHtml(field.rendered) : field.rendered;
25591 }
25592 return "";
25593 }
25594 function titleForBody(body) {
25595 const line = body.split(/\r?\n/).find((item) => item.trim().length > 0)?.trim();
25596 const title = line && line.length > 0 ? line : DEFAULT_STICKY_TITLE;
25597 return truncate(title, TITLE_MAX);
25598 }
25599 function generatedTitle(body) {
25600 const collapsed = body.replace(/\s+/g, " ").trim();
25601 const title = collapsed || DEFAULT_STICKY_TITLE;
25602 return truncate(title, GENERATED_TITLE_MAX);
25603 }
25604 function editorBody(title, content) {
25605 const trimmedTitle = title.trim();
25606 if (!trimmedTitle) {
25607 return content;
25608 }
25609 const firstLine = content.split(/\r?\n/)[0]?.trim();
25610 if (firstLine === trimmedTitle) {
25611 return content;
25612 }
25613 if (!content) {
25614 return trimmedTitle;
25615 }
25616 return `${trimmedTitle}
25617 ${content}`;
25618 }
25619 function noteComponentsForBody(editorValue, fallbackTitle = DEFAULT_STICKY_TITLE) {
25620 const fallback = fallbackTitle.trim() || DEFAULT_STICKY_TITLE;
25621 const title = titleForBody(editorValue);
25622 const firstNewline = editorValue.search(/\r?\n/);
25623 if (firstNewline === -1) {
25624 const resolvedTitle = title === DEFAULT_STICKY_TITLE ? fallback : title;
25625 return {
25626 title: resolvedTitle,
25627 content: "",
25628 excerpt: excerptFor(resolvedTitle)
25629 };
25630 }
25631 let content = editorValue.slice(firstNewline);
25632 content = content.replace(/^\r?\n/, "");
25633 if (content.startsWith("\n")) {
25634 content = content.slice(1);
25635 }
25636 return {
25637 title,
25638 content,
25639 excerpt: excerptFor(content.trim() ? content : title)
25640 };
25641 }
25642 function excerptFor(body) {
25643 const collapsed = body.replace(/[\n\t]+/g, " ").trim();
25644 return truncate(collapsed, EXCERPT_MAX);
25645 }
25646 function removeLegacyMetadataComment(content) {
25647 if (!content.startsWith(LEGACY_METADATA_PREFIX) || !content.includes(LEGACY_METADATA_SUFFIX)) {
25648 return content;
25649 }
25650 const end = content.indexOf(LEGACY_METADATA_SUFFIX);
25651 let body = content.slice(end + LEGACY_METADATA_SUFFIX.length);
25652 if (body.startsWith("\r\n")) {
25653 body = body.slice(2);
25654 } else if (body.startsWith("\n")) {
25655 body = body.slice(1);
25656 }
25657 return body;
25658 }
25659 function stripHtml(value) {
25660 if (typeof document !== "undefined") {
25661 const template = document.createElement("template");
25662 template.innerHTML = value;
25663 return (template.content.textContent ?? "").trim();
25664 }
25665 return value.replace(/<[^>]*>/g, "").trim();
25666 }
25667 function truncate(value, max) {
25668 return value.length > max ? `${value.slice(0, max)}...` : value;
25669 }
25670 function modifiedTimeMs(guideline) {
25671 if (typeof guideline.desktop_mode_modified_ms === "number" && Number.isFinite(guideline.desktop_mode_modified_ms)) {
25672 return guideline.desktop_mode_modified_ms;
25673 }
25674 if (!guideline.modified) {
25675 return 0;
25676 }
25677 const parsed = Date.parse(guideline.modified);
25678 return Number.isFinite(parsed) ? parsed : 0;
25679 }
25680 function isFiniteNumber(value) {
25681 return typeof value === "number" && Number.isFinite(value);
25682 }
25683 class StickyNotesRestError extends Error {
25684 constructor(message, status) {
25685 super(message);
25686 this.name = "StickyNotesRestError";
25687 this.status = status;
25688 }
25689 }
25690 async function resolveStickyTerms(config) {
25691 const terms = await fetchStickyTermCandidates(config);
25692 const picked = pickStickyTerms(
25693 [...terms.artifactTerms, ...terms.artifactsTerms],
25694 terms.noteTerms,
25695 terms.stickyTerms
25696 );
25697 if (picked) {
25698 return picked;
25699 }
25700 const artifact = await ensureTerm(config, {
25701 slug: "artifact",
25702 name: "Artifact",
25703 parent: 0
25704 });
25705 const note = await ensureTerm(config, {
25706 slug: "note",
25707 name: "Note",
25708 parent: artifact.id
25709 });
25710 const sticky = await ensureTerm(config, {
25711 slug: "sticky",
25712 name: "Sticky",
25713 parent: artifact.id
25714 });
25715 return {
25716 stickyTermId: sticky.id,
25717 termIds: uniqueNumbers([artifact.id, note.id, sticky.id])
25718 };
25719 }
25720 async function fetchStickyTermCandidates(config) {
25721 const [artifactTerms, artifactsTerms, noteTerms, stickyTerms] = await Promise.all([
25722 fetchTermsBySlug(config, "artifact"),
25723 fetchTermsBySlug(config, "artifacts"),
25724 fetchTermsBySlug(config, "note"),
25725 fetchTermsBySlug(config, "sticky")
25726 ]);
25727 return {
25728 artifactTerms,
25729 artifactsTerms,
25730 noteTerms,
25731 stickyTerms
25732 };
25733 }
25734 function pickStickyTerms(artifactTerms, noteTerms, stickyTerms) {
25735 if (stickyTerms.length === 0) {
25736 return null;
25737 }
25738 const artifact = artifactTerms.find(
25739 (term) => ["artifact", "artifacts"].includes(term.slug)
25740 ) ?? artifactTerms[0] ?? null;
25741 const sticky = artifact ? stickyTerms.find((term) => Number(term.parent) === artifact.id) ?? stickyTerms[0] : stickyTerms[0];
25742 if (!sticky) {
25743 return null;
25744 }
25745 const note = artifact ? noteTerms.find((term) => Number(term.parent) === artifact.id) ?? null : null;
25746 return {
25747 stickyTermId: sticky.id,
25748 termIds: uniqueNumbers([
25749 artifact?.id,
25750 note?.id,
25751 sticky.id
25752 ])
25753 };
25754 }
25755 async function fetchStickyNotes(config, stickyTermId) {
25756 const guidelines = await requestJson(
25757 config,
25758 pathWithQuery("wp/v2/guidelines", {
25759 context: "edit",
25760 status: "private",
25761 per_page: "100",
25762 orderby: "modified",
25763 order: "desc",
25764 wp_guideline_type: String(stickyTermId)
25765 }),
25766 void 0,
25767 true
25768 );
25769 return guidelines.filter(
25770 (guideline) => Array.isArray(guideline.wp_guideline_type) ? guideline.wp_guideline_type.includes(stickyTermId) : true
25771 ).map(noteFromGuideline);
25772 }
25773 async function saveStickyNote(config, note, terms) {
25774 const components = noteComponentsForBody(note.body, note.title);
25775 const payload = {
25776 status: "private",
25777 title: components.title,
25778 content: components.content,
25779 excerpt: components.excerpt
25780 };
25781 if (note.guidelineId === null) {
25782 payload.wp_guideline_type = terms.termIds;
25783 }
25784 const path = note.guidelineId === null ? "wp/v2/guidelines" : `wp/v2/guidelines/${note.guidelineId}`;
25785 const guideline = await requestJson(
25786 config,
25787 path,
25788 {
25789 method: "POST",
25790 headers: {
25791 "Content-Type": "application/json"
25792 },
25793 body: JSON.stringify(payload)
25794 },
25795 false
25796 );
25797 return noteFromGuideline(guideline);
25798 }
25799 function buildGuidelineEditUrl(adminUrl, guidelineId) {
25800 const url = new URL("post.php", adminUrl);
25801 url.searchParams.set("post", String(guidelineId));
25802 url.searchParams.set("action", "edit");
25803 return url.toString();
25804 }
25805 async function fetchTermsBySlug(config, slug) {
25806 try {
25807 return await requestJson(
25808 config,
25809 pathWithQuery("wp/v2/wp_guideline_type", {
25810 context: "edit",
25811 slug,
25812 per_page: "100"
25813 }),
25814 void 0,
25815 true
25816 );
25817 } catch (error) {
25818 if (error instanceof StickyNotesRestError && (error.status === 404 || error.status === 400)) {
25819 return [];
25820 }
25821 throw error;
25822 }
25823 }
25824 async function ensureTerm(config, term) {
25825 const existing = await fetchTermsBySlug(config, term.slug);
25826 const byParent = existing.find(
25827 (item) => Number(item.parent ?? 0) === term.parent
25828 );
25829 if (byParent) {
25830 return byParent;
25831 }
25832 if (existing[0]) {
25833 return existing[0];
25834 }
25835 try {
25836 return await requestJson(
25837 config,
25838 "wp/v2/wp_guideline_type",
25839 {
25840 method: "POST",
25841 headers: {
25842 "Content-Type": "application/json"
25843 },
25844 body: JSON.stringify(term)
25845 },
25846 true
25847 );
25848 } catch (error) {
25849 const fallback = await fetchTermsBySlug(config, term.slug);
25850 if (fallback[0]) {
25851 return fallback[0];
25852 }
25853 throw error;
25854 }
25855 }
25856 async function requestJson(config, path, init2, silent = true) {
25857 const response = await trackedFetch$1(
25858 joinRestUrl(restRoot(config), path),
25859 init2,
25860 {
25861 source: "desktop-mode/sticky-notes",
25862 silent
25863 }
25864 );
25865 if (!response.ok) {
25866 throw new StickyNotesRestError(
25867 response.statusText || `${DEFAULT_STICKY_TITLE} request failed`,
25868 response.status
25869 );
25870 }
25871 return await response.json();
25872 }
25873 function restRoot(config) {
25874 if (config.restUrl) {
25875 return config.restUrl;
25876 }
25877 return `${window.location.origin}/wp-json/`;
25878 }
25879 function pathWithQuery(path, query) {
25880 const params = new URLSearchParams();
25881 Object.entries(query).forEach(([key, value]) => {
25882 params.set(key, value);
25883 });
25884 return `${path}?${params.toString()}`;
25885 }
25886 function uniqueNumbers(values) {
25887 const out = [];
25888 values.forEach((value) => {
25889 if (typeof value === "number" && Number.isFinite(value) && !out.includes(value)) {
25890 out.push(value);
25891 }
25892 });
25893 return out;
25894 }
25895 const SUBSCRIBE_FIELD = "desktop_mode_sticky_notes_subscribe";
25896 const RESPONSE_FIELD = "desktop_mode_sticky_notes";
25897 let started$3 = false;
25898 let target = null;
25899 function startStickyNotesHeartbeat(nextTarget) {
25900 target = nextTarget;
25901 if (started$3) {
25902 return;
25903 }
25904 started$3 = true;
25905 heartbeat.contribute(
25906 SUBSCRIBE_FIELD,
25907 () => target?.getHeartbeatSubscription()
25908 );
25909 heartbeat.subscribe(
25910 RESPONSE_FIELD,
25911 (payload) => {
25912 target?.applyHeartbeatPayload(payload);
25913 }
25914 );
25915 }
25916 const GEOMETRY_KEY = "desktop-mode-sticky-notes-geometry";
25917 const DEFAULT_WIDTH = 264;
25918 const DEFAULT_HEIGHT = 176;
25919 const MIN_WIDTH = 180;
25920 const MIN_HEIGHT = 128;
25921 const EDGE_PADDING = 16;
25922 const SAVE_DEBOUNCE_MS = 1e3;
25923 class StickyNotesLayer {
25924 constructor(options) {
25925 this.root = null;
25926 this.terms = null;
25927 this.controllers = /* @__PURE__ */ new Map();
25928 this.contextMenuInstalled = false;
25929 this.desktopHooksInstalled = false;
25930 this.highWaterMs = 0;
25931 this.zIndexCounter = 0;
25932 this.host = options.host;
25933 this.config = options.config;
25934 this.available = options.available ?? true;
25935 this.openArtifact = options.openArtifact;
25936 this.getActiveDesktopId = options.getActiveDesktopId ?? (() => "desktop-1");
25937 this.onError = options.onError;
25938 }
25939 async boot() {
25940 if (!this.available) {
25941 return;
25942 }
25943 try {
25944 this.terms = await resolveStickyTerms(this.config);
25945 if (!this.terms) {
25946 return;
25947 }
25948 this.installContextMenu();
25949 this.installDesktopHooks();
25950 const notes = await fetchStickyNotes(
25951 this.config,
25952 this.terms.stickyTermId
25953 );
25954 this.bumpHighWaterFromNotes(notes);
25955 startStickyNotesHeartbeat(this);
25956 if (notes.length === 0) {
25957 return;
25958 }
25959 this.ensureRoot();
25960 sortNotesByModified(notes).forEach(
25961 (note, index2) => this.upsert(note, index2)
25962 );
25963 } catch (error) {
25964 if (error instanceof Error) {
25965 console.debug("[desktop-mode] Sticky notes unavailable:", error.message);
25966 }
25967 }
25968 }
25969 createNote(body = "") {
25970 if (!this.terms) {
25971 return;
25972 }
25973 const note = {
25974 localId: `local:${Date.now()}:${Math.random().toString(36).slice(2)}`,
25975 guidelineId: null,
25976 title: body.trim() ? generatedTitle(body) : DEFAULT_STICKY_TITLE,
25977 body,
25978 termIds: this.terms.termIds
25979 };
25980 const controller = this.upsert(note, this.controllers.size, {
25981 activate: true
25982 });
25983 controller.focus();
25984 }
25985 upsert(note, index2, options = {}) {
25986 this.ensureRoot();
25987 const key = noteKey(note);
25988 const existing = this.controllers.get(key);
25989 if (existing) {
25990 existing.replace(note);
25991 if (options.activate) {
25992 this.bringToFront(existing);
25993 }
25994 return existing;
25995 }
25996 const controller = new StickyNoteController({
25997 layer: this,
25998 note,
25999 index: index2
26000 });
26001 this.controllers.set(key, controller);
26002 this.root?.appendChild(controller.element);
26003 this.assignZIndex(controller);
26004 this.applyDesktopVisibility(controller);
26005 if (options.activate) {
26006 this.bringToFront(controller);
26007 }
26008 return controller;
26009 }
26010 ensureRoot() {
26011 if (this.root) {
26012 return this.root;
26013 }
26014 const root = document.createElement("section");
26015 root.className = "desktop-mode-sticky-notes";
26016 root.setAttribute("aria-label", __("Sticky notes"));
26017 this.host.appendChild(root);
26018 this.root = root;
26019 return root;
26020 }
26021 installContextMenu() {
26022 if (this.contextMenuInstalled) {
26023 return;
26024 }
26025 this.contextMenuInstalled = true;
26026 addFilter(
26027 "desktop-mode.wallpaper-context-menu",
26028 "desktop-mode/sticky-notes",
26029 (items) => {
26030 if (!Array.isArray(items) || !this.terms) {
26031 return items;
26032 }
26033 if (items.some(
26034 (item) => item.id === "new-sticky-note"
26035 )) {
26036 return items;
26037 }
26038 return [
26039 ...items,
26040 {
26041 id: "new-sticky-note",
26042 label: __("New sticky note"),
26043 icon: "dashicons-edit-page",
26044 sort: 14,
26045 onClick: () => this.createNote()
26046 }
26047 ];
26048 }
26049 );
26050 }
26051 installDesktopHooks() {
26052 if (this.desktopHooksInstalled) {
26053 return;
26054 }
26055 this.desktopHooksInstalled = true;
26056 addAction(
26057 HOOKS.DESKTOP_SWITCHED,
26058 "desktop-mode/sticky-notes",
26059 () => this.refreshDesktopVisibility()
26060 );
26061 addAction(
26062 HOOKS.DESKTOP_CLOSED,
26063 "desktop-mode/sticky-notes",
26064 (detail) => {
26065 this.migrateDesktopAssignments(detail?.desktopId, detail?.migratedTo);
26066 this.refreshDesktopVisibility();
26067 }
26068 );
26069 }
26070 save(note) {
26071 if (!this.terms) {
26072 return Promise.reject(new Error(__("Sticky term is unavailable.")));
26073 }
26074 return saveStickyNote(this.config, note, this.terms);
26075 }
26076 getHeartbeatSubscription() {
26077 if (!this.terms) {
26078 return void 0;
26079 }
26080 return {
26081 stickyTermId: this.terms.stickyTermId,
26082 knownIds: this.knownGuidelineIds(),
26083 version: this.highWaterMs
26084 };
26085 }
26086 applyHeartbeatPayload(payload) {
26087 for (const guideline of payload.notes ?? []) {
26088 const note = noteFromGuideline(guideline);
26089 this.upsertRemote(note);
26090 }
26091 for (const id of payload.removed ?? []) {
26092 this.forgetGuidelineId(id);
26093 }
26094 if (typeof payload.serverTimeMs === "number" && Number.isFinite(payload.serverTimeMs) && payload.serverTimeMs > this.highWaterMs) {
26095 this.highWaterMs = payload.serverTimeMs;
26096 }
26097 if (payload.truncated) {
26098 void this.reloadFromServer();
26099 }
26100 }
26101 openNoteArtifact(note) {
26102 if (note.guidelineId === null) {
26103 return;
26104 }
26105 this.openArtifact(
26106 buildGuidelineEditUrl(this.config.adminUrl, note.guidelineId),
26107 note.title,
26108 note.guidelineId
26109 );
26110 }
26111 notifyError(message) {
26112 this.onError?.(message);
26113 }
26114 hostSize() {
26115 return {
26116 width: Math.max(1, this.host.clientWidth),
26117 height: Math.max(1, this.host.clientHeight)
26118 };
26119 }
26120 defaultGeometry(index2) {
26121 const { width: hostWidth, height: hostHeight } = this.hostSize();
26122 const width = Math.min(
26123 DEFAULT_WIDTH,
26124 Math.max(MIN_WIDTH, hostWidth - EDGE_PADDING * 2)
26125 );
26126 const height = Math.min(
26127 DEFAULT_HEIGHT,
26128 Math.max(MIN_HEIGHT, hostHeight - EDGE_PADDING * 2)
26129 );
26130 const offset = index2 % 8 * 28;
26131 const left = clamp(
26132 hostWidth - width - 32 - offset,
26133 EDGE_PADDING,
26134 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
26135 );
26136 const top = clamp(
26137 32 + offset,
26138 EDGE_PADDING,
26139 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
26140 );
26141 return {
26142 x: left / hostWidth,
26143 y: top / hostHeight,
26144 width,
26145 height
26146 };
26147 }
26148 forget(controller) {
26149 this.controllers.delete(noteKey(controller.note));
26150 controller.dispose();
26151 controller.element.remove();
26152 if (this.controllers.size === 0) {
26153 this.root?.remove();
26154 this.root = null;
26155 }
26156 }
26157 replaceControllerKey(oldKey, controller) {
26158 const newKey = noteKey(controller.note);
26159 this.controllers.delete(oldKey);
26160 this.controllers.set(newKey, controller);
26161 moveStoredGeometry(oldKey, newKey);
26162 this.applyDesktopVisibility(controller);
26163 }
26164 bumpHighWaterFromNote(note) {
26165 const modifiedMs = noteModifiedMs(note);
26166 if (modifiedMs > this.highWaterMs) {
26167 this.highWaterMs = modifiedMs;
26168 }
26169 }
26170 bringToFront(controller) {
26171 controller.setZIndex(this.nextZIndex());
26172 }
26173 geometryForNote(note, index2) {
26174 const key = noteKey(note);
26175 const loaded = loadGeometry(key);
26176 const desktopId = this.normalizeDesktopId(loaded?.desktopId);
26177 const geometry = loaded ? { ...loaded, desktopId } : { ...this.defaultGeometry(index2), desktopId };
26178 if (!loaded || loaded.desktopId !== geometry.desktopId) {
26179 saveGeometry(key, geometry);
26180 }
26181 return geometry;
26182 }
26183 upsertRemote(note) {
26184 const key = noteKey(note);
26185 const existing = this.controllers.get(key);
26186 if (existing) {
26187 if (!existing.shouldReplaceFromRemote(note)) {
26188 this.bumpHighWaterFromNote(note);
26189 return existing;
26190 }
26191 existing.replace(note);
26192 this.bumpHighWaterFromNote(note);
26193 return existing;
26194 }
26195 const controller = this.upsert(note, this.controllers.size);
26196 this.bumpHighWaterFromNote(note);
26197 return controller;
26198 }
26199 forgetGuidelineId(guidelineId) {
26200 for (const controller of this.controllers.values()) {
26201 if (controller.note.guidelineId === guidelineId) {
26202 this.forget(controller);
26203 return;
26204 }
26205 }
26206 }
26207 knownGuidelineIds() {
26208 const ids = [];
26209 for (const controller of this.controllers.values()) {
26210 if (controller.note.guidelineId !== null) {
26211 ids.push(controller.note.guidelineId);
26212 }
26213 }
26214 return ids;
26215 }
26216 bumpHighWaterFromNotes(notes) {
26217 notes.forEach((note) => this.bumpHighWaterFromNote(note));
26218 }
26219 assignZIndex(controller) {
26220 controller.setZIndex(this.nextZIndex());
26221 }
26222 nextZIndex() {
26223 this.zIndexCounter += 1;
26224 return this.zIndexCounter;
26225 }
26226 applyDesktopVisibility(controller) {
26227 controller.setVisible(this.isNoteOnActiveDesktop(controller.note));
26228 }
26229 refreshDesktopVisibility() {
26230 for (const controller of this.controllers.values()) {
26231 this.applyDesktopVisibility(controller);
26232 }
26233 }
26234 isNoteOnActiveDesktop(note) {
26235 const key = noteKey(note);
26236 const geometry = loadGeometry(key);
26237 const desktopId = this.normalizeDesktopId(geometry?.desktopId);
26238 if (geometry && geometry.desktopId !== desktopId) {
26239 saveGeometry(key, { ...geometry, desktopId });
26240 }
26241 return desktopId === this.activeDesktopId();
26242 }
26243 migrateDesktopAssignments(desktopId, migratedTo) {
26244 if (!desktopId || !migratedTo || desktopId === migratedTo) {
26245 return;
26246 }
26247 const map = readGeometryMap();
26248 let changed = false;
26249 Object.entries(map).forEach(([key, geometry]) => {
26250 if (geometry.desktopId === desktopId) {
26251 map[key] = {
26252 ...geometry,
26253 desktopId: this.normalizeDesktopId(migratedTo)
26254 };
26255 changed = true;
26256 }
26257 });
26258 if (changed) {
26259 writeGeometryMap(map);
26260 }
26261 }
26262 activeDesktopId() {
26263 try {
26264 const id = this.getActiveDesktopId();
26265 return typeof id === "string" && id ? id : "desktop-1";
26266 } catch {
26267 return "desktop-1";
26268 }
26269 }
26270 normalizeDesktopId(desktopId) {
26271 if (!desktopId) {
26272 return this.activeDesktopId();
26273 }
26274 return desktopId;
26275 }
26276 async reloadFromServer() {
26277 if (!this.terms) {
26278 return;
26279 }
26280 try {
26281 const notes = await fetchStickyNotes(
26282 this.config,
26283 this.terms.stickyTermId
26284 );
26285 const ids = /* @__PURE__ */ new Set();
26286 sortNotesByModified(notes).forEach((note) => {
26287 if (note.guidelineId !== null) {
26288 ids.add(note.guidelineId);
26289 }
26290 this.upsertRemote(note);
26291 });
26292 this.knownGuidelineIds().forEach((id) => {
26293 if (!ids.has(id)) {
26294 this.forgetGuidelineId(id);
26295 }
26296 });
26297 } catch {
26298 }
26299 }
26300 }
26301 class StickyNoteController {
26302 constructor(options) {
26303 this.saveTimer = null;
26304 this.geometryTimer = null;
26305 this.saving = false;
26306 this.saveAgain = false;
26307 this.resizeObserver = null;
26308 this.disposed = false;
26309 this.layer = options.layer;
26310 this.note = options.note;
26311 this.index = options.index;
26312 this.element = document.createElement("article");
26313 this.element.className = "desktop-mode-sticky-note";
26314 this.element.dataset.stickyNoteId = noteKey(this.note);
26315 this.titleEl = document.createElement("span");
26316 this.editor = document.createElement("wpd-textarea");
26317 this.statusEl = document.createElement("wpd-save-status");
26318 this.openButton = document.createElement("wpd-window-button");
26319 this.paint();
26320 this.applyGeometry(this.layer.geometryForNote(this.note, this.index));
26321 this.element.addEventListener(
26322 "pointerdown",
26323 () => this.layer.bringToFront(this),
26324 { capture: true }
26325 );
26326 this.element.addEventListener("focusin", () => this.layer.bringToFront(this));
26327 this.watchResize();
26328 }
26329 focus() {
26330 window.setTimeout(() => this.editor.focusInput?.(), 0);
26331 }
26332 replace(note) {
26333 this.note = note;
26334 this.element.dataset.stickyNoteId = noteKey(this.note);
26335 this.titleEl.textContent = this.note.title;
26336 this.editor.setAttribute("value", this.note.body);
26337 this.refreshOpenButton();
26338 }
26339 shouldReplaceFromRemote(note) {
26340 if (this.hasLocalChanges()) {
26341 return false;
26342 }
26343 const currentMs = noteModifiedMs(this.note);
26344 const incomingMs = noteModifiedMs(note);
26345 if (currentMs > 0 && incomingMs > 0 && incomingMs <= currentMs && this.note.title === note.title && this.note.body === note.body) {
26346 return false;
26347 }
26348 return true;
26349 }
26350 setZIndex(zIndex) {
26351 this.element.style.zIndex = String(zIndex);
26352 }
26353 setVisible(visible) {
26354 this.element.style.display = visible ? "" : "none";
26355 }
26356 dispose() {
26357 this.disposed = true;
26358 if (this.saveTimer !== null) {
26359 window.clearTimeout(this.saveTimer);
26360 this.saveTimer = null;
26361 }
26362 if (this.geometryTimer !== null) {
26363 window.clearTimeout(this.geometryTimer);
26364 this.geometryTimer = null;
26365 }
26366 this.resizeObserver?.disconnect();
26367 this.resizeObserver = null;
26368 }
26369 paint() {
26370 this.element.innerHTML = "";
26371 this.element.style.minWidth = `${MIN_WIDTH}px`;
26372 this.element.style.minHeight = `${MIN_HEIGHT}px`;
26373 const header = document.createElement("div");
26374 header.className = "desktop-mode-sticky-note__header";
26375 const grip = document.createElement("span");
26376 grip.className = "desktop-mode-sticky-note__grip";
26377 grip.setAttribute("aria-hidden", "true");
26378 this.titleEl.className = "desktop-mode-sticky-note__title";
26379 this.titleEl.textContent = this.note.title;
26380 this.statusEl.setAttribute("mode", "icon");
26381 this.statusEl.setAttribute("phase", "idle");
26382 this.statusEl.className = "desktop-mode-sticky-note__status";
26383 this.openButton.setAttribute("icon", "detach");
26384 this.openButton.setAttribute("title", __("Open artifact"));
26385 this.openButton.className = "desktop-mode-sticky-note__open";
26386 this.openButton.addEventListener("wpd-button-activate", () => {
26387 this.layer.openNoteArtifact(this.note);
26388 });
26389 const close = document.createElement("wpd-window-button");
26390 close.setAttribute("icon", "close");
26391 close.setAttribute("danger", "");
26392 close.setAttribute("title", __("Hide sticky note"));
26393 close.className = "desktop-mode-sticky-note__close";
26394 close.addEventListener("wpd-button-activate", () => this.close());
26395 header.append(grip, this.titleEl, this.statusEl, this.openButton, close);
26396 header.addEventListener("pointerdown", (event) => this.startDrag(event));
26397 this.editor.className = "desktop-mode-sticky-note__editor";
26398 this.editor.setAttribute("aria-label", __("Sticky note text"));
26399 this.editor.setAttribute("rows", "8");
26400 this.editor.setAttribute("value", this.note.body);
26401 this.installEditorKeyboardGuard();
26402 this.editor.addEventListener("wpd-input-change", (event) => {
26403 const detail = event.detail;
26404 this.note.body = detail.value;
26405 this.note.title = titleForBody(detail.value);
26406 this.titleEl.textContent = this.note.title;
26407 this.setPhase("pending");
26408 this.scheduleSave();
26409 });
26410 this.editor.addEventListener("wpd-input-commit", () => this.flushSave());
26411 this.element.append(header, this.editor);
26412 this.refreshOpenButton();
26413 }
26414 installEditorKeyboardGuard() {
26415 ["keydown", "keypress", "keyup"].forEach((eventName) => {
26416 this.editor.addEventListener(eventName, (event) => {
26417 event.stopPropagation();
26418 });
26419 });
26420 }
26421 refreshOpenButton() {
26422 const disabled = this.note.guidelineId === null;
26423 this.openButton.classList.toggle("is-disabled", disabled);
26424 this.openButton.setAttribute("aria-disabled", disabled ? "true" : "false");
26425 }
26426 close() {
26427 if (this.note.guidelineId === null && this.note.body.trim().length === 0) {
26428 this.layer.forget(this);
26429 return;
26430 }
26431 this.flushSave();
26432 this.layer.forget(this);
26433 }
26434 scheduleSave() {
26435 if (this.note.guidelineId === null && this.note.body.trim().length === 0) {
26436 this.setPhase("idle");
26437 return;
26438 }
26439 if (this.saveTimer !== null) {
26440 window.clearTimeout(this.saveTimer);
26441 }
26442 this.saveTimer = window.setTimeout(() => {
26443 this.saveTimer = null;
26444 void this.save();
26445 }, SAVE_DEBOUNCE_MS);
26446 }
26447 flushSave() {
26448 if (this.saveTimer !== null) {
26449 window.clearTimeout(this.saveTimer);
26450 this.saveTimer = null;
26451 }
26452 if (this.note.guidelineId !== null || this.note.body.trim().length > 0) {
26453 void this.save();
26454 }
26455 }
26456 async save() {
26457 if (this.saving) {
26458 this.saveAgain = true;
26459 this.setPhase("pending");
26460 return;
26461 }
26462 this.saving = true;
26463 this.setPhase("saving");
26464 const bodyAtSave = this.note.body;
26465 try {
26466 const saved = await this.layer.save({
26467 ...this.note,
26468 body: bodyAtSave
26469 });
26470 if (this.disposed) {
26471 return;
26472 }
26473 const oldKey = noteKey(this.note);
26474 this.note.guidelineId = saved.guidelineId;
26475 this.note.modified = saved.modified;
26476 this.note.link = saved.link;
26477 this.note.termIds = saved.termIds.length > 0 ? saved.termIds : this.note.termIds;
26478 if (this.note.body === bodyAtSave) {
26479 this.note.title = saved.title;
26480 this.titleEl.textContent = saved.title;
26481 }
26482 if (oldKey !== noteKey(this.note)) {
26483 this.element.dataset.stickyNoteId = noteKey(this.note);
26484 this.layer.replaceControllerKey(oldKey, this);
26485 }
26486 this.layer.bumpHighWaterFromNote(this.note);
26487 this.refreshOpenButton();
26488 this.setPhase("saved");
26489 } catch (error) {
26490 if (this.disposed) {
26491 return;
26492 }
26493 const message = error instanceof Error ? error.message : __("Could not save sticky note.");
26494 this.setPhase("failed", message);
26495 this.layer.notifyError(message);
26496 } finally {
26497 this.saving = false;
26498 if (!this.disposed && this.saveAgain) {
26499 this.saveAgain = false;
26500 this.scheduleSave();
26501 }
26502 }
26503 }
26504 setPhase(phase, error) {
26505 this.statusEl.setAttribute("phase", phase);
26506 if (error) {
26507 this.statusEl.setAttribute("error", error);
26508 this.statusEl.setAttribute("title", error);
26509 } else {
26510 this.statusEl.removeAttribute("error");
26511 this.statusEl.removeAttribute("title");
26512 }
26513 }
26514 hasLocalChanges() {
26515 const phase = this.statusEl.getAttribute("phase");
26516 return this.saveTimer !== null || this.saving || this.saveAgain || phase === "pending" || phase === "failed";
26517 }
26518 startDrag(event) {
26519 if (event.button !== 0) {
26520 return;
26521 }
26522 const target2 = event.target;
26523 if (target2?.closest("wpd-window-button, wpd-save-status")) {
26524 return;
26525 }
26526 event.preventDefault();
26527 const startRect = this.element.getBoundingClientRect();
26528 const hostRect = this.layerHostRect();
26529 const startLeft = startRect.left - hostRect.left;
26530 const startTop = startRect.top - hostRect.top;
26531 const startX = event.clientX;
26532 const startY = event.clientY;
26533 this.element.classList.add("desktop-mode-sticky-note--dragging");
26534 this.element.setPointerCapture?.(event.pointerId);
26535 const move = (moveEvent) => {
26536 const width = this.element.offsetWidth;
26537 const height = this.element.offsetHeight;
26538 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26539 const left = clamp(
26540 startLeft + moveEvent.clientX - startX,
26541 EDGE_PADDING,
26542 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
26543 );
26544 const top = clamp(
26545 startTop + moveEvent.clientY - startY,
26546 EDGE_PADDING,
26547 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
26548 );
26549 this.element.style.left = `${left}px`;
26550 this.element.style.top = `${top}px`;
26551 };
26552 const up = (upEvent) => {
26553 this.element.classList.remove("desktop-mode-sticky-note--dragging");
26554 this.element.releasePointerCapture?.(upEvent.pointerId);
26555 document.removeEventListener("pointermove", move);
26556 document.removeEventListener("pointerup", up);
26557 this.persistGeometry();
26558 };
26559 document.addEventListener("pointermove", move);
26560 document.addEventListener("pointerup", up);
26561 }
26562 applyGeometry(geometry) {
26563 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26564 const width = clamp(geometry.width, MIN_WIDTH, hostWidth - EDGE_PADDING * 2);
26565 const height = clamp(geometry.height, MIN_HEIGHT, hostHeight - EDGE_PADDING * 2);
26566 const left = clamp(
26567 geometry.x * hostWidth,
26568 EDGE_PADDING,
26569 Math.max(EDGE_PADDING, hostWidth - width - EDGE_PADDING)
26570 );
26571 const top = clamp(
26572 geometry.y * hostHeight,
26573 EDGE_PADDING,
26574 Math.max(EDGE_PADDING, hostHeight - height - EDGE_PADDING)
26575 );
26576 this.element.style.left = `${left}px`;
26577 this.element.style.top = `${top}px`;
26578 this.element.style.width = `${width}px`;
26579 this.element.style.height = `${height}px`;
26580 }
26581 watchResize() {
26582 if (typeof ResizeObserver === "undefined") {
26583 return;
26584 }
26585 this.resizeObserver = new ResizeObserver(() => {
26586 if (this.geometryTimer !== null) {
26587 window.clearTimeout(this.geometryTimer);
26588 }
26589 this.geometryTimer = window.setTimeout(() => {
26590 this.geometryTimer = null;
26591 this.persistGeometry();
26592 }, 150);
26593 });
26594 this.resizeObserver.observe(this.element);
26595 }
26596 persistGeometry() {
26597 const { width: hostWidth, height: hostHeight } = this.layer.hostSize();
26598 const left = parseFloat(this.element.style.left) || 0;
26599 const top = parseFloat(this.element.style.top) || 0;
26600 const existing = loadGeometry(noteKey(this.note));
26601 saveGeometry(noteKey(this.note), {
26602 ...existing ?? {},
26603 x: clamp(left / hostWidth, 0, 1),
26604 y: clamp(top / hostHeight, 0, 1),
26605 width: this.element.offsetWidth,
26606 height: this.element.offsetHeight
26607 });
26608 }
26609 layerHostRect() {
26610 const parent = this.element.parentElement?.parentElement;
26611 return (parent ?? document.body).getBoundingClientRect();
26612 }
26613 }
26614 function bootStickyNotes(options) {
26615 const layer = new StickyNotesLayer(options);
26616 void layer.boot();
26617 return layer;
26618 }
26619 function noteKey(note) {
26620 return note.guidelineId === null ? note.localId : `guideline:${note.guidelineId}`;
26621 }
26622 function noteModifiedMs(note) {
26623 if (typeof note.modifiedMs === "number" && Number.isFinite(note.modifiedMs)) {
26624 return note.modifiedMs;
26625 }
26626 if (!note.modified) {
26627 return 0;
26628 }
26629 const parsed = Date.parse(note.modified);
26630 return Number.isFinite(parsed) ? parsed : 0;
26631 }
26632 function sortNotesByModified(notes) {
26633 return [...notes].sort((a, b) => noteModifiedMs(a) - noteModifiedMs(b));
26634 }
26635 function loadGeometry(key) {
26636 const map = readGeometryMap();
26637 const value = map[key];
26638 if (!value || !Number.isFinite(value.x) || !Number.isFinite(value.y) || !Number.isFinite(value.width) || !Number.isFinite(value.height)) {
26639 return null;
26640 }
26641 return value;
26642 }
26643 function saveGeometry(key, geometry) {
26644 const map = readGeometryMap();
26645 map[key] = geometry;
26646 writeGeometryMap(map);
26647 }
26648 function moveStoredGeometry(oldKey, newKey) {
26649 if (oldKey === newKey) {
26650 return;
26651 }
26652 const map = readGeometryMap();
26653 if (map[oldKey]) {
26654 map[newKey] = map[oldKey];
26655 delete map[oldKey];
26656 writeGeometryMap(map);
26657 }
26658 }
26659 function readGeometryMap() {
26660 try {
26661 const raw = window.localStorage.getItem(GEOMETRY_KEY);
26662 return raw ? JSON.parse(raw) : {};
26663 } catch {
26664 return {};
26665 }
26666 }
26667 function writeGeometryMap(map) {
26668 try {
26669 window.localStorage.setItem(GEOMETRY_KEY, JSON.stringify(map));
26670 } catch {
26671 }
26672 }
26673 function clamp(value, min, max) {
26674 if (max < min) {
26675 return min;
26676 }
26677 return Math.min(max, Math.max(min, value));
26678 }
26679 const clock = {
26680 id: "clock",
26681 // Labels/descriptions on built-in defs stay string-literal at
26682 // module-eval time so the extract-pot pass picks them up. The
26683 // values are wrapped in `__()` so they translate at runtime.
26684 get label() {
26685 return __("Clock");
26686 },
26687 get description() {
26688 return __("Local time and date, refreshed every second.");
26689 },
26690 icon: "dashicons-clock",
26691 mount: (container) => {
26692 container.classList.add("desktop-mode-widget-clock");
26693 const time = document.createElement("div");
26694 time.className = "desktop-mode-widget-clock__time";
26695 container.appendChild(time);
26696 const date = document.createElement("div");
26697 date.className = "desktop-mode-widget-clock__date";
26698 container.appendChild(date);
26699 const render2 = () => {
26700 const now = /* @__PURE__ */ new Date();
26701 time.textContent = now.toLocaleTimeString(void 0, {
26702 hour: "2-digit",
26703 minute: "2-digit"
26704 });
26705 date.textContent = now.toLocaleDateString(void 0, {
26706 weekday: "long",
26707 month: "short",
26708 day: "numeric"
26709 });
26710 };
26711 render2();
26712 const msUntilNextSecond = 1e3 - Date.now() % 1e3;
26713 let interval = null;
26714 const kickoff = window.setTimeout(() => {
26715 render2();
26716 interval = window.setInterval(render2, 1e3);
26717 }, msUntilNextSecond);
26718 return () => {
26719 window.clearTimeout(kickoff);
26720 if (interval !== null) {
26721 window.clearInterval(interval);
26722 }
26723 };
26724 }
26725 };
26726 function registerBuiltInWidgets() {
26727 register(clock);
26728 }
26729 function createWidgetRegistrySync(deps2) {
26730 const { layer } = deps2;
26731 const registered = /* @__PURE__ */ new Set();
26732 const loadedScripts = /* @__PURE__ */ new Set();
26733 const ensureScript = async (entry) => {
26734 if (!entry.scriptUrl || loadedScripts.has(entry.scriptUrl)) {
26735 return;
26736 }
26737 try {
26738 await loadVendorScript(entry.scriptUrl, {
26739 translations: entry.scriptTranslations,
26740 l10n: entry.scriptL10n,
26741 before: entry.scriptBefore,
26742 after: entry.scriptAfter
26743 });
26744 } catch (err) {
26745 doAction(HOOKS.SHELL_ERROR, {
26746 scope: "widget-script-load",
26747 id: entry.id,
26748 error: err
26749 });
26750 return;
26751 }
26752 loadedScripts.add(entry.scriptUrl);
26753 };
26754 const buildDefFromEntry = (entry) => {
26755 const globals = window.desktopModeWidgets || {};
26756 const mount = globals[entry.id];
26757 if (!mount) {
26758 doAction(HOOKS.SHELL_ERROR, {
26759 scope: "widget-missing-mount",
26760 id: entry.id,
26761 error: new Error(
26762 `[desktop-mode] No mount callback on window.desktopModeWidgets["${entry.id}"]. Plugin script loaded but didn't register. Check the plugin's enqueue + global assignment.`
26763 )
26764 });
26765 return null;
26766 }
26767 return {
26768 id: entry.id,
26769 label: entry.label,
26770 description: entry.description,
26771 icon: entry.icon,
26772 movable: entry.movable,
26773 resizable: entry.resizable,
26774 minWidth: entry.minWidth || void 0,
26775 minHeight: entry.minHeight || void 0,
26776 maxWidth: entry.maxWidth || void 0,
26777 maxHeight: entry.maxHeight || void 0,
26778 defaultWidth: entry.defaultWidth || void 0,
26779 defaultHeight: entry.defaultHeight || void 0,
26780 mount
26781 };
26782 };
26783 const registerEntry = async (entry) => {
26784 if (registered.has(entry.id)) {
26785 return;
26786 }
26787 await ensureScript(entry);
26788 const def = buildDefFromEntry(entry);
26789 if (!def) {
26790 return;
26791 }
26792 try {
26793 register(def);
26794 } catch (err) {
26795 doAction(HOOKS.SHELL_ERROR, {
26796 scope: "widget-register",
26797 id: entry.id,
26798 error: err
26799 });
26800 return;
26801 }
26802 registered.add(entry.id);
26803 refreshWidgetPicker();
26804 if (layer) {
26805 layer.mountIfEnabled(entry.id);
26806 }
26807 };
26808 const unregisterEntry = (id) => {
26809 if (!registered.has(id)) {
26810 return;
26811 }
26812 layer?.unmount(id);
26813 unregister(id);
26814 registered.delete(id);
26815 refreshWidgetPicker();
26816 };
26817 return async (list2) => {
26818 const incoming = /* @__PURE__ */ new Set();
26819 for (const entry of list2) {
26820 incoming.add(entry.id);
26821 }
26822 for (const id of Array.from(registered)) {
26823 if (!incoming.has(id)) {
26824 unregisterEntry(id);
26825 }
26826 }
26827 for (const entry of list2) {
26828 if (!registered.has(entry.id)) {
26829 await registerEntry(entry);
26830 }
26831 }
26832 };
26833 }
26834 const WPD_COMPONENT_TAGS = [
26835 "wpd-section",
26836 "wpd-button",
26837 "wpd-swatch",
26838 "wpd-swatch-grid",
26839 "wpd-segmented",
26840 "wpd-segment",
26841 "wpd-select",
26842 "wpd-option",
26843 "wpd-multiselect",
26844 "wpd-color-field",
26845 "wpd-range-field",
26846 "wpd-text-field",
26847 "wpd-number-field",
26848 "wpd-checkbox",
26849 "wpd-checkbox-label",
26850 "wpd-toast",
26851 "wpd-toast-container",
26852 "wpd-tabs",
26853 "wpd-tab",
26854 "wpd-tabpanel",
26855 "wpd-window-button",
26856 "wpd-menu",
26857 "wpd-menu-item",
26858 "wpd-context-menu",
26859 "wpd-context-menu-option",
26860 "wpd-confirm-dialog",
26861 "wpd-modal",
26862 "wpd-user-search",
26863 "wpd-role-picker",
26864 "wpd-flyout",
26865 "wpd-tab-chip",
26866 "wpd-stack",
26867 "wpd-cluster",
26868 "wpd-icon",
26869 "wpd-body",
26870 "wpd-panel",
26871 "wpd-row",
26872 "wpd-grid",
26873 "wpd-display",
26874 "wpd-empty-state",
26875 "wpd-key",
26876 "wpd-code",
26877 "wpd-badge",
26878 "wpd-ribbon",
26879 "wpd-tile",
26880 "wpd-log",
26881 "wpd-steps",
26882 "wpd-step",
26883 "wpd-table",
26884 "wpd-spinner",
26885 "wpd-relative-time",
26886 "wpd-avatar",
26887 "wpd-textarea",
26888 "wpd-chip",
26889 "wpd-tag-input",
26890 "wpd-form",
26891 "wpd-save-status",
26892 "wpd-category-picker",
26893 "wpd-crumb-chain",
26894 "wpd-card",
26895 "wpd-rating-summary",
26896 "wpd-notice",
26897 "wpd-progress-bar"
26898 ];
26899 const KNOWN = new Set(WPD_COMPONENT_TAGS);
26900 const WARN_GRACE_MS = 2e3;
26901 const warnedTags = /* @__PURE__ */ new Set();
26902 const observedRoots = /* @__PURE__ */ new WeakSet();
26903 let started$2 = false;
26904 function distance(a, b) {
26905 const m = a.length;
26906 const n = b.length;
26907 if (m === 0) {
26908 return n;
26909 }
26910 if (n === 0) {
26911 return m;
26912 }
26913 const dp = new Array(n + 1);
26914 for (let j = 0; j <= n; j++) {
26915 dp[j] = j;
26916 }
26917 for (let i = 1; i <= m; i++) {
26918 let prev = dp[0];
26919 dp[0] = i;
26920 for (let j = 1; j <= n; j++) {
26921 const tmp = dp[j];
26922 dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
26923 prev = tmp;
26924 }
26925 }
26926 return dp[n];
26927 }
26928 function suggest(tag) {
26929 let best = null;
26930 let bestD = Infinity;
26931 for (const known of KNOWN) {
26932 const d = distance(tag, known);
26933 if (d < bestD) {
26934 bestD = d;
26935 best = known;
26936 }
26937 }
26938 return bestD > 0 && bestD <= 3 ? best : null;
26939 }
26940 function folderFor(tag) {
26941 return tag.startsWith("wpd-") ? tag.slice(4) : tag;
26942 }
26943 function warnFor(tag, sample) {
26944 if (warnedTags.has(tag)) {
26945 return;
26946 }
26947 warnedTags.add(tag);
26948 const isKnown = KNOWN.has(tag);
26949 if (isKnown) {
26950 const folder = folderFor(tag);
26951 console.error(
26952 `[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.
26953
26954 Fix — side-effect-import the component module from wherever you render it:
26955
26956 import '<rel>/ui/components/${folder}/${folder}';
26957
26958 Or pull every wpd-* component in one go (heavier — only do this from an entry bundle):
26959
26960 import '<rel>/ui/components';
26961
26962 See docs/components-reference.md for the full list.`,
26963 "\nFirst offending element:",
26964 sample
26965 );
26966 return;
26967 }
26968 const guess = suggest(tag);
26969 if (guess) {
26970 console.error(
26971 `[wp.desktop] <${tag}> is not a registered wpd-* component. Did you mean <${guess}>?
26972
26973 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'.`,
26974 "\nFirst offending element:",
26975 sample
26976 );
26977 return;
26978 }
26979 console.error(
26980 `[wp.desktop] <${tag}> looks like a wpd-* tag but no component by that name exists.
26981
26982 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.`,
26983 "\nFirst offending element:",
26984 sample
26985 );
26986 }
26987 function checkElement(el) {
26988 const tag = el.tagName.toLowerCase();
26989 if (!tag.startsWith("wpd-")) {
26990 return;
26991 }
26992 if (warnedTags.has(tag)) {
26993 return;
26994 }
26995 if (customElements.get(tag)) {
26996 return;
26997 }
26998 let settled = false;
26999 customElements.whenDefined(tag).then(() => {
27000 settled = true;
27001 });
27002 setTimeout(() => {
27003 if (settled) {
27004 return;
27005 }
27006 if (customElements.get(tag)) {
27007 return;
27008 }
27009 warnFor(tag, el);
27010 }, WARN_GRACE_MS);
27011 }
27012 function walk(root) {
27013 if (root instanceof Element) {
27014 checkElement(root);
27015 if (root.shadowRoot) {
27016 observeRoot(root.shadowRoot);
27017 }
27018 }
27019 const all2 = root.querySelectorAll("*");
27020 for (let i = 0; i < all2.length; i++) {
27021 const el = all2[i];
27022 checkElement(el);
27023 if (el.shadowRoot) {
27024 observeRoot(el.shadowRoot);
27025 }
27026 }
27027 }
27028 function observeRoot(root) {
27029 if (observedRoots.has(root)) {
27030 return;
27031 }
27032 observedRoots.add(root);
27033 walk(root);
27034 const mo = new MutationObserver((records) => {
27035 for (let i = 0; i < records.length; i++) {
27036 const added = records[i].addedNodes;
27037 for (let j = 0; j < added.length; j++) {
27038 const node = added[j];
27039 if (node.nodeType === 1) {
27040 walk(node);
27041 }
27042 }
27043 }
27044 });
27045 mo.observe(root, { childList: true, subtree: true });
27046 }
27047 function patchAttachShadow() {
27048 const proto = Element.prototype;
27049 const original = proto.attachShadow;
27050 if (original.__wpdPatched) {
27051 return;
27052 }
27053 const patched = function(init2) {
27054 const root = original.call(this, init2);
27055 if (root.mode === "open") {
27056 observeRoot(root);
27057 }
27058 return root;
27059 };
27060 patched.__wpdPatched = true;
27061 proto.attachShadow = patched;
27062 }
27063 function startMissingImportWarner() {
27064 if (started$2) {
27065 return;
27066 }
27067 if (typeof document === "undefined") {
27068 return;
27069 }
27070 started$2 = true;
27071 patchAttachShadow();
27072 observeRoot(document);
27073 }
27074 const TRASHABLE_SHORTCUT_KINDS = /* @__PURE__ */ new Set(["post"]);
27075 function getMyWordpressTrashApi() {
27076 const api = window.wp?.desktop?.myWordpress;
27077 return api && typeof api.trashEntity === "function" ? api : null;
27078 }
27079 const TRASH_DROP_ACTIVE_ATTR = "data-desktop-mode-trash-drop-active";
27080 const RECYCLE_BIN_WINDOW_ID = "desktop-mode-recycle-bin";
27081 const BIN_TILE_SELECTORS = [
27082 `.desktop-mode-file-tile[data-file-ref="${RECYCLE_BIN_WINDOW_ID}"]`,
27083 `[data-icon-id="${RECYCLE_BIN_WINDOW_ID}"]`,
27084 `[data-system-id="${RECYCLE_BIN_WINDOW_ID}"]`
27085 ];
27086 function findBinTile() {
27087 for (const sel of BIN_TILE_SELECTORS) {
27088 const el = document.querySelector(sel);
27089 if (el instanceof HTMLElement) {
27090 return el;
27091 }
27092 }
27093 return null;
27094 }
27095 let _installed = false;
27096 let _dockDeregister = null;
27097 let _windowDeregister = null;
27098 let _binMutationObserver = null;
27099 function isDesktopFilePayload(session) {
27100 return session.payload.type === "desktop-file";
27101 }
27102 function isShortcutPayload(session) {
27103 return session.payload.type === "shortcut";
27104 }
27105 function isTrashableShortcut(data) {
27106 if (!data.kind || !data.ref || !data.entityId) {
27107 return false;
27108 }
27109 if (!TRASHABLE_SHORTCUT_KINDS.has(data.kind)) {
27110 return false;
27111 }
27112 const numericRef = Number.parseInt(data.ref, 10);
27113 if (!Number.isFinite(numericRef) || numericRef <= 0) {
27114 return false;
27115 }
27116 return getMyWordpressTrashApi() !== null;
27117 }
27118 function registerOn(dragManager, id, el) {
27119 return dragManager.registerDropTarget({
27120 id,
27121 element: el,
27122 // Override the ghost-chip label: while the cursor is over
27123 // the bin the user is trashing, not creating a shortcut /
27124 // moving the placement. The DragManager swaps this in for
27125 // the payload-default "Drop here to create shortcut" /
27126 // "Drop here to move" chip text whenever this target is the
27127 // current accept-mode target.
27128 acceptLabel: __("Move to Trash", "desktop-mode"),
27129 // Reject the drop UP FRONT when the viewer can't trash the
27130 // payload's placement (e.g. an item inside a read-only
27131 // shared folder, or someone else's tile in a shared
27132 // namespace). `accept` flipping to `false` means the
27133 // drop-active highlight never lights up + onDrop never
27134 // fires + the drag manager surfaces a `rejected` outcome.
27135 // The user sees the icon snap back instead of attempting a
27136 // REST call that would 403 and only log to the console.
27137 accept: (payload) => {
27138 if (payload.type === "desktop-file") {
27139 const data = payload.data;
27140 const placement = data?.placement;
27141 if (!placement) {
27142 return false;
27143 }
27144 if (placement.file?.ref === RECYCLE_BIN_WINDOW_ID) {
27145 return false;
27146 }
27147 return placement.canTrash !== false;
27148 }
27149 if (payload.type === "shortcut") {
27150 const data = payload.data;
27151 return isTrashableShortcut(data);
27152 }
27153 return false;
27154 },
27155 onEnter: () => {
27156 el.setAttribute(TRASH_DROP_ACTIVE_ATTR, "");
27157 },
27158 onLeave: () => {
27159 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
27160 },
27161 onDrop: (session) => {
27162 el.removeAttribute(TRASH_DROP_ACTIVE_ATTR);
27163 if (isDesktopFilePayload(session)) {
27164 const placement = session.payload.data.placement;
27165 void trashByFileType(placement);
27166 return;
27167 }
27168 if (isShortcutPayload(session)) {
27169 const data = session.payload.data;
27170 const api = getMyWordpressTrashApi();
27171 if (!api?.trashEntity || !data.entityId) {
27172 return;
27173 }
27174 const numericRef = Number.parseInt(data.ref, 10);
27175 if (!Number.isFinite(numericRef) || numericRef <= 0) {
27176 return;
27177 }
27178 void api.trashEntity(data.entityId, numericRef).catch(
27179 (err) => {
27180 console.error(
27181 "[desktop-mode] recycle-bin: shortcut trash failed:",
27182 err
27183 );
27184 }
27185 );
27186 }
27187 }
27188 });
27189 }
27190 function installRecycleBinDropTargets(dragManager) {
27191 if (_installed) {
27192 return;
27193 }
27194 _installed = true;
27195 const reprobeTile = () => {
27196 const el = findBinTile();
27197 if (!el) {
27198 _dockDeregister?.();
27199 _dockDeregister = null;
27200 return;
27201 }
27202 if (_dockDeregister && getRegisteredElementId(dragManager) === el) {
27203 return;
27204 }
27205 _dockDeregister?.();
27206 _dockDeregister = registerOn(dragManager, "recycle-bin-dock", el);
27207 };
27208 reprobeTile();
27209 document.addEventListener("desktop-mode-files-changed", reprobeTile);
27210 addAction(
27211 HOOKS.DESKTOP_ICONS_RENDERED,
27212 "desktop-mode/files/recycle-bin-icons-target",
27213 reprobeTile
27214 );
27215 addAction(
27216 HOOKS.DOCK_AFTER_RENDER,
27217 "desktop-mode/files/recycle-bin-dock-target",
27218 reprobeTile
27219 );
27220 if (typeof MutationObserver !== "undefined") {
27221 _binMutationObserver = new MutationObserver(() => {
27222 reprobeTile();
27223 });
27224 const desktopArea = document.getElementById("desktop-mode-area") ?? document.body;
27225 _binMutationObserver.observe(desktopArea, {
27226 childList: true,
27227 subtree: true
27228 });
27229 }
27230 addAction(
27231 HOOKS.WINDOW_OPENED,
27232 "desktop-mode/files/recycle-bin-window-target",
27233 (detail) => {
27234 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
27235 return;
27236 }
27237 _windowDeregister?.();
27238 _windowDeregister = null;
27239 const el = document.querySelector(
27240 "[data-desktop-mode-recycle-bin-root]"
27241 );
27242 if (el instanceof HTMLElement) {
27243 _windowDeregister = registerOn(
27244 dragManager,
27245 "recycle-bin-window",
27246 el
27247 );
27248 }
27249 }
27250 );
27251 addAction(
27252 HOOKS.WINDOW_CLOSED,
27253 "desktop-mode/files/recycle-bin-window-cleanup",
27254 (detail) => {
27255 if (detail.windowId !== RECYCLE_BIN_WINDOW_ID) {
27256 return;
27257 }
27258 _windowDeregister?.();
27259 _windowDeregister = null;
27260 }
27261 );
27262 }
27263 function getRegisteredElementId(dragManager) {
27264 const t = dragManager.debug().listTargets().find((target2) => target2.id === "recycle-bin-dock");
27265 return t ? t.element : null;
27266 }
27267 let started$1 = false;
27268 let highWaterMs = 0;
27269 function startFilesHeartbeat() {
27270 if (started$1) {
27271 return;
27272 }
27273 started$1 = true;
27274 heartbeat.contribute("desktop_mode_files_subscribe", () => {
27275 const state2 = getFilesState();
27276 const folderVersions = {};
27277 for (const [id, folder] of state2.folders) {
27278 folderVersions[String(id)] = folder.updatedAtMs;
27279 }
27280 return {
27281 folderVersions,
27282 placementsVersion: highWaterMs,
27283 sharesVersion: sharesStore().state.sharesVersion
27284 };
27285 });
27286 heartbeat.subscribe("desktop_mode_files", (payload) => {
27287 applyDelta(payload);
27288 });
27289 }
27290 function applyDelta(payload) {
27291 const folders = payload.folders ?? [];
27292 for (const folder of folders) {
27293 upsertFolder(folder, "remote");
27294 if (folder.updatedAtMs > highWaterMs) {
27295 highWaterMs = folder.updatedAtMs;
27296 }
27297 }
27298 const placements = payload.placements ?? [];
27299 for (const placement of placements) {
27300 upsertPlacement(placement, "remote");
27301 if (placement.updatedAtMs > highWaterMs) {
27302 highWaterMs = placement.updatedAtMs;
27303 }
27304 }
27305 const removed = payload.removed ?? {};
27306 for (const id of removed.folders ?? []) {
27307 removeFolder(id, "remote");
27308 }
27309 for (const id of removed.placements ?? []) {
27310 removePlacement(id, "remote");
27311 }
27312 if (typeof payload.serverTimeMs === "number" && payload.serverTimeMs > highWaterMs) {
27313 highWaterMs = payload.serverTimeMs;
27314 }
27315 const pending2 = payload.shares?.pending;
27316 if (Array.isArray(pending2) && pending2.length > 0) {
27317 ingestPendingInvites(pending2);
27318 }
27319 if (payload.truncated) {
27320 const hydrated = Array.from(getFilesState().hydratedFolders);
27321 for (const folderId of hydrated) {
27322 void listPlacements(folderId).then((res) => {
27323 setFolderPlacements(folderId, res.placements);
27324 }).catch(() => {
27325 });
27326 }
27327 }
27328 }
27329 let started = false;
27330 const unsubscribers = [];
27331 function startFilesRestoreSync() {
27332 if (started) {
27333 return;
27334 }
27335 started = true;
27336 const onChange = (payload) => {
27337 const detail = payload;
27338 if (!detail || detail.action !== "untrashed") {
27339 return;
27340 }
27341 resyncFromServer();
27342 };
27343 unsubscribers.push(
27344 subscribe$2("desktop-mode.placement.changed", onChange),
27345 subscribe$2("desktop-mode.shortcut.changed", onChange),
27346 subscribe$2("desktop-mode.folder.changed", onChange)
27347 );
27348 }
27349 function resyncFromServer() {
27350 void listFolders().then((res) => {
27351 setFolders(res.folders);
27352 }).catch((err) => {
27353 console.error(
27354 "[desktop-mode] files restore-sync: listFolders failed",
27355 err
27356 );
27357 });
27358 const hydrated = Array.from(getFilesState().hydratedFolders);
27359 for (const folderId of hydrated) {
27360 void listPlacements(folderId).then((res) => {
27361 setFolderPlacements(folderId, res.placements);
27362 }).catch((err) => {
27363 console.error(
27364 "[desktop-mode] files restore-sync: listPlacements failed for",
27365 folderId,
27366 err
27367 );
27368 });
27369 }
27370 }
27371 const MENU_CLASS = "desktop-mode-wallpaper-menu";
27372 let activeMenu = null;
27373 function isWallpaperMenuOpen() {
27374 return activeMenu !== null;
27375 }
27376 let openGeneration = 0;
27377 function openWallpaperMenu(host, pos, items, options = {}) {
27378 closeWallpaperMenu();
27379 const myGen = ++openGeneration;
27380 openWithShellOverlays(
27381 () => myGen === openGeneration,
27382 () => openWallpaperMenuImmediate(host, pos, items, options)
27383 );
27384 }
27385 function openWallpaperMenuImmediate(host, pos, items, options = {}) {
27386 if (items.length === 0) {
27387 return;
27388 }
27389 items = items.slice().sort((a, b) => {
27390 const sa = typeof a.sort === "number" ? a.sort : 100;
27391 const sb = typeof b.sort === "number" ? b.sort : 100;
27392 if (sa !== sb) {
27393 return sa - sb;
27394 }
27395 return a.label.localeCompare(b.label);
27396 });
27397 const menu = document.createElement("wpd-context-menu");
27398 menu.setAttribute("open", "");
27399 menu.classList.add(MENU_CLASS);
27400 menu.style.left = `${pos.x}px`;
27401 menu.style.top = `${pos.y}px`;
27402 const itemById = /* @__PURE__ */ new Map();
27403 let activeFlyout2 = null;
27404 let activeFlyoutParent = null;
27405 const closeActiveFlyout = () => {
27406 if (activeFlyout2) {
27407 activeFlyout2.remove();
27408 activeFlyout2 = null;
27409 activeFlyoutParent = null;
27410 }
27411 };
27412 for (const item of items) {
27413 itemById.set(item.id, item);
27414 const opt = document.createElement("wpd-context-menu-option");
27415 opt.dataset.menuItemId = item.id;
27416 opt.setAttribute("value", item.id);
27417 if (item.heading) {
27418 opt.setAttribute("heading", "");
27419 }
27420 if (item.disabled) {
27421 opt.setAttribute("disabled", "");
27422 }
27423 if (item.icon) {
27424 opt.setAttribute("icon", sanitizeClass(item.icon));
27425 }
27426 const hasChildren2 = Array.isArray(item.children) && item.children.length > 0;
27427 if (hasChildren2) {
27428 opt.setAttribute("has-children", "");
27429 }
27430 opt.textContent = item.label;
27431 opt.addEventListener("mouseenter", () => {
27432 if (hasChildren2) {
27433 openFlyout2(item, opt);
27434 return;
27435 }
27436 closeActiveFlyout();
27437 });
27438 menu.appendChild(opt);
27439 }
27440 menu.addEventListener("wpd-context-menu-pick", (e) => {
27441 const detail = e.detail;
27442 const item = itemById.get(detail.id) ?? null;
27443 if (!item) {
27444 return;
27445 }
27446 if (Array.isArray(item.children) && item.children.length > 0) {
27447 e.stopPropagation();
27448 if (activeFlyoutParent && activeFlyoutParent.id === item.id) {
27449 closeActiveFlyout();
27450 return;
27451 }
27452 const anchor = menu.querySelector(
27453 `[data-menu-item-id="${item.id}"]`
27454 );
27455 if (anchor) {
27456 openFlyout2(item, anchor);
27457 }
27458 return;
27459 }
27460 closeWallpaperMenu();
27461 void item.onClick(new MouseEvent("click"));
27462 });
27463 function openFlyout2(parent, anchor) {
27464 closeActiveFlyout();
27465 const fly = document.createElement("wpd-context-menu");
27466 fly.setAttribute("open", "");
27467 fly.classList.add(MENU_CLASS, `${MENU_CLASS}--flyout`);
27468 fly.dataset.parentId = parent.id;
27469 const sortedKids = (parent.children ?? []).slice().sort((a, b) => {
27470 const sa = typeof a.sort === "number" ? a.sort : 100;
27471 const sb = typeof b.sort === "number" ? b.sort : 100;
27472 if (sa !== sb) {
27473 return sa - sb;
27474 }
27475 return a.label.localeCompare(b.label);
27476 });
27477 for (const child of sortedKids) {
27478 const kopt = document.createElement("wpd-context-menu-option");
27479 kopt.dataset.menuItemId = child.id;
27480 kopt.setAttribute("value", child.id);
27481 if (child.icon) {
27482 kopt.setAttribute("icon", sanitizeClass(child.icon));
27483 }
27484 if (child.disabled) {
27485 kopt.setAttribute("disabled", "");
27486 }
27487 if (child.checked) {
27488 kopt.setAttribute("checked", "");
27489 }
27490 kopt.textContent = child.label;
27491 kopt.addEventListener("wpd-context-menu-pick", (e) => {
27492 e.stopPropagation();
27493 closeWallpaperMenu();
27494 void child.onClick(new MouseEvent("click"));
27495 });
27496 fly.appendChild(kopt);
27497 }
27498 document.body.appendChild(fly);
27499 activeFlyout2 = fly;
27500 activeFlyoutParent = parent;
27501 positionFlyout2(fly, anchor);
27502 }
27503 function positionFlyout2(fly, anchor) {
27504 const ar = anchor.getBoundingClientRect();
27505 fly.style.position = "fixed";
27506 fly.style.left = `${ar.right}px`;
27507 fly.style.top = `${ar.top}px`;
27508 const fr = fly.getBoundingClientRect();
27509 if (fr.right > window.innerWidth) {
27510 fly.style.left = `${Math.max(0, ar.left - fr.width)}px`;
27511 }
27512 if (fr.bottom > window.innerHeight) {
27513 fly.style.top = `${Math.max(0, window.innerHeight - fr.height - 8)}px`;
27514 }
27515 }
27516 host.appendChild(menu);
27517 activeMenu = menu;
27518 const rect = menu.getBoundingClientRect();
27519 if (rect.right > window.innerWidth) {
27520 menu.style.left = `${Math.max(0, window.innerWidth - rect.width - 8)}px`;
27521 }
27522 if (rect.bottom > window.innerHeight) {
27523 menu.style.top = `${Math.max(0, window.innerHeight - rect.height - 8)}px`;
27524 }
27525 const detach = attachDismissable(menu, {
27526 close: () => closeWallpaperMenu(),
27527 siblingSelectors: [`.${MENU_CLASS}--flyout`],
27528 excludeOutsideTarget: options.excludeOutsideTarget
27529 });
27530 menu.addEventListener("wallpaper-menu-closed", detach);
27531 doAction("desktop-mode.wallpaper-menu.opened", { items: items.map((i) => i.id) });
27532 }
27533 function closeWallpaperMenu() {
27534 if (!activeMenu) {
27535 return;
27536 }
27537 document.querySelectorAll(`.${MENU_CLASS}--flyout`).forEach((el) => el.remove());
27538 activeMenu.dispatchEvent(new CustomEvent("wallpaper-menu-closed"));
27539 activeMenu.remove();
27540 activeMenu = null;
27541 doAction("desktop-mode.wallpaper-menu.closed", {});
27542 }
27543 function buildMenuItems(deps2) {
27544 const builtIn = [
27545 {
27546 id: "create-folder",
27547 label: deps2.labels.createFolder,
27548 icon: "dashicons-portfolio",
27549 sort: 10,
27550 onClick: () => deps2.createFolder()
27551 },
27552 {
27553 id: "new-url",
27554 label: deps2.labels.newUrl,
27555 icon: "dashicons-admin-links",
27556 sort: 12,
27557 onClick: () => deps2.createUrl()
27558 },
27559 {
27560 id: "sort-by",
27561 label: deps2.labels.sortHeading,
27562 icon: "dashicons-sort",
27563 sort: 16,
27564 onClick: () => void 0,
27565 children: [
27566 {
27567 id: "sort-name-asc",
27568 label: deps2.labels.sortNameAsc,
27569 sort: 10,
27570 checked: deps2.currentSortMode === "name-asc",
27571 onClick: () => deps2.sortIcons("name-asc")
27572 },
27573 {
27574 id: "sort-name-desc",
27575 label: deps2.labels.sortNameDesc,
27576 sort: 20,
27577 checked: deps2.currentSortMode === "name-desc",
27578 onClick: () => deps2.sortIcons("name-desc")
27579 },
27580 {
27581 id: "sort-date-desc",
27582 label: deps2.labels.sortDateDesc,
27583 sort: 30,
27584 checked: deps2.currentSortMode === "date-desc",
27585 onClick: () => deps2.sortIcons("date-desc")
27586 },
27587 {
27588 id: "sort-date-asc",
27589 label: deps2.labels.sortDateAsc,
27590 sort: 40,
27591 checked: deps2.currentSortMode === "date-asc",
27592 onClick: () => deps2.sortIcons("date-asc")
27593 }
27594 ]
27595 },
27596 ...deps2.includeShowDesktop === false ? [] : [
27597 {
27598 id: "show-desktop",
27599 label: deps2.labels.showDesktop,
27600 icon: "dashicons-desktop",
27601 sort: 20,
27602 onClick: () => deps2.toggleShowDesktop()
27603 }
27604 ],
27605 {
27606 id: "os-settings",
27607 label: deps2.labels.osSettings,
27608 icon: "dashicons-admin-generic",
27609 sort: 30,
27610 onClick: () => deps2.openOsSettings()
27611 }
27612 ];
27613 const serverItems = (deps2.serverItems ?? []).map(
27614 (s) => serverItemToMenuItem(s, deps2)
27615 );
27616 const merged = [...builtIn, ...serverItems];
27617 const filtered = applyFilters(
27618 "desktop-mode.wallpaper-context-menu",
27619 merged
27620 );
27621 return Array.isArray(filtered) ? filtered : merged;
27622 }
27623 function serverItemToMenuItem(server, deps2) {
27624 return {
27625 id: server.id,
27626 label: server.label,
27627 icon: server.icon,
27628 sort: server.sort,
27629 disabled: server.disabled,
27630 onClick: () => {
27631 if (server.callbackId) {
27632 const cb = deps2.serverCallbacks?.[server.callbackId];
27633 if (typeof cb === "function") {
27634 return cb();
27635 }
27636 }
27637 doAction("desktop-mode.wallpaper-context-menu.activated", {
27638 id: server.id,
27639 callbackId: server.callbackId ?? ""
27640 });
27641 }
27642 };
27643 }
27644 function sanitizeClass(raw) {
27645 return raw.replace(/[^a-zA-Z0-9_-]/g, "");
27646 }
27647 const ROOT_CLASS = "desktop-mode-url-dialog";
27648 let active = null;
27649 function closeUrlDialog() {
27650 if (!active) {
27651 return;
27652 }
27653 active.dispatchEvent(new CustomEvent("url-dialog-closed"));
27654 active.remove();
27655 active = null;
27656 doAction("desktop-mode.files.url-dialog.closed", {});
27657 }
27658 function openUrlDialog(options) {
27659 closeUrlDialog();
27660 const decision = applyFilters(
27661 "desktop-mode.files.url-dialog",
27662 null,
27663 options
27664 );
27665 if (decision === false) {
27666 return;
27667 }
27668 const overlay = document.createElement("div");
27669 overlay.className = `${ROOT_CLASS}__overlay desktop-mode-create-folder-dialog__overlay`;
27670 overlay.setAttribute("role", "presentation");
27671 const dialog2 = document.createElement("div");
27672 dialog2.className = `${ROOT_CLASS} desktop-mode-create-folder-dialog`;
27673 dialog2.setAttribute("role", "dialog");
27674 dialog2.setAttribute("aria-modal", "true");
27675 dialog2.setAttribute("aria-labelledby", `${ROOT_CLASS}-title`);
27676 const title = document.createElement("h2");
27677 title.id = `${ROOT_CLASS}-title`;
27678 title.className = "desktop-mode-create-folder-dialog__title";
27679 title.textContent = options.title;
27680 dialog2.appendChild(title);
27681 if (options.description) {
27682 const desc = document.createElement("p");
27683 desc.className = `${ROOT_CLASS}__description`;
27684 desc.textContent = options.description;
27685 dialog2.appendChild(desc);
27686 }
27687 const nameField = document.createElement("wpd-text-field");
27688 nameField.setAttribute("label", options.nameLabel ?? "Name");
27689 nameField.setAttribute("value", options.initialName ?? "");
27690 nameField.setAttribute("placeholder", "My web app");
27691 nameField.setAttribute("autocomplete", "off");
27692 dialog2.appendChild(nameField);
27693 const urlField = document.createElement("wpd-text-field");
27694 urlField.setAttribute("label", options.urlLabel ?? "URL");
27695 urlField.setAttribute("value", options.initialUrl ?? "https://");
27696 urlField.setAttribute("placeholder", "https://example.com");
27697 urlField.setAttribute("type", "url");
27698 urlField.setAttribute("autocomplete", "off");
27699 dialog2.appendChild(urlField);
27700 const error = document.createElement("p");
27701 error.className = "desktop-mode-create-folder-dialog__error";
27702 error.hidden = true;
27703 error.setAttribute("role", "alert");
27704 dialog2.appendChild(error);
27705 const actions = document.createElement("div");
27706 actions.className = "desktop-mode-create-folder-dialog__actions";
27707 const cancel = document.createElement("button");
27708 cancel.type = "button";
27709 cancel.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--secondary";
27710 cancel.textContent = "Cancel";
27711 const submit = document.createElement("button");
27712 submit.type = "button";
27713 submit.className = "desktop-mode-create-folder-dialog__btn desktop-mode-create-folder-dialog__btn--primary";
27714 submit.textContent = options.submitLabel ?? "Create";
27715 actions.appendChild(cancel);
27716 actions.appendChild(submit);
27717 dialog2.appendChild(actions);
27718 overlay.appendChild(dialog2);
27719 document.body.appendChild(overlay);
27720 active = overlay;
27721 queueMicrotask(() => {
27722 const input = nameField.shadowRoot?.querySelector("input");
27723 input?.focus();
27724 input?.select();
27725 });
27726 doAction("desktop-mode.files.url-dialog.opened", {});
27727 const readValue = (field) => {
27728 const v = field.value;
27729 if (typeof v === "string") {
27730 return v;
27731 }
27732 return field.shadowRoot?.querySelector("input")?.value ?? "";
27733 };
27734 const setBusy = (busy) => {
27735 nameField.disabled = busy;
27736 urlField.disabled = busy;
27737 cancel.disabled = busy;
27738 submit.disabled = busy;
27739 dialog2.classList.toggle("desktop-mode-create-folder-dialog--busy", busy);
27740 };
27741 const showError = (msg) => {
27742 error.textContent = msg;
27743 error.hidden = false;
27744 };
27745 const doCancel = () => {
27746 closeUrlDialog();
27747 options.onCancel?.();
27748 };
27749 const doSubmit = async () => {
27750 const url = readValue(urlField).trim();
27751 if (!url) {
27752 showError("Please enter a URL.");
27753 return;
27754 }
27755 const finalUrl = /^[a-z][a-z0-9+\-.]*:/i.test(url) ? url : `https://${url}`;
27756 try {
27757 new URL(finalUrl);
27758 } catch {
27759 showError("That doesn't look like a valid URL.");
27760 return;
27761 }
27762 const name = readValue(nameField).trim();
27763 error.hidden = true;
27764 setBusy(true);
27765 try {
27766 await options.onSubmit({ name, url: finalUrl });
27767 closeUrlDialog();
27768 } catch (err) {
27769 setBusy(false);
27770 showError(err instanceof Error ? err.message : "Could not save.");
27771 }
27772 };
27773 cancel.addEventListener("click", () => doCancel());
27774 submit.addEventListener("click", () => void doSubmit());
27775 overlay.addEventListener("click", (e) => {
27776 if (e.target === overlay) {
27777 doCancel();
27778 }
27779 });
27780 const onKey = (e) => {
27781 if (e.key === "Escape") {
27782 e.preventDefault();
27783 doCancel();
27784 } else if (e.key === "Enter" && !e.isComposing) {
27785 e.preventDefault();
27786 void doSubmit();
27787 }
27788 };
27789 dialog2.addEventListener("keydown", onKey);
27790 overlay.addEventListener("url-dialog-closed", () => {
27791 dialog2.removeEventListener("keydown", onKey);
27792 });
27793 }
27794 const _earlyReadyQueue = [];
27795 let _earlyReady = false;
27796 (function installEarlyDesktopShim() {
27797 const w = window;
27798 if (!w.wp) {
27799 w.wp = {};
27800 }
27801 if (w.wp.desktop) {
27802 return;
27803 }
27804 const shim = {
27805 whenReady(cb) {
27806 if (typeof cb !== "function") {
27807 return;
27808 }
27809 if (_earlyReady) {
27810 Promise.resolve().then(cb);
27811 return;
27812 }
27813 _earlyReadyQueue.push(cb);
27814 },
27815 ready(cb) {
27816 shim.whenReady(cb);
27817 },
27818 isReady() {
27819 return _earlyReady;
27820 }
27821 };
27822 w.wp.desktop = shim;
27823 })();
27824 const OS_SETTINGS_WINDOW_ID = "desktop-mode-os-settings";
27825 let _idleBootQueue = [];
27826 let _idleBootTimeout = Number.POSITIVE_INFINITY;
27827 let _idleBootScheduled = false;
27828 function scheduleIdleBoot(cb, timeout = 1500) {
27829 _idleBootQueue.push(cb);
27830 if (timeout < _idleBootTimeout) {
27831 _idleBootTimeout = timeout;
27832 }
27833 if (_idleBootScheduled) {
27834 return;
27835 }
27836 _idleBootScheduled = true;
27837 const drain = () => {
27838 const callbacks = _idleBootQueue;
27839 _idleBootQueue = [];
27840 _idleBootTimeout = Number.POSITIVE_INFINITY;
27841 _idleBootScheduled = false;
27842 for (const fn of callbacks) {
27843 try {
27844 fn();
27845 } catch (err) {
27846 if (typeof console !== "undefined") {
27847 console.error(
27848 "[desktop-mode] scheduleIdleBoot callback threw:",
27849 err
27850 );
27851 }
27852 }
27853 }
27854 };
27855 if (typeof window.requestIdleCallback === "function") {
27856 window.requestIdleCallback(drain, { timeout: _idleBootTimeout });
27857 } else {
27858 window.setTimeout(drain, 0);
27859 }
27860 }
27861 function init() {
27862 const config = window.desktopModeConfig;
27863 if (!config) {
27864 return;
27865 }
27866 const desktopArea = document.getElementById("desktop-mode-area");
27867 if (!desktopArea) {
27868 return;
27869 }
27870 const manager = new WindowManager(desktopArea);
27871 const wallpaperEl = document.getElementById("desktop-mode-wallpaper");
27872 const pluginUrl = config.pluginUrl || "";
27873 let wallpaperLayer = null;
27874 if (wallpaperEl) {
27875 wallpaperLayer = new WallpaperLayer(wallpaperEl, pluginUrl);
27876 }
27877 const widgetsEl = document.getElementById("desktop-mode-widgets");
27878 let widgetLayer = null;
27879 registerBuiltInWidgets();
27880 installDefaultDockRailRenderer();
27881 if (widgetsEl) {
27882 widgetLayer = new WidgetLayer(widgetsEl, pluginUrl);
27883 }
27884 registerModule({
27885 id: "pixijs",
27886 url: `${pluginUrl}/assets/vendor/pixi.min.js`,
27887 isReady: () => typeof window.PIXI !== "undefined"
27888 });
27889 const osSettings = new OsSettings(
27890 {
27891 mediaUrl: config.mediaUrl,
27892 restNonce: config.restNonce,
27893 canUpload: !!config.canUpload,
27894 isAdmin: !!config.currentUserIsAdmin,
27895 aiPlatformSettings: config.aiPlatformSettings ?? null,
27896 aiPlatformSettingsUrl: config.aiPlatformSettingsUrl ?? "",
27897 extendedOptions: config.extendedOptions ?? null,
27898 extendedOptionsUrl: config.extendedOptionsUrl ?? "",
27899 osSettingsPanelBundleUrl: config.osSettingsPanelBundleUrl ?? ""
27900 },
27901 wallpaperLayer ?? new WallpaperLayer(document.createElement("div"), pluginUrl)
27902 );
27903 osSettings.apply();
27904 const aiAssistant = new AiAssistantStub(
27905 {
27906 aiSearchUrl: config.aiSearchUrl ?? "",
27907 aiSearchStreamUrl: config.aiSearchStreamUrl ?? "",
27908 restNonce: config.restNonce,
27909 // Transport picker lives in OS Settings → AI Settings. Read
27910 // live (not captured at construction) so a change applies on
27911 // the next search without a page reload.
27912 getTransport: () => osSettings.getOsSettingsSnapshot().ai.transport
27913 },
27914 config.aiAssistantBundleUrl ?? ""
27915 );
27916 aiAssistant.attachAsk(
27917 createAsk({
27918 config: () => config,
27919 fallbackContext: () => ({
27920 close: () => aiAssistant.close(),
27921 openInWindow: (url, title, icon) => {
27922 manager.open({
27923 url,
27924 title,
27925 icon: icon ?? "dashicons-admin-generic"
27926 });
27927 },
27928 confirm: (msg) => wpdConfirm({ message: msg })
27929 })
27930 })
27931 );
27932 const dragBridge = new DragBridge();
27933 const dragManager = new DragManager();
27934 document.addEventListener(DRAG_EVENTS.START, (e) => {
27935 const detail = e.detail;
27936 const payload = detail?.payload;
27937 if (!payload) {
27938 return;
27939 }
27940 if (payload.type !== "shortcut" && payload.type !== "desktop-file") {
27941 return;
27942 }
27943 const bridgePayload = payload.data?.bridgePayload;
27944 if (bridgePayload) {
27945 dragBridge.start(bridgePayload);
27946 }
27947 });
27948 document.addEventListener(DRAG_EVENTS.END, () => {
27949 dragBridge.end();
27950 });
27951 scheduleIdleBoot(() => installIframeDropTargets(dragManager));
27952 window.addEventListener("message", (e) => {
27953 if (e.origin !== window.location.origin) {
27954 return;
27955 }
27956 const data = e.data;
27957 if (!data || data.type !== "desktop-mode-drop-failed") {
27958 return;
27959 }
27960 showToast({
27961 message: "Could not insert into the editor."
27962 });
27963 });
27964 registerPalette({
27965 id: "desktop-mode-ai-assistant",
27966 label: "AI Assistant",
27967 open: () => aiAssistant.open(),
27968 close: () => aiAssistant.close(),
27969 isOpen: () => aiAssistant.isOpen
27970 });
27971 installPaletteShortcut();
27972 installWindowSwitcherShortcut(manager);
27973 installDesktopArrowShortcuts(manager);
27974 scheduleIdleBoot(() => {
27975 new IframeCommandBridge({
27976 manager,
27977 adminUrl: config.adminUrl
27978 }).install();
27979 new ShellCommandHarvester({
27980 manager,
27981 adminUrl: config.adminUrl
27982 }).install();
27983 });
27984 document.addEventListener("desktop-mode-open-ai", () => {
27985 openPaletteOnly("desktop-mode-ai-assistant");
27986 });
27987 const bottomDockEl = document.getElementById("desktop-mode-dock");
27988 const shellEl = document.getElementById("desktop-mode-shell");
27989 const shellBody = shellEl?.querySelector(
27990 ".desktop-mode-shell__body"
27991 );
27992 let layoutDispatcher = null;
27993 const nativeWindows = createNativeWindowSync({
27994 manager,
27995 appendSystemTile: (item) => layoutDispatcher?.appendSystemTile(item),
27996 removeSystemTile: (id) => layoutDispatcher?.removeSystemTile(id)
27997 });
27998 const syncNativeWindows = nativeWindows.sync;
27999 bindNativeUrlRemap({
28000 getSnapshot: () => osSettings.getOsSettingsSnapshot(),
28001 openById: (id) => nativeWindows.openById(id),
28002 adminUrl: config.adminUrl
28003 });
28004 const findDockEntryForUrl2 = (url) => {
28005 const targetSlug = deriveWindowId(url, config.adminUrl);
28006 const items = layoutDispatcher ? layoutDispatcher.getMenuItems() : config.dockItems ?? [];
28007 for (const item of items) {
28008 if (deriveWindowId(item.url, config.adminUrl) === targetSlug) {
28009 return {
28010 title: item.title,
28011 icon: item.icon,
28012 url: item.url,
28013 submenu: item.submenu,
28014 multi: item.multi
28015 };
28016 }
28017 for (const sub of item.submenu ?? []) {
28018 if (deriveWindowId(sub.url, config.adminUrl) === targetSlug) {
28019 return {
28020 title: sub.title,
28021 // Sub-menu entries inherit the parent tile's
28022 // icon — that's the dock's own convention and
28023 // avoids painting a generic glyph on a window
28024 // the user knows by its parent's identity.
28025 icon: item.icon,
28026 // `url` holds the PARENT tile's landing page, so
28027 // the new window's synthetic "back to parent"
28028 // tab links to the dock URL (themes.php) rather
28029 // than to the sub-page itself.
28030 url: item.url,
28031 multi: item.multi
28032 };
28033 }
28034 }
28035 }
28036 return null;
28037 };
28038 bindAdminLinkDispatch({
28039 adminUrl: config.adminUrl,
28040 deriveSlug: (url) => deriveWindowId(url, config.adminUrl),
28041 openWindow: (windowConfig) => {
28042 void manager.open(windowConfig);
28043 },
28044 findDockEntry: findDockEntryForUrl2
28045 });
28046 registerNativeUrlRemap({
28047 id: "desktop-mode-posts",
28048 nativeWindowId: "desktop-mode-posts",
28049 matches: (_url, parsed) => {
28050 if (!parsed.pathname.endsWith("/edit.php")) {
28051 return false;
28052 }
28053 const postType = parsed.searchParams.get("post_type");
28054 return !postType || postType === "post";
28055 },
28056 enabled: (snapshot) => snapshot.nativePostsEnabled === true
28057 });
28058 registerNativeUrlRemap({
28059 id: "desktop-mode-pages",
28060 nativeWindowId: "desktop-mode-pages",
28061 matches: (_url, parsed) => {
28062 if (!parsed.pathname.endsWith("/edit.php")) {
28063 return false;
28064 }
28065 return parsed.searchParams.get("post_type") === "page";
28066 },
28067 enabled: (snapshot) => snapshot.nativePagesEnabled === true
28068 });
28069 registerNativeUrlRemap({
28070 id: "desktop-mode-users",
28071 nativeWindowId: "desktop-mode-users",
28072 matches: (_url, parsed) => parsed.pathname.endsWith("/users.php"),
28073 enabled: (snapshot) => snapshot.nativeUsersEnabled === true
28074 });
28075 registerNativeUrlRemap({
28076 id: "desktop-mode-user-edit",
28077 nativeWindowId: "desktop-mode-user-edit",
28078 matches: (_url, parsed) => {
28079 const path = parsed.pathname;
28080 if (path.endsWith("/profile.php")) {
28081 return true;
28082 }
28083 if (path.endsWith("/user-edit.php")) {
28084 return parsed.searchParams.has("user_id");
28085 }
28086 return false;
28087 },
28088 enabled: (snapshot) => snapshot.nativeUsersEnabled === true,
28089 onMatch: (_url, parsed) => {
28090 const userId = parseInt(
28091 parsed.searchParams.get("user_id") ?? "0",
28092 10
28093 );
28094 if (userId > 0) {
28095 setUserEditTarget(userId);
28096 }
28097 }
28098 });
28099 registerNativeUrlRemap({
28100 id: "desktop-mode-comments",
28101 nativeWindowId: "desktop-mode-comments",
28102 matches: (_url, parsed) => parsed.pathname.endsWith("/edit-comments.php"),
28103 enabled: (snapshot) => snapshot.nativeCommentsEnabled === true
28104 });
28105 registerNativeUrlRemap({
28106 id: "desktop-mode-plugins",
28107 nativeWindowId: "desktop-mode-plugins",
28108 matches: (_url, parsed) => {
28109 const path = parsed.pathname;
28110 return path.endsWith("/plugins.php") || path.endsWith("/plugin-install.php");
28111 },
28112 enabled: (snapshot) => snapshot.nativePluginsEnabled === true,
28113 onMatch: (_url, parsed) => {
28114 const tab = parsed.pathname.endsWith("/plugin-install.php") ? "browse" : "installed";
28115 void Promise.resolve().then(() => tabTarget).then((m) => {
28116 m.setPluginsWindowTab(tab);
28117 });
28118 }
28119 });
28120 if (bottomDockEl && shellEl && shellBody && config.dockItems) {
28121 desktopArea.classList.add("desktop-mode-area--with-dock");
28122 const initialLayout = osSettings.getOsSettingsSnapshot().desktopLayout;
28123 const renderIcons2 = (icons) => {
28124 renderDesktopIcons(desktopArea, icons, {
28125 openWindow: nativeWindows.openById,
28126 manager,
28127 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
28128 });
28129 };
28130 layoutDispatcher = createLayoutDispatcher(
28131 {
28132 shellRoot: shellEl,
28133 shellBody,
28134 bottomDockEl,
28135 desktopArea,
28136 windowManager: manager,
28137 adminUrl: config.adminUrl,
28138 renderIcons: renderIcons2,
28139 getSettings: () => {
28140 const snap = osSettings.getOsSettingsSnapshot();
28141 return {
28142 itemVisibility: snap.itemVisibility,
28143 dockOrder: snap.dockOrder
28144 };
28145 }
28146 },
28147 initialLayout,
28148 config.dockItems,
28149 config.desktopIcons
28150 );
28151 layoutDispatcher.appendSystemTile(
28152 {
28153 id: OS_SETTINGS_WINDOW_ID,
28154 title: "OS Settings",
28155 icon: "dashicons-desktop",
28156 // "Open" for the dock dot means "open on the currently
28157 // active desktop." OS Settings on another desktop
28158 // shouldn't paint the dot on the active view.
28159 isOpen: () => {
28160 const win = manager.getById(OS_SETTINGS_WINDOW_ID);
28161 if (!win) {
28162 return false;
28163 }
28164 return (win.config.desktopId || manager.getActiveDesktopId()) === manager.getActiveDesktopId();
28165 },
28166 onOpen: openOsSettings
28167 },
28168 "core"
28169 );
28170 if (!isStandaloneDisplay()) {
28171 layoutDispatcher.appendSystemTile(
28172 getInstallTileDef(
28173 config.pwa?.appName || "WordPress",
28174 showToast
28175 ),
28176 "core"
28177 );
28178 }
28179 window.matchMedia("(display-mode: standalone)").addEventListener("change", (e) => {
28180 if (e.matches) {
28181 layoutDispatcher?.removeSystemTile(
28182 "desktop-mode-pwa-install"
28183 );
28184 }
28185 });
28186 void isLikelyInstalled().then((installed2) => {
28187 if (installed2) {
28188 layoutDispatcher?.removeSystemTile(
28189 "desktop-mode-pwa-install"
28190 );
28191 }
28192 });
28193 }
28194 function openOsSettings(opts = {}) {
28195 if (opts.tabId) {
28196 osSettings.activeTabId = opts.tabId;
28197 }
28198 void manager.open({
28199 id: OS_SETTINGS_WINDOW_ID,
28200 baseId: OS_SETTINGS_WINDOW_ID,
28201 url: "#os-settings",
28202 title: "OS Settings",
28203 icon: "dashicons-desktop",
28204 native: true,
28205 render: (body) => osSettings.renderPanel(body),
28206 width: 820,
28207 height: 720,
28208 minWidth: 560,
28209 minHeight: 480
28210 });
28211 if (opts.tabId) {
28212 osSettings.focusTab(opts.tabId);
28213 }
28214 }
28215 function openBugReport() {
28216 void manager.open({
28217 id: BUG_REPORT_WINDOW_ID,
28218 baseId: BUG_REPORT_WINDOW_ID,
28219 url: `#${BUG_REPORT_WINDOW_ID}`,
28220 title: "Report a bug",
28221 icon: "dashicons-buddicons-replies",
28222 native: true,
28223 render: (body) => renderBugReport(body),
28224 width: 560,
28225 height: 620,
28226 minWidth: 420,
28227 minHeight: 480
28228 });
28229 }
28230 document.addEventListener("desktop-mode-open-bug-report", () => {
28231 openBugReport();
28232 });
28233 if (layoutDispatcher) {
28234 layoutDispatcher.appendSystemTile(
28235 {
28236 id: BUG_REPORT_WINDOW_ID,
28237 title: "Report a bug",
28238 icon: "dashicons-buddicons-replies",
28239 isOpen: () => {
28240 const win = manager.getById(BUG_REPORT_WINDOW_ID);
28241 if (!win) {
28242 return false;
28243 }
28244 return (win.config.desktopId || manager.getActiveDesktopId()) === manager.getActiveDesktopId();
28245 },
28246 onOpen: openBugReport
28247 },
28248 "core"
28249 );
28250 layoutDispatcher.appendSystemTile(
28251 getExitDesktopModeTileDef(),
28252 "core"
28253 );
28254 }
28255 const dock = layoutDispatcher?.getPrimary() ?? null;
28256 void syncNativeWindows(
28257 Array.isArray(config.nativeWindows) ? config.nativeWindows : []
28258 );
28259 const hasSession = hasRestorableSession(config.session);
28260 const sessionRestore = hasSession ? restoreSession(manager, config, desktopArea).catch((err) => {
28261 if (typeof console !== "undefined") {
28262 console.error("[desktop-mode] session restore failed:", err);
28263 }
28264 }) : Promise.resolve();
28265 const defaultEnabled = config.defaultWindow?.enabled !== false;
28266 const defaultUrlEarly = config.defaultWindow?.url ?? "";
28267 const isNativeDefault = typeof defaultUrlEarly === "string" && defaultUrlEarly.startsWith("native:");
28268 if (shouldAutoOpenCurrentPage({
28269 fromPortal: config.fromPortal,
28270 fromPortalIntent: config.fromPortalIntent,
28271 hasSession,
28272 defaultEnabled,
28273 isNativeDefault
28274 })) {
28275 void sessionRestore.then(
28276 () => openCurrentPage(manager, config).catch((err) => {
28277 if (typeof console !== "undefined") {
28278 console.error("[desktop-mode] openCurrentPage failed:", err);
28279 }
28280 })
28281 );
28282 }
28283 const saveSession = createSessionSaver(manager, config);
28284 wireSessionEvents(saveSession);
28285 const setDefaultWindow = async (url) => {
28286 try {
28287 const response = await trackedFetch(
28288 manager,
28289 config.defaultWindowUrl,
28290 {
28291 method: "POST",
28292 credentials: "same-origin",
28293 headers: {
28294 "Content-Type": "application/json",
28295 "X-WP-Nonce": config.restNonce
28296 },
28297 body: JSON.stringify({ url })
28298 },
28299 { source: "desktop-mode/default-window" }
28300 );
28301 if (!response.ok) {
28302 throw new Error(`HTTP ${response.status}`);
28303 }
28304 const data = await response.json();
28305 config.defaultWindow = data;
28306 document.dispatchEvent(
28307 new CustomEvent("desktop-mode-default-window-changed", {
28308 detail: data
28309 })
28310 );
28311 } catch (err) {
28312 doAction(HOOKS.SHELL_ERROR, { scope: "default-window-save", error: err });
28313 if (typeof console !== "undefined") {
28314 console.error(
28315 "[desktop-mode] Failed to save default window:",
28316 err
28317 );
28318 }
28319 }
28320 };
28321 manager.onToggleStartupRequested = (win) => {
28322 const currentPref = config.defaultWindow;
28323 const isNative = !!win.config.native;
28324 const winValue = isNative ? `native:${win.id}` : win.getCurrentUrl();
28325 const matchesCurrent = isNative ? currentPref?.url === winValue : urlMatchKey(currentPref?.url ?? "") === urlMatchKey(winValue);
28326 const alreadyDefault = !!currentPref?.enabled && matchesCurrent;
28327 void setDefaultWindow(alreadyDefault ? null : winValue);
28328 };
28329 if (config.defaultWindow?.enabled && config.fromPortal && !config.fromPortalIntent && !hasSession && isNativeDefault) {
28330 const nativeId = defaultUrlEarly.slice("native:".length);
28331 queueMicrotask(() => {
28332 if (nativeId === OS_SETTINGS_WINDOW_ID) {
28333 openOsSettings();
28334 return;
28335 }
28336 void nativeWindows.openById(nativeId);
28337 });
28338 }
28339 const placeSystemTile = (item) => {
28340 layoutDispatcher?.appendSystemTile(item);
28341 };
28342 const syncServerWidgets = createWidgetRegistrySync({
28343 layer: widgetLayer
28344 });
28345 void syncServerWidgets(
28346 Array.isArray(config.serverWidgets) ? config.serverWidgets : []
28347 );
28348 const syncServerWallpapers = createWallpaperRegistrySync({
28349 osSettings
28350 });
28351 void syncServerWallpapers(
28352 Array.isArray(config.serverWallpapers) ? config.serverWallpapers : []
28353 );
28354 const syncServerCommands = createCommandRegistrySync();
28355 void syncServerCommands(
28356 Array.isArray(config.serverCommandScripts) ? config.serverCommandScripts : [],
28357 Array.isArray(config.serverCommands) ? config.serverCommands : []
28358 );
28359 const syncServerSettingsTabs = createSettingsTabRegistrySync();
28360 void syncServerSettingsTabs(
28361 Array.isArray(config.serverSettingsTabScripts) ? config.serverSettingsTabScripts : [],
28362 Array.isArray(config.serverSettingsTabs) ? config.serverSettingsTabs : []
28363 );
28364 const syncServerTitleBarButtons = createTitleBarButtonRegistrySync();
28365 void syncServerTitleBarButtons(
28366 Array.isArray(config.serverTitleBarButtonScripts) ? config.serverTitleBarButtonScripts : []
28367 );
28368 const syncServerUnfocusEffects = createUnfocusEffectRegistrySync();
28369 void syncServerUnfocusEffects(
28370 Array.isArray(config.serverUnfocusEffectScripts) ? config.serverUnfocusEffectScripts : []
28371 );
28372 startUnfocusEngine({ manager, osSettings });
28373 const syncServerDockRailRenderers = createDockRailRendererSync();
28374 void syncServerDockRailRenderers(
28375 Array.isArray(config.serverDockRailRendererScripts) ? config.serverDockRailRendererScripts : []
28376 );
28377 const syncServerWindowThemes = createWindowThemeRegistrySync();
28378 void syncServerWindowThemes(
28379 Array.isArray(config.serverWindowThemeScripts) ? config.serverWindowThemeScripts : [],
28380 Array.isArray(config.serverWindowThemes) ? config.serverWindowThemes : []
28381 );
28382 registerBuiltInControls();
28383 const syncServerWindowControls = createWindowControlRegistrySync();
28384 void syncServerWindowControls(
28385 Array.isArray(config.serverWindowControlScripts) ? config.serverWindowControlScripts : [],
28386 Array.isArray(config.serverWindowControls) ? config.serverWindowControls : []
28387 );
28388 const syncServerWindowSlots = createWindowSlotRegistrySync();
28389 void syncServerWindowSlots(
28390 Array.isArray(config.serverWindowSlotScripts) ? config.serverWindowSlotScripts : [],
28391 Array.isArray(config.serverWindowSlots) ? config.serverWindowSlots : []
28392 );
28393 applyServerWindowNotices(
28394 Array.isArray(config.serverWindowNotices) ? config.serverWindowNotices : []
28395 );
28396 const syncServerWindowChromes = createWindowChromeRegistrySync();
28397 void syncServerWindowChromes(
28398 Array.isArray(config.serverWindowChromeScripts) ? config.serverWindowChromeScripts : [],
28399 Array.isArray(config.serverWindowChromes) ? config.serverWindowChromes : []
28400 );
28401 const connectionBridge = createConnectionBridge(manager);
28402 attachBroadcastBus(manager);
28403 scheduleIdleBoot(() => installBroadcastReceiver());
28404 installWindowLoadingTransitions();
28405 addAction(
28406 "desktop-mode.shell.toast",
28407 "desktop-mode/shell-toast",
28408 (payload) => {
28409 if (!payload || typeof payload.message !== "string") {
28410 return;
28411 }
28412 showToast({
28413 message: payload.message,
28414 action: payload.action,
28415 duration: payload.duration
28416 });
28417 }
28418 );
28419 const cfgWithBin = config;
28420 const cfgCountRaw = cfgWithBin.recycleBinCount;
28421 startRecycleBinBadge(
28422 Number(cfgCountRaw) || 0,
28423 typeof cfgWithBin.recycleBinCountUrl === "string" ? cfgWithBin.recycleBinCountUrl : ""
28424 );
28425 registerBuiltInPeekRenderers({
28426 getRecycleBinCount: _currentRecycleBinBadge
28427 });
28428 window.__desktopModeConnectionBridge = connectionBridge;
28429 addAction(HOOKS.WINDOW_CLOSED, "desktop-mode/connection-cleanup", (e) => {
28430 if (e?.windowId) {
28431 connectionBridge.onWindowClosed(e.windowId);
28432 }
28433 });
28434 addAction(HOOKS.IFRAME_READY, "desktop-mode/connection-rearm", (e) => {
28435 if (e?.windowId) {
28436 connectionBridge.onIframeReady(e.windowId);
28437 }
28438 });
28439 const registerWindow = createRegisterWindow(manager);
28440 const renderIcons = (icons) => {
28441 if (layoutDispatcher) {
28442 layoutDispatcher.applyDesktopIcons(icons);
28443 return;
28444 }
28445 renderDesktopIcons(desktopArea, icons, {
28446 openWindow: nativeWindows.openById,
28447 manager,
28448 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
28449 });
28450 };
28451 const refreshMenu = bindMenuRefresh({
28452 layoutDispatcher,
28453 desktopArea,
28454 config,
28455 syncNativeWindows,
28456 syncServerWidgets,
28457 syncServerWallpapers,
28458 syncServerCommands,
28459 syncServerSettingsTabs,
28460 syncServerTitleBarButtons,
28461 syncServerUnfocusEffects,
28462 syncServerDockRailRenderers,
28463 renderIcons
28464 });
28465 osSettings.subscribeOsSettings((snapshot) => {
28466 if (!layoutDispatcher) {
28467 return;
28468 }
28469 const prevLayout = layoutDispatcher.getLayout();
28470 layoutDispatcher.setLayout(snapshot.desktopLayout);
28471 desktopApi.dock = layoutDispatcher.getPrimary();
28472 desktopApi.sideDock = layoutDispatcher.getSide();
28473 desktopApi.desktopLayout = snapshot.desktopLayout;
28474 if (prevLayout === snapshot.desktopLayout) {
28475 layoutDispatcher.refresh();
28476 }
28477 syncShortcutsWithVisibility(
28478 snapshot.itemVisibility,
28479 snapshot.dockPromotedPositions
28480 );
28481 setCurrentLayout(snapshot.desktopLayout);
28482 });
28483 installShortcutsSync(
28484 () => osSettings.getOsSettingsSnapshot().itemVisibility,
28485 () => osSettings.getOsSettingsSnapshot().dockPromotedPositions
28486 );
28487 setCurrentLayout(osSettings.getOsSettingsSnapshot().desktopLayout);
28488 const desktopApi = buildPublicApi({
28489 manager,
28490 dock,
28491 layoutDispatcher,
28492 osSettings,
28493 iconsApi,
28494 filesApi,
28495 saveSession,
28496 widgetLayer,
28497 registerWindow,
28498 openWindowById: nativeWindows.openById,
28499 openNewWindowById: nativeWindows.openNewById,
28500 placeSystemTile,
28501 setDefaultWindow,
28502 refreshMenu,
28503 openOsSettings,
28504 aiAssistant,
28505 dragBridge,
28506 dragManager,
28507 connect: connectionBridge.connect,
28508 getConnection: connectionBridge.getConnection,
28509 config
28510 });
28511 installPublicApi(desktopApi);
28512 scheduleIdleBoot(() => installRecycleBinDropTargets(dragManager));
28513 bootHeartbeatBus();
28514 scheduleIdleBoot(() => bootNonceRefresh());
28515 bootStickyNotes({
28516 host: desktopArea,
28517 config,
28518 // Only boot when the Gutenberg Guidelines experiment is live
28519 // server-side; otherwise the layer's REST probes would 404. The
28520 // flag is `undefined` on shells older than the one that added it
28521 // → the layer treats that as available (boot and swallow).
28522 available: config.stickyNotes?.available,
28523 getActiveDesktopId: () => manager.getActiveDesktopId(),
28524 openArtifact: (url, title) => {
28525 const id = deriveWindowId(url, config.adminUrl);
28526 void manager.open({
28527 id,
28528 baseId: id,
28529 url,
28530 title,
28531 icon: "dashicons-edit-page"
28532 });
28533 },
28534 onError: (message) => {
28535 showToast({ message });
28536 }
28537 });
28538 installOpenDeps({
28539 openUrl: ({ id, url, title, icon }) => {
28540 if (tryNativeUrlRemap(url)) {
28541 return true;
28542 }
28543 void manager.open({ id, baseId: id, url, title, icon });
28544 return true;
28545 },
28546 openNativeWindow: (id) => nativeWindows.openById(id),
28547 deriveWindowId: (url) => deriveWindowId(url, config.adminUrl)
28548 });
28549 setUserAssociations(
28550 config.userFileAssociations ?? {}
28551 );
28552 if (typeof config.filesUrl === "string" && config.filesUrl) {
28553 installRestDeps({
28554 baseUrl: config.filesUrl,
28555 nonce: config.restNonce
28556 });
28557 const rootHost = document.getElementById("desktop-mode-area");
28558 if (rootHost) {
28559 const layerHandle = mountFilesLayer(rootHost, 0);
28560 const reveal = () => {
28561 if (!desktopArea.classList.contains("desktop-mode-area--booting")) {
28562 return;
28563 }
28564 requestAnimationFrame(() => {
28565 desktopArea.classList.remove("desktop-mode-area--booting");
28566 });
28567 };
28568 const safetyTimer = setTimeout(reveal, 2e3);
28569 void layerHandle.hydrated.then(() => {
28570 clearTimeout(safetyTimer);
28571 reveal();
28572 });
28573 }
28574 }
28575 scheduleIdleBoot(() => startFilesHeartbeat());
28576 scheduleIdleBoot(() => startFilesRestoreSync());
28577 scheduleIdleBoot(() => bootPresenceProbe());
28578 doAction(HOOKS.COMPONENTS_REGISTERED, { tags: [...WPD_COMPONENT_TAGS] });
28579 registerBuiltInCommands();
28580 bootstrapPwa(config, showToast);
28581 const overlayPreload = () => {
28582 preloadShellOverlays(config.shellOverlaysBundleUrl ?? "");
28583 preloadWindowSystem(config.windowSystemBundleUrl ?? "");
28584 };
28585 if (typeof window.requestIdleCallback === "function") {
28586 window.requestIdleCallback(overlayPreload, { timeout: 1500 });
28587 } else {
28588 window.setTimeout(overlayPreload, 0);
28589 }
28590 doAction(HOOKS.INIT, { config });
28591 _earlyReady = true;
28592 const queued = _earlyReadyQueue.splice(0);
28593 for (const cb of queued) {
28594 try {
28595 cb();
28596 } catch (err) {
28597 doAction(HOOKS.SHELL_ERROR, {
28598 scope: "when-ready-cb",
28599 error: err
28600 });
28601 if (typeof console !== "undefined") {
28602 console.error("[desktop-mode] whenReady cb threw:", err);
28603 }
28604 }
28605 }
28606 osSettings.apply();
28607 widgetLayer?.hydrate();
28608 window.addEventListener("pagehide", () => {
28609 wallpaperLayer?.teardownActive();
28610 widgetLayer?.disposeAll();
28611 });
28612 bindShellLifecycle();
28613 bindTopWindowLinkInterceptor(manager, config);
28614 const relayoutRoot = (transform, persist2 = true) => {
28615 const root = filesApi.store.getState().placementsByFolder.get(0) ?? [];
28616 const ordered = transform(root);
28617 const rowsPerCol = Math.max(
28618 1,
28619 Math.floor((desktopArea.clientHeight - 16) / 110)
28620 );
28621 const occupied = /* @__PURE__ */ new Set();
28622 let i = 0;
28623 for (const p of ordered) {
28624 const cell = snapToEmptyCell(
28625 16 + Math.floor(i / rowsPerCol) * 96,
28626 16 + i % rowsPerCol * 110,
28627 occupied,
28628 desktopArea
28629 );
28630 occupied.add(`${cell.col},${cell.row}`);
28631 i++;
28632 if (p.x === cell.x && p.y === cell.y) {
28633 continue;
28634 }
28635 filesApi.store.upsertPlacement({
28636 ...p,
28637 x: cell.x,
28638 y: cell.y,
28639 sortOrder: i
28640 });
28641 if (!persist2) {
28642 continue;
28643 }
28644 void updatePlacement(p.id, {
28645 x: cell.x,
28646 y: cell.y,
28647 sortOrder: i
28648 }).catch((err) => {
28649 console.error("[desktop-mode] relayout persist failed", err);
28650 });
28651 }
28652 };
28653 const rootSortTransform = (mode) => (arr) => {
28654 const sorted = arr.slice();
28655 switch (mode) {
28656 case "name-asc":
28657 sorted.sort(
28658 (a, b) => a.file.title.localeCompare(b.file.title)
28659 );
28660 break;
28661 case "name-desc":
28662 sorted.sort(
28663 (a, b) => b.file.title.localeCompare(a.file.title)
28664 );
28665 break;
28666 case "date-asc":
28667 sorted.sort((a, b) => a.updatedAtMs - b.updatedAtMs);
28668 break;
28669 case "date-desc":
28670 sorted.sort((a, b) => b.updatedAtMs - a.updatedAtMs);
28671 break;
28672 }
28673 return sorted;
28674 };
28675 const ROOT_SORT_MODE_KEY = "desktop-mode:root-sort-mode";
28676 const isRootSortMode = (v) => v === "name-asc" || v === "name-desc" || v === "date-asc" || v === "date-desc";
28677 let rootSortMode = (() => {
28678 try {
28679 const raw = window.localStorage.getItem(ROOT_SORT_MODE_KEY);
28680 return isRootSortMode(raw) ? raw : null;
28681 } catch {
28682 return null;
28683 }
28684 })();
28685 const setRootSortMode = (mode) => {
28686 rootSortMode = mode;
28687 try {
28688 if (mode) {
28689 window.localStorage.setItem(ROOT_SORT_MODE_KEY, mode);
28690 } else {
28691 window.localStorage.removeItem(ROOT_SORT_MODE_KEY);
28692 }
28693 } catch {
28694 }
28695 };
28696 addAction(
28697 "desktop-mode.files.tile-manually-placed",
28698 "desktop-mode/root-sort-clear",
28699 (payload) => {
28700 const folderId = payload?.folderId;
28701 if (folderId === 0) {
28702 setRootSortMode(null);
28703 }
28704 }
28705 );
28706 if (typeof ResizeObserver !== "undefined") {
28707 let lastW = desktopArea.clientWidth;
28708 let lastH = desktopArea.clientHeight;
28709 const ro = new ResizeObserver(() => {
28710 if (!rootSortMode) {
28711 return;
28712 }
28713 const w = desktopArea.clientWidth;
28714 const h = desktopArea.clientHeight;
28715 if (w === lastW && h === lastH) {
28716 return;
28717 }
28718 lastW = w;
28719 lastH = h;
28720 relayoutRoot(rootSortTransform(rootSortMode), false);
28721 });
28722 ro.observe(desktopArea);
28723 }
28724 let pointerdownOnWallpaper = false;
28725 desktopArea.addEventListener("pointerdown", (e) => {
28726 if (!e.isPrimary) {
28727 return;
28728 }
28729 pointerdownOnWallpaper = e.target === desktopArea;
28730 });
28731 desktopArea.addEventListener("click", (e) => {
28732 if (!osSettings.state.showDesktopOnWallpaperClick) {
28733 return;
28734 }
28735 if (e.target !== desktopArea) {
28736 return;
28737 }
28738 if (!pointerdownOnWallpaper) {
28739 return;
28740 }
28741 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
28742 return;
28743 }
28744 if (isWallpaperMenuOpen()) {
28745 return;
28746 }
28747 if (dragManager.recentlyEndedDrag()) {
28748 return;
28749 }
28750 manager.toggleShowDesktop();
28751 });
28752 desktopArea.addEventListener("contextmenu", (e) => {
28753 if (e.target !== desktopArea) {
28754 return;
28755 }
28756 e.preventDefault();
28757 const clientX = e.clientX;
28758 const clientY = e.clientY;
28759 (() => {
28760 if (desktopArea.classList.contains("desktop-mode-area--overview")) {
28761 return;
28762 }
28763 if (isWallpaperMenuOpen()) {
28764 closeWallpaperMenu();
28765 return;
28766 }
28767 const dropClient = { x: clientX, y: clientY };
28768 const cellAtClick = () => {
28769 const rect = desktopArea.getBoundingClientRect();
28770 const rawX = Math.max(0, dropClient.x - rect.left);
28771 const rawY = Math.max(0, dropClient.y - rect.top);
28772 const occupied = buildOccupiedSet(
28773 filesApi.store.getState().placementsByFolder.get(0) ?? []
28774 );
28775 return snapToEmptyCell(rawX, rawY, occupied, desktopArea);
28776 };
28777 const createUrlPlacement = (dialogTitle, description) => {
28778 openUrlDialog({
28779 title: dialogTitle,
28780 description,
28781 nameLabel: "Name",
28782 urlLabel: "URL",
28783 submitLabel: "Create",
28784 onSubmit: async ({ name, url }) => {
28785 const cell = cellAtClick();
28786 const placement = await createPlacement({
28787 type: "link",
28788 ref: url,
28789 parentId: 0,
28790 x: cell.x,
28791 y: cell.y,
28792 meta: name ? { name } : void 0
28793 });
28794 filesApi.store.upsertPlacement(placement);
28795 }
28796 });
28797 };
28798 const items = buildMenuItems({
28799 createFolder: () => {
28800 openCreateFolderDialog({
28801 onSubmit: async (name) => {
28802 const folder = await createFolder({ name });
28803 const cell = cellAtClick();
28804 const placement = await createPlacement({
28805 type: "folder",
28806 ref: String(folder.id),
28807 parentId: 0,
28808 x: cell.x,
28809 y: cell.y
28810 });
28811 filesApi.store.upsertFolder(folder);
28812 filesApi.store.upsertPlacement(placement);
28813 }
28814 });
28815 },
28816 createUrl: () => createUrlPlacement(
28817 "New URL",
28818 "Opens the URL in a new browser tab."
28819 ),
28820 toggleShowDesktop: () => manager.toggleShowDesktop(),
28821 openOsSettings: () => openOsSettings(),
28822 sortIcons: (mode) => {
28823 setRootSortMode(mode);
28824 relayoutRoot(rootSortTransform(mode));
28825 },
28826 currentSortMode: rootSortMode,
28827 includeShowDesktop: !osSettings.state.showDesktopOnWallpaperClick,
28828 labels: {
28829 createFolder: "New folder",
28830 showDesktop: "Show desktop",
28831 osSettings: "OS Settings",
28832 sortHeading: "Sort by",
28833 sortNameAsc: "Name (A → Z)",
28834 sortNameDesc: "Name (Z → A)",
28835 sortDateAsc: "Date (oldest first)",
28836 sortDateDesc: "Date (newest first)",
28837 newUrl: "New URL"
28838 },
28839 serverItems: config.serverWallpaperMenuItems ?? []
28840 });
28841 openWallpaperMenu(
28842 document.body,
28843 { x: clientX, y: clientY },
28844 items
28845 );
28846 })();
28847 });
28848 void Promise.resolve().then(() => index).then((mod) => {
28849 mod.bootOsFileDrop({
28850 config: config.dropConfig,
28851 mediaUrl: config.mediaUrl,
28852 restNonce: config.restNonce
28853 });
28854 });
28855 document.dispatchEvent(
28856 new CustomEvent("desktop-mode-init", {
28857 detail: { config, restored: hasSession }
28858 })
28859 );
28860 }
28861 startMissingImportWarner();
28862 if (document.readyState === "loading") {
28863 document.addEventListener("DOMContentLoaded", init);
28864 } else {
28865 init();
28866 }
28867 const _initial = {
28868 tab: null,
28869 requestedAt: 0
28870 };
28871 let _store = null;
28872 function getStore() {
28873 if (_store) {
28874 return _store;
28875 }
28876 const w = window;
28877 const factory = w.wp?.desktop?.createSharedStore;
28878 if (typeof factory !== "function") {
28879 return null;
28880 }
28881 _store = factory(
28882 "desktop-mode/plugins-window/tab-target",
28883 () => ({ ..._initial })
28884 );
28885 return _store;
28886 }
28887 function setPluginsWindowTab(tab) {
28888 const store2 = getStore();
28889 if (store2) {
28890 store2.state.tab = tab;
28891 store2.state.requestedAt = Date.now();
28892 store2.notify();
28893 return;
28894 }
28895 const w = window;
28896 w._wpdPluginsWindowTab = { tab, requestedAt: Date.now() };
28897 }
28898 function consumePluginsWindowTab() {
28899 const store2 = getStore();
28900 if (store2) {
28901 const tab = store2.state.tab;
28902 if (tab !== null) {
28903 store2.state.tab = null;
28904 store2.state.requestedAt = 0;
28905 store2.notify();
28906 }
28907 return tab;
28908 }
28909 const w = window;
28910 const prev = w._wpdPluginsWindowTab;
28911 if (prev) {
28912 w._wpdPluginsWindowTab = { tab: null, requestedAt: 0 };
28913 return prev.tab;
28914 }
28915 return null;
28916 }
28917 function subscribePluginsWindowTab(cb) {
28918 const store2 = getStore();
28919 if (!store2) {
28920 return () => {
28921 };
28922 }
28923 return store2.subscribe((state2) => cb({ ...state2 }));
28924 }
28925 const tabTarget = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
28926 __proto__: null,
28927 consumePluginsWindowTab,
28928 setPluginsWindowTab,
28929 subscribePluginsWindowTab
28930 }, Symbol.toStringTag, { value: "Module" }));
28931 const FILE_DROP_HOOKS = {
28932 /**
28933 * Filter — fires once per drop, after the manager has parsed
28934 * the OS `DataTransfer` into `File[]` and BEFORE the mime /
28935 * size filter runs.
28936 *
28937 * Signature: `(files: File[], ctx: DropContext) => File[]`.
28938 * Return an empty array to abort the drop silently.
28939 */
28940 FILES_DETECTED: "desktop-mode.drop.files-detected",
28941 /**
28942 * Action — fires after the mime / size filter has rejected
28943 * one or more files. Payload: `{ rejections: DropRejection[],
28944 * context: DropContext }`. The shell toasts a default message;
28945 * subscribers can surface a custom UX (a side panel with the
28946 * list, an analytics call).
28947 */
28948 FILES_REJECTED: "desktop-mode.drop.files-rejected",
28949 /**
28950 * Filter — fires per file before the upload dialog renders.
28951 * Receives `DropFileEntry` (the underlying file + the
28952 * manager's default `fields`). Mutate `fields` (or return a
28953 * new object) to change what the user sees in the form.
28954 *
28955 * Signature: `(entry: DropFileEntry, ctx: DropContext)
28956 * => DropFileEntry`.
28957 */
28958 DIALOG_FIELDS: "desktop-mode.drop.dialog-fields",
28959 /**
28960 * Filter — last call before the manager `POST`s to
28961 * `wp/v2/media`. Receives `{ file: File, fields:
28962 * DropDialogFields, mime: string }`. Return `null` to cancel
28963 * the upload entirely (e.g. a plugin handled it via a
28964 * different endpoint).
28965 *
28966 * Signature: `(payload, ctx: DropContext) => payload | null`.
28967 */
28968 BEFORE_UPLOAD: "desktop-mode.drop.before-upload",
28969 /**
28970 * Action — fires once `BEFORE_UPLOAD` has cleared and the XHR
28971 * is `open()`ed, immediately before `send()`. Payload:
28972 * `{ file: File, fields: DropDialogFields, context: DropContext,
28973 * abort: () => void }`. The `abort` handle aborts the in-flight
28974 * request; the manager rejects with `UploadAbortedError` and
28975 * fires `UPLOAD_FAILED` with that error.
28976 *
28977 * Pair with `UPLOAD_PROGRESS` to drive a progress UI; pair with
28978 * `AFTER_UPLOAD` / `UPLOAD_FAILED` to know when the upload ends.
28979 *
28980 * @since 0.31.0
28981 */
28982 UPLOAD_STARTED: "desktop-mode.drop.upload-started",
28983 /**
28984 * Action — fires for every `XMLHttpRequestUpload.progress` event.
28985 * Payload: `{ file: File, fields: DropDialogFields, context:
28986 * DropContext, loaded: number, total: number, indeterminate:
28987 * boolean }`. `total` is `0` and `indeterminate` is `true` when
28988 * the request body length isn't known (rare for multipart, but
28989 * possible on transcoding proxies); subscribers should treat
28990 * that as an indeterminate state.
28991 *
28992 * A synthetic 100%-loaded event is dispatched once the `upload`
28993 * stream emits `load` so a HUD can show a definite "wrapping up"
28994 * state while the server finishes the response.
28995 *
28996 * @since 0.31.0
28997 */
28998 UPLOAD_PROGRESS: "desktop-mode.drop.upload-progress",
28999 /**
29000 * Action — fires after a successful upload. Payload:
29001 * `{ file: File, result: DropUploadResult, fields:
29002 * DropDialogFields, context: DropContext }`.
29003 *
29004 * The `file` field carries the same `File` reference that
29005 * `UPLOAD_STARTED` / `UPLOAD_PROGRESS` exposed (i.e. the
29006 * payload returned by the `BEFORE_UPLOAD` filter, in case a
29007 * plugin swapped the file). Subscribers tracking per-file
29008 * state — progress HUDs, sequence counters — should match on
29009 * this identity rather than the filename: two drops of
29010 * `photo.jpg` from different folders would otherwise route
29011 * each other's success event to the wrong row.
29012 *
29013 * @since 0.31.0 the `file` field was added; pre-0.31.0 code
29014 * that destructured `{ result, fields, context }` keeps working.
29015 */
29016 AFTER_UPLOAD: "desktop-mode.drop.after-upload",
29017 /**
29018 * Action — fires after an upload fails. Payload:
29019 * `{ file: File, error: Error, context: DropContext }`.
29020 * `error` is an `UploadAbortedError` when the failure came
29021 * from the caller invoking the `abort()` handle on
29022 * `UPLOAD_STARTED`.
29023 *
29024 * `file` carries the same identity as `UPLOAD_STARTED` /
29025 * `UPLOAD_PROGRESS` / `AFTER_UPLOAD` — the post-`BEFORE_UPLOAD`
29026 * `File`, in case a plugin swapped it. Match by reference, not
29027 * filename: a HUD that keys its row map on the started-File
29028 * needs the same key here, otherwise the row stays stuck in
29029 * "running" after a failure when a `BEFORE_UPLOAD` filter
29030 * replaced the file.
29031 */
29032 UPLOAD_FAILED: "desktop-mode.drop.upload-failed"
29033 };
29034 const IFRAME_PASSTHROUGH_SELECTORS = [
29035 ".components-drop-zone",
29036 "[data-drop-zone]",
29037 ".uploader-window",
29038 ".media-frame-content"
29039 ];
29040 function dragHasFiles(ev) {
29041 const types = ev.dataTransfer?.types;
29042 if (!types) {
29043 return false;
29044 }
29045 const list2 = types;
29046 if (typeof list2.includes === "function") {
29047 return list2.includes("Files");
29048 }
29049 if (typeof list2.contains === "function") {
29050 return list2.contains("Files");
29051 }
29052 for (let i = 0; i < list2.length; i++) {
29053 if (list2[i] === "Files") {
29054 return true;
29055 }
29056 }
29057 return false;
29058 }
29059 function resolveWindowIdFromSource(source) {
29060 if (!source) {
29061 return void 0;
29062 }
29063 const iframes = document.querySelectorAll("iframe");
29064 for (const f of Array.from(iframes)) {
29065 if (f.contentWindow === source) {
29066 const host = f.closest("[data-window-id]");
29067 return host?.getAttribute("data-window-id") || void 0;
29068 }
29069 }
29070 return void 0;
29071 }
29072 function mountOsFileDropManager(opts) {
29073 const host = window;
29074 if (host.__desktopModeOsFileDropMounted) {
29075 return host.__desktopModeOsFileDropMounted;
29076 }
29077 if (!opts.config.enabled) {
29078 return mountNoOp();
29079 }
29080 const overlayEl = ensureDropOverlay();
29081 let dragDepth = 0;
29082 let dragWatchdog = null;
29083 const resetOverlay = () => {
29084 dragDepth = 0;
29085 overlayEl.classList.remove("is-active");
29086 if (dragWatchdog !== null) {
29087 clearTimeout(dragWatchdog);
29088 dragWatchdog = null;
29089 }
29090 };
29091 const bumpWatchdog = () => {
29092 if (dragWatchdog !== null) {
29093 clearTimeout(dragWatchdog);
29094 }
29095 dragWatchdog = setTimeout(resetOverlay, 250);
29096 };
29097 const onDragEnter = (ev) => {
29098 if (!dragHasFiles(ev)) {
29099 return;
29100 }
29101 ev.preventDefault();
29102 dragDepth++;
29103 overlayEl.classList.add("is-active");
29104 bumpWatchdog();
29105 };
29106 const onDragOver = (ev) => {
29107 if (!dragHasFiles(ev)) {
29108 return;
29109 }
29110 if (ev.defaultPrevented) {
29111 resetOverlay();
29112 return;
29113 }
29114 ev.preventDefault();
29115 if (ev.dataTransfer) {
29116 ev.dataTransfer.dropEffect = "copy";
29117 }
29118 bumpWatchdog();
29119 };
29120 const onDragLeave = () => {
29121 dragDepth = Math.max(0, dragDepth - 1);
29122 if (dragDepth === 0) {
29123 overlayEl.classList.remove("is-active");
29124 }
29125 };
29126 const onDrop = (ev) => {
29127 if (!dragHasFiles(ev)) {
29128 return;
29129 }
29130 if (ev.defaultPrevented) {
29131 resetOverlay();
29132 return;
29133 }
29134 ev.preventDefault();
29135 resetOverlay();
29136 const files = ev.dataTransfer?.files ? Array.from(ev.dataTransfer.files) : [];
29137 if (files.length === 0) {
29138 return;
29139 }
29140 const ctx = classifyDropTarget(ev);
29141 void handleFiles(files, ctx, opts);
29142 };
29143 const onDragEnd2 = () => resetOverlay();
29144 const onVisibilityChange = () => {
29145 if (document.visibilityState === "hidden") {
29146 resetOverlay();
29147 }
29148 };
29149 const onIframeMessage = (ev) => {
29150 if (ev.origin !== window.location.origin) {
29151 return;
29152 }
29153 const data = ev.data;
29154 if (!data || data.type !== "desktop-mode-os-file-drop") {
29155 return;
29156 }
29157 if (!Array.isArray(data.files) || data.files.length === 0) {
29158 return;
29159 }
29160 const files = data.files.filter((f) => f instanceof File);
29161 if (files.length === 0) {
29162 return;
29163 }
29164 const windowId = resolveWindowIdFromSource(ev.source);
29165 if (!windowId) {
29166 return;
29167 }
29168 const ctx = {
29169 surface: "iframe",
29170 windowId,
29171 x: typeof data.x === "number" ? data.x : 0,
29172 y: typeof data.y === "number" ? data.y : 0
29173 };
29174 dragDepth = 0;
29175 overlayEl.classList.remove("is-active");
29176 void handleFiles(files, ctx, opts);
29177 };
29178 window.addEventListener("dragenter", onDragEnter);
29179 window.addEventListener("dragover", onDragOver);
29180 window.addEventListener("dragleave", onDragLeave);
29181 window.addEventListener("drop", onDrop);
29182 window.addEventListener("dragend", onDragEnd2);
29183 document.addEventListener("visibilitychange", onVisibilityChange);
29184 window.addEventListener("blur", onDragEnd2);
29185 window.addEventListener("message", onIframeMessage);
29186 const manager = {
29187 dispose: () => {
29188 window.removeEventListener("dragenter", onDragEnter);
29189 window.removeEventListener("dragover", onDragOver);
29190 window.removeEventListener("dragleave", onDragLeave);
29191 window.removeEventListener("drop", onDrop);
29192 window.removeEventListener("dragend", onDragEnd2);
29193 document.removeEventListener(
29194 "visibilitychange",
29195 onVisibilityChange
29196 );
29197 window.removeEventListener("blur", onDragEnd2);
29198 window.removeEventListener("message", onIframeMessage);
29199 overlayEl.remove();
29200 delete window.__desktopModeOsFileDropMounted;
29201 }
29202 };
29203 host.__desktopModeOsFileDropMounted = manager;
29204 return manager;
29205 }
29206 function ensureDropOverlay() {
29207 const existing = document.querySelector(".desktop-mode-os-drop-overlay");
29208 if (existing) {
29209 return existing;
29210 }
29211 const el = document.createElement("div");
29212 el.className = "desktop-mode-os-drop-overlay";
29213 el.setAttribute("aria-hidden", "true");
29214 el.style.cssText = [
29215 "position:fixed",
29216 "inset:0",
29217 "pointer-events:none",
29218 "z-index:200",
29219 "opacity:0",
29220 "transition:opacity 120ms ease",
29221 "background:radial-gradient(circle at center, rgba(34,113,177,0.18) 0%, rgba(34,113,177,0.06) 60%, transparent 100%)",
29222 "box-shadow:inset 0 0 0 3px rgba(34,113,177,0.55)"
29223 ].join(";");
29224 const label = document.createElement("div");
29225 label.style.cssText = [
29226 "position:absolute",
29227 "top:50%",
29228 "left:50%",
29229 "transform:translate(-50%,-50%)",
29230 "padding:14px 22px",
29231 "border-radius:12px",
29232 "background:rgba(20,20,24,0.78)",
29233 "color:#fff",
29234 "font:600 14px/1.2 -apple-system,BlinkMacSystemFont,sans-serif",
29235 "letter-spacing:0.02em"
29236 ].join(";");
29237 label.textContent = "Drop to upload";
29238 el.appendChild(label);
29239 document.body.appendChild(el);
29240 const style = document.createElement("style");
29241 style.textContent = ".desktop-mode-os-drop-overlay.is-active{opacity:1!important;}";
29242 document.head.appendChild(style);
29243 return el;
29244 }
29245 function mountNoOp() {
29246 const cancel = (ev) => {
29247 if (!dragHasFiles(ev)) {
29248 return;
29249 }
29250 const target2 = ev.target;
29251 if (target2?.closest && IFRAME_PASSTHROUGH_SELECTORS.some((s) => target2.closest(s))) {
29252 return;
29253 }
29254 ev.preventDefault();
29255 };
29256 window.addEventListener("dragover", cancel);
29257 window.addEventListener("drop", cancel);
29258 const host = window;
29259 const manager = {
29260 dispose: () => {
29261 window.removeEventListener("dragover", cancel);
29262 window.removeEventListener("drop", cancel);
29263 delete host.__desktopModeOsFileDropMounted;
29264 }
29265 };
29266 host.__desktopModeOsFileDropMounted = manager;
29267 return manager;
29268 }
29269 function classifyDropTarget(ev) {
29270 const x = ev.clientX;
29271 const y = ev.clientY;
29272 let node = ev.target;
29273 while (node && node !== document.body) {
29274 if (node.tagName === "IFRAME") {
29275 const id = node.closest(
29276 "[data-window-id]"
29277 );
29278 return {
29279 surface: "iframe",
29280 windowId: id?.getAttribute("data-window-id") || void 0,
29281 x,
29282 y
29283 };
29284 }
29285 if (node.hasAttribute("data-window-id")) {
29286 return {
29287 surface: "window",
29288 windowId: node.getAttribute("data-window-id") || void 0,
29289 x,
29290 y
29291 };
29292 }
29293 if (node.classList.contains("desktop-mode-folder-grid")) {
29294 return { surface: "folder", x, y };
29295 }
29296 if (node.id === "desktop-mode-wallpaper" || node.classList.contains("desktop-mode-wallpaper") || node.classList.contains("desktop-mode-desktop")) {
29297 return { surface: "wallpaper", x, y };
29298 }
29299 node = node.parentElement;
29300 }
29301 return { surface: "unknown", x, y };
29302 }
29303 async function handleFiles(rawFiles, ctx, opts) {
29304 const detected = applyFilters(
29305 FILE_DROP_HOOKS.FILES_DETECTED,
29306 rawFiles,
29307 ctx
29308 );
29309 if (!Array.isArray(detected) || detected.length === 0) {
29310 return;
29311 }
29312 const { accepted, rejected } = partitionByPolicy(
29313 detected,
29314 opts.config
29315 );
29316 if (rejected.length > 0) {
29317 doAction(FILE_DROP_HOOKS.FILES_REJECTED, {
29318 rejections: rejected,
29319 context: ctx
29320 });
29321 showToast({
29322 message: rejected.length === 1 ? rejected[0].message : `${rejected.length} files couldn't be uploaded.`
29323 });
29324 }
29325 if (accepted.length === 0) {
29326 return;
29327 }
29328 const entries = accepted.map(({ file, mime }) => {
29329 const base = {
29330 file,
29331 mime,
29332 fields: defaultFields(file, mime)
29333 };
29334 const filtered = applyFilters(
29335 FILE_DROP_HOOKS.DIALOG_FIELDS,
29336 base,
29337 ctx
29338 );
29339 if (!filtered || typeof filtered !== "object" || !("fields" in filtered) || typeof filtered.fields !== "object") {
29340 return base;
29341 }
29342 return filtered;
29343 });
29344 await opts.openDialog(entries, ctx);
29345 }
29346 function partitionByPolicy(files, config) {
29347 const accepted = [];
29348 const rejected = [];
29349 for (const file of files) {
29350 if (file.size === 0) {
29351 rejected.push({
29352 file,
29353 reason: "empty",
29354 message: `“${file.name}” is empty.`
29355 });
29356 continue;
29357 }
29358 if (config.maxSize > 0 && file.size > config.maxSize) {
29359 rejected.push({
29360 file,
29361 reason: "size",
29362 message: `“${file.name}” exceeds the ${formatBytes$1(
29363 config.maxSize
29364 )} upload limit.`
29365 });
29366 continue;
29367 }
29368 const mime = resolveAllowedMime(
29369 file,
29370 config.allowedMimes,
29371 config.extToMime
29372 );
29373 if (!mime) {
29374 rejected.push({
29375 file,
29376 reason: "mime",
29377 message: `“${file.name}” is not an allowed file type.`
29378 });
29379 continue;
29380 }
29381 accepted.push({ file, mime });
29382 }
29383 return { accepted, rejected };
29384 }
29385 function resolveAllowedMime(file, allowedMimes, extToMime) {
29386 if (allowedMimes.length === 0) {
29387 return null;
29388 }
29389 const lower = file.type.toLowerCase();
29390 if (lower && allowedMimes.includes(lower)) {
29391 return lower;
29392 }
29393 const ext = extensionOf(file.name);
29394 if (!ext) {
29395 return null;
29396 }
29397 if (extToMime) {
29398 for (const [key, mime] of Object.entries(extToMime)) {
29399 if (key.split("|").includes(ext) && allowedMimes.includes(mime)) {
29400 return mime;
29401 }
29402 }
29403 return null;
29404 }
29405 const guess = EXTENSION_GUESSES[ext];
29406 if (guess && allowedMimes.includes(guess)) {
29407 return guess;
29408 }
29409 return null;
29410 }
29411 const EXTENSION_GUESSES = {
29412 jpg: "image/jpeg",
29413 jpeg: "image/jpeg",
29414 png: "image/png",
29415 gif: "image/gif",
29416 webp: "image/webp",
29417 avif: "image/avif",
29418 heic: "image/heic",
29419 heif: "image/heif",
29420 svg: "image/svg+xml",
29421 mp4: "video/mp4",
29422 mov: "video/quicktime",
29423 webm: "video/webm",
29424 mp3: "audio/mpeg",
29425 wav: "audio/wav",
29426 pdf: "application/pdf"
29427 };
29428 function extensionOf(name) {
29429 const dot = name.lastIndexOf(".");
29430 if (dot < 0) {
29431 return "";
29432 }
29433 return name.slice(dot + 1).toLowerCase();
29434 }
29435 function defaultFields(file, mime) {
29436 const safeName = sanitizeFilename(file.name);
29437 const ext = extensionOf(safeName);
29438 const stem = ext ? safeName.slice(0, safeName.length - ext.length - 1) : safeName;
29439 const title = humanize(stem);
29440 return {
29441 title,
29442 altText: mime.startsWith("image/") ? title : "",
29443 caption: "",
29444 description: "",
29445 filename: safeName
29446 };
29447 }
29448 function sanitizeFilename(name) {
29449 const cleaned = name.replace(/[\\/]/g, "-").replace(/[\x00-\x1f\x7f]/g, "").replace(/\s+/g, " ").replace(/ *- */g, "-").replace(/-+/g, "-").trim().replace(/^[-.]+|[-.]+$/g, "");
29450 return cleaned || "upload";
29451 }
29452 function humanize(stem) {
29453 const spaced = stem.replace(/[-_]+/g, " ").trim();
29454 if (!spaced) {
29455 return "Upload";
29456 }
29457 return spaced.charAt(0).toUpperCase() + spaced.slice(1);
29458 }
29459 function formatBytes$1(bytes) {
29460 if (bytes >= 1024 * 1024) {
29461 return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
29462 }
29463 if (bytes >= 1024) {
29464 return `${(bytes / 1024).toFixed(0)} KB`;
29465 }
29466 return `${bytes} B`;
29467 }
29468 function formatBytes(bytes) {
29469 if (!Number.isFinite(bytes) || bytes <= 0) {
29470 return "0 B";
29471 }
29472 const units = ["B", "KB", "MB", "GB", "TB"];
29473 let v = bytes;
29474 let i = 0;
29475 while (v >= 1024 && i < units.length - 1) {
29476 v /= 1024;
29477 i++;
29478 }
29479 const decimals = v >= 100 || i === 0 ? 0 : 1;
29480 return `${v.toFixed(decimals)} ${units[i]}`;
29481 }
29482 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}}`;
29483 const _WpdProgressBar = class _WpdProgressBar extends Component {
29484 constructor() {
29485 super(...arguments);
29486 this._ownedAriaLabel = null;
29487 }
29488 render() {
29489 return html`<div class="root" part="root">
29490 <div class="header" part="header" hidden>
29491 <span class="label" part="label"></span>
29492 <span class="percent" part="percent"></span>
29493 </div>
29494 <div class="track" part="track">
29495 <div class="fill" part="fill"></div>
29496 </div>
29497 </div>`;
29498 }
29499 requestUpdate() {
29500 super.requestUpdate();
29501 queueMicrotask(() => this._paint());
29502 }
29503 connectedCallback() {
29504 super.connectedCallback();
29505 queueMicrotask(() => this._paint());
29506 }
29507 _paint() {
29508 const root = this.shadowRoot;
29509 if (!root) {
29510 return;
29511 }
29512 const max = this._readMax();
29513 const indeterminate = this.hasAttribute("indeterminate") || max <= 0;
29514 const value = indeterminate ? 0 : this._readValue(max);
29515 const ratio = indeterminate ? 0 : value / max;
29516 const percent = Math.round(ratio * 100);
29517 const label = this.getAttribute("label") ?? "";
29518 const showPercent = this.hasAttribute("show-percent");
29519 const fill = root.querySelector(".fill");
29520 if (fill && !indeterminate) {
29521 fill.style.width = `${(ratio * 100).toFixed(2)}%`;
29522 } else if (fill && indeterminate) {
29523 fill.style.removeProperty("width");
29524 }
29525 const header = root.querySelector(".header");
29526 const labelEl = root.querySelector(".label");
29527 const percentEl = root.querySelector(".percent");
29528 if (header && labelEl && percentEl) {
29529 const visible = label || showPercent && !indeterminate;
29530 header.hidden = !visible;
29531 labelEl.textContent = label;
29532 percentEl.hidden = !(showPercent && !indeterminate);
29533 percentEl.textContent = `${percent}%`;
29534 }
29535 this._syncAria(max, value, indeterminate, label);
29536 const track = root.querySelector(".track");
29537 if (track) {
29538 track.setAttribute("role", "progressbar");
29539 track.setAttribute("aria-valuemin", "0");
29540 if (indeterminate) {
29541 track.removeAttribute("aria-valuenow");
29542 track.removeAttribute("aria-valuemax");
29543 } else {
29544 track.setAttribute("aria-valuemax", String(max));
29545 track.setAttribute("aria-valuenow", String(value));
29546 }
29547 if (label) {
29548 track.setAttribute("aria-label", label);
29549 } else {
29550 track.removeAttribute("aria-label");
29551 }
29552 }
29553 }
29554 _syncAria(max, value, indeterminate, label) {
29555 this.setAttribute("role", "progressbar");
29556 this.setAttribute("aria-valuemin", "0");
29557 if (indeterminate) {
29558 this.removeAttribute("aria-valuenow");
29559 this.removeAttribute("aria-valuemax");
29560 } else {
29561 this.setAttribute("aria-valuemax", String(max));
29562 this.setAttribute("aria-valuenow", String(value));
29563 }
29564 const existing = this.getAttribute("aria-label");
29565 if (label) {
29566 if (existing === null || existing === this._ownedAriaLabel) {
29567 this.setAttribute("aria-label", label);
29568 this._ownedAriaLabel = label;
29569 }
29570 } else if (existing !== null && existing === this._ownedAriaLabel) {
29571 this.removeAttribute("aria-label");
29572 this._ownedAriaLabel = null;
29573 }
29574 }
29575 _readMax() {
29576 const attr = this.getAttribute("max");
29577 if (attr === null) {
29578 return 100;
29579 }
29580 const raw = parseFloat(attr);
29581 return Number.isFinite(raw) ? raw : 100;
29582 }
29583 _readValue(max) {
29584 const raw = parseFloat(this.getAttribute("value") ?? "0");
29585 if (!Number.isFinite(raw)) {
29586 return 0;
29587 }
29588 if (raw < 0) {
29589 return 0;
29590 }
29591 if (raw > max) {
29592 return max;
29593 }
29594 return raw;
29595 }
29596 };
29597 _WpdProgressBar.props = [
29598 "value",
29599 "max",
29600 "indeterminate",
29601 "tone",
29602 "label",
29603 "showPercent"
29604 ];
29605 _WpdProgressBar.styles = [styles];
29606 _WpdProgressBar.help = {
29607 title: "Progress bar",
29608 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.",
29609 status: "experimental",
29610 since: "0.31.0",
29611 props: [
29612 {
29613 name: "value",
29614 type: "number",
29615 default: "0",
29616 description: "Current progress. Clamped to `[0, max]`."
29617 },
29618 {
29619 name: "max",
29620 type: "number",
29621 default: "100",
29622 description: "Maximum value. Setting `max <= 0` forces indeterminate."
29623 },
29624 {
29625 name: "indeterminate",
29626 type: "boolean",
29627 description: "Show the sweeping indeterminate animation instead of a value-driven fill. The `value` attribute is ignored while this is set."
29628 },
29629 {
29630 name: "tone",
29631 type: '"default" | "success" | "warning" | "danger"',
29632 default: "default",
29633 description: "Tints the fill from the shared status palette."
29634 },
29635 {
29636 name: "label",
29637 type: "string",
29638 description: "Optional inline label rendered above the track. Also wired into `aria-label` when set."
29639 },
29640 {
29641 name: "show-percent",
29642 type: "boolean",
29643 description: "Render a right-aligned percent readout next to the label. Only meaningful in determinate mode."
29644 }
29645 ],
29646 cssProps: [
29647 {
29648 name: "--wpd-progress-track-bg",
29649 default: "var(--desktop-mode-control-bg, rgba(0,0,0,0.08))"
29650 },
29651 {
29652 name: "--wpd-progress-fill",
29653 default: "var(--wp-admin-theme-color, #2271b1)"
29654 },
29655 { name: "--wpd-progress-height", default: "6px" },
29656 { name: "--wpd-progress-radius", default: "999px" },
29657 { name: "--wpd-progress-label-color", default: "inherit" },
29658 { name: "--wpd-progress-label-size", default: "12px" },
29659 { name: "--wpd-progress-label-gap", default: "4px" }
29660 ],
29661 example: html`<wpd-progress-bar
29662 value="42"
29663 label="Uploading hero.jpg"
29664 show-percent
29665 ></wpd-progress-bar>`
29666 };
29667 let WpdProgressBar = _WpdProgressBar;
29668 defineComponent("wpd-progress-bar", WpdProgressBar);
29669 const ROWS = /* @__PURE__ */ new Map();
29670 let panel = null;
29671 function mountUploadProgressHud() {
29672 if (document.body.hasAttribute("data-desktop-mode-suppress-upload-hud")) {
29673 return;
29674 }
29675 if (window.__wpdUploadHud) {
29676 return;
29677 }
29678 window.__wpdUploadHud = true;
29679 const ns = "desktop-mode/os-file-drop-hud";
29680 addAction(
29681 FILE_DROP_HOOKS.UPLOAD_STARTED,
29682 ns,
29683 (payload) => onStarted(payload.file, payload.fields, payload.abort)
29684 );
29685 addAction(
29686 FILE_DROP_HOOKS.UPLOAD_PROGRESS,
29687 ns,
29688 (payload) => onProgress(
29689 payload.file,
29690 payload.loaded,
29691 payload.total,
29692 payload.indeterminate
29693 )
29694 );
29695 addAction(
29696 FILE_DROP_HOOKS.AFTER_UPLOAD,
29697 ns,
29698 (payload) => onComplete(payload.file, payload.fields, payload.result)
29699 );
29700 addAction(
29701 FILE_DROP_HOOKS.UPLOAD_FAILED,
29702 ns,
29703 (payload) => onFailed(payload.file, payload.error)
29704 );
29705 }
29706 function onStarted(file, fields, abort) {
29707 const p = ensurePanel();
29708 const row = document.createElement("div");
29709 row.className = "desktop-mode-upload-hud__row";
29710 const meta = document.createElement("div");
29711 meta.className = "desktop-mode-upload-hud__meta";
29712 const name = document.createElement("div");
29713 name.className = "desktop-mode-upload-hud__name";
29714 name.textContent = fields.filename || file.name;
29715 name.title = fields.filename || file.name;
29716 const statusEl = document.createElement("div");
29717 statusEl.className = "desktop-mode-upload-hud__status";
29718 statusEl.textContent = "Uploading…";
29719 meta.append(name, statusEl);
29720 const bar = document.createElement("wpd-progress-bar");
29721 bar.setAttribute("indeterminate", "");
29722 bar.setAttribute("show-percent", "");
29723 const actions = document.createElement("div");
29724 actions.className = "desktop-mode-upload-hud__actions";
29725 const cancelBtn = document.createElement("wpd-button");
29726 cancelBtn.setAttribute("variant", "tertiary");
29727 cancelBtn.setAttribute("size", "small");
29728 cancelBtn.textContent = "Cancel";
29729 cancelBtn.addEventListener("click", () => {
29730 const r = ROWS.get(file);
29731 if (!r) {
29732 return;
29733 }
29734 if (r.state === "running") {
29735 r.statusEl.textContent = "Cancelling…";
29736 r.cancelBtn.disabled = true;
29737 r.abort();
29738 } else {
29739 dismissRow(r);
29740 }
29741 });
29742 actions.appendChild(cancelBtn);
29743 row.append(meta, bar, actions);
29744 p.querySelector(".desktop-mode-upload-hud__list").appendChild(row);
29745 ROWS.set(file, {
29746 file,
29747 abort,
29748 root: row,
29749 bar,
29750 statusEl,
29751 cancelBtn,
29752 state: "running",
29753 lingerTimer: null
29754 });
29755 updateHeader();
29756 }
29757 function onProgress(file, loaded, total, indeterminate) {
29758 const r = ROWS.get(file);
29759 if (!r || r.state !== "running") {
29760 return;
29761 }
29762 if (indeterminate || total <= 0) {
29763 r.bar.setAttribute("indeterminate", "");
29764 r.statusEl.textContent = `${formatBytes(loaded)} sent`;
29765 } else {
29766 r.bar.removeAttribute("indeterminate");
29767 r.bar.setAttribute("max", String(total));
29768 r.bar.setAttribute("value", String(loaded));
29769 r.statusEl.textContent = `${formatBytes(loaded)} / ${formatBytes(total)}`;
29770 }
29771 }
29772 function onComplete(file, fields, result) {
29773 const r = ROWS.get(file);
29774 if (!r) {
29775 return;
29776 }
29777 r.state = "success";
29778 r.bar.removeAttribute("indeterminate");
29779 r.bar.setAttribute("value", "100");
29780 r.bar.setAttribute("max", "100");
29781 r.bar.setAttribute("tone", "success");
29782 r.statusEl.textContent = "Uploaded";
29783 r.cancelBtn.textContent = "Dismiss";
29784 r.lingerTimer = setTimeout(() => dismissRow(r), 2500);
29785 updateHeader();
29786 activity.publish("desktop-mode/upload-hud-complete", {
29787 filename: fields.filename || result.filename,
29788 attachmentId: result.id
29789 });
29790 }
29791 function onFailed(file, error) {
29792 const r = ROWS.get(file);
29793 if (!r) {
29794 return;
29795 }
29796 r.bar.removeAttribute("indeterminate");
29797 r.bar.setAttribute("tone", "danger");
29798 r.cancelBtn.textContent = "Dismiss";
29799 r.cancelBtn.disabled = false;
29800 if (error.name === "UploadAbortedError") {
29801 r.state = "aborted";
29802 r.statusEl.textContent = "Cancelled";
29803 } else {
29804 r.state = "failed";
29805 r.statusEl.textContent = error.message || "Upload failed";
29806 }
29807 updateHeader();
29808 }
29809 function dismissRow(r) {
29810 if (r.lingerTimer) {
29811 clearTimeout(r.lingerTimer);
29812 }
29813 ROWS.delete(r.file);
29814 r.root.remove();
29815 updateHeader();
29816 if (ROWS.size === 0 && panel) {
29817 panel.hidden = true;
29818 }
29819 }
29820 function ensurePanel() {
29821 if (panel && panel.isConnected) {
29822 panel.hidden = false;
29823 return panel;
29824 }
29825 const p = document.createElement("div");
29826 p.className = "desktop-mode-upload-hud";
29827 p.setAttribute("role", "region");
29828 p.setAttribute("aria-label", "Uploads");
29829 const header = document.createElement("div");
29830 header.className = "desktop-mode-upload-hud__header";
29831 const title = document.createElement("div");
29832 title.className = "desktop-mode-upload-hud__title";
29833 title.textContent = "Uploads";
29834 const closeBtn = document.createElement("button");
29835 closeBtn.type = "button";
29836 closeBtn.className = "desktop-mode-upload-hud__close";
29837 closeBtn.setAttribute("aria-label", "Hide upload panel");
29838 closeBtn.textContent = "×";
29839 closeBtn.addEventListener("click", () => {
29840 for (const r of [...ROWS.values()]) {
29841 if (r.state !== "running") {
29842 dismissRow(r);
29843 }
29844 }
29845 if (ROWS.size === 0) {
29846 p.hidden = true;
29847 }
29848 });
29849 header.append(title, closeBtn);
29850 const list2 = document.createElement("div");
29851 list2.className = "desktop-mode-upload-hud__list";
29852 p.append(header, list2);
29853 document.body.appendChild(p);
29854 panel = p;
29855 return p;
29856 }
29857 function updateHeader() {
29858 if (!panel) {
29859 return;
29860 }
29861 const title = panel.querySelector(
29862 ".desktop-mode-upload-hud__title"
29863 );
29864 if (!title) {
29865 return;
29866 }
29867 const total = ROWS.size;
29868 const running = [...ROWS.values()].filter((r) => r.state === "running").length;
29869 if (running > 0) {
29870 title.textContent = running === total ? `Uploading ${running} file${running === 1 ? "" : "s"}…` : `${running} of ${total} uploading…`;
29871 } else if (total > 0) {
29872 title.textContent = `Uploads (${total})`;
29873 } else {
29874 title.textContent = "Uploads";
29875 }
29876 }
29877 function mountMediaLibraryRefresher() {
29878 if (document.body.hasAttribute(
29879 "data-desktop-mode-suppress-media-library-refresh"
29880 )) {
29881 return;
29882 }
29883 const sentinel = window;
29884 if (sentinel.__wpdMediaLibraryRefresher) {
29885 return;
29886 }
29887 sentinel.__wpdMediaLibraryRefresher = true;
29888 addAction(
29889 FILE_DROP_HOOKS.AFTER_UPLOAD,
29890 "desktop-mode/os-file-drop-library-refresh",
29891 () => refreshOpenLibraries()
29892 );
29893 }
29894 function refreshOpenLibraries() {
29895 const iframes = document.querySelectorAll("iframe");
29896 for (const frame of Array.from(iframes)) {
29897 if (!isMediaLibraryUrl(resolveIframeUrl(frame))) {
29898 continue;
29899 }
29900 try {
29901 frame.contentWindow?.location.reload();
29902 } catch {
29903 const reloadHref = resolveIframeUrl(frame);
29904 if (reloadHref) {
29905 frame.setAttribute("src", reloadHref);
29906 }
29907 }
29908 }
29909 }
29910 function resolveIframeUrl(frame) {
29911 try {
29912 return frame.contentWindow?.location.href ?? frame.src ?? "";
29913 } catch {
29914 return frame.src ?? "";
29915 }
29916 }
29917 function isMediaLibraryUrl(url) {
29918 if (!url) {
29919 return false;
29920 }
29921 return /\/wp-admin\/upload\.php(?:[?#]|$)/.test(url);
29922 }
29923 function bootOsFileDrop(args) {
29924 const config = args.config || {
29925 enabled: false,
29926 allowedMimes: [],
29927 maxSize: 0
29928 };
29929 mountUploadProgressHud();
29930 mountMediaLibraryRefresher();
29931 mountOsFileDropManager({
29932 config,
29933 mediaUrl: args.mediaUrl,
29934 restNonce: args.restNonce,
29935 openDialog: async (entries, ctx) => {
29936 const { openUploadDialog: openUploadDialog2 } = await Promise.resolve().then(() => dialog);
29937 await openUploadDialog2({
29938 entries,
29939 context: ctx,
29940 mediaUrl: args.mediaUrl,
29941 restNonce: args.restNonce
29942 });
29943 }
29944 });
29945 }
29946 const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
29947 __proto__: null,
29948 FILE_DROP_HOOKS,
29949 bootOsFileDrop
29950 }, Symbol.toStringTag, { value: "Module" }));
29951 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}`;
29952 const _WpdTextField = class _WpdTextField extends Component {
29953 constructor() {
29954 super(...arguments);
29955 this._revealed = false;
29956 }
29957 connectedCallback() {
29958 super.connectedCallback();
29959 ensureAutoId(this);
29960 }
29961 render() {
29962 const label = this.label || "";
29963 const value = this.value ?? "";
29964 const placeholder = this.placeholder || "";
29965 const disabled = this.disabled !== null;
29966 const readonly = this.readonly !== null;
29967 const declaredAutocomplete = this.autocomplete;
29968 const declaredType = this.type || "text";
29969 const isPassword = declaredType === "password";
29970 let autocomplete = declaredAutocomplete || "off";
29971 if (isPassword && (!declaredAutocomplete || autocomplete === "off")) {
29972 autocomplete = "new-password";
29973 }
29974 const maxLength = this.maxlength;
29975 const minLength = this.minlength;
29976 const pattern = this.pattern || "";
29977 const name = this.name || "";
29978 const suffix = this.suffix || "";
29979 const invalid = this.invalid !== null;
29980 const reveal = this.reveal !== null;
29981 const isPasswordIntent = declaredType === "password";
29982 const isMasked = isPasswordIntent && !(reveal && this._revealed);
29983 let effectiveType;
29984 if (isPasswordIntent) {
29985 effectiveType = "text";
29986 } else if (reveal && this._revealed) {
29987 effectiveType = "text";
29988 } else {
29989 effectiveType = declaredType;
29990 }
29991 const rowClass = reveal ? "wpd-text-field__row wpd-text-field__row--has-reveal" : "wpd-text-field__row";
29992 const inputClass = isMasked ? "wpd-text-field__input wpd-text-field__input--masked" : "wpd-text-field__input";
29993 const hostId = this.id || "wpd-unnamed";
29994 const inputId = `${hostId}__input`;
29995 return html`
29996 ${label ? html`<label
29997 class="wpd-text-field__label"
29998 for=${inputId}
29999 >${label}</label>` : html``}
30000 <span class=${rowClass}>
30001 <input
30002 id=${inputId}
30003 class=${inputClass}
30004 type=${effectiveType}
30005 .value=${value}
30006 placeholder=${placeholder}
30007 ?disabled=${disabled}
30008 ?readonly=${readonly}
30009 autocomplete=${autocomplete}
30010 maxlength=${maxLength ?? ""}
30011 minlength=${minLength ?? ""}
30012 pattern=${pattern}
30013 name=${name}
30014 aria-invalid=${invalid ? "true" : "false"}
30015 aria-label=${label || ""}
30016 @input=${(e) => this._onInput(e)}
30017 @change=${(e) => this._onChange(e)}
30018 @keydown=${(e) => this._onKeyDown(e)}
30019 />
30020 ${suffix ? html`<span class="wpd-text-field__suffix">${suffix}</span>` : html``}
30021 ${reveal ? this._renderRevealButton(disabled) : html``}
30022 </span>
30023 `;
30024 }
30025 _renderRevealButton(disabled) {
30026 const label = this._revealed ? "Hide" : "Show";
30027 return html`
30028 <button
30029 type="button"
30030 class="wpd-text-field__reveal"
30031 aria-label=${label}
30032 aria-pressed=${this._revealed ? "true" : "false"}
30033 ?disabled=${disabled}
30034 tabindex="0"
30035 @click=${() => this._onToggleReveal()}
30036 >
30037 ${this._revealed ? _iconEyeOff() : _iconEye()}
30038 </button>
30039 `;
30040 }
30041 _onToggleReveal() {
30042 this._revealed = !this._revealed;
30043 this.requestUpdate();
30044 }
30045 _onInput(e) {
30046 const input = e.target;
30047 this.value = input.value;
30048 this.emit("wpd-input-change", { value: input.value });
30049 }
30050 _onChange(e) {
30051 const input = e.target;
30052 this.emit("wpd-input-commit", { value: input.value });
30053 }
30054 _onKeyDown(e) {
30055 if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.metaKey) {
30056 const input = e.target;
30057 this.emit("wpd-submit", { value: input.value });
30058 }
30059 }
30060 };
30061 _WpdTextField.props = [
30062 "label",
30063 "value",
30064 "placeholder",
30065 "disabled",
30066 "readonly",
30067 "autocomplete",
30068 "type",
30069 "maxlength",
30070 "minlength",
30071 "pattern",
30072 "name",
30073 "suffix",
30074 "invalid",
30075 "reveal"
30076 ];
30077 _WpdTextField.styles = [textFieldStyles];
30078 _WpdTextField.help = {
30079 title: "Text field",
30080 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.",
30081 status: "stable",
30082 since: "0.5.0",
30083 props: [
30084 { name: "label", type: "string", description: "Visible label above the input." },
30085 { name: "value", type: "string", description: "Current input value; reflected two-way." },
30086 { name: "placeholder", type: "string", description: "Native placeholder string." },
30087 { name: "disabled", type: "boolean attribute", description: "Disables the native input." },
30088 { name: "readonly", type: "boolean attribute", description: "Marks the input readonly." },
30089 {
30090 name: "autocomplete",
30091 type: "string",
30092 default: "off",
30093 description: "Forwarded to the native input autocomplete attribute."
30094 },
30095 {
30096 name: "type",
30097 type: "string",
30098 default: "text",
30099 description: "Native input type (text, password, email, search, tel, url)."
30100 },
30101 { name: "maxlength", type: "integer (string)", description: "Native maxlength." },
30102 { name: "minlength", type: "integer (string)", description: "Native minlength." },
30103 { name: "pattern", type: "regex string", description: "Native validation pattern." },
30104 { name: "name", type: "string", description: "Forwarded to the native input for form submission." },
30105 { name: "suffix", type: "string", description: "Text rendered inside the right edge of the input row." },
30106 {
30107 name: "invalid",
30108 type: "boolean attribute",
30109 description: "Marks the field aria-invalid and applies the error style."
30110 },
30111 {
30112 name: "reveal",
30113 type: "boolean attribute",
30114 description: 'On type="password" fields, adds an eye-icon toggle that flips the input between hidden and visible text.'
30115 }
30116 ],
30117 events: [
30118 {
30119 name: "wpd-input-change",
30120 description: "Fires on every input keystroke.",
30121 detail: "{ value: string }"
30122 },
30123 {
30124 name: "wpd-input-commit",
30125 description: "Fires on the native change event (blur / Enter).",
30126 detail: "{ value: string }"
30127 },
30128 {
30129 name: "wpd-submit",
30130 description: "Fires when the user presses Enter (without Shift/Alt/Meta).",
30131 detail: "{ value: string }"
30132 }
30133 ],
30134 cssProps: [
30135 { name: "--desktop-mode-text", description: "Text colour." },
30136 { name: "--desktop-mode-muted", description: "Label + suffix colour." },
30137 { name: "--desktop-mode-border", description: "Input outline." },
30138 { name: "--desktop-mode-window-bg", description: "Input background." }
30139 ],
30140 example: html`
30141 <wpd-stack gap="8">
30142 <wpd-text-field label="Note title" value="Untitled" placeholder="Name this note"></wpd-text-field>
30143 <wpd-text-field type="password" reveal label="API key"></wpd-text-field>
30144 </wpd-stack>
30145 `
30146 };
30147 let WpdTextField = _WpdTextField;
30148 defineComponent("wpd-text-field", WpdTextField);
30149 function _iconEye() {
30150 return html`
30151 <svg
30152 viewBox="0 0 16 16"
30153 width="14"
30154 height="14"
30155 fill="none"
30156 stroke="currentColor"
30157 stroke-width="1.5"
30158 stroke-linecap="round"
30159 stroke-linejoin="round"
30160 aria-hidden="true"
30161 focusable="false"
30162 >
30163 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
30164 <circle cx="8" cy="8" r="2" />
30165 </svg>
30166 `;
30167 }
30168 function _iconEyeOff() {
30169 return html`
30170 <svg
30171 viewBox="0 0 16 16"
30172 width="14"
30173 height="14"
30174 fill="none"
30175 stroke="currentColor"
30176 stroke-width="1.5"
30177 stroke-linecap="round"
30178 stroke-linejoin="round"
30179 aria-hidden="true"
30180 focusable="false"
30181 >
30182 <path d="M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z" />
30183 <circle cx="8" cy="8" r="2" />
30184 <line x1="2" y1="2" x2="14" y2="14" />
30185 </svg>
30186 `;
30187 }
30188 async function uploadFile(args) {
30189 const initial = {
30190 file: args.file,
30191 mime: args.mime,
30192 fields: args.fields
30193 };
30194 const filtered = applyFilters(
30195 FILE_DROP_HOOKS.BEFORE_UPLOAD,
30196 initial,
30197 args.context
30198 );
30199 if (!filtered) {
30200 throw new UploadCancelledError();
30201 }
30202 const body = new FormData();
30203 const renamed = filtered.fields.filename !== filtered.file.name ? new File([filtered.file], filtered.fields.filename, {
30204 type: filtered.mime || filtered.file.type
30205 }) : filtered.file;
30206 body.append("file", renamed);
30207 body.append("title", filtered.fields.title);
30208 body.append("alt_text", filtered.fields.altText);
30209 body.append("caption", filtered.fields.caption);
30210 body.append("description", filtered.fields.description);
30211 return new Promise((resolve2, reject) => {
30212 const xhr = new XMLHttpRequest();
30213 xhr.open("POST", args.mediaUrl, true);
30214 xhr.withCredentials = true;
30215 xhr.setRequestHeader("X-WP-Nonce", args.restNonce);
30216 xhr.responseType = "text";
30217 let aborted = false;
30218 let bodyFullySent = false;
30219 let cancelRequested = false;
30220 const abort = () => {
30221 cancelRequested = true;
30222 if (bodyFullySent) {
30223 return;
30224 }
30225 aborted = true;
30226 try {
30227 xhr.abort();
30228 } catch {
30229 }
30230 };
30231 doAction(FILE_DROP_HOOKS.UPLOAD_STARTED, {
30232 file: filtered.file,
30233 fields: filtered.fields,
30234 context: args.context,
30235 abort
30236 });
30237 xhr.upload.addEventListener("progress", (e) => {
30238 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
30239 file: filtered.file,
30240 fields: filtered.fields,
30241 context: args.context,
30242 loaded: e.loaded,
30243 total: e.lengthComputable ? e.total : 0,
30244 indeterminate: !e.lengthComputable
30245 });
30246 });
30247 xhr.upload.addEventListener("load", () => {
30248 bodyFullySent = true;
30249 doAction(FILE_DROP_HOOKS.UPLOAD_PROGRESS, {
30250 file: filtered.file,
30251 fields: filtered.fields,
30252 context: args.context,
30253 loaded: filtered.file.size,
30254 total: filtered.file.size,
30255 indeterminate: false
30256 });
30257 });
30258 xhr.addEventListener("error", () => {
30259 if (aborted) {
30260 return;
30261 }
30262 const error = new Error("Network error during upload.");
30263 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
30264 // `filtered.file` — same identity as UPLOAD_STARTED /
30265 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
30266 // that swapped the File would otherwise route this
30267 // failure to a row keyed by the original (pre-swap)
30268 // File, leaving the HUD row stuck in "running".
30269 file: filtered.file,
30270 error,
30271 context: args.context
30272 });
30273 reject(error);
30274 });
30275 xhr.addEventListener("abort", () => {
30276 const error = new UploadAbortedError();
30277 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
30278 // `filtered.file` — same identity as UPLOAD_STARTED /
30279 // _PROGRESS / AFTER_UPLOAD. A BEFORE_UPLOAD filter
30280 // that swapped the File would otherwise route this
30281 // failure to a row keyed by the original (pre-swap)
30282 // File, leaving the HUD row stuck in "running".
30283 file: filtered.file,
30284 error,
30285 context: args.context
30286 });
30287 reject(error);
30288 });
30289 xhr.addEventListener("load", () => {
30290 if (aborted) {
30291 return;
30292 }
30293 if (xhr.status < 200 || xhr.status >= 300) {
30294 const message = extractXhrMessage(xhr);
30295 const error = new Error(message);
30296 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
30297 file: filtered.file,
30298 error,
30299 context: args.context
30300 });
30301 reject(error);
30302 return;
30303 }
30304 let data;
30305 try {
30306 data = JSON.parse(xhr.responseText);
30307 } catch (err) {
30308 const error = err instanceof Error ? err : new Error("Could not parse server response.");
30309 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
30310 file: filtered.file,
30311 error,
30312 context: args.context
30313 });
30314 reject(error);
30315 return;
30316 }
30317 if (cancelRequested && data.id) {
30318 void deleteAttachment(
30319 args.mediaUrl,
30320 args.restNonce,
30321 data.id
30322 );
30323 const error = new UploadAbortedError();
30324 doAction(FILE_DROP_HOOKS.UPLOAD_FAILED, {
30325 file: filtered.file,
30326 error,
30327 context: args.context
30328 });
30329 reject(error);
30330 return;
30331 }
30332 const result = {
30333 id: data.id,
30334 url: data.source_url,
30335 mime: data.mime_type || filtered.mime,
30336 title: data.title?.rendered || filtered.fields.title,
30337 filename: data.media_details?.file || filtered.fields.filename
30338 };
30339 doAction(FILE_DROP_HOOKS.AFTER_UPLOAD, {
30340 file: filtered.file,
30341 result,
30342 fields: filtered.fields,
30343 context: args.context
30344 });
30345 resolve2(result);
30346 });
30347 xhr.send(body);
30348 });
30349 }
30350 class UploadCancelledError extends Error {
30351 constructor() {
30352 super("Upload cancelled by desktop-mode.drop.before-upload filter.");
30353 this.name = "UploadCancelledError";
30354 }
30355 }
30356 class UploadAbortedError extends Error {
30357 constructor() {
30358 super("Upload aborted by the caller.");
30359 this.name = "UploadAbortedError";
30360 }
30361 }
30362 function deleteAttachment(mediaUrl, restNonce, id) {
30363 const url = `${mediaUrl.replace(/\/$/, "")}/${id}?force=true`;
30364 const cleanup = new XMLHttpRequest();
30365 cleanup.open("DELETE", url, true);
30366 cleanup.withCredentials = true;
30367 cleanup.setRequestHeader("X-WP-Nonce", restNonce);
30368 return new Promise((resolve2) => {
30369 cleanup.addEventListener("loadend", () => {
30370 if (cleanup.status < 200 || cleanup.status >= 300) {
30371 console.warn(
30372 `[os-file-drop] late-cancel cleanup failed for attachment ${id} (HTTP ${cleanup.status}). The attachment remains in the Media Library; delete it manually.`
30373 );
30374 }
30375 resolve2();
30376 });
30377 cleanup.addEventListener("error", () => {
30378 console.warn(
30379 `[os-file-drop] late-cancel cleanup network error for attachment ${id}. The attachment remains in the Media Library; delete it manually.`
30380 );
30381 resolve2();
30382 });
30383 try {
30384 cleanup.send();
30385 } catch (err) {
30386 console.warn(
30387 `[os-file-drop] late-cancel cleanup could not be dispatched for attachment ${id}:`,
30388 err
30389 );
30390 resolve2();
30391 }
30392 });
30393 }
30394 function extractXhrMessage(xhr) {
30395 const fallback = `Upload failed (HTTP ${xhr.status}).`;
30396 const text = xhr.responseText;
30397 if (!text) {
30398 return fallback;
30399 }
30400 try {
30401 const data = JSON.parse(text);
30402 if (data && typeof data.message === "string") {
30403 return data.message;
30404 }
30405 } catch {
30406 }
30407 return fallback;
30408 }
30409 async function openUploadDialog(args) {
30410 if (args.entries.length === 0) {
30411 return;
30412 }
30413 const modal = document.createElement("wpd-modal");
30414 modal.setAttribute("open", "");
30415 modal.setAttribute("size", "md");
30416 modal.setAttribute(
30417 "title",
30418 args.entries.length === 1 ? "Upload to Media Library" : `Upload ${args.entries.length} files to Media Library`
30419 );
30420 document.body.appendChild(modal);
30421 const draft = args.entries.map((entry) => ({
30422 ...entry.fields
30423 }));
30424 const renderBody = () => {
30425 modal.innerHTML = "";
30426 const list2 = document.createElement("div");
30427 list2.style.cssText = "display:flex;flex-direction:column;gap:18px;max-height:60vh;overflow:auto;padding-right:6px;";
30428 args.entries.forEach((entry, i) => {
30429 list2.appendChild(renderEntry(entry, draft[i], i + 1));
30430 });
30431 modal.appendChild(list2);
30432 const footer = document.createElement("div");
30433 footer.setAttribute("slot", "footer");
30434 footer.style.cssText = "display:flex;gap:8px;justify-content:flex-end;";
30435 const cancel = document.createElement("wpd-button");
30436 cancel.setAttribute("variant", "secondary");
30437 cancel.textContent = "Cancel";
30438 cancel.addEventListener("click", () => {
30439 modal.remove();
30440 });
30441 const upload = document.createElement("wpd-button");
30442 upload.setAttribute("variant", "primary");
30443 upload.textContent = args.entries.length === 1 ? "Upload" : `Upload ${args.entries.length} files`;
30444 upload.addEventListener("click", () => {
30445 void runUploads(upload, cancel);
30446 });
30447 footer.appendChild(cancel);
30448 footer.appendChild(upload);
30449 modal.appendChild(footer);
30450 };
30451 const renderEntry = (entry, fields, index2) => {
30452 const wrap = document.createElement("div");
30453 wrap.style.cssText = "display:flex;flex-direction:column;gap:8px;border:1px solid rgba(255,255,255,0.08);border-radius:10px;padding:14px;";
30454 const heading = document.createElement("div");
30455 heading.style.cssText = "display:flex;gap:10px;align-items:center;font-weight:600;";
30456 const tag = document.createElement("span");
30457 tag.textContent = args.entries.length === 1 ? "" : `#${index2} · `;
30458 tag.style.opacity = "0.6";
30459 const fname = document.createElement("span");
30460 fname.textContent = entry.file.name;
30461 fname.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
30462 const size = document.createElement("span");
30463 size.textContent = `${entry.mime || "unknown"} · ${formatBytes(
30464 entry.file.size
30465 )}`;
30466 size.style.cssText = "opacity:0.6;font-size:12px;";
30467 heading.appendChild(tag);
30468 heading.appendChild(fname);
30469 heading.appendChild(size);
30470 wrap.appendChild(heading);
30471 wrap.appendChild(textField("Title", fields.title, (v) => fields.title = v));
30472 wrap.appendChild(textField("Filename", fields.filename, (v) => fields.filename = v));
30473 if (entry.mime.startsWith("image/")) {
30474 wrap.appendChild(
30475 textField("Alt text", fields.altText, (v) => fields.altText = v)
30476 );
30477 }
30478 wrap.appendChild(textField("Caption", fields.caption, (v) => fields.caption = v));
30479 wrap.appendChild(
30480 textareaField("Description", fields.description, (v) => fields.description = v)
30481 );
30482 return wrap;
30483 };
30484 const runUploads = async (uploadBtn, cancelBtn) => {
30485 uploadBtn.disabled = true;
30486 cancelBtn.disabled = true;
30487 uploadBtn.textContent = "Uploading…";
30488 const total = args.entries.length;
30489 let successes = 0;
30490 let failures = 0;
30491 let cancelled = 0;
30492 const failureDetails = [];
30493 for (let i = 0; i < total; i++) {
30494 const entry = args.entries[i];
30495 try {
30496 await uploadFile({
30497 file: entry.file,
30498 mime: entry.mime,
30499 fields: draft[i],
30500 context: args.context,
30501 mediaUrl: args.mediaUrl,
30502 restNonce: args.restNonce
30503 });
30504 successes++;
30505 } catch (err) {
30506 if (err instanceof UploadCancelledError) {
30507 cancelled++;
30508 continue;
30509 }
30510 if (err instanceof UploadAbortedError) {
30511 cancelled++;
30512 continue;
30513 }
30514 failures++;
30515 const message = err instanceof Error ? err.message : "Upload failed.";
30516 failureDetails.push(`“${entry.file.name}” — ${message}`);
30517 }
30518 }
30519 modal.remove();
30520 showBatchSummaryToast({
30521 total,
30522 successes,
30523 failures,
30524 cancelled,
30525 failureDetails
30526 });
30527 };
30528 renderBody();
30529 await new Promise((resolve2) => {
30530 modal.addEventListener("wpd-modal-cancel", () => {
30531 modal.remove();
30532 resolve2();
30533 });
30534 const observer = new MutationObserver(() => {
30535 if (!modal.isConnected) {
30536 observer.disconnect();
30537 resolve2();
30538 }
30539 });
30540 observer.observe(document.body, { childList: true, subtree: true });
30541 });
30542 }
30543 function textField(label, value, onChange) {
30544 const el = document.createElement("wpd-text-field");
30545 el.setAttribute("label", label);
30546 el.setAttribute("value", value);
30547 el.addEventListener("input", () => {
30548 const v = el.value;
30549 if (typeof v === "string") {
30550 onChange(v);
30551 }
30552 });
30553 return el;
30554 }
30555 function textareaField(label, value, onChange) {
30556 const el = document.createElement("wpd-textarea");
30557 el.setAttribute("label", label);
30558 el.setAttribute("value", value);
30559 el.setAttribute("rows", "3");
30560 el.addEventListener("input", () => {
30561 const v = el.value;
30562 if (typeof v === "string") {
30563 onChange(v);
30564 }
30565 });
30566 return el;
30567 }
30568 function showBatchSummaryToast(args) {
30569 const { total, successes, failures, cancelled, failureDetails } = args;
30570 if (total === 0) {
30571 return;
30572 }
30573 if (total === 1) {
30574 if (successes === 1) {
30575 showToast({ message: "Uploaded to Media Library." });
30576 } else if (failures === 1 && failureDetails[0]) {
30577 showToast({ message: failureDetails[0] });
30578 } else if (cancelled === 1) {
30579 showToast({ message: "Upload cancelled." });
30580 }
30581 return;
30582 }
30583 if (successes === total) {
30584 showToast({
30585 message: `Uploaded ${successes} files to Media Library.`
30586 });
30587 return;
30588 }
30589 if (cancelled === total) {
30590 showToast({ message: "All uploads cancelled." });
30591 return;
30592 }
30593 if (failures === total) {
30594 showToast({
30595 message: failures === 1 && failureDetails[0] ? failureDetails[0] : `${failures} uploads failed.`
30596 });
30597 return;
30598 }
30599 const parts = [];
30600 if (successes > 0) {
30601 parts.push(
30602 `Uploaded ${successes} file${successes === 1 ? "" : "s"}.`
30603 );
30604 }
30605 if (cancelled > 0) {
30606 parts.push(`Cancelled ${cancelled}.`);
30607 }
30608 if (failures > 0) {
30609 parts.push(`Failed ${failures}.`);
30610 }
30611 showToast({ message: parts.join(" ") });
30612 }
30613 const dialog = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
30614 __proto__: null,
30615 openUploadDialog
30616 }, Symbol.toStringTag, { value: "Module" }));
30617 exports.clampGeometryToViewport = clampGeometryToViewport;
30618 Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
30619 return exports;
30620 }({});
30621